From 1b3f77a46863ebff171c45493e0796fac89b1092 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Fri, 26 Aug 2016 08:55:40 -0700 Subject: [PATCH] Add more tests to external code repo ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=131406104 --- .../google/registry/dns/writer/clouddns/BUILD | 39 ++ .../writer/clouddns/CloudDnsWriterTest.java | 468 ++++++++++++++++++ .../registry/model/domain/LrpTokenTest.java | 91 ++++ .../google/registry/monitoring/metrics/BUILD | 36 ++ .../monitoring/metrics/CounterTest.java | 187 +++++++ .../GoogleJsonResponseExceptionHelper.java | 171 +++++++ .../metrics/LabelDescriptorTest.java | 50 ++ .../metrics/MetricExporterTest.java | 130 +++++ .../metrics/MetricRegistryImplTest.java | 157 ++++++ .../metrics/MetricReporterTest.java | 70 +++ .../monitoring/metrics/MetricSchemaTest.java | 62 +++ .../metrics/StackdriverWriterTest.java | 332 +++++++++++++ .../monitoring/metrics/StoredMetricTest.java | 100 ++++ .../monitoring/metrics/VirtualMetricTest.java | 64 +++ 14 files changed, 1957 insertions(+) create mode 100644 javatests/google/registry/dns/writer/clouddns/BUILD create mode 100644 javatests/google/registry/dns/writer/clouddns/CloudDnsWriterTest.java create mode 100644 javatests/google/registry/model/domain/LrpTokenTest.java create mode 100644 javatests/google/registry/monitoring/metrics/BUILD create mode 100644 javatests/google/registry/monitoring/metrics/CounterTest.java create mode 100644 javatests/google/registry/monitoring/metrics/GoogleJsonResponseExceptionHelper.java create mode 100644 javatests/google/registry/monitoring/metrics/LabelDescriptorTest.java create mode 100644 javatests/google/registry/monitoring/metrics/MetricExporterTest.java create mode 100644 javatests/google/registry/monitoring/metrics/MetricRegistryImplTest.java create mode 100644 javatests/google/registry/monitoring/metrics/MetricReporterTest.java create mode 100644 javatests/google/registry/monitoring/metrics/MetricSchemaTest.java create mode 100644 javatests/google/registry/monitoring/metrics/StackdriverWriterTest.java create mode 100644 javatests/google/registry/monitoring/metrics/StoredMetricTest.java create mode 100644 javatests/google/registry/monitoring/metrics/VirtualMetricTest.java diff --git a/javatests/google/registry/dns/writer/clouddns/BUILD b/javatests/google/registry/dns/writer/clouddns/BUILD new file mode 100644 index 000000000..caca6172b --- /dev/null +++ b/javatests/google/registry/dns/writer/clouddns/BUILD @@ -0,0 +1,39 @@ +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 = "clouddns", + srcs = glob(["*Test.java"]), + deps = [ + "//apiserving/discoverydata/cloud/dns:cloud_dns_v2beta1_versioned", + "//java/com/google/common/base", + "//java/com/google/common/collect", + "//java/com/google/common/io", + "//java/com/google/common/net", + "//third_party/java/joda_time", + "//third_party/java/jsr305_annotations", + "//third_party/java/jsr330_inject", + "//third_party/java/junit", + "//third_party/java/mockito", + "//third_party/java/objectify:objectify-v4_1", + "//third_party/java/servlet/servlet_api", + "//third_party/java/truth", + "//java/google/registry/dns/writer/clouddns", + "//java/google/registry/model", + "//java/google/registry/util", + "//javatests/google/registry/testing", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["*Test.java"]), + deps = [":clouddns"], +) diff --git a/javatests/google/registry/dns/writer/clouddns/CloudDnsWriterTest.java b/javatests/google/registry/dns/writer/clouddns/CloudDnsWriterTest.java new file mode 100644 index 000000000..419da91da --- /dev/null +++ b/javatests/google/registry/dns/writer/clouddns/CloudDnsWriterTest.java @@ -0,0 +1,468 @@ +// Copyright 2016 The Domain Registry 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.dns.writer.clouddns; + +import static com.google.common.io.BaseEncoding.base16; +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.newDomainResource; +import static google.registry.testing.DatastoreHelper.newHostResource; +import static google.registry.testing.DatastoreHelper.persistResource; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.services.dns.Dns; +import com.google.api.services.dns.model.Change; +import com.google.api.services.dns.model.ResourceRecordSet; +import com.google.api.services.dns.model.ResourceRecordSetsListResponse; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.common.net.InetAddresses; +import com.googlecode.objectify.Ref; +import google.registry.dns.writer.clouddns.CloudDnsWriter.ZoneStateException; +import google.registry.model.domain.DomainResource; +import google.registry.model.domain.secdns.DelegationSignerData; +import google.registry.model.eppcommon.StatusValue; +import google.registry.model.host.HostResource; +import google.registry.testing.AppEngineRule; +import google.registry.testing.ExceptionRule; +import google.registry.util.Retrier; +import google.registry.util.SystemClock; +import google.registry.util.SystemSleeper; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.concurrent.Callable; +import javax.annotation.Nullable; +import org.joda.time.Duration; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +/** Test case for {@link CloudDnsWriter}. */ +@RunWith(MockitoJUnitRunner.class) +public class CloudDnsWriterTest { + + private static final Inet4Address IPv4 = (Inet4Address) InetAddresses.forString("127.0.0.1"); + private static final Inet6Address IPv6 = (Inet6Address) InetAddresses.forString("::1"); + private static final DelegationSignerData DS_DATA = + DelegationSignerData.create(12345, 3, 1, base16().decode("1234567890ABCDEF")); + private static final Duration DEFAULT_TTL = Duration.standardSeconds(180); + + @Mock private Dns dnsConnection; + @Mock private Dns.ResourceRecordSets resourceRecordSets; + @Mock private Dns.ResourceRecordSets.List listResourceRecordSetsRequest; + @Mock private Dns.Changes changes; + @Mock private Dns.Changes.Create createChangeRequest; + @Mock private Callable mutateZoneCallable; + @Captor ArgumentCaptor recordNameCaptor; + @Captor ArgumentCaptor changeCaptor; + private CloudDnsWriter writer; + private ImmutableSet stubZone; + + @Rule public final ExceptionRule thrown = new ExceptionRule(); + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + @Before + public void setUp() throws Exception { + createTld("tld"); + writer = + new CloudDnsWriter( + dnsConnection, + "projectId", + "zoneName", + DEFAULT_TTL, + new SystemClock(), + new Retrier(new SystemSleeper(), 5)); + + // Create an empty zone. + stubZone = ImmutableSet.of(); + + when(dnsConnection.changes()).thenReturn(changes); + when(dnsConnection.resourceRecordSets()).thenReturn(resourceRecordSets); + when(resourceRecordSets.list(anyString(), anyString())) + .thenReturn(listResourceRecordSetsRequest); + when(listResourceRecordSetsRequest.setName(recordNameCaptor.capture())) + .thenReturn(listResourceRecordSetsRequest); + // Return records from our stub zone when a request to list the records is executed + when(listResourceRecordSetsRequest.execute()) + .thenAnswer( + new Answer() { + @Override + public ResourceRecordSetsListResponse answer(InvocationOnMock invocationOnMock) + throws Throwable { + return new ResourceRecordSetsListResponse() + .setRrsets( + FluentIterable.from(stubZone) + .filter( + new Predicate() { + @Override + public boolean apply( + @Nullable ResourceRecordSet resourceRecordSet) { + if (resourceRecordSet == null) { + return false; + } + return resourceRecordSet + .getName() + .equals(recordNameCaptor.getValue()); + } + }) + .toList()); + } + }); + + when(changes.create(anyString(), anyString(), changeCaptor.capture())) + .thenReturn(createChangeRequest); + // Change our stub zone when a request to change the records is executed + when(createChangeRequest.execute()) + .thenAnswer( + new Answer() { + @Override + public Change answer(InvocationOnMock invocationOnMock) throws IOException { + Change requestedChange = changeCaptor.getValue(); + ImmutableSet toDelete = + ImmutableSet.copyOf(requestedChange.getDeletions()); + ImmutableSet toAdd = + ImmutableSet.copyOf(requestedChange.getAdditions()); + // Fail if the records to delete has records that aren't in the stub zone. + // This matches documented Google Cloud DNS behavior. + if (!Sets.difference(toDelete, stubZone).isEmpty()) { + throw new IOException(); + } + stubZone = + Sets.union(Sets.difference(stubZone, toDelete).immutableCopy(), toAdd) + .immutableCopy(); + return requestedChange; + } + }); + } + + private void verifyZone(ImmutableSet expectedRecords) throws Exception { + // Trigger zone changes + writer.close(); + + assertThat(stubZone).containsExactlyElementsIn(expectedRecords); + } + + /** Returns a a zone cut with records for a domain */ + private static ImmutableSet fakeDomainRecords( + String domainName, + int v4InBailiwickNameservers, + int v6InBailiwickNameservers, + int externalNameservers, + int dsRecords) { + ImmutableSet.Builder recordSetBuilder = new ImmutableSet.Builder<>(); + + // Add IPv4 in-bailiwick nameservers + if (v4InBailiwickNameservers > 0) { + ImmutableList.Builder nameserverHostnames = new ImmutableList.Builder<>(); + for (int i = 0; i < v4InBailiwickNameservers; i++) { + nameserverHostnames.add(i + ".ip4." + domainName + "."); + } + + recordSetBuilder.add( + new ResourceRecordSet() + .setKind("dns#resourceRecordSet") + .setType("NS") + .setName(domainName + ".") + .setTtl((int) DEFAULT_TTL.getStandardSeconds()) + .setRrdatas(nameserverHostnames.build())); + + // Add glue for IPv4 in-bailiwick nameservers + for (int i = 0; i < v4InBailiwickNameservers; i++) { + recordSetBuilder.add( + new ResourceRecordSet() + .setKind("dns#resourceRecordSet") + .setType("A") + .setName(i + ".ip4." + domainName + ".") + .setTtl((int) DEFAULT_TTL.getStandardSeconds()) + .setRrdatas(ImmutableList.of(IPv4.toString()))); + } + } + + // Add IPv6 in-bailiwick nameservers + if (v6InBailiwickNameservers > 0) { + ImmutableList.Builder nameserverHostnames = new ImmutableList.Builder<>(); + for (int i = 0; i < v6InBailiwickNameservers; i++) { + nameserverHostnames.add(i + ".ip6." + domainName + "."); + } + + recordSetBuilder.add( + new ResourceRecordSet() + .setKind("dns#resourceRecordSet") + .setType("NS") + .setName(domainName + ".") + .setTtl((int) DEFAULT_TTL.getStandardSeconds()) + .setRrdatas(nameserverHostnames.build())); + + // Add glue for IPv6 in-bailiwick nameservers + for (int i = 0; i < v6InBailiwickNameservers; i++) { + recordSetBuilder.add( + new ResourceRecordSet() + .setKind("dns#resourceRecordSet") + .setType("AAAA") + .setName(i + ".ip6." + domainName + ".") + .setTtl((int) DEFAULT_TTL.getStandardSeconds()) + .setRrdatas(ImmutableList.of(IPv6.toString()))); + } + } + + // Add external nameservers + if (externalNameservers > 0) { + ImmutableList.Builder nameserverHostnames = new ImmutableList.Builder<>(); + for (int i = 0; i < externalNameservers; i++) { + nameserverHostnames.add(i + ".external."); + } + + recordSetBuilder.add( + new ResourceRecordSet() + .setKind("dns#resourceRecordSet") + .setType("NS") + .setName(domainName + ".") + .setTtl((int) DEFAULT_TTL.getStandardSeconds()) + .setRrdatas(nameserverHostnames.build())); + } + + // Add DS records + if (dsRecords > 0) { + ImmutableList.Builder dsRecordData = new ImmutableList.Builder<>(); + + for (int i = 0; i < dsRecords; i++) { + dsRecordData.add( + DelegationSignerData.create( + i, DS_DATA.getAlgorithm(), DS_DATA.getDigestType(), DS_DATA.getDigest()) + .toRrData()); + } + recordSetBuilder.add( + new ResourceRecordSet() + .setKind("dns#resourceRecordSet") + .setType("DS") + .setName(domainName + ".") + .setTtl((int) DEFAULT_TTL.getStandardSeconds()) + .setRrdatas(dsRecordData.build())); + } + + return recordSetBuilder.build(); + } + + /** Returns a domain to be persisted in the datastore. */ + private static DomainResource fakeDomain( + String domainName, ImmutableSet nameservers, int numDsRecords) { + ImmutableSet.Builder dsDataBuilder = new ImmutableSet.Builder<>(); + + for (int i = 0; i < numDsRecords; i++) { + dsDataBuilder.add( + DelegationSignerData.create( + i, DS_DATA.getAlgorithm(), DS_DATA.getDigestType(), DS_DATA.getDigest())); + } + + ImmutableSet.Builder> hostResourceRefBuilder = new ImmutableSet.Builder<>(); + for (HostResource nameserver : nameservers) { + hostResourceRefBuilder.add(Ref.create(nameserver)); + } + + return newDomainResource(domainName) + .asBuilder() + .setNameservers(hostResourceRefBuilder.build()) + .setDsData(dsDataBuilder.build()) + .build(); + } + + /** Returns a nameserver used for its NS record. */ + private static HostResource fakeHost(String nameserver, InetAddress... addresses) { + return newHostResource(nameserver) + .asBuilder() + .setInetAddresses(ImmutableSet.copyOf(addresses)) + .build(); + } + + @Test + public void testLoadDomain_nonExistentDomain() throws Exception { + writer.publishDomain("example.tld"); + + verifyZone(ImmutableSet.of()); + } + + @Test + public void testLoadDomain_noDsDataOrNameservers() throws Exception { + persistResource(fakeDomain("example.tld", ImmutableSet.of(), 0)); + writer.publishDomain("example.tld"); + + verifyZone(fakeDomainRecords("example.tld", 0, 0, 0, 0)); + } + + @Test + public void testLoadDomain_deleteOldData() throws Exception { + stubZone = fakeDomainRecords("example.tld", 2, 2, 2, 2); + persistResource(fakeDomain("example.tld", ImmutableSet.of(), 0)); + writer.publishDomain("example.tld"); + + verifyZone(fakeDomainRecords("example.tld", 0, 0, 0, 0)); + } + + @Test + public void testLoadDomain_withExternalNs() throws Exception { + persistResource( + fakeDomain("example.tld", ImmutableSet.of(persistResource(fakeHost("0.external"))), 0)); + writer.publishDomain("example.tld"); + + verifyZone(fakeDomainRecords("example.tld", 0, 0, 1, 0)); + } + + @Test + public void testLoadDomain_withDsData() throws Exception { + persistResource( + fakeDomain("example.tld", ImmutableSet.of(persistResource(fakeHost("0.external"))), 1)); + writer.publishDomain("example.tld"); + + verifyZone(fakeDomainRecords("example.tld", 0, 0, 1, 1)); + } + + @Test + public void testLoadDomain_withInBailiwickNs_IPv4() throws Exception { + persistResource( + fakeDomain( + "example.tld", + ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))), + 0)) + .asBuilder() + .addSubordinateHost("0.ip4.example.tld") + .build(); + writer.publishDomain("example.tld"); + + verifyZone(fakeDomainRecords("example.tld", 1, 0, 0, 0)); + } + + @Test + public void testLoadDomain_withInBailiwickNs_IPv6() throws Exception { + persistResource( + fakeDomain( + "example.tld", + ImmutableSet.of(persistResource(fakeHost("0.ip6.example.tld", IPv6))), + 0)) + .asBuilder() + .addSubordinateHost("0.ip6.example.tld") + .build(); + writer.publishDomain("example.tld"); + + verifyZone(fakeDomainRecords("example.tld", 0, 1, 0, 0)); + } + + @Test + public void testLoadHost_externalHost() throws Exception { + writer.publishHost("ns1.example.com"); + + // external hosts should not be published in our zone + verifyZone(ImmutableSet.of()); + } + + @Test + public void testLoadHost_removeStaleNsRecords() throws Exception { + // Initialize the zone with both NS records + stubZone = fakeDomainRecords("example.tld", 2, 0, 0, 0); + + // Model the domain with only one NS record -- this is equivalent to creating it + // with two NS records and then deleting one + persistResource( + fakeDomain( + "example.tld", + ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))), + 0)) + .asBuilder() + .addSubordinateHost("0.ip4.example.tld") + .build(); + + // Ask the writer to delete the deleted NS record and glue + writer.publishHost("1.ip4.example.tld"); + + verifyZone(fakeDomainRecords("example.tld", 1, 0, 0, 0)); + } + + @Test + @SuppressWarnings("unchecked") + public void retryMutateZoneOnError() throws Exception { + try (CloudDnsWriter spyWriter = spy(writer)) { + when(mutateZoneCallable.call()).thenThrow(ZoneStateException.class).thenReturn(null); + when(spyWriter.getMutateZoneCallback( + Matchers.>>any())) + .thenReturn(mutateZoneCallable); + } + + verify(mutateZoneCallable, times(2)).call(); + } + + @Test + public void testLoadDomain_withClientHold() throws Exception { + persistResource( + fakeDomain( + "example.tld", + ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))), + 0) + .asBuilder() + .addStatusValue(StatusValue.CLIENT_HOLD) + .build()); + writer.publishDomain("example.tld"); + + verifyZone(ImmutableSet.of()); + } + + @Test + public void testLoadDomain_withServerHold() throws Exception { + persistResource( + fakeDomain( + "example.tld", + ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))), + 0) + .asBuilder() + .addStatusValue(StatusValue.SERVER_HOLD) + .build()); + + writer.publishDomain("example.tld"); + + verifyZone(ImmutableSet.of()); + } + + @Test + public void testLoadDomain_withPendingDelete() throws Exception { + persistResource( + fakeDomain( + "example.tld", + ImmutableSet.of(persistResource(fakeHost("0.ip4.example.tld", IPv4))), + 0) + .asBuilder() + .addStatusValue(StatusValue.PENDING_DELETE) + .build()); + writer.publishDomain("example.tld"); + + verifyZone(ImmutableSet.of()); + } +} diff --git a/javatests/google/registry/model/domain/LrpTokenTest.java b/javatests/google/registry/model/domain/LrpTokenTest.java new file mode 100644 index 000000000..bf5f261de --- /dev/null +++ b/javatests/google/registry/model/domain/LrpTokenTest.java @@ -0,0 +1,91 @@ +// Copyright 2016 The Domain Registry 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.model.domain; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.testing.DatastoreHelper.persistActiveDomainApplication; +import static google.registry.testing.DatastoreHelper.persistResource; + +import com.google.common.collect.ImmutableSet; +import com.googlecode.objectify.Key; +import google.registry.model.EntityTestCase; +import google.registry.model.reporting.HistoryEntry; +import google.registry.testing.ExceptionRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +/** Unit tests for {@link LrpToken}. */ +public class LrpTokenTest extends EntityTestCase { + + LrpToken unredeemedToken; + LrpToken redeemedToken; + + @Rule + public final ExceptionRule thrown = new ExceptionRule(); + + @Before + public void setUp() throws Exception { + createTld("tld"); + DomainApplication lrpApplication = persistActiveDomainApplication("domain.tld"); + HistoryEntry applicationCreateHistoryEntry = persistResource(new HistoryEntry.Builder() + .setParent(lrpApplication) + .setType(HistoryEntry.Type.DOMAIN_APPLICATION_CREATE) + .build()); + unredeemedToken = persistResource( + new LrpToken.Builder() + .setAssignee("1:1020304") + .setToken("a0b1c2d3e4f5g6") + .setValidTlds(ImmutableSet.of("tld")) + .build()); + redeemedToken = persistResource( + new LrpToken.Builder() + .setAssignee("2:org.testdomain") + .setToken("h0i1j2k3l4m") + .setRedemptionHistoryEntry( + Key.create(applicationCreateHistoryEntry)) + .setValidTlds(ImmutableSet.of("tld")) + .build()); + } + + @Test + public void testPersistence() throws Exception { + assertThat(ofy().load().entity(redeemedToken).now()).isEqualTo(redeemedToken); + } + + @Test + public void testSuccess_loadByToken() throws Exception { + assertThat(ofy().load().key(Key.create(LrpToken.class, "a0b1c2d3e4f5g6")).now()) + .isEqualTo(unredeemedToken); + } + + @Test + public void testSuccess_loadByAssignee() throws Exception { + assertThat(ofy().load().type(LrpToken.class).filter("assignee", "1:1020304").first().now()) + .isEqualTo(unredeemedToken); + } + @Test + public void testSuccess_isRedeemed() throws Exception { + assertThat(redeemedToken.isRedeemed()).isTrue(); + assertThat(unredeemedToken.isRedeemed()).isFalse(); + } + + @Test + public void testIndexing() throws Exception { + verifyIndexing(redeemedToken, "assignee", "token"); + } +} diff --git a/javatests/google/registry/monitoring/metrics/BUILD b/javatests/google/registry/monitoring/metrics/BUILD new file mode 100644 index 000000000..690462d6d --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/BUILD @@ -0,0 +1,36 @@ +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 = "metrics", + srcs = glob(["*.java"]), + deps = [ + "//google/monitoring:monitoring_java_lib", + "//java/com/google/api/client/googleapis/json", + "//java/com/google/api/client/http", + "//java/com/google/api/client/json/jackson2", + "//java/com/google/common/base", + "//java/com/google/common/collect", + "//java/com/google/common/util/concurrent", + "//third_party/java/joda_time", + "//third_party/java/junit", + "//third_party/java/mockito", + "//third_party/java/truth", + "//java/google/registry/monitoring/metrics", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["*Test.java"]), + deps = [ + ":metrics", + ], +) diff --git a/javatests/google/registry/monitoring/metrics/CounterTest.java b/javatests/google/registry/monitoring/metrics/CounterTest.java new file mode 100644 index 000000000..9ca2f6de4 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/CounterTest.java @@ -0,0 +1,187 @@ +// Copyright 2016 The Domain Registry 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.joda.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link Counter}. */ +@RunWith(MockitoJUnitRunner.class) +public class CounterTest { + + @Test + public void testGetCardinality_reflectsCurrentCardinality() { + Counter counter = + new Counter( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar"))); + assertThat(counter.getCardinality()).isEqualTo(0); + + counter.increment("foo"); + assertThat(counter.getCardinality()).isEqualTo(1); + counter.increment("bar"); + assertThat(counter.getCardinality()).isEqualTo(2); + counter.increment("foo"); + assertThat(counter.getCardinality()).isEqualTo(2); + } + + @Test + public void testIncrementBy_wrongLabelValueCount_throwsException() { + Counter counter = + new Counter( + "/metric", + "description", + "vdn", + ImmutableSet.of( + LabelDescriptor.create("label1", "bar"), LabelDescriptor.create("label2", "bar"))); + + try { + counter.increment("blah"); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testIncrement_incrementsValues() { + Counter counter = + new Counter( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar"))); + + assertThat(counter.getTimestampedValues()).isEmpty(); + + // use package-private incrementBy once to set the start timestamp predictably. + counter.incrementBy(1, new Instant(1337), ImmutableList.of("test_value1")); + assertThat(counter.getTimestampedValues(new Instant(1337))) + .containsExactly( + MetricPoint.create(counter, ImmutableList.of("test_value1"), new Instant(1337), 1L)); + + counter.increment("test_value1"); + assertThat(counter.getTimestampedValues(new Instant(1337))) + .containsExactly( + MetricPoint.create(counter, ImmutableList.of("test_value1"), new Instant(1337), 2L)); + } + + @Test + public void testIncrementBy_incrementsValues() { + Counter counter = + new Counter( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar"))); + + assertThat(counter.getTimestampedValues()).isEmpty(); + + counter.incrementBy(1, new Instant(1337), ImmutableList.of("test_value1")); + assertThat(counter.getTimestampedValues(new Instant(1337))) + .containsExactly( + MetricPoint.create(counter, ImmutableList.of("test_value1"), new Instant(1337), 1L)); + + counter.set(-10L, new Instant(1337), ImmutableList.of("test_value2")); + counter.incrementBy(5, new Instant(1337), ImmutableList.of("test_value2")); + assertThat(counter.getTimestampedValues(new Instant(1337))) + .containsExactly( + MetricPoint.create(counter, ImmutableList.of("test_value1"), new Instant(1337), 1L), + MetricPoint.create(counter, ImmutableList.of("test_value2"), new Instant(1337), -5L)); + } + + @Test + public void testIncrementBy_negativeOffset_throwsException() { + Counter counter = + new Counter( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar"))); + + try { + counter.incrementBy(-1L, "foo"); + fail("Test should not allow non-negative offsets"); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("The offset provided must be non-negative"); + } + } + + @Test + public void testResetAll_resetsAllValuesAndStartTimestamps() { + Counter counter = + new Counter( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar"))); + + counter.incrementBy(3, new Instant(1337), ImmutableList.of("foo")); + counter.incrementBy(5, new Instant(1338), ImmutableList.of("moo")); + + assertThat(counter.getTimestampedValues(new Instant(1400))) + .containsExactly( + MetricPoint.create( + counter, ImmutableList.of("foo"), new Instant(1337), new Instant(1400), 3L), + MetricPoint.create( + counter, ImmutableList.of("moo"), new Instant(1338), new Instant(1400), 5L)); + + counter.reset(new Instant(1339)); + + assertThat(counter.getTimestampedValues(new Instant(1400))) + .containsExactly( + MetricPoint.create( + counter, ImmutableList.of("foo"), new Instant(1339), new Instant(1400), 0L), + MetricPoint.create( + counter, ImmutableList.of("moo"), new Instant(1339), new Instant(1400), 0L)); + } + + @Test + public void testReset_resetsValuesAndStartTimestamps() { + Counter counter = + new Counter( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar"))); + + counter.incrementBy(3, new Instant(1337), ImmutableList.of("foo")); + counter.incrementBy(5, new Instant(1338), ImmutableList.of("moo")); + + assertThat(counter.getTimestampedValues(new Instant(1400))) + .containsExactly( + MetricPoint.create( + counter, ImmutableList.of("foo"), new Instant(1337), new Instant(1400), 3L), + MetricPoint.create( + counter, ImmutableList.of("moo"), new Instant(1338), new Instant(1400), 5L)); + + counter.reset(new Instant(1339), ImmutableList.of("foo")); + + assertThat(counter.getTimestampedValues(new Instant(1400))) + .containsExactly( + MetricPoint.create( + counter, ImmutableList.of("foo"), new Instant(1339), new Instant(1400), 0L), + MetricPoint.create( + counter, ImmutableList.of("moo"), new Instant(1338), new Instant(1400), 5L)); + } +} diff --git a/javatests/google/registry/monitoring/metrics/GoogleJsonResponseExceptionHelper.java b/javatests/google/registry/monitoring/metrics/GoogleJsonResponseExceptionHelper.java new file mode 100644 index 000000000..7ba5f5de2 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/GoogleJsonResponseExceptionHelper.java @@ -0,0 +1,171 @@ +// Copyright 2016 The Domain Registry 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; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.json.jackson2.JacksonFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** A helper to create instances of {@link GoogleJsonResponseException}. */ +public class GoogleJsonResponseExceptionHelper { + /** + * @param statusCode the status code that should be in the returned {@link + * GoogleJsonResponseException} + * @return a {@link GoogleJsonResponseException} with the status code {@code statusCode} + * @throws IOException shouldn't occur + */ + public static GoogleJsonResponseException create(int statusCode) throws IOException { + HttpResponse response = createHttpResponse(statusCode, null); + return GoogleJsonResponseException.from(new JacksonFactory(), response); + } + + public static HttpResponse createHttpResponse(int statusCode, InputStream content) + throws IOException { + FakeHttpTransport transport = new FakeHttpTransport(statusCode, content); + HttpRequestFactory factory = transport.createRequestFactory(); + HttpRequest request = + factory.buildRequest( + "foo", new GenericUrl("http://example.com/bar"), new EmptyHttpContent()); + request.setThrowExceptionOnExecuteError(false); + return request.execute(); + } + + private static class FakeHttpTransport extends HttpTransport { + private final int statusCode; + private final InputStream content; + + FakeHttpTransport(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new FakeLowLevelHttpRequest(statusCode, content); + } + } + + private static class FakeLowLevelHttpRequest extends LowLevelHttpRequest { + private final int statusCode; + private final InputStream content; + + FakeLowLevelHttpRequest(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + public void addHeader(String name, String value) throws IOException { + // Nothing! + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + return new FakeLowLevelHttpResponse(statusCode, content); + } + } + + private static class FakeLowLevelHttpResponse extends LowLevelHttpResponse { + private final int statusCode; + private final InputStream content; + + FakeLowLevelHttpResponse(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + public InputStream getContent() throws IOException { + return content; + } + + @Override + public String getContentEncoding() throws IOException { + return null; + } + + @Override + public long getContentLength() throws IOException { + return 0; + } + + @Override + public String getContentType() throws IOException { + return "text/json"; + } + + @Override + public String getStatusLine() throws IOException { + return null; + } + + @Override + public int getStatusCode() throws IOException { + return statusCode; + } + + @Override + public String getReasonPhrase() throws IOException { + return null; + } + + @Override + public int getHeaderCount() throws IOException { + return 0; + } + + @Override + public String getHeaderName(int index) throws IOException { + return null; + } + + @Override + public String getHeaderValue(int index) throws IOException { + return null; + } + } + + private static class EmptyHttpContent implements HttpContent { + @Override + public long getLength() throws IOException { + return 0; + } + + @Override + public String getType() { + return "text/json"; + } + + @Override + public boolean retrySupported() { + return false; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + // Nothing! + } + } +} diff --git a/javatests/google/registry/monitoring/metrics/LabelDescriptorTest.java b/javatests/google/registry/monitoring/metrics/LabelDescriptorTest.java new file mode 100644 index 000000000..c2260a39d --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/LabelDescriptorTest.java @@ -0,0 +1,50 @@ +// Copyright 2016 The Domain Registry 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; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link LabelDescriptor}. */ +@RunWith(JUnit4.class) +public class LabelDescriptorTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void testCreate_invalidLabel_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Label name must match the regex"); + LabelDescriptor.create("@", "description"); + } + + @Test + public void testCreate_blankNameField_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Name must not be empty"); + LabelDescriptor.create("", "description"); + } + + @Test + public void testCreate_blankDescriptionField_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Description must not be empty"); + LabelDescriptor.create("name", ""); + } +} diff --git a/javatests/google/registry/monitoring/metrics/MetricExporterTest.java b/javatests/google/registry/monitoring/metrics/MetricExporterTest.java new file mode 100644 index 000000000..4e6f3b3ec --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/MetricExporterTest.java @@ -0,0 +1,130 @@ +// Copyright 2016 The Domain Registry 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Service.State; +import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link MetricExporter}. */ +@RunWith(MockitoJUnitRunner.class) +public class MetricExporterTest { + + @Mock private MetricWriter writer; + @Mock private MetricPoint point; + private MetricExporter exporter; + private BlockingQueue>>> writeQueue; + private final Optional>> poisonPill = Optional.absent(); + private final Optional>> emptyBatch = + Optional.of(ImmutableList.>of()); + + @Before + public void setUp() throws Exception { + writeQueue = new ArrayBlockingQueue<>(1); + exporter = new MetricExporter(writeQueue, writer, Executors.defaultThreadFactory()); + } + + @Test + public void testRun_takesFromQueue_whileRunning() throws Exception { + exporter.startAsync().awaitRunning(); + + insertAndAssert(emptyBatch); + // Insert more batches to verify that the exporter hasn't gotten stuck + insertAndAssert(emptyBatch); + insertAndAssert(emptyBatch); + + assertThat(exporter.state()).isEqualTo(State.RUNNING); + } + + @Test + public void testRun_terminates_afterPoisonPill() throws Exception { + exporter.startAsync().awaitRunning(); + + insertAndAssert(poisonPill); + try { + exporter.awaitTerminated(500, TimeUnit.MILLISECONDS); + } catch (TimeoutException timeout) { + fail("MetricExporter did not reach the TERMINATED state after receiving a poison pill"); + } + + assertThat(exporter.state()).isEqualTo(State.TERMINATED); + } + + @Test + public void testRun_staysRunning_afterIOException() throws Exception { + Optional>> threeBatch = + Optional.of(ImmutableList.of(point, point, point)); + doThrow(new IOException()).when(writer).write(Matchers.>any()); + exporter.startAsync(); + + insertAndAssert(threeBatch); + // Insert another batch in order to block until the exporter has processed the last one + insertAndAssert(threeBatch); + // Insert another to make sure the exporter hasn't gotten stuck + insertAndAssert(threeBatch); + + assertThat(exporter.state()).isNotEqualTo(State.FAILED); + } + + @Test + public void testRun_writesMetrics() throws Exception { + Optional>> threeBatch = + Optional.of(ImmutableList.of(point, point, point)); + exporter.startAsync(); + + insertAndAssert(threeBatch); + // Insert another batch in order to block until the exporter has processed the last one + insertAndAssert(threeBatch); + + // Force the exporter to finish so that the verify counts below are deterministic + insertAndAssert(poisonPill); + try { + exporter.awaitTerminated(500, TimeUnit.MILLISECONDS); + } catch (TimeoutException timeout) { + fail("MetricExporter did not reach the TERMINATED state after receiving a poison pill"); + } + + assertThat(exporter.state()).isNotEqualTo(State.FAILED); + verify(writer, times(6)).write(point); + verify(writer, times(2)).flush(); + } + + /** + * Helper method to insert into the {@link BlockingQueue} and assert that the item has been + * enqueued. + */ + private void insertAndAssert(Optional>> batch) throws Exception { + boolean isTaken = writeQueue.offer(batch, 500, TimeUnit.MILLISECONDS); + assertThat(isTaken).isTrue(); + } +} diff --git a/javatests/google/registry/monitoring/metrics/MetricRegistryImplTest.java b/javatests/google/registry/monitoring/metrics/MetricRegistryImplTest.java new file mode 100644 index 000000000..80fdfa73c --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/MetricRegistryImplTest.java @@ -0,0 +1,157 @@ +// Copyright 2016 The Domain Registry 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.MetricSchema.Kind; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link MetricRegistryImpl}. + * + *

