diff --git a/build.gradle b/build.gradle
index a70cc1f48..92c094a95 100644
--- a/build.gradle
+++ b/build.gradle
@@ -221,9 +221,18 @@ subprojects {
}
afterEvaluate {
- if (rootProject.enableDependencyLocking.toBoolean()) {
- // Lock application dependencies except for the gradle-license-report
- // plugin. See dependency_lic.gradle for the reason why.
+ if (rootProject.enableDependencyLocking.toBoolean()
+ && project.name != 'integration') {
+ // The ':integration' project runs server/schema integration tests using
+ // dynamically specified jars with no transitive dependency. Therefore
+ // dependency-locking does not make sense. Furthermore, during
+ // evaluation it resolves the 'testRuntime' configuration, making it
+ // immutable. Locking activation would trigger an invalid operation
+ // exception.
+ //
+ // For all other projects, due to problem with the gradle-license-report
+ // plugin, the dependencyLicenseReport configuration must opt out of
+ // dependency-locking. See dependency_lic.gradle for the reason why.
//
// To selectively activate dependency locking without hardcoding them
// in the 'configurations' block, the following code must run after
diff --git a/core/build.gradle b/core/build.gradle
index d9b78bca5..e58a8f2eb 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -16,6 +16,7 @@ import java.lang.reflect.Constructor
plugins {
id 'java-library'
+ id 'maven-publish'
}
// Path to code generated by ad hoc tasks in this project. A separate path is
@@ -112,6 +113,11 @@ configurations {
soy
closureCompiler
+ // Published jars that are used for server/schema compatibility tests.
+ // See the integration project
+ // for details.
+ nomulus_test
+
// Exclude non-canonical servlet-api jars. Our AppEngine deployment uses
// javax.servlet:servlet-api:2.5
// For reasons we do not understand, marking the following dependencies as
@@ -814,6 +820,66 @@ test {
createUberJar('nomulus', 'nomulus', 'google.registry.tools.RegistryTool')
project.nomulus.dependsOn project(':third_party').jar
+// A jar with classes and resources from main sourceSet, excluding internal
+// data. See comments on configurations.nomulus_test above for details.
+task nomulusFossJar (type: Jar) {
+ archiveBaseName = 'nomulus'
+ archiveClassifier = 'public'
+ from (project.sourceSets.main.output) {
+ exclude 'google/registry/config/files/**'
+ }
+ from (project.sourceSets.main.output) {
+ include 'google/registry/config/files/default-config.yaml'
+ include 'google/registry/config/files/nomulus-config-unittest.yaml'
+ }
+}
+
+// An UberJar of registry test classes, resources and all dependencies.
+// See comments on configurations.nomulus_test above for details.
+// TODO(weiminyu): extract shareable code with root.ext.createUberJar
+task testUberJar (
+ type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+ mergeServiceFiles()
+ archiveBaseName = 'nomulus-tests'
+ archiveClassifier = 'alldeps'
+ zip64 = true
+ archiveVersion = ''
+ configurations = [project.configurations.testRuntimeClasspath]
+ from project.sourceSets.test.output
+ // Excludes signature files that accompany some dependency jars, like
+ // bonuncycastle. If they are present, only classes from those signed jars are
+ // made available to the class loader.
+ // see https://discuss.gradle.org/t/signing-a-custom-gradle-plugin-thats-downloaded-by-the-build-system-from-github/1365
+ exclude "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA"
+ // Exclude SQL schema, which is a test dependency.
+ exclude 'sql/flyway/**'
+ // ShadowJar includes java source files when used on sourceSets.test.output.
+ // They are not needed.
+ exclude '**/*.java'
+}
+
+artifacts {
+ nomulus_test nomulusFossJar
+ nomulus_test testUberJar
+}
+
+publishing {
+ repositories {
+ maven {
+ url project.publish_repo
+ }
+ }
+ publications {
+ nomulusTestsPublication(MavenPublication) {
+ groupId 'google.registry'
+ artifactId 'nomulus_test'
+ version project.nomulus_version
+ artifact nomulusFossJar
+ artifact testUberJar
+ }
+ }
+}
+
task buildToolImage(dependsOn: nomulus, type: Exec) {
commandLine 'docker', 'build', '-t', 'nomulus-tool', '.'
}
diff --git a/core/src/test/java/google/registry/model/transaction/JpaTransactionManagerRule.java b/core/src/test/java/google/registry/model/transaction/JpaTransactionManagerRule.java
index ed8ceec45..cb98977ff 100644
--- a/core/src/test/java/google/registry/model/transaction/JpaTransactionManagerRule.java
+++ b/core/src/test/java/google/registry/model/transaction/JpaTransactionManagerRule.java
@@ -85,6 +85,7 @@ public class JpaTransactionManagerRule extends ExternalResource {
private static final String POSTGRES_DB_NAME = "postgres";
// Name of the optional property that specifies the root path of the golden schema.
+ // TODO(weiminyu): revert this. The :integration project offers a better solution.
@VisibleForTesting
static final String GOLDEN_SCHEMA_RESOURCE_ROOT_PROP = "sql_schema_resource_root";
diff --git a/db/build.gradle b/db/build.gradle
index 053fcaa12..d3b25c7d1 100644
--- a/db/build.gradle
+++ b/db/build.gradle
@@ -106,17 +106,18 @@ task compileApiJar(type: Jar) {
configurations {
compileApi
+ schema
}
artifacts {
- archives schemaJar
compileApi compileApiJar
+ schema schemaJar
}
publishing {
repositories {
maven {
- url project.schema_publish_repo
+ url project.publish_repo
}
}
publications {
diff --git a/gradle.properties b/gradle.properties
index 55bac8145..21db673a1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,7 +22,10 @@ dbName=postgres
dbUser=
dbPassword=
-# Maven repository of the Cloud SQL schema jar, which contains the
-# SQL DDL scripts.
-schema_publish_repo=
+# Maven repository that hosts the Cloud SQL schema jar and the registry
+# server test jars. Such jars are needed for server/schema integration tests.
+# Please refer to integration project
+# for more information.
+publish_repo=
schema_version=
+nomulus_version=
diff --git a/integration/README.md b/integration/README.md
new file mode 100644
index 000000000..557bd745a
--- /dev/null
+++ b/integration/README.md
@@ -0,0 +1,120 @@
+## Summary
+
+This project runs cross-version server/schema integration tests with arbitrary
+version pairs. It may be used by presubmit tests and continuous-integration
+tests, or as a gating test during release and/or deployment.
+
+## Maven Dependencies
+
+This release process is expected to publish the following Maven dependencies to
+a well-known repository:
+
+* google.registry:schema, which contains the schema DDL scripts. This is done
+ by the ':db:publish' task.
+* google.registry:nomulus_test, which contains the nomulus classes and
+ dependencies needed for the integration tests. This is done by the
+ ':core:publish' task.
+
+After each deployment in sandbox or production, the deployment process is
+expected to save the version tag of the binary or schema along with the
+environment. These tags will be made available to test runners.
+
+## Usage
+
+The ':integration:sqlIntegrationTest' task is the test runner. It uses the
+following properties:
+
+* nomulus_version: a Registry server release tag, or 'local' if the code in
+ the local Git tree should be used.
+* schema_version: a schema release tag, or 'local' if the code in the local
+ Git tree should be used.
+* publish_repo: the Maven repository where release jars may be found. This is
+ required if neither of the above is 'local'.
+
+Given a program 'fetch_version_tag' that retrieves the currently deployed
+version tag of SQL schema or server binary in a particular environment (which as
+mentioned earlier are saved by the deployment process), the following code
+snippet checks if the current PR or local clone has schema changes, and if yes,
+tests the production server's version with the new schema.
+
+```shell
+current_prod_schema=$(fetch_version_tag schema production)
+current_prod_server=$(fetch_version_tag server production)
+schema_changes=$(git diff ${current_prod_schema} --name-only \
+ ./db/src/main/resources/sql/flyway/ | wc -l)
+[[ schema_changes -gt 0 ]] && ./gradlew :integration:sqlIntegrationTest \
+ -Ppublish_repo=${REPO} -Pschema_version=local \
+ -Pnomulus_version=current_prod_server
+```
+
+## Implementation Notes
+
+### Run Tests from Jar
+
+Gradle test runner does not look for runnable tests in jars. We must extract
+tests to a directory. For now, only the SqlIntegrationTestSuite.class needs to
+be extracted. Gradle has no trouble finding its member classes.
+
+### Hibernate Behavior
+
+If all :core classes (main and test) and dependencies are assembled in a single
+jar, Hibernate behaves strangely: every time an EntityManagerFactory is created,
+regardless of what Entity classes are specified, Hibernate would scan the entire
+jar for all Entity classes and complain about duplicate mapping (due to the
+TestEntity classes declared by tests).
+
+We worked around this problem by creating two jars from :core:
+
+* The nomulus-public.jar: contains the classes and resources in the main
+ sourceSet (and excludes internal files under the config package).
+* The nomulus-tests-alldeps.jar: contains the test classes as well as all
+ dependencies.
+
+## Alternatives Tried
+
+### Use Git Branches
+
+One alternative is to rely on Git branches to set up the classes. For example,
+the shell snippet shown earlier can be implemented as:
+
+```shell
+current_prod_schema=$(fetch_version_tag schema production)
+current_prod_server=$(fetch_version_tag server production)
+schema_changes=$(git diff ${current_prod_schema} --name-only \
+ ./db/src/main/resources/sql/flyway/ | wc -l)
+
+if [[ schema_changes -gt 0 ]]; then
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
+ schema_folder=$(mktemp -d)
+ ./gradlew :db:schemaJar && cp ./db/build/libs/schema.jar ${schema_folder}
+ git checkout ${current_prod_server}
+ ./gradlew sqlIntegrationTest \
+ -Psql_schema_resource_root=${schema_folder}/schema.jar
+ git checkout ${current_branch}
+fi
+```
+
+The drawbacks of this approach include:
+
+* Switching branches back and forth is error-prone and risky, especially when
+ we run this as a gating test during release.
+* Switching branches makes implicit assumptions on how the test platform would
+ check out the repository (e.g., whether we may be on a headless branch when
+ we switch).
+* The generated jar is not saved, making it harder to troubleshoot.
+* To use this locally during development, the Git tree must not have
+ uncommitted changes.
+
+### Smaller Jars
+
+Another alternative follows the same idea as our current approach. However,
+instead of including dependencies in a fat jar, it simply records their versions
+in a file. At testing time these dependencies will be imported into the gradle
+project file with forced resolution (e.g., testRuntime ('junit:junit:4.12)'
+{forced = true} ). This way the published jars will be smaller.
+
+This approach conflicts with our current dependency-locking processing. Due to
+issues with the license-check plugin, dependency-locking is activated after all
+projects are evaluated. This approach will resolve some configurations in :core
+(and make them immutable) during evaluation, causing the lock-activation (which
+counts as a mutation) call to fail.
diff --git a/integration/build.gradle b/integration/build.gradle
new file mode 100644
index 000000000..134604afb
--- /dev/null
+++ b/integration/build.gradle
@@ -0,0 +1,99 @@
+// 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.
+
+// This source-less project is used to run cross-release server/SQL integration
+// tests. See the README.md file in this folder for more information.
+
+import static com.google.common.base.Preconditions.checkArgument
+import static com.google.common.base.Strings.isNullOrEmpty
+
+if (schema_version == '' || nomulus_version == '') {
+ return
+}
+
+def USE_LOCAL = 'local'
+
+if (schema_version != USE_LOCAL || nomulus_version != USE_LOCAL) {
+ checkArgument(
+ !isNullOrEmpty(publish_repo),
+ 'The publish_repo is required when remote jars are needed.')
+
+ repositories {
+ maven {
+ url project.publish_repo
+ }
+ }
+}
+
+def testUberJarName = ''
+
+dependencies {
+ gradleLint.ignore('unused-dependency') {
+ if (schema_version == USE_LOCAL) {
+ testRuntime project(path: ':db', configuration: 'schema')
+ } else {
+ testRuntime "google.registry:schema:${schema_version}"
+ }
+ if (nomulus_version == USE_LOCAL) {
+ testRuntime project(path: ':core', configuration: 'nomulus_test')
+ testUberJarName = 'nomulus-tests-alldeps.jar'
+ } else {
+ testRuntime "google.registry:nomulus_test:${nomulus_version}:public"
+ testRuntime "google.registry:nomulus_test:${nomulus_version}:alldeps"
+ testUberJarName = "nomulus_test-${nomulus_version}-alldeps.jar"
+ }
+ }
+}
+
+configurations.testRuntime.transitive = false
+
+def unpackedTestDir = "${projectDir}/build/unpackedTests/${nomulus_version}"
+
+// Extracts SqlIntegrationTestSuite.class to a temp folder. Gradle's test
+// runner only looks for runnable tests on a regular file system. However,
+// it can load member classes of test suites from jars.
+task extractSqlIntegrationTestSuite (type: Copy) {
+ doFirst {
+ file(unpackedTestDir).mkdirs()
+ }
+ outputs.dir unpackedTestDir
+ from zipTree(
+ configurations.testRuntime
+ .filter { it.name == testUberJarName}
+ .singleFile).matching {
+ include 'google/registry/**/SqlIntegrationTestSuite.class'
+ }
+ into unpackedTestDir
+ includeEmptyDirs = false
+}
+
+// TODO(weiminyu): inherit from FilteringTest (defined in :core).
+task sqlIntegrationTest(type: Test) {
+ testClassesDirs = files(unpackedTestDir)
+ classpath = configurations.testRuntime
+ include 'google/registry/schema/integration/SqlIntegrationTestSuite.*'
+
+ dependsOn extractSqlIntegrationTestSuite
+
+ finalizedBy tasks.create('removeUnpackedTests') {
+ doLast {
+ delete file(unpackedTestDir)
+ }
+ }
+
+ // Disable incremental build/test since Gradle cannot detect changes
+ // in dependencies on its own. Will not fix since this test is typically
+ // run once (in presubmit or ci tests).
+ outputs.upToDateWhen { false }
+}
diff --git a/release/cloudbuild-nomulus.yaml b/release/cloudbuild-nomulus.yaml
index a3f7fbaf1..11af6762e 100644
--- a/release/cloudbuild-nomulus.yaml
+++ b/release/cloudbuild-nomulus.yaml
@@ -65,8 +65,9 @@ steps:
# Build and package the deployment files for production.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
args: ['release/build_nomulus_for_env.sh', 'production', 'output']
-# Tentatively build Cloud SQL schema jar here, before schema release process
-# is finalized.
+# Tentatively build and publish Cloud SQL schema jar here, before schema release
+# process is finalized. Also publish nomulus:core jars that are needed for
+# server/schema compatibility tests.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
args:
@@ -74,9 +75,20 @@ steps:
- |
set -e
./gradlew \
- :db:schemaJar \
+ :db:publish \
-PmavenUrl=https://storage.googleapis.com/domain-registry-maven-repository/maven \
- -PpluginsUrl=https://storage.googleapis.com/domain-registry-maven-repository/plugins
+ -PpluginsUrl=https://storage.googleapis.com/domain-registry-maven-repository/plugins \
+ -Ppublish_repo=gcs://${PROJECT_ID}-deployed-tags/maven \
+ -Pschema_version=${TAG_NAME}
+ ./gradlew \
+ :core:publish \
+ -PmavenUrl=https://storage.googleapis.com/domain-registry-maven-repository/maven \
+ -PpluginsUrl=https://storage.googleapis.com/domain-registry-maven-repository/plugins \
+ -Ppublish_repo=gcs://${PROJECT_ID}-deployed-tags/maven \
+ -Pnomulus_version=${TAG_NAME}
+ # Upload schema jar for use by schema deployment.
+ # TODO(weiminyu): consider using the jar in maven repo during deployment and
+ # stop the upload here.
cp db/build/libs/schema.jar output/
# The tarballs and jars to upload to GCS.
artifacts:
diff --git a/settings.gradle b/settings.gradle
index 26b58c582..a93bfce7f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -30,6 +30,7 @@ rootProject.name = 'nomulus'
include 'common'
include 'core'
include 'db'
+include 'integration'
include 'networking'
include 'prober'
include 'proxy'