diff --git a/java/google/registry/monitoring/metrics/contrib/AbstractMetricSubject.java b/java/google/registry/monitoring/metrics/contrib/AbstractMetricSubject.java new file mode 100644 index 000000000..e6a893e40 --- /dev/null +++ b/java/google/registry/monitoring/metrics/contrib/AbstractMetricSubject.java @@ -0,0 +1,210 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.monitoring.metrics.contrib; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import google.registry.monitoring.metrics.Metric; +import google.registry.monitoring.metrics.MetricPoint; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Base truth subject for asserting things about {@link Metric} instances. + * + *

For use with the Google Truth framework. + */ +abstract class AbstractMetricSubject< + T, M extends Metric, S extends AbstractMetricSubject> + extends Subject { + + /** And chainer to allow fluent assertions. */ + public static class And> { + + private final S subject; + + And(S subject) { + this.subject = subject; + } + + public S and() { + return subject; + } + } + + @SuppressWarnings("unchecked") + And andChainer() { + return new And<>((S) this); + } + + /** List of label value tuples about which an assertion has been made so far. + * + *

Used to track what tuples have been seen, in order to support hasNoOtherValues() assertions. + */ + protected final Set> expectedNondefaultLabelTuples = new HashSet<>(); + + /** + * Function to convert a metric point to a nice string representation for use in error messages. + */ + protected final Function, String> metricPointConverter = + new Function, String>() { + @Override + public String apply(MetricPoint metricPoint) { + return String.format( + "%s => %s", + Joiner.on(':').join(metricPoint.labelValues()), + getMessageRepresentation(metricPoint.value())); + } + }; + + protected AbstractMetricSubject(FailureStrategy strategy, M actual) { + super(strategy, checkNotNull(actual)); + } + + /** + * Returns the string representation of the subject. + * + *

For metrics, it makes sense to use the metric name, as given in the schema. + */ + @Override + public String actualCustomStringRepresentation() { + return actual().getMetricSchema().name(); + } + + /** + * Asserts that the metric has a given value for the specified label values. + * + * @param value the value which the metric should have + * @param labels the labels for which the value is being asserted; the number and order of labels + * should match the definition of the metric + */ + public And hasValueForLabels(T value, String... labels) { + MetricPoint metricPoint = findMetricPointForLabels(ImmutableList.copyOf(labels)); + if (metricPoint == null) { + failWithBadResults( + "has a value for labels", + Joiner.on(':').join(labels), + "has labeled values", + Lists.transform( + Ordering.>natural().sortedCopy(actual().getTimestampedValues()), + metricPointConverter)); + } + if (!metricPoint.value().equals(value)) { + failWithBadResults( + String.format("has a value of %s for labels", value), + Joiner.on(':').join(labels), + "has a value of", + getMessageRepresentation(metricPoint.value())); + } + expectedNondefaultLabelTuples.add(ImmutableList.copyOf(labels)); + return andChainer(); + } + + /** + * Asserts that the metric has any (non-default) value for the specified label values. + * + * @param labels the labels for which the value is being asserted; the number and order of labels + * should match the definition of the metric + */ + public And hasAnyValueForLabels(String... labels) { + MetricPoint metricPoint = findMetricPointForLabels(ImmutableList.copyOf(labels)); + if (metricPoint == null) { + failWithBadResults( + "has a value for labels", + Joiner.on(':').join(labels), + "has labeled values", + Lists.transform( + Ordering.>natural().sortedCopy(actual().getTimestampedValues()), + metricPointConverter)); + } + if (hasDefaultValue(metricPoint)) { + failWithBadResults( + "has a non-default value for labels", + Joiner.on(':').join(labels), + "has a value of", + getMessageRepresentation(metricPoint.value())); + } + expectedNondefaultLabelTuples.add(ImmutableList.copyOf(labels)); + return andChainer(); + } + + /** + * Asserts that the metric does not have a (non-default) value for the specified label values. + */ + protected And doesNotHaveAnyValueForLabels(String... labels) { + MetricPoint metricPoint = findMetricPointForLabels(ImmutableList.copyOf(labels)); + if (metricPoint != null) { + failWithBadResults( + "has no value for labels", + Joiner.on(':').join(labels), + "has a value of", + getMessageRepresentation(metricPoint.value())); + } + return andChainer(); + } + + /** + * Asserts that the metric has no (non-default) values other than those about which an assertion + * has already been made. + */ + public And hasNoOtherValues() { + for (MetricPoint metricPoint : actual().getTimestampedValues()) { + if (!expectedNondefaultLabelTuples.contains(metricPoint.labelValues())) { + if (!hasDefaultValue(metricPoint)) { + failWithBadResults( + "has", + "no other nondefault values", + "has labeled values", + Lists.transform( + Ordering.>natural().sortedCopy(actual().getTimestampedValues()), + metricPointConverter)); + } + return andChainer(); + } + } + return andChainer(); + } + + private @Nullable MetricPoint findMetricPointForLabels(ImmutableList labels) { + if (actual().getMetricSchema().labels().size() != labels.size()) { + return null; + } + for (MetricPoint metricPoint : actual().getTimestampedValues()) { + if (metricPoint.labelValues().equals(labels)) { + return metricPoint; + } + } + return null; + } + + /** + * Returns true if the metric point has a non-default value. + * + *

This should be overridden by subclasses. E.g. for incrementable metrics, the method should + * return true if the value is not zero, and so on. + */ + protected abstract boolean hasDefaultValue(MetricPoint metricPoint); + + /** Returns a string representation of a metric point value, for use in error messages. */ + protected abstract String getMessageRepresentation(T value); +} diff --git a/java/google/registry/monitoring/metrics/contrib/BUILD b/java/google/registry/monitoring/metrics/contrib/BUILD new file mode 100644 index 000000000..e646eaf42 --- /dev/null +++ b/java/google/registry/monitoring/metrics/contrib/BUILD @@ -0,0 +1,17 @@ +package( + default_testonly = 1, + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) # Apache 2.0 + +java_library( + name = "contrib", + srcs = glob(["*.java"]), + deps = [ + "//java/google/registry/monitoring/metrics", + "@com_google_code_findbugs_jsr305", + "@com_google_guava", + "@com_google_truth", + ], +) diff --git a/java/google/registry/monitoring/metrics/contrib/EventMetricSubject.java b/java/google/registry/monitoring/metrics/contrib/EventMetricSubject.java new file mode 100644 index 000000000..937ac09e6 --- /dev/null +++ b/java/google/registry/monitoring/metrics/contrib/EventMetricSubject.java @@ -0,0 +1,109 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.monitoring.metrics.contrib; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.SubjectFactory; +import google.registry.monitoring.metrics.Distribution; +import google.registry.monitoring.metrics.EventMetric; +import google.registry.monitoring.metrics.MetricPoint; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Truth subject for the {@link EventMetric} class. + * + *

