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'