From 91281c7f74f252bf15bee37201acff32974a7c3a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 24 Apr 2026 09:22:13 -0700 Subject: [PATCH] Add containsPointsSatisfying to metric data asserts Parallels hasAttributesSatisfying's subset semantics: each listed assertion must be satisfied by at least one point, extras are allowed, and a single point may satisfy multiple assertions. The existing hasPointsSatisfying requires the caller to enumerate every point (via satisfiesExactlyInAnyOrder), which is awkward when point cardinality varies across runs (e.g. JVM runtime telemetry snapshots). Today callers drop down to raw AssertJ: sum.satisfies(s -> assertThat(s.getPoints()) .anyMatch(p -> p.getAttributes().equals(...)) .anyMatch(p -> p.getAttributes().equals(...))) which is noisy and loses the typed PointAssert. containsPointsSatisfying keeps the typed chain. Added to: DoubleGaugeAssert, DoubleSumAssert, LongGaugeAssert, LongSumAssert, HistogramAssert, ExponentialHistogramAssert, SummaryAssert. --- CHANGELOG.md | 8 +++ .../opentelemetry-sdk-testing.txt | 36 +++++++++- .../testing/assertj/DoubleGaugeAssert.java | 27 ++++++++ .../sdk/testing/assertj/DoubleSumAssert.java | 26 ++++++++ .../assertj/ExponentialHistogramAssert.java | 27 ++++++++ .../sdk/testing/assertj/HistogramAssert.java | 27 ++++++++ .../sdk/testing/assertj/LongGaugeAssert.java | 26 ++++++++ .../sdk/testing/assertj/LongSumAssert.java | 26 ++++++++ .../sdk/testing/assertj/SummaryAssert.java | 26 ++++++++ .../testing/assertj/MetricAssertionsTest.java | 66 +++++++++++++++++++ 10 files changed, 294 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 575ed21164c..c1100d51ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### SDK + +#### Testing + +* Add `containsPointsSatisfying` to metric data asserts for "each given assertion must be + satisfied by at least one point, extras allowed" checks on sum, gauge, histogram, exponential + histogram, and summary data + ## Version 1.61.0 (2026-04-10) ### API diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt index 2a9943f3f13..814b6984bbd 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-testing.txt @@ -1,2 +1,36 @@ Comparing source compatibility of opentelemetry-sdk-testing-1.62.0-SNAPSHOT.jar against opentelemetry-sdk-testing-1.61.0.jar -No changes. \ No newline at end of file +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.DoubleGaugeAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.testing.assertj.DoubleGaugeAssert containsPointsSatisfying(java.util.function.Consumer[]) + +++ NEW ANNOTATION: java.lang.SafeVarargs + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.DoubleGaugeAssert containsPointsSatisfying(java.lang.Iterable>) +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.DoubleSumAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.testing.assertj.DoubleSumAssert containsPointsSatisfying(java.util.function.Consumer[]) + +++ NEW ANNOTATION: java.lang.SafeVarargs + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.DoubleSumAssert containsPointsSatisfying(java.lang.Iterable>) +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.ExponentialHistogramAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.testing.assertj.ExponentialHistogramAssert containsPointsSatisfying(java.util.function.Consumer[]) + +++ NEW ANNOTATION: java.lang.SafeVarargs + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.ExponentialHistogramAssert containsPointsSatisfying(java.lang.Iterable>) +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.HistogramAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.testing.assertj.HistogramAssert containsPointsSatisfying(java.util.function.Consumer[]) + +++ NEW ANNOTATION: java.lang.SafeVarargs + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.HistogramAssert containsPointsSatisfying(java.lang.Iterable>) +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.LongGaugeAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.testing.assertj.LongGaugeAssert containsPointsSatisfying(java.util.function.Consumer[]) + +++ NEW ANNOTATION: java.lang.SafeVarargs + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.LongGaugeAssert containsPointsSatisfying(java.lang.Iterable>) +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.LongSumAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.testing.assertj.LongSumAssert containsPointsSatisfying(java.util.function.Consumer[]) + +++ NEW ANNOTATION: java.lang.SafeVarargs + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.LongSumAssert containsPointsSatisfying(java.lang.Iterable>) +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.testing.assertj.SummaryAssert (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.testing.assertj.SummaryAssert containsPointsSatisfying(java.util.function.Consumer[]) + +++ NEW ANNOTATION: java.lang.SafeVarargs + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.testing.assertj.SummaryAssert containsPointsSatisfying(java.lang.Iterable>) diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleGaugeAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleGaugeAssert.java index 04b80ede27d..3144fd4232b 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleGaugeAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleGaugeAssert.java @@ -45,4 +45,31 @@ public DoubleGaugeAssert hasPointsSatisfying( .satisfiesExactlyInAnyOrder(AssertUtil.toConsumers(assertions, DoublePointAssert::new)); return this; } + + /** + * Asserts that for each given assertion, at least one point in the gauge satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final DoubleGaugeAssert containsPointsSatisfying( + Consumer... assertions) { + return containsPointsSatisfying(Arrays.asList(assertions)); + } + + /** + * Asserts that for each given assertion, at least one point in the gauge satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + public DoubleGaugeAssert containsPointsSatisfying( + Iterable> assertions) { + isNotNull(); + for (Consumer assertion : assertions) { + assertThat(actual.getPoints()) + .anySatisfy(point -> assertion.accept(new DoublePointAssert(point))); + } + return this; + } } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleSumAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleSumAssert.java index 8b06f609816..2007831b062 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleSumAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/DoubleSumAssert.java @@ -88,4 +88,30 @@ public DoubleSumAssert hasPointsSatisfying( .satisfiesExactlyInAnyOrder(AssertUtil.toConsumers(assertions, DoublePointAssert::new)); return this; } + + /** + * Asserts that for each given assertion, at least one point in the sum satisfies it. Extra points + * that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final DoubleSumAssert containsPointsSatisfying(Consumer... assertions) { + return containsPointsSatisfying(Arrays.asList(assertions)); + } + + /** + * Asserts that for each given assertion, at least one point in the sum satisfies it. Extra points + * that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + public DoubleSumAssert containsPointsSatisfying( + Iterable> assertions) { + isNotNull(); + for (Consumer assertion : assertions) { + assertThat(actual.getPoints()) + .anySatisfy(point -> assertion.accept(new DoublePointAssert(point))); + } + return this; + } } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/ExponentialHistogramAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/ExponentialHistogramAssert.java index 451868345a9..4dd06fc22c9 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/ExponentialHistogramAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/ExponentialHistogramAssert.java @@ -74,4 +74,31 @@ public ExponentialHistogramAssert hasPointsSatisfying( AssertUtil.toConsumers(assertions, ExponentialHistogramPointAssert::new)); return this; } + + /** + * Asserts that for each given assertion, at least one point in the exponential histogram + * satisfies it. Extra points that match none of the assertions are allowed, and a single point + * may satisfy multiple assertions. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final ExponentialHistogramAssert containsPointsSatisfying( + Consumer... assertions) { + return containsPointsSatisfying(Arrays.asList(assertions)); + } + + /** + * Asserts that for each given assertion, at least one point in the exponential histogram + * satisfies it. Extra points that match none of the assertions are allowed, and a single point + * may satisfy multiple assertions. + */ + public ExponentialHistogramAssert containsPointsSatisfying( + Iterable> assertions) { + isNotNull(); + for (Consumer assertion : assertions) { + assertThat(actual.getPoints()) + .anySatisfy(point -> assertion.accept(new ExponentialHistogramPointAssert(point))); + } + return this; + } } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/HistogramAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/HistogramAssert.java index ba6ffb6dcab..3bd20083770 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/HistogramAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/HistogramAssert.java @@ -72,4 +72,31 @@ public HistogramAssert hasPointsSatisfying( .satisfiesExactlyInAnyOrder(AssertUtil.toConsumers(assertions, HistogramPointAssert::new)); return this; } + + /** + * Asserts that for each given assertion, at least one point in the histogram satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final HistogramAssert containsPointsSatisfying( + Consumer... assertions) { + return containsPointsSatisfying(Arrays.asList(assertions)); + } + + /** + * Asserts that for each given assertion, at least one point in the histogram satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + public HistogramAssert containsPointsSatisfying( + Iterable> assertions) { + isNotNull(); + for (Consumer assertion : assertions) { + assertThat(actual.getPoints()) + .anySatisfy(point -> assertion.accept(new HistogramPointAssert(point))); + } + return this; + } } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongGaugeAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongGaugeAssert.java index f0c410096af..d470775f6bc 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongGaugeAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongGaugeAssert.java @@ -44,4 +44,30 @@ public LongGaugeAssert hasPointsSatisfying( .satisfiesExactlyInAnyOrder(AssertUtil.toConsumers(assertions, LongPointAssert::new)); return this; } + + /** + * Asserts that for each given assertion, at least one point in the gauge satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final LongGaugeAssert containsPointsSatisfying(Consumer... assertions) { + return containsPointsSatisfying(Arrays.asList(assertions)); + } + + /** + * Asserts that for each given assertion, at least one point in the gauge satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + public LongGaugeAssert containsPointsSatisfying( + Iterable> assertions) { + isNotNull(); + for (Consumer assertion : assertions) { + assertThat(actual.getPoints()) + .anySatisfy(point -> assertion.accept(new LongPointAssert(point))); + } + return this; + } } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongSumAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongSumAssert.java index 128a95ebce8..767ecc5f633 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongSumAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/LongSumAssert.java @@ -87,4 +87,30 @@ public LongSumAssert hasPointsSatisfying( .satisfiesExactlyInAnyOrder(AssertUtil.toConsumers(assertions, LongPointAssert::new)); return this; } + + /** + * Asserts that for each given assertion, at least one point in the sum satisfies it. Extra points + * that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final LongSumAssert containsPointsSatisfying(Consumer... assertions) { + return containsPointsSatisfying(Arrays.asList(assertions)); + } + + /** + * Asserts that for each given assertion, at least one point in the sum satisfies it. Extra points + * that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + public LongSumAssert containsPointsSatisfying( + Iterable> assertions) { + isNotNull(); + for (Consumer assertion : assertions) { + assertThat(actual.getPoints()) + .anySatisfy(point -> assertion.accept(new LongPointAssert(point))); + } + return this; + } } diff --git a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/SummaryAssert.java b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/SummaryAssert.java index fc5cf20640c..cc902b403dc 100644 --- a/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/SummaryAssert.java +++ b/sdk/testing/src/main/java/io/opentelemetry/sdk/testing/assertj/SummaryAssert.java @@ -52,4 +52,30 @@ public SummaryAssert hasPointsSatisfying( .satisfiesExactlyInAnyOrder(AssertUtil.toConsumers(assertions, SummaryPointAssert::new)); return this; } + + /** + * Asserts that for each given assertion, at least one point in the summary satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + @SafeVarargs + @SuppressWarnings("varargs") + public final SummaryAssert containsPointsSatisfying(Consumer... assertions) { + return containsPointsSatisfying(Arrays.asList(assertions)); + } + + /** + * Asserts that for each given assertion, at least one point in the summary satisfies it. Extra + * points that match none of the assertions are allowed, and a single point may satisfy multiple + * assertions. + */ + public SummaryAssert containsPointsSatisfying( + Iterable> assertions) { + isNotNull(); + for (Consumer assertion : assertions) { + assertThat(actual.getPoints()) + .anySatisfy(point -> assertion.accept(new SummaryPointAssert(point))); + } + return this; + } } diff --git a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java index a6c5d4def3d..ecd49cbb9cf 100644 --- a/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java +++ b/sdk/testing/src/test/java/io/opentelemetry/sdk/testing/assertj/MetricAssertionsTest.java @@ -1288,4 +1288,70 @@ void summary_failure() { point -> point.hasValuesSatisfying(value -> value.hasValue(3.0))))) .isInstanceOf(AssertionError.class); } + + @Test + void containsPointsSatisfying() { + assertThat(DOUBLE_GAUGE_METRIC) + .hasDoubleGaugeSatisfying( + gauge -> gauge.containsPointsSatisfying(point -> point.hasValue(3.0))); + assertThat(LONG_GAUGE_METRIC) + .hasLongGaugeSatisfying( + gauge -> + gauge.containsPointsSatisfying( + point -> point.hasValue(Long.MAX_VALUE), point -> point.hasValue(1))); + assertThat(DOUBLE_SUM_METRIC) + .hasDoubleSumSatisfying(sum -> sum.containsPointsSatisfying(point -> point.hasValue(3.0))); + assertThat(LONG_SUM_METRIC) + .hasLongSumSatisfying( + sum -> sum.containsPointsSatisfying(point -> point.hasValue(Long.MAX_VALUE))); + assertThat(HISTOGRAM_METRIC) + .hasHistogramSatisfying( + histogram -> histogram.containsPointsSatisfying(point -> point.hasSum(15))); + assertThat(EXPONENTIAL_HISTOGRAM_METRIC) + .hasExponentialHistogramSatisfying( + histogram -> histogram.containsPointsSatisfying(point -> point.hasSum(10.0))); + assertThat(SUMMARY_METRIC) + .hasSummarySatisfying( + summary -> summary.containsPointsSatisfying(point -> point.hasSum(2.0))); + } + + @Test + void containsPointsSatisfyingFailure() { + // single assertion that matches no point + assertThatThrownBy( + () -> + assertThat(DOUBLE_GAUGE_METRIC) + .hasDoubleGaugeSatisfying( + gauge -> gauge.containsPointsSatisfying(point -> point.hasValue(42.0)))) + .isInstanceOf(AssertionError.class); + // one of multiple assertions unmatched + assertThatThrownBy( + () -> + assertThat(LONG_GAUGE_METRIC) + .hasLongGaugeSatisfying( + gauge -> + gauge.containsPointsSatisfying( + point -> point.hasValue(Long.MAX_VALUE), + point -> point.hasValue(42)))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(HISTOGRAM_METRIC) + .hasHistogramSatisfying( + histogram -> histogram.containsPointsSatisfying(point -> point.hasSum(42)))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(EXPONENTIAL_HISTOGRAM_METRIC) + .hasExponentialHistogramSatisfying( + histogram -> + histogram.containsPointsSatisfying(point -> point.hasSum(42.0)))) + .isInstanceOf(AssertionError.class); + assertThatThrownBy( + () -> + assertThat(SUMMARY_METRIC) + .hasSummarySatisfying( + summary -> summary.containsPointsSatisfying(point -> point.hasSum(42.0)))) + .isInstanceOf(AssertionError.class); + } }