Skip to content

Commit 0e5fd2c

Browse files
authored
Merge pull request #4 from SylvainJuge/assertions-refactoring-more
refactor with custom assertj assertions
2 parents 2c85c38 + 06a968f commit 0e5fd2c

File tree

8 files changed

+744
-421
lines changed

8 files changed

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

0 commit comments

Comments
 (0)