// 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.base.Joiner; import com.google.common.collect.BoundType; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; import com.google.common.truth.FailureMetadata; import google.registry.monitoring.metrics.Distribution; import google.registry.monitoring.metrics.ImmutableDistribution; import google.registry.monitoring.metrics.Metric; import google.registry.monitoring.metrics.MetricPoint; import google.registry.monitoring.metrics.MutableDistribution; import java.util.Map; import javax.annotation.Nullable; /** * Truth subject for the {@link Metric} class. * *

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

  assertThat(myDistributionMetric)
 *       .hasAnyValueForLabels("label1", "label2", "label3")
 *       .and()
 *       .hasNoOtherValues();
 *   assertThat(myDistributionMetric)
 *       .doesNotHaveAnyValueForLabels("label1", "label2");
 *   assertThat(myDistributionMetric)
 *       .hasDataSetForLabels(ImmutableSet.of(data1, data2, data3), "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 DistributionMetricSubject extends AbstractMetricSubject { /** {@link Subject.Factory} for assertions about {@link Metric} objects. */ /** Static assertThat({@link Metric}) shortcut method. */ public static DistributionMetricSubject assertThat(@Nullable Metric metric) { return assertAbout(DistributionMetricSubject::new).that(metric); } private DistributionMetricSubject(FailureMetadata metadata, Metric actual) { super(metadata, 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(); } /** * Asserts that the distribution for the given label can be constructed from the given data set. * *

Note that this only tests that the distribution has the same binned histogram, along with * the same mean, and sum of squared deviation as it would if it had recorded the specified data * points. It could have in fact collected different data points that resulted in the same * distribution, but that information is lost to us and cannot be tested. */ public And hasDataSetForLabels( ImmutableSet dataSet, String... labels) { ImmutableList> metricPoints = actual().getTimestampedValues(); if (metricPoints.isEmpty()) { failWithBadResults( "has a distribution for labels", Joiner.on(':').join(labels), "has", "no values"); } MutableDistribution targetDistribution = new MutableDistribution(metricPoints.get(0).value().distributionFitter()); dataSet.forEach(data -> targetDistribution.add(data.doubleValue())); return hasValueForLabels(ImmutableDistribution.copyOf(targetDistribution), labels); } }