From 3fb799f1128635b6b0616764237d2b3878a462e5 Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Thu, 12 Sep 2019 15:16:12 -0400 Subject: [PATCH] Add schema deployment tests (#265) * Add schema deployment tests Updated flyway schema script files so that they reflect what is currently deployed in alpha: ClaimsList and PremiumList related elements. Put post-schema-push pg_dump output in nomulus.golden.sql as the authoritative schema. Also added test to verify that the schema pushed by flyway will result in exactly the golden schema. Upgraded testcontainers to 1.12.1. Added a custom Truth subject for better diffing of multi-line text blocks. Removed claims_list.sql and premium_list.sql, as we do not have use for them. * Add schema deployment tests Updated flyway schema script files so that they reflect what is currently deployed in alpha: ClaimsList and PremiumList related elements. Put post-schema-push pg_dump output in nomulus.golden.sql as the authoritative schema. Also added test to verify that the schema pushed by flyway will result in exactly the golden schema. Upgraded testcontainers to 1.12.1. Added a custom Truth subject for better diffing of multi-line text blocks. Removed claims_list.sql and premium_list.sql, as we do not have use for them. * Add schema deployment tests Updated flyway schema script files so that they reflect what is currently deployed in alpha: ClaimsList and PremiumList related elements. Put post-schema-push pg_dump output in nomulus.golden.sql as the authoritative schema. Also added test to verify that the schema pushed by flyway will result in exactly the golden schema. Upgraded testcontainers to 1.12.1. Added a custom Truth subject for better diffing of multi-line text blocks. Removed claims_list.sql and premium_list.sql, as we do not have use for them. --- db/build.gradle | 16 +- ...l => V1__create_claims_list_and_entry.sql} | 2 +- .../V2__create_premium_list_and_entry.sql} | 29 +-- .../resources/sql/schema/nomulus.golden.sql | 194 +++++++++++++++-- .../resources/sql/schema/premium_list.sql | 28 --- .../registry/sql/flyway/SchemaTest.java | 108 ++++++++++ .../registry/testing/TextDiffSubject.java | 201 ++++++++++++++++++ .../registry/testing/TextDiffSubjectTest.java | 107 ++++++++++ .../registry/testing/text-diff-actual.txt | 2 + .../registry/testing/text-diff-expected.txt | 3 + .../registry/testing/text-sidebyside-diff.txt | 5 + .../registry/testing/text-unified-diff.txt | 6 + .../resources/testcontainer/mount/README.md | 4 + dependencies.gradle | 7 +- 14 files changed, 651 insertions(+), 61 deletions(-) rename db/src/main/resources/sql/flyway/{V1__new_claims_list_and_entry.sql => V1__create_claims_list_and_entry.sql} (96%) rename db/src/main/resources/sql/{schema/claims_list.sql => flyway/V2__create_premium_list_and_entry.sql} (52%) delete mode 100644 db/src/main/resources/sql/schema/premium_list.sql create mode 100644 db/src/test/java/google/registry/sql/flyway/SchemaTest.java create mode 100644 db/src/test/java/google/registry/testing/TextDiffSubject.java create mode 100644 db/src/test/java/google/registry/testing/TextDiffSubjectTest.java create mode 100644 db/src/test/resources/google/registry/testing/text-diff-actual.txt create mode 100644 db/src/test/resources/google/registry/testing/text-diff-expected.txt create mode 100644 db/src/test/resources/google/registry/testing/text-sidebyside-diff.txt create mode 100644 db/src/test/resources/google/registry/testing/text-unified-diff.txt create mode 100644 db/src/test/resources/testcontainer/mount/README.md diff --git a/db/build.gradle b/db/build.gradle index c7e9f9ea4..0a90411da 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -87,10 +87,20 @@ flyway { } dependencies { - runtimeOnly 'org.flywaydb:flyway-core:5.2.4' + def deps = rootProject.dependencyMap - runtimeOnly 'com.google.cloud.sql:postgres-socket-factory:1.0.12' - runtimeOnly 'org.postgresql:postgresql:42.2.5' + compile deps['org.flywaydb:flyway-core'] + + runtimeOnly deps['com.google.cloud.sql:postgres-socket-factory'] + runtimeOnly deps['org.postgresql:postgresql'] + + testCompile deps['com.google.flogger:flogger'] + testRuntime deps['com.google.flogger:flogger-system-backend'] + testCompile deps['com.google.truth:truth'] + testCompile deps['io.github.java-diff-utils:java-diff-utils'] + testCompile deps['org.testcontainers:postgresql'] + testCompile deps['junit:junit'] + testCompile project(':third_party') } // Ensure that resources are rebuilt before running Flyway tasks diff --git a/db/src/main/resources/sql/flyway/V1__new_claims_list_and_entry.sql b/db/src/main/resources/sql/flyway/V1__create_claims_list_and_entry.sql similarity index 96% rename from db/src/main/resources/sql/flyway/V1__new_claims_list_and_entry.sql rename to db/src/main/resources/sql/flyway/V1__create_claims_list_and_entry.sql index 87c69723f..f4132bbdf 100644 --- a/db/src/main/resources/sql/flyway/V1__new_claims_list_and_entry.sql +++ b/db/src/main/resources/sql/flyway/V1__create_claims_list_and_entry.sql @@ -25,7 +25,7 @@ primary key (revision_id) ); - alter table "ClaimsEntry" + alter table if exists "ClaimsEntry" add constraint FKlugn0q07ayrtar87dqi3vs3c8 foreign key (revision_id) references "ClaimsList"; diff --git a/db/src/main/resources/sql/schema/claims_list.sql b/db/src/main/resources/sql/flyway/V2__create_premium_list_and_entry.sql similarity index 52% rename from db/src/main/resources/sql/schema/claims_list.sql rename to db/src/main/resources/sql/flyway/V2__create_premium_list_and_entry.sql index 85e3504ee..e911d7201 100644 --- a/db/src/main/resources/sql/schema/claims_list.sql +++ b/db/src/main/resources/sql/flyway/V2__create_premium_list_and_entry.sql @@ -12,16 +12,21 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -CREATE TABLE `ClaimsList` ( - revision_id BIGSERIAL NOT NULL, - creation_timestamp TIMESTAMPTZ NOT NULL, - PRIMARY KEY (revision_id) -); + create table "PremiumEntry" ( + revision_id int8 not null, + price numeric(19, 2) not null, + domain_label text not null, + primary key (revision_id, domain_label) + ); -CREATE TABLE `ClaimsEntry` ( - revision_id int8 NOT NULL, - claim_key TEXT NOT NULL, - domain_label TEXT NOT NULL, - PRIMARY KEY (revision_id, domain_label), - FOREIGN KEY (revision_id) REFERENCES `ClaimsList`(revision_id) -); + create table "PremiumList" ( + revision_id bigserial not null, + creation_timestamp timestamptz not null, + currency bytea not null, + primary key (revision_id) + ); + + alter table if exists "PremiumEntry" + add constraint FKqebdja3jkx9c9cnqnrw9g9ocu + foreign key (revision_id) + references "PremiumList"; diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index 5ec31dd06..6a3fcff30 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -1,18 +1,182 @@ +-- +-- PostgreSQL database dump +-- - create table "ClaimsEntry" ( - revision_id int8 not null, - claim_key text not null, - domain_label text not null, - primary key (revision_id, domain_label) - ); +-- Dumped from database version 9.6.12 +-- Dumped by pg_dump version 9.6.12 - create table "ClaimsList" ( - revision_id bigserial not null, - creation_timestamp timestamptz not null, - primary key (revision_id) - ); +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; + + +-- +-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; + + +SET default_tablespace = ''; + +SET default_with_oids = false; + +-- +-- Name: ClaimsEntry; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."ClaimsEntry" ( + revision_id bigint NOT NULL, + claim_key text NOT NULL, + domain_label text NOT NULL +); + + +-- +-- Name: ClaimsList; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."ClaimsList" ( + revision_id bigint NOT NULL, + creation_timestamp timestamp with time zone NOT NULL +); + + +-- +-- Name: ClaimsList_revision_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public."ClaimsList_revision_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: ClaimsList_revision_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public."ClaimsList_revision_id_seq" OWNED BY public."ClaimsList".revision_id; + + +-- +-- Name: PremiumEntry; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."PremiumEntry" ( + revision_id bigint NOT NULL, + price numeric(19,2) NOT NULL, + domain_label text NOT NULL +); + + +-- +-- Name: PremiumList; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public."PremiumList" ( + revision_id bigint NOT NULL, + creation_timestamp timestamp with time zone NOT NULL, + currency bytea NOT NULL +); + + +-- +-- Name: PremiumList_revision_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public."PremiumList_revision_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: PremiumList_revision_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public."PremiumList_revision_id_seq" OWNED BY public."PremiumList".revision_id; + + +-- +-- Name: ClaimsList revision_id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."ClaimsList" ALTER COLUMN revision_id SET DEFAULT nextval('public."ClaimsList_revision_id_seq"'::regclass); + + +-- +-- Name: PremiumList revision_id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."PremiumList" ALTER COLUMN revision_id SET DEFAULT nextval('public."PremiumList_revision_id_seq"'::regclass); + + +-- +-- Name: ClaimsEntry ClaimsEntry_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."ClaimsEntry" + ADD CONSTRAINT "ClaimsEntry_pkey" PRIMARY KEY (revision_id, domain_label); + + +-- +-- Name: ClaimsList ClaimsList_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."ClaimsList" + ADD CONSTRAINT "ClaimsList_pkey" PRIMARY KEY (revision_id); + + +-- +-- Name: PremiumEntry PremiumEntry_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."PremiumEntry" + ADD CONSTRAINT "PremiumEntry_pkey" PRIMARY KEY (revision_id, domain_label); + + +-- +-- Name: PremiumList PremiumList_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."PremiumList" + ADD CONSTRAINT "PremiumList_pkey" PRIMARY KEY (revision_id); + + +-- +-- Name: ClaimsEntry fklugn0q07ayrtar87dqi3vs3c8; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."ClaimsEntry" + ADD CONSTRAINT fklugn0q07ayrtar87dqi3vs3c8 FOREIGN KEY (revision_id) REFERENCES public."ClaimsList"(revision_id); + + +-- +-- Name: PremiumEntry fkqebdja3jkx9c9cnqnrw9g9ocu; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."PremiumEntry" + ADD CONSTRAINT fkqebdja3jkx9c9cnqnrw9g9ocu FOREIGN KEY (revision_id) REFERENCES public."PremiumList"(revision_id); + + +-- +-- PostgreSQL database dump complete +-- - alter table "ClaimsEntry" - add constraint FKlugn0q07ayrtar87dqi3vs3c8 - foreign key (revision_id) - references "ClaimsList"; diff --git a/db/src/main/resources/sql/schema/premium_list.sql b/db/src/main/resources/sql/schema/premium_list.sql deleted file mode 100644 index 2cc1b7862..000000000 --- a/db/src/main/resources/sql/schema/premium_list.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Copyright 2019 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. - -CREATE TABLE "PremiumList" ( - revision_id BIGSERIAL NOT NULL, - creation_timestamp TIMESTAMPTZ NOT NULL, - currency TEXT NOT NULL, - PRIMARY KEY (revision_id) -); - -CREATE TABLE "PremiumEntry" ( - revision_id BIGSERIAL NOT NULL, - price NUMERIC(12, 2) NOT NULL, - domain_label TEXT NOT NULL, - primary key (revision_id, domain_label), - FOREIGN KEY (revision_id) REFERENCES "PremiumList"(revision_id) -); diff --git a/db/src/test/java/google/registry/sql/flyway/SchemaTest.java b/db/src/test/java/google/registry/sql/flyway/SchemaTest.java new file mode 100644 index 000000000..c0c4bd08b --- /dev/null +++ b/db/src/test/java/google/registry/sql/flyway/SchemaTest.java @@ -0,0 +1,108 @@ +// Copyright 2019 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.sql.flyway; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.TextDiffSubject.assertThat; + +import com.google.common.base.Joiner; +import com.google.common.io.Resources; +import java.io.File; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import org.flywaydb.core.Flyway; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.PostgreSQLContainer; + +/** Unit tests about Cloud SQL schema. */ +@RunWith(JUnit4.class) +public class SchemaTest { + + // Resource path that is mapped to the testcontainer instance. + private static final String MOUNTED_RESOURCE_PATH = "testcontainer/mount"; + // The mount point in the container. + private static final String CONTAINER_MOUNT_POINT = "/tmp/pg_dump_out"; + // pg_dump output file name. + private static final String DUMP_OUTPUT_FILE = "dump.txt"; + + /** + * The target database for schema deployment. + * + *

A resource path is mapped to this container in READ_WRITE mode to retrieve the deployed + * schema generated by the 'pg_dump' command. We do not communicate over stdout because + * testcontainer adds spurious newlines. See this link for more + * information. + */ + @Rule + public PostgreSQLContainer sqlContainer = + new PostgreSQLContainer<>("postgres:9.6.12") + .withClasspathResourceMapping( + MOUNTED_RESOURCE_PATH, CONTAINER_MOUNT_POINT, BindMode.READ_WRITE); + + @Test + public void deploySchema_success() throws Exception { + Flyway flyway = + Flyway.configure() + .locations("sql/flyway") + .dataSource( + sqlContainer.getJdbcUrl(), sqlContainer.getUsername(), sqlContainer.getPassword()) + .load(); + + // flyway.migrate() returns the number of newly pushed scripts. This is a variable + // number as our schema evolves. + assertThat(flyway.migrate()).isGreaterThan(0); + flyway.validate(); + + Container.ExecResult execResult = + sqlContainer.execInContainer( + StandardCharsets.UTF_8, + getSchemaDumpCommand(sqlContainer.getUsername(), sqlContainer.getDatabaseName())); + if (execResult.getExitCode() != 0) { + throw new RuntimeException(execResult.toString()); + } + + URL dumpedSchema = + Resources.getResource( + Joiner.on(File.separatorChar).join(MOUNTED_RESOURCE_PATH, DUMP_OUTPUT_FILE)); + + assertThat(dumpedSchema) + .hasSameContentAs(Resources.getResource("sql/schema/nomulus.golden.sql")); + } + + private static String[] getSchemaDumpCommand(String username, String dbName) { + return new String[] { + "pg_dump", + "-h", + "localhost", + "-U", + username, + "-f", + Paths.get(CONTAINER_MOUNT_POINT, DUMP_OUTPUT_FILE).toString(), + "--schema-only", + "--no-owner", + "--no-privileges", + "--exclude-table", + "flyway_schema_history", + dbName + }; + } +} diff --git a/db/src/test/java/google/registry/testing/TextDiffSubject.java b/db/src/test/java/google/registry/testing/TextDiffSubject.java new file mode 100644 index 000000000..8d90b79b2 --- /dev/null +++ b/db/src/test/java/google/registry/testing/TextDiffSubject.java @@ -0,0 +1,201 @@ +// Copyright 2019 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.testing; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.truth.Truth.assertAbout; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.github.difflib.DiffUtils; +import com.github.difflib.UnifiedDiffUtils; +import com.github.difflib.algorithm.DiffException; +import com.github.difflib.patch.Patch; +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRowGenerator; +import com.google.common.base.Ascii; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import com.google.common.truth.Fact; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Compares two multi-line text blocks, and displays their diffs in readable formats. + * + *

User may choose one of the following diff formats: + * + *

+ * + *

Note that if one text block has one trailing newline at the end while another has none, this + * difference will not be shown in the generated diffs. This is the case where two texts may be + * reported as unequal but the diffs appear equal. Fixing this requires special treatment of the + * last line of text. The fix would not be useful in our environment, where all important files are + * covered by a style checker that ensures the presence of a trailing newline. + */ +// TODO(weiminyu): move this class and test to a standalone 'testing' project. Note that the util +// project is not good since it depends back to core. +@SuppressWarnings("unchecked") // On behalf of Raw type Subject; remove after Truth 1.0 upgrade. +public class TextDiffSubject extends Subject { + + private final ImmutableList actual; + private DiffFormat diffFormat = DiffFormat.SIDE_BY_SIDE_MARKDOWN; + + protected TextDiffSubject(FailureMetadata metadata, List actual) { + super(metadata, actual); + this.actual = ImmutableList.copyOf(actual); + } + + public TextDiffSubject withDiffFormat(DiffFormat format) { + this.diffFormat = format; + return this; + } + + public void hasSameContentAs(List expectedContent) { + checkNotNull(expectedContent, "expectedContent"); + ImmutableList expected = ImmutableList.copyOf(expectedContent); + if (expected.equals(actual)) { + return; + } + String diffString = diffFormat.generateDiff(expected, actual); + failWithoutActual( + Fact.simpleFact( + Joiner.on('\n') + .join( + "Files differ in content. Displaying " + Ascii.toLowerCase(diffFormat.name()), + diffString))); + } + + public void hasSameContentAs(URL resourceUrl) throws IOException { + hasSameContentAs(Resources.asCharSource(resourceUrl, UTF_8).readLines()); + } + + public static TextDiffSubject assertThat(List actual) { + return assertAbout(textFactory()).that(ImmutableList.copyOf(checkNotNull(actual, "actual"))); + } + + public static TextDiffSubject assertThat(URL resourceUrl) throws IOException { + return assertThat(Resources.asCharSource(resourceUrl, UTF_8).readLines()); + } + + private static final Subject.Factory> + TEXT_DIFF_SUBJECT_TEXT_FACTORY = TextDiffSubject::new; + + public static Subject.Factory> textFactory() { + return TEXT_DIFF_SUBJECT_TEXT_FACTORY; + } + + static String generateUnifiedDiff( + ImmutableList expectedContent, ImmutableList actualContent) { + Patch diff; + try { + diff = DiffUtils.diff(expectedContent, actualContent); + } catch (DiffException e) { + throw new RuntimeException(e); + } + List unifiedDiff = + UnifiedDiffUtils.generateUnifiedDiff("expected", "actual", expectedContent, diff, 0); + + return Joiner.on('\n').join(unifiedDiff); + } + + static String generateSideBySideDiff( + ImmutableList expectedContent, ImmutableList actualContent) { + DiffRowGenerator generator = + DiffRowGenerator.create() + .showInlineDiffs(true) + .inlineDiffByWord(true) + .oldTag(f -> "~") + .newTag(f -> "**") + .build(); + List rows; + try { + rows = generator.generateDiffRows(expectedContent, actualContent); + } catch (DiffException e) { + throw new RuntimeException(e); + } + + int maxExpectedLineLength = + findMaxLineLength(rows.stream().map(DiffRow::getOldLine).collect(Collectors.toList())); + int maxActualLineLength = + findMaxLineLength(rows.stream().map(DiffRow::getNewLine).collect(Collectors.toList())); + + SideBySideRowFormatter sideBySideRowFormatter = + new SideBySideRowFormatter(maxExpectedLineLength, maxActualLineLength); + + return Joiner.on('\n') + .join( + sideBySideRowFormatter.formatRow("Expected", "Actual", ' '), + sideBySideRowFormatter.formatRow("", "", '-'), + rows.stream() + .map( + row -> + sideBySideRowFormatter.formatRow(row.getOldLine(), row.getNewLine(), ' ')) + .toArray()); + } + + private static int findMaxLineLength(Collection lines) { + return lines.stream() + .max(Comparator.comparingInt(String::length)) + .map(String::length) + .orElse(0); + } + + private static class SideBySideRowFormatter { + private final int maxExpectedLineLength; + private final int maxActualLineLength; + + private SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) { + this.maxExpectedLineLength = maxExpectedLineLength; + this.maxActualLineLength = maxActualLineLength; + } + + public String formatRow(String expected, String actual, char padChar) { + return String.format( + "|%s|%s|", + Strings.padEnd(expected, maxExpectedLineLength, padChar), + Strings.padEnd(actual, maxActualLineLength, padChar)); + } + } + + /** The format used to display diffs when two text blocks are different. */ + public enum DiffFormat { + UNIFIED_DIFF { + @Override + String generateDiff(ImmutableList expected, ImmutableList actual) { + return generateUnifiedDiff(expected, actual); + } + }, + SIDE_BY_SIDE_MARKDOWN { + @Override + String generateDiff(ImmutableList expected, ImmutableList actual) { + return generateSideBySideDiff(expected, actual); + } + }; + + abstract String generateDiff(ImmutableList expected, ImmutableList actual); + } +} diff --git a/db/src/test/java/google/registry/testing/TextDiffSubjectTest.java b/db/src/test/java/google/registry/testing/TextDiffSubjectTest.java new file mode 100644 index 000000000..ef9b73509 --- /dev/null +++ b/db/src/test/java/google/registry/testing/TextDiffSubjectTest.java @@ -0,0 +1,107 @@ +// Copyright 2019 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.testing; + +import static com.google.common.io.Resources.getResource; +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.JUnitBackports.assertThrows; +import static google.registry.testing.TextDiffSubject.assertThat; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import google.registry.testing.TextDiffSubject.DiffFormat; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link TextDiffSubject}. */ +@RunWith(JUnit4.class) +public class TextDiffSubjectTest { + + // Resources for input data. + private static final String ACTUAL_RESOURCE = "google/registry/testing/text-diff-actual.txt"; + private static final String EXPECTED_RESOURCE = "google/registry/testing/text-diff-expected.txt"; + + // Resources for expected diff texts. + private static final String UNIFIED_DIFF_RESOURCE = + "google/registry/testing/text-unified-diff.txt"; + private static final String SIDE_BY_SIDE_DIFF_RESOURCE = + "google/registry/testing/text-sidebyside-diff.txt"; + + @Test + public void unifiedDiff_equal() throws IOException { + assertThat(getResource(ACTUAL_RESOURCE)) + .withDiffFormat(DiffFormat.UNIFIED_DIFF) + .hasSameContentAs(getResource(ACTUAL_RESOURCE)); + } + + @Test + public void sideBySideDiff_equal() throws IOException { + assertThat(getResource(ACTUAL_RESOURCE)) + .withDiffFormat(DiffFormat.SIDE_BY_SIDE_MARKDOWN) + .hasSameContentAs(getResource(ACTUAL_RESOURCE)); + } + + @Test + public void unifedDiff_notEqual() throws IOException { + assertThrows( + AssertionError.class, + () -> + assertThat(getResource(ACTUAL_RESOURCE)) + .withDiffFormat(DiffFormat.UNIFIED_DIFF) + .hasSameContentAs(getResource(EXPECTED_RESOURCE))); + } + + @Test + public void sideBySideDiff_notEqual() throws IOException { + assertThrows( + AssertionError.class, + () -> + assertThat(getResource(ACTUAL_RESOURCE)) + .withDiffFormat(DiffFormat.SIDE_BY_SIDE_MARKDOWN) + .hasSameContentAs(getResource(EXPECTED_RESOURCE))); + } + + @Test + public void displayed_unifiedDiff_noDiff() throws IOException { + ImmutableList actual = readAllLinesFromResource(ACTUAL_RESOURCE); + assertThat(TextDiffSubject.generateUnifiedDiff(actual, actual)).isEqualTo(""); + } + + @Test + public void displayed_unifiedDiff_hasDiff() throws IOException { + ImmutableList actual = readAllLinesFromResource(ACTUAL_RESOURCE); + ImmutableList expected = readAllLinesFromResource(EXPECTED_RESOURCE); + String diff = Joiner.on('\n').join(readAllLinesFromResource(UNIFIED_DIFF_RESOURCE)); + assertThat(TextDiffSubject.generateUnifiedDiff(expected, actual)).isEqualTo(diff); + } + + @Test + public void displayed_sideBySideDiff_hasDiff() throws IOException { + ImmutableList actual = readAllLinesFromResource(ACTUAL_RESOURCE); + ImmutableList expected = readAllLinesFromResource(EXPECTED_RESOURCE); + String diff = Joiner.on('\n').join(readAllLinesFromResource(SIDE_BY_SIDE_DIFF_RESOURCE)); + assertThat(TextDiffSubject.generateSideBySideDiff(expected, actual)).isEqualTo(diff); + } + + private static ImmutableList readAllLinesFromResource(String resourceName) + throws IOException { + return ImmutableList.copyOf( + Resources.readLines(getResource(resourceName), StandardCharsets.UTF_8)); + } +} diff --git a/db/src/test/resources/google/registry/testing/text-diff-actual.txt b/db/src/test/resources/google/registry/testing/text-diff-actual.txt new file mode 100644 index 000000000..bef69f31a --- /dev/null +++ b/db/src/test/resources/google/registry/testing/text-diff-actual.txt @@ -0,0 +1,2 @@ +This is a random file, +with two lines and terminates with a newline. diff --git a/db/src/test/resources/google/registry/testing/text-diff-expected.txt b/db/src/test/resources/google/registry/testing/text-diff-expected.txt new file mode 100644 index 000000000..d09d06e30 --- /dev/null +++ b/db/src/test/resources/google/registry/testing/text-diff-expected.txt @@ -0,0 +1,3 @@ +This is a random file, + +with three lines and terminates without a newline. \ No newline at end of file diff --git a/db/src/test/resources/google/registry/testing/text-sidebyside-diff.txt b/db/src/test/resources/google/registry/testing/text-sidebyside-diff.txt new file mode 100644 index 000000000..1d9ce26a3 --- /dev/null +++ b/db/src/test/resources/google/registry/testing/text-sidebyside-diff.txt @@ -0,0 +1,5 @@ +|Expected |Actual | +|------------------------------------------------------|-----------------------------------------------------| +|This is a random file, |This is a random file, | +| |with **two** lines and terminates **with** a newline.| +|with ~three~ lines and terminates ~without~ a newline.| | \ No newline at end of file diff --git a/db/src/test/resources/google/registry/testing/text-unified-diff.txt b/db/src/test/resources/google/registry/testing/text-unified-diff.txt new file mode 100644 index 000000000..bd7041090 --- /dev/null +++ b/db/src/test/resources/google/registry/testing/text-unified-diff.txt @@ -0,0 +1,6 @@ +--- expected ++++ actual +@@ -2,2 +2,1 @@ +- +-with three lines and terminates without a newline. ++with two lines and terminates with a newline. \ No newline at end of file diff --git a/db/src/test/resources/testcontainer/mount/README.md b/db/src/test/resources/testcontainer/mount/README.md new file mode 100644 index 000000000..aa31c048d --- /dev/null +++ b/db/src/test/resources/testcontainer/mount/README.md @@ -0,0 +1,4 @@ +## Summary + +This folder may be mapped to a testcontainer instance as a volume. Please refer +to SchemaTest.java for context. \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index e46608106..5649ec576 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -45,6 +45,7 @@ ext { 'com.google.closure-stylesheets:closure-stylesheets:1.5.0', 'com.google.cloud:google-cloud-core:1.59.0', 'com.google.cloud:google-cloud-storage:1.59.0', + 'com.google.cloud.sql:postgres-socket-factory:1.0.12', 'com.google.code.findbugs:jsr305:3.0.2', 'com.google.code.gson:gson:2.8.5', 'com.googlecode.json-simple:json-simple:1.1.1', @@ -78,6 +79,7 @@ ext { 'com.sun.xml.bind:jaxb-xjc:2.2.11', 'com.thoughtworks.qdox:qdox:1.12.1', 'dnsjava:dnsjava:2.1.7', + 'io.github.java-diff-utils:java-diff-utils:4.0', 'io.netty:netty-buffer:4.1.31.Final', 'io.netty:netty-codec:4.1.31.Final', 'io.netty:netty-codec-http:4.1.31.Final', @@ -111,6 +113,7 @@ ext { 'org.bouncycastle:bcpg-jdk15on:1.61', 'org.bouncycastle:bcpkix-jdk15on:1.61', 'org.bouncycastle:bcprov-jdk15on:1.61', + 'org.flywaydb:flyway-core:5.2.4', 'org.glassfish.jaxb:jaxb-runtime:2.3.0', 'org.hamcrest:hamcrest-all:1.3', 'org.hamcrest:hamcrest-core:1.3', @@ -125,8 +128,8 @@ ext { 'org.seleniumhq.selenium:selenium-chrome-driver:3.141.59', 'org.seleniumhq.selenium:selenium-java:3.141.59', 'org.seleniumhq.selenium:selenium-remote-driver:3.141.59', - 'org.testcontainers:postgresql:1.11.3', - 'org.testcontainers:selenium:1.10.7', + 'org.testcontainers:postgresql:1.12.1', + 'org.testcontainers:selenium:1.12.1', 'org.yaml:snakeyaml:1.17', 'xerces:xmlParserAPIs:2.6.2', 'xpp3:xpp3:1.1.4c'