Skip to content

Commit a713492

Browse files
Assertions refactoring (#1566)
Co-authored-by: Sylvain Juge <[email protected]>
1 parent b2f04ab commit a713492

File tree

9 files changed

+906
-351
lines changed

9 files changed

+906
-351
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jmxscraper.assertions;
7+
8+
import io.opentelemetry.proto.metrics.v1.Metric;
9+
10+
/** Dedicated Assertj extension to provide convenient fluent API for metrics testing */
11+
public class Assertions extends org.assertj.core.api.Assertions {
12+
13+
public static MetricAssert assertThat(Metric metric) {
14+
return new MetricAssert(metric);
15+
}
16+
}
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jmxscraper.assertions;
7+
8+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
9+
import io.opentelemetry.proto.common.v1.KeyValue;
10+
import io.opentelemetry.proto.metrics.v1.Metric;
11+
import io.opentelemetry.proto.metrics.v1.NumberDataPoint;
12+
import java.util.Arrays;
13+
import java.util.Collection;
14+
import java.util.HashMap;
15+
import java.util.HashSet;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.Set;
19+
import java.util.function.Consumer;
20+
import java.util.stream.Collectors;
21+
import org.assertj.core.api.AbstractAssert;
22+
import org.assertj.core.internal.Integers;
23+
import org.assertj.core.internal.Iterables;
24+
import org.assertj.core.internal.Maps;
25+
import org.assertj.core.internal.Objects;
26+
27+
public class MetricAssert extends AbstractAssert<MetricAssert, Metric> {
28+
29+
private static final Objects objects = Objects.instance();
30+
private static final Iterables iterables = Iterables.instance();
31+
private static final Integers integers = Integers.instance();
32+
private static final Maps maps = Maps.instance();
33+
34+
private boolean strict;
35+
36+
private boolean descriptionChecked;
37+
private boolean unitChecked;
38+
private boolean typeChecked;
39+
private boolean dataPointAttributesChecked;
40+
41+
MetricAssert(Metric actual) {
42+
super(actual, MetricAssert.class);
43+
}
44+
45+
public void setStrict(boolean strict) {
46+
this.strict = strict;
47+
}
48+
49+
public void strictCheck() {
50+
strictCheck("description", /* expectedCheckStatus= */ true, descriptionChecked);
51+
strictCheck("unit", /* expectedCheckStatus= */ true, unitChecked);
52+
strictCheck("type", /* expectedCheckStatus= */ true, typeChecked);
53+
strictCheck(
54+
"data point attributes", /* expectedCheckStatus= */ true, dataPointAttributesChecked);
55+
}
56+
57+
private void strictCheck(
58+
String metricProperty, boolean expectedCheckStatus, boolean actualCheckStatus) {
59+
if (!strict) {
60+
return;
61+
}
62+
String failMsgPrefix = expectedCheckStatus ? "duplicate" : "missing";
63+
info.description(
64+
"%s assertion on %s for metric '%s'", failMsgPrefix, metricProperty, actual.getName());
65+
objects.assertEqual(info, actualCheckStatus, expectedCheckStatus);
66+
}
67+
68+
/**
69+
* Verifies metric description
70+
*
71+
* @param description expected description
72+
* @return this
73+
*/
74+
@CanIgnoreReturnValue
75+
public MetricAssert hasDescription(String description) {
76+
isNotNull();
77+
78+
info.description("unexpected description for metric '%s'", actual.getName());
79+
objects.assertEqual(info, actual.getDescription(), description);
80+
strictCheck("description", /* expectedCheckStatus= */ false, descriptionChecked);
81+
descriptionChecked = true;
82+
return this;
83+
}
84+
85+
/**
86+
* Verifies metric unit
87+
*
88+
* @param unit expected unit
89+
* @return this
90+
*/
91+
@CanIgnoreReturnValue
92+
public MetricAssert hasUnit(String unit) {
93+
isNotNull();
94+
95+
info.description("unexpected unit for metric '%s'", actual.getName());
96+
objects.assertEqual(info, actual.getUnit(), unit);
97+
strictCheck("unit", /* expectedCheckStatus= */ false, unitChecked);
98+
unitChecked = true;
99+
return this;
100+
}
101+
102+
/**
103+
* Verifies the metric is a gauge
104+
*
105+
* @return this
106+
*/
107+
@CanIgnoreReturnValue
108+
public MetricAssert isGauge() {
109+
isNotNull();
110+
111+
info.description("gauge expected for metric '%s'", actual.getName());
112+
objects.assertEqual(info, actual.hasGauge(), true);
113+
strictCheck("type", /* expectedCheckStatus= */ false, typeChecked);
114+
typeChecked = true;
115+
return this;
116+
}
117+
118+
@CanIgnoreReturnValue
119+
private MetricAssert hasSum(boolean monotonic) {
120+
isNotNull();
121+
122+
info.description("sum expected for metric '%s'", actual.getName());
123+
objects.assertEqual(info, actual.hasSum(), true);
124+
125+
String prefix = monotonic ? "monotonic" : "non-monotonic";
126+
info.description(prefix + " sum expected for metric '%s'", actual.getName());
127+
objects.assertEqual(info, actual.getSum().getIsMonotonic(), monotonic);
128+
return this;
129+
}
130+
131+
/**
132+
* Verifies the metric is a counter
133+
*
134+
* @return this
135+
*/
136+
@CanIgnoreReturnValue
137+
public MetricAssert isCounter() {
138+
// counters have a monotonic sum as their value can't decrease
139+
hasSum(true);
140+
strictCheck("type", /* expectedCheckStatus= */ false, typeChecked);
141+
typeChecked = true;
142+
return this;
143+
}
144+
145+
/**
146+
* Verifies the metric is an up-down counter
147+
*
148+
* @return this
149+
*/
150+
@CanIgnoreReturnValue
151+
public MetricAssert isUpDownCounter() {
152+
// up down counters are non-monotonic as their value can increase & decrease
153+
hasSum(false);
154+
strictCheck("type", /* expectedCheckStatus= */ false, typeChecked);
155+
typeChecked = true;
156+
return this;
157+
}
158+
159+
@CanIgnoreReturnValue
160+
public MetricAssert hasDataPointsWithoutAttributes() {
161+
isNotNull();
162+
163+
return checkDataPoints(
164+
dataPoints -> {
165+
dataPointsCommonCheck(dataPoints);
166+
167+
// all data points must not have any attribute
168+
for (NumberDataPoint dataPoint : dataPoints) {
169+
info.description(
170+
"no attribute expected on data point for metric '%s'", actual.getName());
171+
iterables.assertEmpty(info, dataPoint.getAttributesList());
172+
}
173+
});
174+
}
175+
176+
@CanIgnoreReturnValue
177+
private MetricAssert checkDataPoints(Consumer<List<NumberDataPoint>> listConsumer) {
178+
// in practice usually one set of data points is provided but the
179+
// protobuf does not enforce that, so we have to ensure checking at least one
180+
int count = 0;
181+
if (actual.hasGauge()) {
182+
count++;
183+
listConsumer.accept(actual.getGauge().getDataPointsList());
184+
}
185+
if (actual.hasSum()) {
186+
count++;
187+
listConsumer.accept(actual.getSum().getDataPointsList());
188+
}
189+
info.description("at least one set of data points expected for metric '%s'", actual.getName());
190+
integers.assertGreaterThan(info, count, 0);
191+
192+
strictCheck(
193+
"data point attributes", /* expectedCheckStatus= */ false, dataPointAttributesChecked);
194+
dataPointAttributesChecked = true;
195+
return this;
196+
}
197+
198+
@CanIgnoreReturnValue
199+
public MetricAssert hasTypedDataPoints(Collection<String> types) {
200+
return checkDataPoints(
201+
dataPoints -> {
202+
dataPointsCommonCheck(dataPoints);
203+
204+
Set<String> foundValues = new HashSet<>();
205+
for (NumberDataPoint dataPoint : dataPoints) {
206+
List<KeyValue> attributes = dataPoint.getAttributesList();
207+
208+
info.description(
209+
"expected exactly one 'name' attribute for typed data point in metric '%s'",
210+
actual.getName());
211+
iterables.assertHasSize(info, attributes, 1);
212+
213+
objects.assertEqual(info, attributes.get(0).getKey(), "name");
214+
foundValues.add(attributes.get(0).getValue().getStringValue());
215+
}
216+
info.description(
217+
"missing or unexpected type attribute for metric '%s'", actual.getName());
218+
iterables.assertContainsExactlyInAnyOrder(info, foundValues, types.toArray());
219+
});
220+
}
221+
222+
private void dataPointsCommonCheck(List<NumberDataPoint> dataPoints) {
223+
info.description("unable to retrieve data points from metric '%s'", actual.getName());
224+
objects.assertNotNull(info, dataPoints);
225+
226+
// at least one data point must be reported
227+
info.description("at least one data point expected for metric '%s'", actual.getName());
228+
iterables.assertNotEmpty(info, dataPoints);
229+
}
230+
231+
/**
232+
* Verifies that all data points have all the expected attributes
233+
*
234+
* @param attributes expected attributes
235+
* @return this
236+
*/
237+
@SafeVarargs
238+
@CanIgnoreReturnValue
239+
public final MetricAssert hasDataPointsAttributes(Map.Entry<String, String>... attributes) {
240+
return checkDataPoints(
241+
dataPoints -> {
242+
dataPointsCommonCheck(dataPoints);
243+
244+
Map<String, String> attributesMap = new HashMap<>();
245+
for (Map.Entry<String, String> attributeEntry : attributes) {
246+
attributesMap.put(attributeEntry.getKey(), attributeEntry.getValue());
247+
}
248+
for (NumberDataPoint dataPoint : dataPoints) {
249+
Map<String, String> dataPointAttributes = toMap(dataPoint.getAttributesList());
250+
251+
// all attributes must match
252+
info.description(
253+
"missing/unexpected data points attributes for metric '%s'", actual.getName());
254+
containsExactly(dataPointAttributes, attributes);
255+
maps.assertContainsAllEntriesOf(info, dataPointAttributes, attributesMap);
256+
}
257+
});
258+
}
259+
260+
/**
261+
* Verifies that all data points have their attributes match one of the attributes set and that
262+
* all provided attributes sets matched at least once.
263+
*
264+
* @param attributeSets sets of attributes as maps
265+
* @return this
266+
*/
267+
@SafeVarargs
268+
@CanIgnoreReturnValue
269+
@SuppressWarnings("varargs") // required to avoid warning
270+
public final MetricAssert hasDataPointsAttributes(Map<String, String>... attributeSets) {
271+
return checkDataPoints(
272+
dataPoints -> {
273+
dataPointsCommonCheck(dataPoints);
274+
275+
boolean[] matchedSets = new boolean[attributeSets.length];
276+
277+
// validate each datapoint attributes match exactly one of the provided attributes set
278+
for (NumberDataPoint dataPoint : dataPoints) {
279+
Map<String, String> map = toMap(dataPoint.getAttributesList());
280+
281+
int matchCount = 0;
282+
for (int i = 0; i < attributeSets.length; i++) {
283+
if (mapEquals(map, attributeSets[i])) {
284+
matchedSets[i] = true;
285+
matchCount++;
286+
}
287+
}
288+
289+
info.description(
290+
"data point attributes '%s' for metric '%s' must match exactly one of the attribute sets '%s'",
291+
map, actual.getName(), Arrays.asList(attributeSets));
292+
integers.assertEqual(info, matchCount, 1);
293+
}
294+
295+
// check that all attribute sets matched at least once
296+
for (int i = 0; i < matchedSets.length; i++) {
297+
info.description(
298+
"no data point matched attribute set '%s' for metric '%s'",
299+
attributeSets[i], actual.getName());
300+
objects.assertEqual(info, matchedSets[i], true);
301+
}
302+
});
303+
}
304+
305+
/**
306+
* Map equality utility
307+
*
308+
* @param m1 first map
309+
* @param m2 second map
310+
* @return true if the maps have exactly the same keys and values
311+
*/
312+
private static boolean mapEquals(Map<String, String> m1, Map<String, String> m2) {
313+
if (m1.size() != m2.size()) {
314+
return false;
315+
}
316+
return m1.entrySet().stream().allMatch(e -> e.getValue().equals(m2.get(e.getKey())));
317+
}
318+
319+
@SafeVarargs
320+
@SuppressWarnings("varargs") // required to avoid warning
321+
private final void containsExactly(
322+
Map<String, String> map, Map.Entry<String, String>... entries) {
323+
maps.assertContainsExactly(info, map, entries);
324+
}
325+
326+
private static Map<String, String> toMap(List<KeyValue> list) {
327+
return list.stream()
328+
.collect(Collectors.toMap(KeyValue::getKey, kv -> kv.getValue().getStringValue()));
329+
}
330+
}

0 commit comments

Comments
 (0)