mirror of
https://github.com/google/nomulus.git
synced 2025-04-30 12:07:51 +02:00
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.
This commit is contained in:
parent
49777a6caa
commit
005e059d33
14 changed files with 651 additions and 61 deletions
|
@ -87,10 +87,20 @@ flyway {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
runtimeOnly 'org.flywaydb:flyway-core:5.2.4'
|
def deps = rootProject.dependencyMap
|
||||||
|
|
||||||
runtimeOnly 'com.google.cloud.sql:postgres-socket-factory:1.0.12'
|
compile deps['org.flywaydb:flyway-core']
|
||||||
runtimeOnly 'org.postgresql:postgresql:42.2.5'
|
|
||||||
|
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
|
// Ensure that resources are rebuilt before running Flyway tasks
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
primary key (revision_id)
|
primary key (revision_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
alter table "ClaimsEntry"
|
alter table if exists "ClaimsEntry"
|
||||||
add constraint FKlugn0q07ayrtar87dqi3vs3c8
|
add constraint FKlugn0q07ayrtar87dqi3vs3c8
|
||||||
foreign key (revision_id)
|
foreign key (revision_id)
|
||||||
references "ClaimsList";
|
references "ClaimsList";
|
|
@ -12,16 +12,21 @@
|
||||||
-- See the License for the specific language governing permissions and
|
-- See the License for the specific language governing permissions and
|
||||||
-- limitations under the License.
|
-- limitations under the License.
|
||||||
|
|
||||||
CREATE TABLE `ClaimsList` (
|
create table "PremiumEntry" (
|
||||||
revision_id BIGSERIAL NOT NULL,
|
revision_id int8 not null,
|
||||||
creation_timestamp TIMESTAMPTZ NOT NULL,
|
price numeric(19, 2) not null,
|
||||||
PRIMARY KEY (revision_id)
|
domain_label text not null,
|
||||||
);
|
primary key (revision_id, domain_label)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE `ClaimsEntry` (
|
create table "PremiumList" (
|
||||||
revision_id int8 NOT NULL,
|
revision_id bigserial not null,
|
||||||
claim_key TEXT NOT NULL,
|
creation_timestamp timestamptz not null,
|
||||||
domain_label TEXT NOT NULL,
|
currency bytea not null,
|
||||||
PRIMARY KEY (revision_id, domain_label),
|
primary key (revision_id)
|
||||||
FOREIGN KEY (revision_id) REFERENCES `ClaimsList`(revision_id)
|
);
|
||||||
);
|
|
||||||
|
alter table if exists "PremiumEntry"
|
||||||
|
add constraint FKqebdja3jkx9c9cnqnrw9g9ocu
|
||||||
|
foreign key (revision_id)
|
||||||
|
references "PremiumList";
|
|
@ -1,18 +1,182 @@
|
||||||
|
--
|
||||||
|
-- PostgreSQL database dump
|
||||||
|
--
|
||||||
|
|
||||||
create table "ClaimsEntry" (
|
-- Dumped from database version 9.6.12
|
||||||
revision_id int8 not null,
|
-- Dumped by pg_dump version 9.6.12
|
||||||
claim_key text not null,
|
|
||||||
domain_label text not null,
|
|
||||||
primary key (revision_id, domain_label)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table "ClaimsList" (
|
SET statement_timeout = 0;
|
||||||
revision_id bigserial not null,
|
SET lock_timeout = 0;
|
||||||
creation_timestamp timestamptz not null,
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
primary key (revision_id)
|
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";
|
|
||||||
|
|
|
@ -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)
|
|
||||||
);
|
|
108
db/src/test/java/google/registry/sql/flyway/SchemaTest.java
Normal file
108
db/src/test/java/google/registry/sql/flyway/SchemaTest.java
Normal file
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>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 <a
|
||||||
|
* href=https://github.com/testcontainers/testcontainers-java/issues/1854>this link</a> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
201
db/src/test/java/google/registry/testing/TextDiffSubject.java
Normal file
201
db/src/test/java/google/registry/testing/TextDiffSubject.java
Normal file
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>User may choose one of the following diff formats:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link DiffFormat#UNIFIED_DIFF} displays the differences in the unified-diff format
|
||||||
|
* <li>{@link DiffFormat#SIDE_BY_SIDE_MARKDOWN} displays the two text blocks side by side, with
|
||||||
|
* markdown annotations to highlight the differences.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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<String> actual;
|
||||||
|
private DiffFormat diffFormat = DiffFormat.SIDE_BY_SIDE_MARKDOWN;
|
||||||
|
|
||||||
|
protected TextDiffSubject(FailureMetadata metadata, List<String> actual) {
|
||||||
|
super(metadata, actual);
|
||||||
|
this.actual = ImmutableList.copyOf(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextDiffSubject withDiffFormat(DiffFormat format) {
|
||||||
|
this.diffFormat = format;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hasSameContentAs(List<String> expectedContent) {
|
||||||
|
checkNotNull(expectedContent, "expectedContent");
|
||||||
|
ImmutableList<String> 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<String> 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<TextDiffSubject, ImmutableList<String>>
|
||||||
|
TEXT_DIFF_SUBJECT_TEXT_FACTORY = TextDiffSubject::new;
|
||||||
|
|
||||||
|
public static Subject.Factory<TextDiffSubject, ImmutableList<String>> textFactory() {
|
||||||
|
return TEXT_DIFF_SUBJECT_TEXT_FACTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String generateUnifiedDiff(
|
||||||
|
ImmutableList<String> expectedContent, ImmutableList<String> actualContent) {
|
||||||
|
Patch<String> diff;
|
||||||
|
try {
|
||||||
|
diff = DiffUtils.diff(expectedContent, actualContent);
|
||||||
|
} catch (DiffException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
List<String> unifiedDiff =
|
||||||
|
UnifiedDiffUtils.generateUnifiedDiff("expected", "actual", expectedContent, diff, 0);
|
||||||
|
|
||||||
|
return Joiner.on('\n').join(unifiedDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String generateSideBySideDiff(
|
||||||
|
ImmutableList<String> expectedContent, ImmutableList<String> actualContent) {
|
||||||
|
DiffRowGenerator generator =
|
||||||
|
DiffRowGenerator.create()
|
||||||
|
.showInlineDiffs(true)
|
||||||
|
.inlineDiffByWord(true)
|
||||||
|
.oldTag(f -> "~")
|
||||||
|
.newTag(f -> "**")
|
||||||
|
.build();
|
||||||
|
List<DiffRow> 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<String> 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<String> expected, ImmutableList<String> actual) {
|
||||||
|
return generateUnifiedDiff(expected, actual);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SIDE_BY_SIDE_MARKDOWN {
|
||||||
|
@Override
|
||||||
|
String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual) {
|
||||||
|
return generateSideBySideDiff(expected, actual);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
abstract String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||||
|
assertThat(TextDiffSubject.generateUnifiedDiff(actual, actual)).isEqualTo("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void displayed_unifiedDiff_hasDiff() throws IOException {
|
||||||
|
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||||
|
ImmutableList<String> 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<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||||
|
ImmutableList<String> 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<String> readAllLinesFromResource(String resourceName)
|
||||||
|
throws IOException {
|
||||||
|
return ImmutableList.copyOf(
|
||||||
|
Resources.readLines(getResource(resourceName), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
This is a random file,
|
||||||
|
with two lines and terminates with a newline.
|
|
@ -0,0 +1,3 @@
|
||||||
|
This is a random file,
|
||||||
|
|
||||||
|
with three lines and terminates without a newline.
|
|
@ -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.| |
|
|
@ -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.
|
4
db/src/test/resources/testcontainer/mount/README.md
Normal file
4
db/src/test/resources/testcontainer/mount/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This folder may be mapped to a testcontainer instance as a volume. Please refer
|
||||||
|
to SchemaTest.java for context.
|
|
@ -45,6 +45,7 @@ ext {
|
||||||
'com.google.closure-stylesheets:closure-stylesheets:1.5.0',
|
'com.google.closure-stylesheets:closure-stylesheets:1.5.0',
|
||||||
'com.google.cloud:google-cloud-core:1.59.0',
|
'com.google.cloud:google-cloud-core:1.59.0',
|
||||||
'com.google.cloud:google-cloud-storage: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.findbugs:jsr305:3.0.2',
|
||||||
'com.google.code.gson:gson:2.8.5',
|
'com.google.code.gson:gson:2.8.5',
|
||||||
'com.googlecode.json-simple:json-simple:1.1.1',
|
'com.googlecode.json-simple:json-simple:1.1.1',
|
||||||
|
@ -78,6 +79,7 @@ ext {
|
||||||
'com.sun.xml.bind:jaxb-xjc:2.2.11',
|
'com.sun.xml.bind:jaxb-xjc:2.2.11',
|
||||||
'com.thoughtworks.qdox:qdox:1.12.1',
|
'com.thoughtworks.qdox:qdox:1.12.1',
|
||||||
'dnsjava:dnsjava:2.1.7',
|
'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-buffer:4.1.31.Final',
|
||||||
'io.netty:netty-codec:4.1.31.Final',
|
'io.netty:netty-codec:4.1.31.Final',
|
||||||
'io.netty:netty-codec-http: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:bcpg-jdk15on:1.61',
|
||||||
'org.bouncycastle:bcpkix-jdk15on:1.61',
|
'org.bouncycastle:bcpkix-jdk15on:1.61',
|
||||||
'org.bouncycastle:bcprov-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.glassfish.jaxb:jaxb-runtime:2.3.0',
|
||||||
'org.hamcrest:hamcrest-all:1.3',
|
'org.hamcrest:hamcrest-all:1.3',
|
||||||
'org.hamcrest:hamcrest-core: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-chrome-driver:3.141.59',
|
||||||
'org.seleniumhq.selenium:selenium-java:3.141.59',
|
'org.seleniumhq.selenium:selenium-java:3.141.59',
|
||||||
'org.seleniumhq.selenium:selenium-remote-driver:3.141.59',
|
'org.seleniumhq.selenium:selenium-remote-driver:3.141.59',
|
||||||
'org.testcontainers:postgresql:1.11.3',
|
'org.testcontainers:postgresql:1.12.1',
|
||||||
'org.testcontainers:selenium:1.10.7',
|
'org.testcontainers:selenium:1.12.1',
|
||||||
'org.yaml:snakeyaml:1.17',
|
'org.yaml:snakeyaml:1.17',
|
||||||
'xerces:xmlParserAPIs:2.6.2',
|
'xerces:xmlParserAPIs:2.6.2',
|
||||||
'xpp3:xpp3:1.1.4c'
|
'xpp3:xpp3:1.1.4c'
|
||||||
|
|
Loading…
Add table
Reference in a new issue