For use with the Google Truth framework. Usage: + * + *

  assertThat(myEventMetric)
+ *       .hasAnyValueForLabels("label1", "label2", "label3")
+ *       .and()
+ *       .hasNoOtherValues();
+ *   assertThat(myEventMetric)
+ *       .doesNotHaveAnyValueForLabels("label1", "label2");
+ * 
+ * + *

The assertions treat an empty distribution as no value at all. This is not how the data is + * actually stored; event metrics do in fact have an empty distribution after they are reset. But + * it's difficult to write assertions about expected metric data when any number of empty + * distributions can also be present, so they are screened out for convenience. + */ +public final class EventMetricSubject + extends AbstractMetricSubject { + + /** {@link SubjectFactory} for assertions about {@link EventMetric} objects. */ + private static final SubjectFactory + SUBJECT_FACTORY = + new SubjectFactory() { + // The Truth extensibility documentation indicates that the target should be nullable. + @Override + public EventMetricSubject getSubject( + FailureStrategy failureStrategy, @Nullable EventMetric target) { + return new EventMetricSubject(failureStrategy, target); + } + }; + + /** Static assertThat({@link EventMetric}) shortcut method. */ + public static EventMetricSubject assertThat(@Nullable EventMetric metric) { + return assertAbout(SUBJECT_FACTORY).that(metric); + } + + private EventMetricSubject(FailureStrategy strategy, EventMetric actual) { + super(strategy, actual); + } + + /** + * Returns an indication to {@link AbstractMetricSubject#hasNoOtherValues} on whether a {@link + * MetricPoint} has a non-empty distribution. + */ + @Override + protected boolean hasDefaultValue(MetricPoint metricPoint) { + return metricPoint.value().count() == 0; + } + + /** Returns an appropriate string representation of a metric value for use in error messages. */ + @Override + protected String getMessageRepresentation(Distribution distribution) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry, Long> entry : + distribution.intervalCounts().asMapOfRanges().entrySet()) { + if (entry.getValue() != 0L) { + if (first) { + first = false; + } else { + sb.append(','); + } + if (entry.getKey().hasLowerBound()) { + sb.append((entry.getKey().lowerBoundType() == BoundType.CLOSED) ? '[' : '('); + sb.append(entry.getKey().lowerEndpoint()); + } + sb.append(".."); + if (entry.getKey().hasUpperBound()) { + sb.append(entry.getKey().upperEndpoint()); + sb.append((entry.getKey().upperBoundType() == BoundType.CLOSED) ? ']' : ')'); + } + sb.append('='); + sb.append(entry.getValue()); + } + } + sb.append('}'); + return sb.toString(); + } +} diff --git a/java/google/registry/monitoring/metrics/contrib/IncrementableMetricSubject.java b/java/google/registry/monitoring/metrics/contrib/IncrementableMetricSubject.java new file mode 100644 index 000000000..aed68d27d --- /dev/null +++ b/java/google/registry/monitoring/metrics/contrib/IncrementableMetricSubject.java @@ -0,0 +1,91 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.monitoring.metrics.contrib; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.SubjectFactory; +import google.registry.monitoring.metrics.IncrementableMetric; +import google.registry.monitoring.metrics.MetricPoint; +import javax.annotation.Nullable; + +/** + * Truth subject for the {@link IncrementableMetric} class. + * + *