The MetricRegistryImpl is a singleton, so we have to be careful to empty it after every test + * to maintain a blank slate. + */ +@RunWith(JUnit4.class) +public class MetricRegistryImplTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final LabelDescriptor label = + LabelDescriptor.create("test_labelname", "test_labeldescription"); + + @After + public void clearMetrics() { + ImmutableList> metrics = MetricRegistryImpl.getDefault().getRegisteredMetrics(); + + for (Metric metric : metrics) { + MetricRegistryImpl.getDefault().unregisterMetric(metric.getMetricSchema().name()); + } + } + + @Test + public void testRegisterAndUnregister_tracksRegistrations() { + assertThat(MetricRegistryImpl.getDefault().getRegisteredMetrics()).isEmpty(); + + AbstractMetric metric = mock(AbstractMetric.class); + MetricRegistryImpl.getDefault().registerMetric("/test/metric", metric); + + assertThat(MetricRegistryImpl.getDefault().getRegisteredMetrics()).containsExactly(metric); + + MetricRegistryImpl.getDefault().unregisterMetric("/test/metric"); + + assertThat(MetricRegistryImpl.getDefault().getRegisteredMetrics()).isEmpty(); + } + + @Test + public void testNewGauge_createsGauge() { + Metric testGauge = + MetricRegistryImpl.getDefault() + .newGauge( + "/test_metric", + "test_description", + "test_valuedisplayname", + ImmutableSet.of(label), + new Supplier, Long>>() { + @Override + public ImmutableMap, Long> get() { + return ImmutableMap.of(ImmutableList.of("foo"), 1L); + } + }, + Long.class); + + assertThat(testGauge.getValueClass()).isSameAs(Long.class); + assertThat(testGauge.getMetricSchema()) + .isEqualTo( + MetricSchema.create( + "/test_metric", + "test_description", + "test_valuedisplayname", + Kind.GAUGE, + ImmutableSet.of(label))); + } + + @Test + public void testNewCounter_createsCounter() { + IncrementableMetric testCounter = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/test_counter", + "test_description", + "test_valuedisplayname", + ImmutableSet.of(label)); + + assertThat(testCounter.getValueClass()).isSameAs(Long.class); + assertThat(testCounter.getMetricSchema()) + .isEqualTo( + MetricSchema.create( + "/test_counter", + "test_description", + "test_valuedisplayname", + Kind.CUMULATIVE, + ImmutableSet.of(label))); + } + + @Test + public void testNewSettableMetric_createsSettableMetric() { + SettableMetric testMetric = + MetricRegistryImpl.getDefault() + .newSettableMetric( + "/test_metric", + "test_description", + "test_valuedisplayname", + ImmutableSet.of(label), + Boolean.class); + + assertThat(testMetric.getValueClass()).isSameAs(Boolean.class); + assertThat(testMetric.getMetricSchema()) + .isEqualTo( + MetricSchema.create( + "/test_metric", + "test_description", + "test_valuedisplayname", + Kind.GAUGE, + ImmutableSet.of(label))); + } + + @Test + public void testRegister_duplicateMetric_throwsException() { + SettableMetric testMetric = + MetricRegistryImpl.getDefault() + .newSettableMetric( + "/test_metric", + "test_description", + "test_valuedisplayname", + ImmutableSet.of(label), + Boolean.class); + MetricRegistryImpl.getDefault().registerMetric("/test/metric", testMetric); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Duplicate metric of same name"); + MetricRegistryImpl.getDefault().registerMetric("/test/metric", testMetric); + } +} diff --git a/javatests/google/registry/monitoring/metrics/MetricReporterTest.java b/javatests/google/registry/monitoring/metrics/MetricReporterTest.java new file mode 100644 index 000000000..459237328 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/MetricReporterTest.java @@ -0,0 +1,70 @@ +// Copyright 2016 The Domain Registry 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; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link MetricReporter}. */ +@RunWith(MockitoJUnitRunner.class) +public class MetricReporterTest { + + @Mock MetricRegistry registry; + @Mock Metric metric; + @Mock ThreadFactory threadFactory; + @Mock MetricWriter writer; + @Mock MetricSchema metricSchema; + @Mock BlockingQueue>>> writeQueue; + + @Test + public void testRunOneIteration_enqueuesBatch() throws Exception { + Metric metric = + new Counter("/name", "description", "vdn", ImmutableSet.of()); + when(registry.getRegisteredMetrics()).thenReturn(ImmutableList.of(metric, metric)); + MetricReporter reporter = new MetricReporter(writer, 10L, threadFactory, registry, writeQueue); + + reporter.runOneIteration(); + + verify(writeQueue).offer(Optional.of(ImmutableList.>of())); + } + + @Test + public void testShutDown_enqueuesBatchAndPoisonPill() throws Exception { + // Set up a registry with no metrics. + when(registry.getRegisteredMetrics()).thenReturn(ImmutableList.>of()); + MetricReporter reporter = + spy(new MetricReporter(writer, 10L, threadFactory, registry, writeQueue)); + + reporter.shutDown(); + + verify(reporter).runOneIteration(); + InOrder interactions = Mockito.inOrder(writeQueue); + interactions.verify(writeQueue).offer(Optional.of(ImmutableList.>of())); + interactions.verify(writeQueue).offer(Optional.>>absent()); + } +} diff --git a/javatests/google/registry/monitoring/metrics/MetricSchemaTest.java b/javatests/google/registry/monitoring/metrics/MetricSchemaTest.java new file mode 100644 index 000000000..63f5d6bca --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/MetricSchemaTest.java @@ -0,0 +1,62 @@ +// Copyright 2016 The Domain Registry 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; + +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.MetricSchema.Kind; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link MetricSchema}. */ +@RunWith(JUnit4.class) +public class MetricSchemaTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void testCreate_blankNameField_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Name must not be blank"); + MetricSchema.create( + "", "description", "valueDisplayName", Kind.GAUGE, ImmutableSet.of()); + } + + @Test + public void testCreate_blankDescriptionField_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Description must not be blank"); + MetricSchema.create( + "/name", "", "valueDisplayName", Kind.GAUGE, ImmutableSet.of()); + } + + @Test + public void testCreate_blankValueDisplayNameField_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Value Display Name must not be empty"); + MetricSchema.create("/name", "description", "", Kind.GAUGE, ImmutableSet.of()); + } + + @Test + public void testCreate_nakedNames_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Name must be URL-like and start with a '/'"); + MetricSchema.create( + "foo", "description", "valueDisplayName", Kind.GAUGE, ImmutableSet.of()); + } +} diff --git a/javatests/google/registry/monitoring/metrics/StackdriverWriterTest.java b/javatests/google/registry/monitoring/metrics/StackdriverWriterTest.java new file mode 100644 index 000000000..36035590d --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/StackdriverWriterTest.java @@ -0,0 +1,332 @@ +// Copyright 2016 The Domain Registry 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; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.services.monitoring.v3.Monitoring; +import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest; +import com.google.api.services.monitoring.v3.model.MetricDescriptor; +import com.google.api.services.monitoring.v3.model.MonitoredResource; +import com.google.api.services.monitoring.v3.model.Point; +import com.google.api.services.monitoring.v3.model.TimeSeries; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import google.registry.monitoring.metrics.MetricSchema.Kind; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +/** Unit tests for {@link StackdriverWriter}. */ +@RunWith(MockitoJUnitRunner.class) +public class StackdriverWriterTest { + + @Mock private Monitoring client; + @Mock private Monitoring.Projects projects; + @Mock private Monitoring.Projects.MetricDescriptors metricDescriptors; + @Mock private Monitoring.Projects.MetricDescriptors.Get metricDescriptorGet; + @Mock private Monitoring.Projects.TimeSeries timeSeries; + @Mock private Monitoring.Projects.MetricDescriptors.Create metricDescriptorCreate; + @Mock private Monitoring.Projects.TimeSeries.Create timeSeriesCreate; + @Mock private Metric mockMetric; + @Mock private MetricSchema schema; + @Mock MetricPoint metricPoint; + private Counter metric; + private MetricDescriptor descriptor; + private static final String PROJECT = "PROJECT"; + private static final int MAX_QPS = 10; + private static final int MAX_POINTS_PER_REQUEST = 10; + private static final MonitoredResource MONITORED_RESOURCE = new MonitoredResource(); + + @Before + public void setUp() throws Exception { + metric = + new Counter( + "/name", + "desc", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label", "description"))); + descriptor = StackdriverWriter.createMetricDescriptor(metric); + when(client.projects()).thenReturn(projects); + when(projects.metricDescriptors()).thenReturn(metricDescriptors); + when(projects.timeSeries()).thenReturn(timeSeries); + when(metricDescriptors.create(anyString(), any(MetricDescriptor.class))) + .thenReturn(metricDescriptorCreate); + when(metricDescriptorCreate.execute()).thenReturn(descriptor); + when(metricDescriptors.get(anyString())).thenReturn(metricDescriptorGet); + when(metricDescriptorGet.execute()).thenReturn(descriptor); + when(timeSeries.create(anyString(), any(CreateTimeSeriesRequest.class))) + .thenReturn(timeSeriesCreate); + } + + @Test + public void testWrite_maxPoints_flushes() throws Exception { + // The counter must be set once in order for there to be values to send. + metric.set(0L, new Instant(1337), ImmutableList.of("some_value")); + StackdriverWriter writer = + spy( + new StackdriverWriter( + client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST)); + + for (int i = 0; i < MAX_POINTS_PER_REQUEST; i++) { + for (MetricPoint point : metric.getTimestampedValues(new Instant(1337))) { + writer.write(point); + } + } + + verify(writer).flush(); + } + + @Test + public void testWrite_lessThanMaxPoints_doesNotFlush() throws Exception { + // The counter must be set once in order for there to be values to send. + metric.set(0L, new Instant(1337), ImmutableList.of("some_value")); + StackdriverWriter writer = + spy( + new StackdriverWriter( + client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST)); + + for (int i = 0; i < MAX_POINTS_PER_REQUEST - 1; i++) { + for (MetricPoint point : metric.getTimestampedValues(new Instant(1337))) { + writer.write(point); + } + } + + verify(writer, never()).flush(); + } + + @Test + public void testWrite_invalidMetricType_throwsException() throws Exception { + when(mockMetric.getValueClass()) + .thenAnswer( + new Answer>() { + @Override + public Class answer(InvocationOnMock invocation) throws Throwable { + return Object.class; + } + }); + when(mockMetric.getMetricSchema()).thenReturn(schema); + when(mockMetric.getTimestampedValues()).thenReturn(ImmutableList.of(metricPoint)); + when(schema.kind()).thenReturn(Kind.CUMULATIVE); + when(metricPoint.metric()).thenReturn(mockMetric); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + for (MetricPoint point : mockMetric.getTimestampedValues()) { + try { + writer.write(point); + fail("expected IllegalArgumentException"); + } catch (IOException expected) {} + } + } + + @Test + public void testWrite_ManyPoints_flushesTwice() throws Exception { + // The counter must be set once in order for there to be values to send. + metric.set(0L, new Instant(1337), ImmutableList.of("some_value")); + StackdriverWriter writer = + spy( + new StackdriverWriter( + client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST)); + + for (int i = 0; i < MAX_POINTS_PER_REQUEST * 2; i++) { + for (MetricPoint point : metric.getTimestampedValues(new Instant(1337))) { + writer.write(point); + } + } + + verify(writer, times(2)).flush(); + } + + @Test + public void testRegisterMetric_registersWithStackdriver() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + writer.registerMetric(metric); + + verify( + client + .projects() + .metricDescriptors() + .create(PROJECT, StackdriverWriter.createMetricDescriptor(metric))) + .execute(); + } + + @Test + public void registerMetric_doesNotReregisterDupe() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + writer.registerMetric(metric); + writer.registerMetric(metric); + + verify( + client + .projects() + .metricDescriptors() + .create(PROJECT, StackdriverWriter.createMetricDescriptor(metric))) + .execute(); + } + + @Test + public void registerMetric_fetchesStackdriverDefinition() throws Exception { + ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8)); + HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream); + HttpResponseException.Builder httpResponseExceptionBuilder = + new HttpResponseException.Builder(response); + httpResponseExceptionBuilder.setStatusCode(400); + httpResponseExceptionBuilder.setStatusMessage("ALREADY_EXISTS"); + GoogleJsonResponseException exception = + new GoogleJsonResponseException(httpResponseExceptionBuilder, null); + when(metricDescriptorCreate.execute()).thenThrow(exception); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + + writer.registerMetric(metric); + + verify(client.projects().metricDescriptors().get("metric")).execute(); + } + + @Test + public void getEncodedTimeSeries_nullLabels_encodes() throws Exception { + ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8)); + HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(400, inputStream); + HttpResponseException.Builder httpResponseExceptionBuilder = + new HttpResponseException.Builder(response); + httpResponseExceptionBuilder.setStatusCode(400); + httpResponseExceptionBuilder.setStatusMessage("ALREADY_EXISTS"); + GoogleJsonResponseException exception = + new GoogleJsonResponseException(httpResponseExceptionBuilder, null); + when(metricDescriptorCreate.execute()).thenThrow(exception); + when(metricDescriptorGet.execute()) + .thenReturn(new MetricDescriptor().setName("foo").setLabels(null)); + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + writer.registerMetric(metric); + + TimeSeries timeSeries = + writer.getEncodedTimeSeries( + MetricPoint.create(metric, ImmutableList.of("foo"), new Instant(1337), 10L)); + + assertThat(timeSeries.getMetric().getLabels()).isEmpty(); + } + + @Test + public void createMetricDescriptor_simpleMetric_encodes() { + MetricDescriptor descriptor = StackdriverWriter.createMetricDescriptor(metric); + + assertThat(descriptor.getType()).isEqualTo("custom.googleapis.com/name"); + assertThat(descriptor.getValueType()).isEqualTo("INT64"); + assertThat(descriptor.getDescription()).isEqualTo("desc"); + assertThat(descriptor.getDisplayName()).isEqualTo("vdn"); + assertThat(descriptor.getLabels()) + .containsExactly( + new com.google.api.services.monitoring.v3.model.LabelDescriptor() + .setValueType("STRING") + .setKey("label") + .setDescription("description")); + } + + @Test + public void createLabelDescriptors_simpleLabels_encodes() { + ImmutableSet descriptors = + ImmutableSet.of( + LabelDescriptor.create("label1", "description1"), + LabelDescriptor.create("label2", "description2")); + + ImmutableList encodedDescritors = + StackdriverWriter.createLabelDescriptors(descriptors); + + assertThat(encodedDescritors) + .containsExactly( + new com.google.api.services.monitoring.v3.model.LabelDescriptor() + .setValueType("STRING") + .setKey("label1") + .setDescription("description1"), + new com.google.api.services.monitoring.v3.model.LabelDescriptor() + .setValueType("STRING") + .setKey("label2") + .setDescription("description2")); + } + + @Test + public void getEncodedTimeSeries_simplePoint_encodes() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + MetricPoint nativePoint = + MetricPoint.create( + metric, ImmutableList.of("foo"), new Instant(1336), new Instant(1337), 10L); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("INT64"); + assertThat(timeSeries.getMetricKind()).isEqualTo("CUMULATIVE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getInt64Value()).isEqualTo(10L); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.336Z"); + } + + @Test + public void getEncodedTimeSeries_booleanMetric_encodes() throws Exception { + StackdriverWriter writer = + new StackdriverWriter(client, PROJECT, MONITORED_RESOURCE, MAX_QPS, MAX_POINTS_PER_REQUEST); + Metric boolMetric = + new StoredMetric<>( + "/name", + "desc", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label", "description")), + Boolean.class); + MetricDescriptor boolDescriptor = StackdriverWriter.createMetricDescriptor(boolMetric); + when(metricDescriptorCreate.execute()).thenReturn(boolDescriptor); + MetricPoint nativePoint = + MetricPoint.create(boolMetric, ImmutableList.of("foo"), new Instant(1337), true); + + TimeSeries timeSeries = writer.getEncodedTimeSeries(nativePoint); + + assertThat(timeSeries.getValueType()).isEqualTo("BOOL"); + assertThat(timeSeries.getMetricKind()).isEqualTo("GAUGE"); + List points = timeSeries.getPoints(); + assertThat(points).hasSize(1); + Point point = points.get(0); + assertThat(point.getValue().getBoolValue()).isEqualTo(true); + assertThat(point.getInterval().getEndTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + assertThat(point.getInterval().getStartTime()).isEqualTo("1970-01-01T00:00:01.337Z"); + } +} diff --git a/javatests/google/registry/monitoring/metrics/StoredMetricTest.java b/javatests/google/registry/monitoring/metrics/StoredMetricTest.java new file mode 100644 index 000000000..a4ad1c1f2 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/StoredMetricTest.java @@ -0,0 +1,100 @@ +// Copyright 2016 The Domain Registry 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.joda.time.Instant; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link StoredMetric}. */ +@RunWith(JUnit4.class) +public class StoredMetricTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void testGetCardinality_reflectsCurrentCardinality() { + StoredMetric smallMetric = + new StoredMetric<>( + "/metric", "description", "vdn", ImmutableSet.of(), Boolean.class); + assertThat(smallMetric.getCardinality()).isEqualTo(0); + + smallMetric.set(true); + + assertThat(smallMetric.getCardinality()).isEqualTo(1); + + StoredMetric dimensionalMetric = + new StoredMetric<>( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("foo", "bar")), + Boolean.class); + + dimensionalMetric.set(true, "test_value1"); + dimensionalMetric.set(true, "test_value2"); + + assertThat(dimensionalMetric.getCardinality()).isEqualTo(2); + } + + @Test + public void testSet_wrongNumberOfLabels_throwsException() { + StoredMetric dimensionalMetric = + new StoredMetric<>( + "/metric", + "description", + "vdn", + ImmutableSet.of( + LabelDescriptor.create("label1", "bar"), LabelDescriptor.create("label2", "bar")), + Boolean.class); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The count of labelValues must be equal to"); + dimensionalMetric.set(true, "foo"); + } + + @Test + public void testSet_setsValue() { + StoredMetric metric = + new StoredMetric<>( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar")), + Boolean.class); + + assertThat(metric.getTimestampedValues()).isEmpty(); + + metric.set(true, ImmutableList.of("test_value1")); + assertThat(metric.getTimestampedValues(new Instant(1337))) + .containsExactly( + MetricPoint.create(metric, ImmutableList.of("test_value1"), new Instant(1337), true)); + + metric.set(false, ImmutableList.of("test_value1")); + metric.set(true, ImmutableList.of("test_value2")); + assertThat(metric.getTimestampedValues(new Instant(1338))) + .containsExactly( + MetricPoint.create(metric, ImmutableList.of("test_value1"), new Instant(1338), false), + MetricPoint.create(metric, ImmutableList.of("test_value2"), new Instant(1338), true)); + } +} diff --git a/javatests/google/registry/monitoring/metrics/VirtualMetricTest.java b/javatests/google/registry/monitoring/metrics/VirtualMetricTest.java new file mode 100644 index 000000000..d2f243983 --- /dev/null +++ b/javatests/google/registry/monitoring/metrics/VirtualMetricTest.java @@ -0,0 +1,64 @@ +// Copyright 2016 The Domain Registry 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.joda.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link VirtualMetric}. */ +@RunWith(JUnit4.class) +public class VirtualMetricTest { + + private final VirtualMetric metric = + new VirtualMetric<>( + "/metric", + "description", + "vdn", + ImmutableSet.of(LabelDescriptor.create("label1", "bar")), + Suppliers.ofInstance( + ImmutableMap.of( + ImmutableList.of("label_value1"), "value1", + ImmutableList.of("label_value2"), "value2")), + String.class); + + @Test + public void testGetCardinality_afterGetTimestampedValues_returnsLastCardinality() { + metric.getTimestampedValues(); + assertThat(metric.getCardinality()).isEqualTo(2); + } + + @Test + public void testGetCardinality_beforeGetTimestampedValues_returnsZero() { + assertThat(metric.getCardinality()).isEqualTo(0); + } + + @Test + public void testGetTimestampedValues_returnsValues() { + assertThat(metric.getTimestampedValues(new Instant(1337))) + .containsExactly( + MetricPoint.create( + metric, ImmutableList.of("label_value1"), new Instant(1337), "value1"), + MetricPoint.create( + metric, ImmutableList.of("label_value2"), new Instant(1337), "value2")); + } +}