For use with the Google Truth framework. Usage: + * + *

  assertThat(myIncrementableMetric)
+ *       .hasValueForLabels(5, "label1", "label2", "label3")
+ *       .and()
+ *       .hasAnyValueForLabels("label1", "label2", "label4")
+ *       .and()
+ *       .hasNoOtherValues();
+ *   assertThat(myIncrementableMetric)
+ *       .doesNotHaveAnyValueForLabels("label1", "label2");
+ * 
+ * + *

The assertions treat a value of 0 as no value at all. This is not how the data is actually + * stored; zero is a valid value for incrementable metrics, and they do in fact have a value of zero + * after they are reset. But it's difficult to write assertions about expected metric data when any + * number of zero values can also be present, so they are screened out for convenience. + */ +public final class IncrementableMetricSubject + extends AbstractMetricSubject { + + /** {@link SubjectFactory} for assertions about {@link IncrementableMetric} objects. */ + private static final SubjectFactory + SUBJECT_FACTORY = + new SubjectFactory() { + // The Truth extensibility documentation indicates that the target should be nullable. + @Override + public IncrementableMetricSubject getSubject( + FailureStrategy failureStrategy, @Nullable IncrementableMetric target) { + return new IncrementableMetricSubject(failureStrategy, target); + } + }; + + /** Static assertThat({@link IncrementableMetric}) shortcut method. */ + public static IncrementableMetricSubject assertThat(@Nullable IncrementableMetric metric) { + return assertAbout(SUBJECT_FACTORY).that(metric); + } + + private IncrementableMetricSubject(FailureStrategy strategy, IncrementableMetric actual) { + super(strategy, actual); + } + + /** + * Asserts that the metric has a given value for the specified label values. This is a convenience + * method that takes a long instead of a Long, for ease of use. + */ + public And hasValueForLabels(long value, String... labels) { + return hasValueForLabels(Long.valueOf(value), labels); + } + + /** + * Returns an indication to {@link AbstractMetricSubject#hasNoOtherValues} on whether a {@link + * MetricPoint} has a non-zero value. + */ + @Override + protected boolean hasDefaultValue(MetricPoint metricPoint) { + return metricPoint.value() == 0L; + } + + /** Returns an appropriate string representation of a metric value for use in error messages. */ + @Override + protected String getMessageRepresentation(Long value) { + return String.valueOf(value); + } +} diff --git a/javatests/google/registry/monitoring/metrics/contrib/BUILD b/javatests/google/registry/monitoring/metrics/contrib/BUILD new file mode 100644 index 000000000..af9bbb1c0 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/contrib/BUILD @@ -0,0 +1,28 @@ +package( + default_testonly = 1, + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + +load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "contrib", + srcs = glob(["*.java"]), + deps = [ + "//java/google/registry/monitoring/metrics", + "//java/google/registry/monitoring/metrics/contrib", + "@com_google_guava", + "@com_google_truth", + "@junit", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["*Test.java"]), + deps = [ + ":contrib", + ], +) diff --git a/javatests/google/registry/monitoring/metrics/contrib/EventMetricSubjectTest.java b/javatests/google/registry/monitoring/metrics/contrib/EventMetricSubjectTest.java new file mode 100644 index 000000000..4460c5b61 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/contrib/EventMetricSubjectTest.java @@ -0,0 +1,120 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.monitoring.metrics.contrib; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.monitoring.metrics.contrib.EventMetricSubject.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.EventMetric; +import google.registry.monitoring.metrics.LabelDescriptor; +import google.registry.monitoring.metrics.MetricRegistryImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class EventMetricSubjectTest { + + private static final ImmutableSet LABEL_DESCRIPTORS = + ImmutableSet.of( + LabelDescriptor.create("species", "Sheep Species"), + LabelDescriptor.create("color", "Sheep Color")); + + private static final EventMetric metric = + MetricRegistryImpl.getDefault() + .newEventMetric( + "/test/event/sheep", + "Sheep Latency", + "sheeplatency", + LABEL_DESCRIPTORS, + EventMetric.DEFAULT_FITTER); + + @Before + public void before() { + metric.reset(); + metric.record(2.5, "Domestic", "Green"); + metric.record(10, "Bighorn", "Blue"); + } + + @Test + public void testWrongNumberOfLabels_fails() { + try { + assertThat(metric).hasAnyValueForLabels("Domestic"); + fail("Expected assertion error"); + } catch (AssertionError e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Not true that has a value for labels ." + + " It has labeled values <[Bighorn:Blue =>" + + " {[4.0..16.0)=1}, Domestic:Green => {[1.0..4.0)=1}]>"); + } + } + + @Test + public void testDoesNotHaveWrongNumberOfLabels_succeeds() { + assertThat(metric).doesNotHaveAnyValueForLabels("Domestic"); + } + + @Test + public void testHasAnyValueForLabels_success() { + assertThat(metric) + .hasAnyValueForLabels("Domestic", "Green") + .and() + .hasAnyValueForLabels("Bighorn", "Blue") + .and() + .hasNoOtherValues(); + } + + @Test + public void testDoesNotHaveValueForLabels_success() { + assertThat(metric).doesNotHaveAnyValueForLabels("Domestic", "Blue"); + } + + @Test + public void testDoesNotHaveValueForLabels_failure() { + try { + assertThat(metric).doesNotHaveAnyValueForLabels("Domestic", "Green"); + fail("Expected assertion error"); + } catch (AssertionError e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Not true that has no value for labels ." + + " It has a value of <{[1.0..4.0)=1}>"); + } + } + + @Test + public void testUnexpectedValue_failure() { + try { + assertThat(metric) + .hasAnyValueForLabels("Domestic", "Green") + .and() + .hasNoOtherValues(); + fail("Expected assertion error"); + } catch (AssertionError e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Not true that has ." + + " It has labeled values <[Bighorn:Blue =>" + + " {[4.0..16.0)=1}, Domestic:Green => {[1.0..4.0)=1}]>"); + } + } +} diff --git a/javatests/google/registry/monitoring/metrics/contrib/IncrementableMetricSubjectTest.java b/javatests/google/registry/monitoring/metrics/contrib/IncrementableMetricSubjectTest.java new file mode 100644 index 000000000..1098575a1 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/contrib/IncrementableMetricSubjectTest.java @@ -0,0 +1,138 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.monitoring.metrics.contrib; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.monitoring.metrics.contrib.IncrementableMetricSubject.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.IncrementableMetric; +import google.registry.monitoring.metrics.LabelDescriptor; +import google.registry.monitoring.metrics.MetricRegistryImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class IncrementableMetricSubjectTest { + + private static final ImmutableSet LABEL_DESCRIPTORS = + ImmutableSet.of( + LabelDescriptor.create("species", "Sheep Species"), + LabelDescriptor.create("color", "Sheep Color")); + + private static final IncrementableMetric metric = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/test/incrementable/sheep", + "Count of Sheep", + "sheepcount", + LABEL_DESCRIPTORS); + + @Before + public void before() { + metric.reset(); + metric.increment("Domestic", "Green"); + metric.incrementBy(2, "Bighorn", "Blue"); + } + + @Test + public void testWrongNumberOfLabels_fails() { + try { + assertThat(metric).hasValueForLabels(1, "Domestic"); + fail("Expected assertion error"); + } catch (AssertionError e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Not true that has a value for labels ." + + " It has labeled values <[Bighorn:Blue => 2, Domestic:Green => 1]>"); + } + } + + @Test + public void testDoesNotHaveWrongNumberOfLabels_succeeds() { + assertThat(metric).doesNotHaveAnyValueForLabels("Domestic"); + } + + @Test + public void testHasValueForLabels_success() { + assertThat(metric) + .hasValueForLabels(1, "Domestic", "Green") + .and() + .hasValueForLabels(2, "Bighorn", "Blue") + .and() + .hasNoOtherValues(); + } + + @Test + public void testHasAnyValueForLabels_success() { + assertThat(metric) + .hasAnyValueForLabels("Domestic", "Green") + .and() + .hasAnyValueForLabels("Bighorn", "Blue") + .and() + .hasNoOtherValues(); + } + + @Test + public void testDoesNotHaveValueForLabels_success() { + assertThat(metric).doesNotHaveAnyValueForLabels("Domestic", "Blue"); + } + + @Test + public void testDoesNotHaveValueForLabels_failure() { + try { + assertThat(metric).doesNotHaveAnyValueForLabels("Domestic", "Green"); + fail("Expected assertion error"); + } catch (AssertionError e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Not true that has no value for labels ." + + " It has a value of <1>"); + } + } + + @Test + public void testWrongValue_failure() { + try { + assertThat(metric).hasValueForLabels(2, "Domestic", "Green"); + fail("Expected assertion error"); + } catch (AssertionError e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Not true that has a value of 2" + + " for labels . It has a value of <1>"); + } + } + + @Test + public void testUnexpectedValue_failure() { + try { + assertThat(metric).hasValueForLabels(1, "Domestic", "Green").and().hasNoOtherValues(); + fail("Expected assertion error"); + } catch (AssertionError e) { + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Not true that has ." + + " It has labeled values <[Bighorn:Blue => 2, Domestic:Green => 1]>"); + } + } +}