| #!/usr/bin/env python3 | |
| # | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with this | |
| # work for additional information regarding copyright ownership. The ASF | |
| # licenses this file to You 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. | |
| # | |
| """SDK provenance fixtures and an opt-in real Maven parent-metadata regression.""" | |
| import copy | |
| import hashlib | |
| import json | |
| import os | |
| import re | |
| from unittest.mock import patch | |
| from pathlib import Path | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import textwrap | |
| import unittest | |
| import uuid | |
| import xml.etree.ElementTree as ET | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "hugegraph-client/assembly/travis")) | |
| import verify_candidate_image_sdk as sdk | |
| class CandidateParentModelTest(unittest.TestCase): | |
| @unittest.skipUnless(os.environ.get("CANDIDATE_SDK_MAVEN_TEST"), | |
| "Set CANDIDATE_SDK_MAVEN_TEST to a Maven executable for the metadata regression") | |
| def test_license_metadata_can_rebuild_installed_root_parent(self): | |
| # Use the real metadata plugin and unique coordinates, never the user's SDK artifacts. | |
| maven = os.environ["CANDIDATE_SDK_MAVEN_TEST"] | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| settings = root / "settings.xml" | |
| settings.write_text("<settings/>") | |
| header = '<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion>' | |
| flatten = ('<plugin><groupId>org.codehaus.mojo</groupId><artifactId>flatten-maven-plugin</artifactId>' | |
| '<version>1.3.0</version><configuration><updatePomFile>true</updatePomFile>' | |
| '<flattenMode>resolveCiFriendliesOnly</flattenMode></configuration><executions>' | |
| '<execution><phase>process-resources</phase><goals><goal>flatten</goal></goals>' | |
| '</execution></executions></plugin>') | |
| for fixed in (False, True): | |
| group = "org.example.candidatesdk.g" + uuid.uuid4().hex | |
| source = root / group | |
| source.mkdir() | |
| root_pom = (header + '<parent><groupId>org.apache</groupId><artifactId>apache</artifactId>' | |
| '<version>23</version></parent><groupId>' + group + '</groupId>' | |
| '<artifactId>root</artifactId><version>${revision}</version><packaging>pom</packaging>' | |
| '<properties><revision>1.8.0</revision></properties><modules><module>dependency</module>' | |
| '<module>consumer</module></modules><build><plugins><plugin>' | |
| '<artifactId>maven-remote-resources-plugin</artifactId><version>3.3.0</version>' | |
| '</plugin></plugins></build></project>') | |
| (source / "pom.xml").write_text(root_pom) | |
| parent = ('<parent><groupId>' + group + '</groupId><artifactId>root</artifactId>' | |
| '<version>${revision}</version></parent>') | |
| for module in ("dependency", "consumer"): | |
| directory = source / module | |
| directory.mkdir() | |
| body = flatten if module == "dependency" else "" | |
| dependency = (('<dependencies><dependency><groupId>' + group + '</groupId>' | |
| '<artifactId>dependency</artifactId><version>1.8.0</version>' | |
| '</dependency></dependencies>') if module == "consumer" else "") | |
| (directory / "pom.xml").write_text(header + parent + '<artifactId>' + module + '</artifactId>' | |
| + dependency + '<build><plugins>' + body | |
| + '</plugins></build></project>') | |
| original_poms = {p: p.read_bytes() for p in source.rglob("pom.xml")} | |
| def run(*arguments): | |
| return subprocess.run([maven, "--settings", str(settings), | |
| "-Dmaven.repo.local=" + str(root / "m2"), "-B", "-ntp", *arguments], | |
| cwd=source, capture_output=True, text=True, timeout=240) | |
| goals = (["org.codehaus.mojo:flatten-maven-plugin:1.3.0:flatten", "install", | |
| "-Dflatten.mode=resolveCiFriendliesOnly", "-DupdatePomFile=true"] | |
| if fixed else ["install"]) | |
| literal_request = group + ":root:pom:${revision}" | |
| installed = run(*goals, "-DskipTests") | |
| if fixed or installed.returncode == 0: | |
| self.assertEqual(0, installed.returncode, installed.stdout + installed.stderr) | |
| else: | |
| # Newer resolvers fail here; older ones only warn on the leaf metadata goal. | |
| self.assertIn(literal_request, installed.stdout + installed.stderr) | |
| metadata_goal = ("org.apache.maven.plugins:maven-remote-resources-plugin:3.3.0:" | |
| "process@process-resource-bundles") | |
| metadata = run(metadata_goal, "-f", "consumer/pom.xml", "-X") | |
| output = metadata.stdout + metadata.stderr | |
| if fixed: | |
| self.assertEqual(0, metadata.returncode, output) | |
| self.assertNotIn(literal_request, output) | |
| self.assertNotIn("Failed to build parent project", output) | |
| version = ET.parse(source / ".flattened-pom.xml").getroot().findtext( | |
| "{http://maven.apache.org/POM/4.0.0}version") | |
| self.assertEqual("1.8.0", version) | |
| else: | |
| self.assertIn(literal_request, output) | |
| for path, original in original_poms.items(): | |
| self.assertEqual(original, path.read_bytes()) | |
| @unittest.skipUnless(os.environ.get("CANDIDATE_SDK_MAVEN_TEST"), | |
| "Set CANDIDATE_SDK_MAVEN_TEST for the consumer-version regression") | |
| def test_maven_consumers_resolve_the_selected_sdk_version(self): | |
| # A floating SDK changes coordinates; the consumer's fixed default must | |
| # not silently resolve a different release from another repository. | |
| import zipfile | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| group = "org.example.candidatesdk.g" + uuid.uuid4().hex | |
| repository = Path.home() / ".m2/repository" / group.replace(".", "/") | |
| self.addCleanup(shutil.rmtree, repository, True) | |
| artifact = repository / "sdk/1.9.0" | |
| artifact.mkdir(parents=True) | |
| header = '<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion>' | |
| (artifact / "sdk-1.9.0.pom").write_text( | |
| header + '<groupId>' + group + '</groupId><artifactId>sdk</artifactId>' | |
| '<version>1.9.0</version></project>') | |
| with zipfile.ZipFile(artifact / "sdk-1.9.0.jar", "w") as jar: | |
| jar.writestr("sdk-source.txt", "selected upstream revision") | |
| (artifact / "_remote.repositories").write_text("sdk-1.9.0.pom>=\nsdk-1.9.0.jar>=\n") | |
| pom = root / "pom.xml" | |
| pom.write_text(header + '<groupId>org.example.consumer</groupId><artifactId>consumer</artifactId>' | |
| '<version>1</version><properties><hugegraph.version>1.8.0</hugegraph.version>' | |
| '</properties><dependencies><dependency><groupId>' + group + '</groupId>' | |
| '<artifactId>sdk</artifactId><version>${hugegraph.version}</version></dependency>' | |
| '</dependencies></project>') | |
| settings = root / "settings.xml" | |
| settings.write_text("<settings/>") | |
| command = [os.environ["CANDIDATE_SDK_MAVEN_TEST"], "-o", "-ntp", "--settings", str(settings), | |
| "-f", str(pom), "org.apache.maven.plugins:maven-dependency-plugin:3.1.1:build-classpath", | |
| "-Dmdep.outputFile=" + str(root / "classpath")] | |
| env = dict(os.environ, MAVEN_ARGS="-Dhugegraph.version=1.9.0") | |
| # Warm only Maven's public plugin dependencies when this opt-in | |
| # regression runs on a fresh machine; coordinate checks stay offline. | |
| prepared = subprocess.run([arg for arg in command if arg != "-o"], env=env, | |
| capture_output=True, text=True, timeout=90) | |
| self.assertEqual(0, prepared.returncode, prepared.stdout + prepared.stderr) | |
| (root / "classpath").unlink() | |
| env["MAVEN_ARGS"] = "" | |
| wrong = subprocess.run(command, env=env, capture_output=True, text=True, timeout=90) | |
| self.assertNotEqual(0, wrong.returncode, wrong.stdout + wrong.stderr) | |
| self.assertIn(group + ":sdk:jar:1.8.0", wrong.stdout + wrong.stderr) | |
| env["MAVEN_ARGS"] = "-Dhugegraph.version=1.9.0" | |
| selected = subprocess.run(command, env=env, capture_output=True, text=True, timeout=90) | |
| self.assertEqual(0, selected.returncode, selected.stdout + selected.stderr) | |
| self.assertEqual(str(artifact / "sdk-1.9.0.jar"), (root / "classpath").read_text().strip()) | |
| @unittest.skipUnless(os.environ.get("CANDIDATE_SDK_MAVEN_TEST"), | |
| "Set CANDIDATE_SDK_MAVEN_TEST for the aggregate-coordinate regression") | |
| def test_toolchain_dist_coordinate_keeps_the_release_archive_name(self): | |
| # Exercise Maven's real reactor selector and interpolation without packaging the full distribution. | |
| with tempfile.TemporaryDirectory() as temporary: | |
| output = Path(temporary) / "effective-pom.xml" | |
| source = Path(__file__).resolve().parents[2] | |
| result = subprocess.run([os.environ["CANDIDATE_SDK_MAVEN_TEST"], "-f", str(source / "pom.xml"), | |
| "-pl", ":hugegraph-toolchain-dist", "-ntp", "-B", | |
| "org.apache.maven.plugins:maven-help-plugin:3.2.0:effective-pom", | |
| "-Doutput=" + str(output)], capture_output=True, text=True, timeout=90) | |
| self.assertEqual(0, result.returncode, result.stdout + result.stderr) | |
| model = ET.parse(output).getroot() | |
| ns = "{http://maven.apache.org/POM/4.0.0}" | |
| self.assertEqual("org.apache.hugegraph", model.findtext(ns + "groupId")) | |
| self.assertEqual("hugegraph-toolchain-dist", model.findtext(ns + "artifactId")) | |
| self.assertEqual("1.8.0", model.findtext(ns + "version")) | |
| properties = model.find(ns + "properties") | |
| self.assertEqual("hugegraph-toolchain", properties.findtext(ns + "release.name")) | |
| self.assertEqual("apache-hugegraph-toolchain-1.8.0", properties.findtext(ns + "final.name")) | |
| self.assertEqual("hugegraph-toolchain-dist-1.8.0", model.findtext(ns + "build/" + ns + "finalName")) | |
| class CandidateDistributionTest(unittest.TestCase): | |
| def setUp(self): | |
| self.source_commit = sdk.COMMIT | |
| context = patch.dict(os.environ, {"CANDIDATE_SOURCE_COMMIT": self.source_commit}) | |
| context.start() | |
| self.addCleanup(context.stop) | |
| def fixture(self, root, revision="1.8.0"): | |
| repository = root / "m2" | |
| artifacts, modules = [], [] | |
| for source_pom in sorted(sdk.REQUIRED_MODULES): | |
| artifact = Path(source_pom).parent.name if source_pom != "pom.xml" else "hugegraph" | |
| packaging = "pom" if (source_pom.count("/") == 1 and artifact != "hugegraph-struct") or source_pom == "pom.xml" else "jar" | |
| files = [] | |
| for extension in (["pom", "jar"] if packaging == "jar" else ["pom"]): | |
| filename = artifact + "-" + revision + "." + extension | |
| path = repository / "org/apache/hugegraph" / artifact / revision / filename | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_bytes(filename.encode()) | |
| with (path.parent / "_remote.repositories").open("a") as origins: | |
| origins.write(filename + ">=\n") | |
| relative = str(path.relative_to(repository)) | |
| files.append(relative) | |
| artifacts.append({"path": relative, "sha256": sdk.digest(path), "source_reactor_install": True}) | |
| modules.append({"group_id": "org.apache.hugegraph", "artifact_id": artifact, | |
| "version": revision, "packaging": packaging, | |
| "source_pom": source_pom, "files": files}) | |
| manifest = {"repository": sdk.REPOSITORY, "commit": self.source_commit, | |
| "source_revision": revision, "java_version": "17", | |
| "required_sdk_modules": modules, "artifacts": artifacts} | |
| self.write_manifest(repository, manifest) | |
| return repository, manifest | |
| def test_floating_sdk_version_preserves_verified_coordinates_and_maven_arguments(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root, "1.9.0-SNAPSHOT") | |
| environment = root / "github-env" | |
| # Fixed-baseline validation must still reject the newer revision. | |
| with self.assertRaises(RuntimeError): | |
| sdk.validate_sdk(repository) | |
| original = "--settings settings.xml -Dmaven.repo.local=/old -Dhugegraph.version=1.8.0" | |
| with patch.dict(os.environ, {"MAVEN_ARGS": original, "MAVEN_OPTS": "-Xmx1g -Dsdk.validation.mode=release"}): | |
| sdk.configure_upstream(repository, environment) | |
| lines = dict(line.split("=", 1) for line in environment.read_text().splitlines()) | |
| self.assertEqual("1.9.0-SNAPSHOT", lines["CANDIDATE_SDK_VERSION"]) | |
| for key in ("MAVEN_ARGS", "MAVEN_OPTS"): | |
| self.assertIn("-Dmaven.repo.local=" + str(repository), lines[key]) | |
| self.assertIn("-Dhugegraph.version=1.9.0-SNAPSHOT", lines[key]) | |
| self.assertIn("-Dsdk.validation.mode=candidate", lines[key]) | |
| for property_name in ("maven.repo.local", "hugegraph.version", "sdk.validation.mode"): | |
| self.assertEqual(1, lines[key].count("-D" + property_name + "=")) | |
| self.assertIn("--settings settings.xml", lines["MAVEN_ARGS"]) | |
| self.assertIn("-Xmx1g", lines["MAVEN_OPTS"]) | |
| with patch.dict(os.environ, lines): | |
| sdk.validate_sdk(repository) | |
| distribution = self.distribution(root, repository, manifest) | |
| sdk.validate_distribution(repository, distribution, "loader") | |
| # Hash checks must remain enforced for the floating version. | |
| jar = repository / next(a["path"] for a in manifest["artifacts"] | |
| if a["path"].endswith(".jar")) | |
| jar.write_bytes(b"changed SDK") | |
| with self.assertRaises(RuntimeError): | |
| sdk.validate_sdk(repository) | |
| with self.assertRaises(RuntimeError): | |
| sdk.configure_upstream(repository, root / "invalid-env") | |
| self.assertFalse((root / "invalid-env").exists()) | |
| def test_configuration_and_distribution_cli_options_are_separate_steps(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root, "1.8.0") | |
| distribution = self.distribution(root, repository, manifest) | |
| environment = root / "github-env" | |
| result = subprocess.run(["python3", sdk.__file__, str(repository), | |
| "--configure-upstream-env", str(environment), | |
| "--distribution", str(distribution), "--module", "loader"], | |
| capture_output=True, text=True, timeout=10) | |
| self.assertEqual(2, result.returncode) | |
| self.assertIn("cannot be combined", result.stderr) | |
| self.assertFalse(environment.exists()) | |
| def test_floating_distribution_cli_uses_its_explicit_sdk_version(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root, "1.9.0-SNAPSHOT") | |
| distribution = self.distribution(root, repository, manifest) | |
| result = subprocess.run(["python3", sdk.__file__, str(repository), "--version", "1.9.0-SNAPSHOT", | |
| "--distribution", str(distribution), "--module", "hubble"], | |
| capture_output=True, text=True, timeout=10) | |
| self.assertEqual(0, result.returncode, result.stderr) | |
| def write_manifest(self, repository, manifest): | |
| (repository / "candidate-sdk-manifest.json").write_text(json.dumps(manifest)) | |
| def distribution(self, root, repository, manifest): | |
| directory = root / "distribution" | |
| library = directory / "lib" | |
| library.mkdir(parents=True) | |
| self.write_manifest(directory, manifest) | |
| for item in manifest["artifacts"]: | |
| path = repository / item["path"] | |
| if path.suffix == ".jar": | |
| shutil.copyfile(path, library / path.name) | |
| (library / "hugegraph-client-1.8.0.jar").write_bytes(b"toolchain fixture") | |
| return directory | |
| def test_distribution_requires_original_manifest_and_exact_sdk_bytes(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root) | |
| directory = self.distribution(root, repository, manifest) | |
| for module in ("loader", "tools", "hubble"): | |
| self.assertEqual(9, len(sdk.validate_distribution(repository, directory, module))) | |
| common = directory / "lib/hugegraph-common-1.8.0.jar" | |
| common.write_bytes(b"Central artifact with the same GAV") | |
| with self.assertRaisesRegex(RuntimeError, "changed packaged"): | |
| sdk.validate_distribution(repository, directory, "loader") | |
| def test_stale_missing_and_duplicate_libraries_fail_closed(self): | |
| for scenario in ("missing", "old_version", "duplicate", "wrong_manifest", "missing_manifest", "symlink"): | |
| with self.subTest(scenario=scenario), tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root) | |
| directory = self.distribution(root, repository, manifest) | |
| common = directory / "lib/hugegraph-common-1.8.0.jar" | |
| if scenario == "missing": | |
| common.unlink() | |
| elif scenario in ("old_version", "duplicate"): | |
| old = common.with_name("hugegraph-common-1.5.0.jar") | |
| if scenario == "old_version": | |
| common.rename(old) | |
| else: | |
| shutil.copyfile(common, old) | |
| elif scenario == "wrong_manifest": | |
| wrong = copy.deepcopy(manifest) | |
| wrong["commit"] = "0" * 40 | |
| self.write_manifest(directory, wrong) | |
| elif scenario == "missing_manifest": | |
| (directory / "candidate-sdk-manifest.json").unlink() | |
| else: | |
| common.unlink() | |
| common.symlink_to(repository / "org/apache/hugegraph/hugegraph-common/1.8.0/hugegraph-common-1.8.0.jar") | |
| with self.assertRaises(RuntimeError): | |
| sdk.validate_distribution(repository, directory, "loader") | |
| def test_hubble_requires_core_and_other_modules_do_not(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root) | |
| directory = self.distribution(root, repository, manifest) | |
| (directory / "lib/hugegraph-core-1.8.0.jar").unlink() | |
| sdk.validate_distribution(repository, directory, "tools") | |
| with self.assertRaisesRegex(RuntimeError, "hugegraph-core"): | |
| sdk.validate_distribution(repository, directory, "hubble") | |
| def test_repository_hashes_and_source_install_are_required(self): | |
| for scenario in ("wrong_source", "wrong_origin", "missing_parent", "changed_pom", "false_origin"): | |
| with self.subTest(scenario=scenario), tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root) | |
| directory = self.distribution(root, repository, manifest) | |
| pom = repository / "org/apache/hugegraph/hugegraph/1.8.0/hugegraph-1.8.0.pom" | |
| if scenario == "wrong_source": | |
| manifest["commit"] = "0" * 40 | |
| elif scenario == "false_origin": | |
| manifest["artifacts"][0]["source_reactor_install"] = "false" | |
| elif scenario == "wrong_origin": | |
| (pom.parent / "_remote.repositories").write_text(pom.name + ">central=\n") | |
| elif scenario == "changed_pom": | |
| pom.write_bytes(b"Central POM") | |
| else: | |
| pom.unlink() | |
| self.write_manifest(repository, manifest) | |
| with self.assertRaises(RuntimeError): | |
| sdk.validate_distribution(repository, directory, "hubble") | |
| def test_reactor_installs_do_not_change_the_locked_sdk_contract(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, manifest = self.fixture(root) | |
| outputs = [] | |
| for artifact, extension in (("hugegraph-toolchain", "pom"), | |
| ("hugegraph-toolchain-dist", "jar"), | |
| ("hugegraph-client", "jar"), | |
| ("hugegraph-loader", "jar"), | |
| ("hubble-be", "jar")): | |
| path = repository / "org/apache/hugegraph" / artifact / "1.8.0" / (artifact + "-1.8.0." + extension) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_bytes(b"previous Toolchain output") | |
| manifest["artifacts"].append({"path": str(path.relative_to(repository)), | |
| "sha256": sdk.digest(path), "source_reactor_install": True}) | |
| outputs.append(path) | |
| # The distinct Server dist coordinate is an immutable source input. | |
| server_dist = repository / "org/apache/hugegraph/hugegraph-dist/1.8.0/hugegraph-dist-1.8.0.jar" | |
| server_dist.parent.mkdir(parents=True, exist_ok=True) | |
| server_dist.write_bytes(b"locked Server distribution") | |
| manifest["artifacts"].append({"path": str(server_dist.relative_to(repository)), | |
| "sha256": sdk.digest(server_dist), "source_reactor_install": True}) | |
| self.write_manifest(repository, manifest) | |
| directory = self.distribution(root, repository, manifest) | |
| # Aggregate dist JARs are not module runtime libraries. | |
| (directory / "lib/hugegraph-dist-1.8.0.jar").unlink() | |
| (directory / "lib/hugegraph-toolchain-dist-1.8.0.jar").unlink() | |
| before = (repository / "candidate-sdk-manifest.json").read_bytes() | |
| for path in outputs: | |
| # Simulate parent/Client install, then Loader install, then Hubble install. | |
| path.write_bytes(b"new reactor output after compile") | |
| if path.suffix == ".jar" and path.name != "hugegraph-toolchain-dist-1.8.0.jar": | |
| shutil.copyfile(path, directory / "lib" / path.name) | |
| for module in ("loader", "tools", "hubble"): | |
| sdk.validate_distribution(repository, directory, module) | |
| self.assertEqual(before, (repository / "candidate-sdk-manifest.json").read_bytes()) | |
| self.assertEqual(b"locked Server distribution", server_dist.read_bytes()) | |
| server_dist.write_bytes(b"overwritten Server distribution") | |
| with self.assertRaisesRegex(RuntimeError, "SDK artifact hash mismatch"): | |
| sdk.validate_sdk(repository) | |
| server_dist.write_bytes(b"locked Server distribution") | |
| common = repository / "org/apache/hugegraph/hugegraph-common/1.8.0/hugegraph-common-1.8.0.jar" | |
| common.write_bytes(b"replaced Server SDK input") | |
| with self.assertRaisesRegex(RuntimeError, "SDK artifact hash mismatch"): | |
| sdk.validate_distribution(repository, directory, "hubble") | |
| def test_legacy_dist_output_at_another_revision_does_not_change_server_inputs(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, manifest = self.fixture(Path(temporary), "1.9.0-SNAPSHOT") | |
| legacy = repository / "org/apache/hugegraph/hugegraph-dist/1.8.0/hugegraph-dist-1.8.0.jar" | |
| legacy.parent.mkdir(parents=True) | |
| legacy.write_bytes(b"legacy Toolchain output") | |
| manifest["artifacts"].append({"path": str(legacy.relative_to(repository)), | |
| "sha256": sdk.digest(legacy), "source_reactor_install": True}) | |
| self.write_manifest(repository, manifest) | |
| legacy.write_bytes(b"updated legacy Toolchain output") | |
| sdk.validate_sdk(repository, "1.9.0-SNAPSHOT") | |
| def test_source_context_preserves_full_identity_checks(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, manifest = self.fixture(Path(temporary)) | |
| manifest["commit"] = sdk.COMMIT[:6] + "b" * 34 | |
| self.write_manifest(repository, manifest) | |
| with self.assertRaisesRegex(RuntimeError, "SDK source does not match"): | |
| sdk.validate_sdk(repository) | |
| with patch.dict(os.environ, {"CANDIDATE_SOURCE_COMMIT": sdk.COMMIT[:6]}): | |
| with self.assertRaisesRegex(RuntimeError, "Invalid candidate SDK manifest"): | |
| sdk.validate_sdk(repository) | |
| def test_explicit_resolved_context_supports_latest_master_provenance(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, manifest = self.fixture(Path(temporary)) | |
| selected = "b" * 40 | |
| manifest["commit"] = selected | |
| self.write_manifest(repository, manifest) | |
| with patch.dict(os.environ, {"CANDIDATE_SOURCE_COMMIT": selected}): | |
| sdk.validate_sdk(repository) | |
| with self.assertRaisesRegex(RuntimeError, "SDK source does not match"): | |
| sdk.validate_sdk(repository) | |
| def test_standalone_packaging_uses_the_local_lock(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, _ = self.fixture(Path(temporary)) | |
| with patch.dict(os.environ, {}, clear=True): | |
| sdk.validate_sdk(repository) | |
| def test_candidate_manifest_requires_the_supported_build_jdk(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, manifest = self.fixture(Path(temporary)) | |
| for version in ("11", "18", "21"): | |
| with self.subTest(version=version): | |
| manifest["java_version"] = version | |
| self.write_manifest(repository, manifest) | |
| with self.assertRaisesRegex(RuntimeError, "Java version"): | |
| sdk.validate_sdk(repository) | |
| for version in (17.9, 17, True, "17.0"): | |
| with self.subTest(version=version): | |
| manifest["java_version"] = version | |
| self.write_manifest(repository, manifest) | |
| with self.assertRaisesRegex(RuntimeError, "Invalid candidate SDK manifest"): | |
| sdk.validate_sdk(repository) | |
| manifest["java_version"] = "17" | |
| self.write_manifest(repository, manifest) | |
| sdk.validate_sdk(repository) | |
| def test_malformed_manifests_have_controlled_diagnostics(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, manifest = self.fixture(Path(temporary)) | |
| bad = copy.deepcopy(manifest) | |
| bad["java_version"] = "not an integer password=secret" | |
| malformed = ("{", "null", "[]", "{}", json.dumps(bad), | |
| json.dumps({**manifest, "required_sdk_modules": None}), | |
| json.dumps({**manifest, "artifacts": [None]})) | |
| for content in malformed: | |
| with self.subTest(content=content[:20]): | |
| (repository / "candidate-sdk-manifest.json").write_text(content) | |
| with self.assertRaisesRegex(RuntimeError, "Invalid candidate SDK manifest") as failure: | |
| sdk.validate_sdk(repository) | |
| self.assertNotIn("secret", str(failure.exception)) | |
| result = subprocess.run(["python3", str(Path(sdk.__file__)), str(repository)], | |
| capture_output=True, text=True, timeout=5) | |
| self.assertEqual(1, result.returncode) | |
| self.assertNotIn("Traceback", result.stderr) | |
| self.assertNotIn("secret", result.stderr) | |
| class ReleaseDistributionTest(unittest.TestCase): | |
| def fixture(self, root, version="1.8.0"): | |
| repository, directory = root / "m2", root / "distribution" | |
| library = directory / "lib" | |
| library.mkdir(parents=True) | |
| for artifact in ("hugegraph-common", "hg-pd-common", "hg-pd-client", "hg-pd-grpc", "hugegraph-core"): | |
| parent = repository / "org/apache/hugegraph" / artifact / version | |
| parent.mkdir(parents=True) | |
| pom = ('<project xmlns="http://maven.apache.org/POM/4.0.0"><parent>' | |
| '<groupId>org.apache.hugegraph</groupId><artifactId>hugegraph</artifactId>' | |
| '<version>' + version + '</version></parent><artifactId>' + artifact + '</artifactId></project>') | |
| (parent / (artifact + "-" + version + ".pom")).write_text(pom) | |
| jar = parent / (artifact + "-" + version + ".jar") | |
| jar.write_bytes(artifact.encode()) | |
| shutil.copyfile(jar, library / jar.name) | |
| (library / "hugegraph-client-1.8.0.jar").write_bytes(b"Toolchain output") | |
| return repository, directory | |
| def test_release_uses_exact_maven_artifacts_without_a_candidate_manifest(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, directory = self.fixture(Path(temporary)) | |
| for module in ("loader", "tools", "hubble"): | |
| self.assertEqual(5, len(sdk.validate_release_sdk(repository, "1.8.0", directory, module))) | |
| command = ["python3", str(Path(sdk.__file__)), str(repository), "--mode", "release", | |
| "--version", "1.8.0", "--distribution", str(directory), "--module", "hubble"] | |
| result = subprocess.run(command, capture_output=True, text=True, timeout=5) | |
| self.assertEqual(0, result.returncode, result.stderr) | |
| self.assertIn("release SDK 1.8.0", result.stdout) | |
| def test_release_rejects_missing_mixed_and_replaced_sdk_inputs(self): | |
| scenarios = ("missing_jar", "missing_pom", "missing_library", "mixed_version", "duplicate_version", | |
| "replaced_library", "stale_repo_manifest", "stale_package_manifest", "wrong_pom_version", | |
| "invalid_pom", "repository_symlink", "package_symlink", "library_directory_symlink") | |
| for scenario in scenarios: | |
| with self.subTest(scenario=scenario), tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| repository, directory = self.fixture(root) | |
| common = repository / "org/apache/hugegraph/hugegraph-common/1.8.0/hugegraph-common-1.8.0.jar" | |
| packaged = directory / "lib" / common.name | |
| pom = common.with_suffix(".pom") | |
| if scenario == "missing_jar": | |
| common.unlink() | |
| elif scenario == "missing_pom": | |
| pom.unlink() | |
| elif scenario == "missing_library": | |
| packaged.unlink() | |
| elif scenario in ("mixed_version", "duplicate_version"): | |
| old = packaged.with_name("hugegraph-common-1.7.0.jar") | |
| shutil.copyfile(packaged, old) | |
| if scenario == "mixed_version": | |
| packaged.unlink() | |
| elif scenario == "replaced_library": | |
| packaged.write_bytes(b"Other repository's same GAV") | |
| elif scenario in ("stale_repo_manifest", "stale_package_manifest"): | |
| target = repository if scenario == "stale_repo_manifest" else directory | |
| (target / "candidate-sdk-manifest.json").write_text("{}") | |
| elif scenario == "wrong_pom_version": | |
| pom.write_text(pom.read_text().replace("1.8.0", "1.7.0")) | |
| elif scenario == "invalid_pom": | |
| pom.write_text("<project>") | |
| elif scenario == "repository_symlink": | |
| common.unlink() | |
| common.symlink_to(packaged) | |
| elif scenario == "library_directory_symlink": | |
| external = root / "external-library" | |
| (directory / "lib").rename(external) | |
| (directory / "lib").symlink_to(external, target_is_directory=True) | |
| else: | |
| packaged.unlink() | |
| packaged.symlink_to(common) | |
| with self.assertRaises(RuntimeError): | |
| sdk.validate_release_sdk(repository, "1.8.0", directory, "loader") | |
| def test_release_hubble_requires_core_but_loader_does_not(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, directory = self.fixture(Path(temporary)) | |
| (directory / "lib/hugegraph-core-1.8.0.jar").unlink() | |
| sdk.validate_release_sdk(repository, "1.8.0", directory, "loader") | |
| with self.assertRaisesRegex(RuntimeError, "hugegraph-core"): | |
| sdk.validate_release_sdk(repository, "1.8.0", directory, "hubble") | |
| def test_release_requires_transitive_pd_common_library(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, directory = self.fixture(Path(temporary)) | |
| (directory / "lib/hg-pd-common-1.8.0.jar").unlink() | |
| for module in ("loader", "tools", "hubble"): | |
| with self.subTest(module=module), self.assertRaisesRegex(RuntimeError, "hg-pd-common"): | |
| sdk.validate_release_sdk(repository, "1.8.0", directory, module) | |
| def test_sdk_rejects_unresolved_and_unspecified_versions(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, directory = self.fixture(Path(temporary)) | |
| with self.assertRaisesRegex(RuntimeError, "concrete non-SNAPSHOT version"): | |
| sdk.validate_release_sdk(repository, "${revision}", directory, "loader") | |
| result = subprocess.run(["python3", str(Path(sdk.__file__)), str(repository), "--mode", "release"], | |
| capture_output=True, text=True, timeout=5) | |
| self.assertNotEqual(0, result.returncode) | |
| self.assertIn("--version is required", result.stderr) | |
| def test_release_rejects_snapshot_even_with_matching_artifacts(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| repository, directory = self.fixture(Path(temporary), "1.9.0-SNAPSHOT") | |
| with self.assertRaisesRegex(RuntimeError, "concrete non-SNAPSHOT version"): | |
| sdk.validate_release_sdk(repository, "1.9.0-SNAPSHOT", directory, "hubble") | |
| command = ["python3", str(Path(sdk.__file__)), str(repository), "--mode", "release", | |
| "--version", "1.9.0-SNAPSHOT", "--distribution", str(directory), "--module", "hubble"] | |
| result = subprocess.run(command, capture_output=True, text=True, timeout=5) | |
| self.assertNotEqual(0, result.returncode) | |
| self.assertIn("concrete non-SNAPSHOT version", result.stderr) | |
| class SDKActionTest(unittest.TestCase): | |
| def test_source_build_defaults_agree_without_changing_historical_runtime_fixture(self): | |
| root = Path(__file__).resolve().parents[2] | |
| action = (root / ".github/actions/setup-candidate-sdk/action.yml").read_text() | |
| defaults = re.findall(r"default: '([0-9a-f]{40})'", action) | |
| self.assertEqual([sdk.COMMIT, sdk.COMMIT], defaults) | |
| for name in (".github/workflows/ci.yml", | |
| "hugegraph-loader/Dockerfile", "hugegraph-hubble/Dockerfile"): | |
| self.assertIn(sdk.COMMIT, (root / name).read_text()) | |
| image = (root / ".github/workflows/image-ci.yml").read_text() | |
| self.assertIn("uses: ./.github/actions/setup-candidate-sdk", image) | |
| self.assertNotIn("expected-commit:", image) | |
| for name in ("hugegraph-loader/Dockerfile", "hugegraph-hubble/Dockerfile"): | |
| self.assertIn("-Dhugegraph.version=1.8.0 -Dsdk.validation.mode=candidate", (root / name).read_text()) | |
| upstream = (root / ".github/workflows/upstream-compatibility.yml").read_text() | |
| self.assertIn("ref: master", upstream) | |
| self.assertNotIn("validation-mode: release", upstream) | |
| release = (root / "hugegraph-client/assembly/travis/release.py").read_text() | |
| self.assertIn('VERSION = "1.7.0"', release) | |
| self.assertIn('apache-hugegraph-incubating-1.7.0.tar.gz', release) | |
| def run_action(self, root, revision, corrupt=None, reuse=False, reuse_corrupt=None, consumers=1, empty_reuse=False): | |
| fixture = root / "fixture" | |
| candidate = CandidateDistributionTest() | |
| candidate.source_commit = sdk.COMMIT if revision == "1.8.0" else "b" * 40 | |
| repository, manifest = candidate.fixture(fixture, revision) | |
| if reuse: | |
| manifest.update(source_dir='/original/producer/source', java_home='/original/producer/jdk') | |
| if corrupt == "source": | |
| manifest["commit"] = "0" * 40 | |
| elif corrupt == "module": | |
| manifest["required_sdk_modules"].pop() | |
| elif corrupt == "hash": | |
| (repository / manifest["artifacts"][0]["path"]).write_bytes(b"changed SDK") | |
| elif corrupt == "origin": | |
| manifest["artifacts"][0]["source_reactor_install"] = False | |
| candidate.write_manifest(repository, manifest) | |
| workspace = root / "workspace" | |
| installer = workspace / "hugegraph-client/assembly/travis/install-candidate-sdk.sh" | |
| installer.parent.mkdir(parents=True) | |
| installer.write_text('#!/bin/bash\nset -euo pipefail\n' | |
| 'if [[ -n "${SDK_INSTALL_COUNT:-}" ]]; then echo install >> "$SDK_INSTALL_COUNT"; fi\n' | |
| 'cp -R "$SDK_FIXTURE/." "$3/"\n' | |
| 'mkdir -p "$2/hugegraph-server"\n' | |
| 'printf archive > "$2/hugegraph-server/apache-hugegraph-fixture.tar.gz"\n') | |
| verifier = workspace / "hugegraph-client/assembly/travis/verify_candidate_image_sdk.py" | |
| verifier.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copyfile(sdk.__file__, verifier) | |
| runner = root / "runner" | |
| runner.mkdir() | |
| home = root / "home" | |
| home.mkdir() | |
| environment, output = root / "env", root / "output" | |
| action = (Path(__file__).resolve().parents[2] / ".github/actions/setup-candidate-sdk/action.yml").read_text() | |
| script = textwrap.dedent(action.split(" run: |\n", 1)[1].split("\n - name:", 1)[0]) | |
| if empty_reuse: | |
| workflow = (Path(__file__).resolve().parents[2] / '.github/workflows/hubble-ci.yml').read_text() | |
| marker = ' - name: Validate requested SDK reuse\n' | |
| if marker in workflow: | |
| guard = workflow.split(marker, 1)[1].split('\n - name:', 1)[0] | |
| script = textwrap.dedent(guard.split(' run: |\n', 1)[1]) + '\n' + script | |
| env = dict(os.environ, HOME=str(home), RUNNER_TEMP=str(runner), GITHUB_WORKSPACE=str(workspace), | |
| GITHUB_ENV=str(environment), GITHUB_OUTPUT=str(output), SDK_FIXTURE=str(repository), | |
| SERVER_COMMIT=candidate.source_commit, SERVER_REPOSITORY=sdk.REPOSITORY, | |
| SERVER_FETCH_REF=candidate.source_commit, MAVEN_OPTS="-Xmx1g", | |
| MAVEN_ARGS="-ntp --settings settings.xml", SDK_FIXTURE_ARTIFACT='', | |
| SDK_INSTALL_COUNT=str(root / 'installer-count')) | |
| if reuse: | |
| helpers = workspace / 'hugegraph-client/assembly/travis' | |
| source_helpers = Path(sdk.__file__).parent | |
| for name in ('fixture.sh', 'manifest.py', 'release.py', 'service-wait.sh'): | |
| shutil.copyfile(source_helpers / name, helpers / name) | |
| action_input = workspace / '.github/actions/setup-candidate-sdk/action.yml' | |
| action_input.parent.mkdir(parents=True) | |
| action_input.write_text(action) | |
| jdk = root / 'jdk/bin' | |
| jdk.mkdir(parents=True) | |
| java = jdk / 'java' | |
| java.write_text('''#!/bin/bash | |
| cat >&2 <<PROPERTIES | |
| java.vendor = ${SDK_TEST_VENDOR:-Eclipse Adoptium} | |
| java.runtime.version = ${SDK_TEST_RUNTIME:-17.0.17+10} | |
| java.vm.version = ${SDK_TEST_RUNTIME:-17.0.17+10} | |
| java.specification.version = 17 | |
| os.arch = ${SDK_TEST_ARCH:-amd64} | |
| openjdk version "17.0.17" | |
| PROPERTIES | |
| ''') | |
| java.chmod(0o755) | |
| maven = jdk / 'mvn' | |
| maven.write_text('''#!/bin/bash | |
| if [[ "$*" != -version ]]; then echo forbidden-build >&2; exit 99; fi | |
| echo "Apache Maven ${SDK_TEST_MAVEN:-3.9.11} (fixture)" | |
| ''') | |
| maven.chmod(0o755) | |
| env.update(JAVA_HOME=str(jdk.parent), PATH=str(jdk) + os.pathsep + env['PATH'], | |
| SDK_INSTALL_COUNT=str(root / 'installer-count'), FIXTURE_PLATFORM='Linux-X64') | |
| fixture_env = dict(env, FIXTURE_REPOSITORY=sdk.REPOSITORY, | |
| FIXTURE_COMMIT=candidate.source_commit, FIXTURE_JAVA='17', | |
| FIXTURE_EXPORT_CANDIDATE_SDK='true', FIXTURE_MODE='build', | |
| FIXTURE_DIR=str(root / 'producer')) | |
| # Actual fixture orchestration, with a test-local SDK installer and version probes only. | |
| producer = subprocess.run(['bash', str(helpers / 'fixture.sh')], env=fixture_env, | |
| capture_output=True, text=True, timeout=20) | |
| self.assertEqual(0, producer.returncode, producer.stderr) | |
| expected_installs = 1 | |
| caches = list((home / 'hugegraph-fixture-cache').iterdir()) | |
| self.assertEqual(1, len(caches)) | |
| fixture_env['FIXTURE_SDK_CACHE_HIT'] = 'true' | |
| if reuse_corrupt == 'server-only-cache': | |
| original = json.loads((root / 'producer/manifest.json').read_text()) | |
| identity = ''.join(original[key] + '\n' for key in ('repository', 'commit', 'java', 'config')) | |
| legacy = home / 'hugegraph-fixture-cache' / hashlib.sha256(identity.encode()).hexdigest() | |
| legacy.mkdir() | |
| for name in ('server.tar.gz', 'manifest.json'): | |
| shutil.copyfile(root / 'producer' / name, legacy / name) | |
| shutil.rmtree(caches[0]) | |
| fixture_env['FIXTURE_SDK_CACHE_HIT'] = 'false' | |
| expected_installs = 2 | |
| elif reuse_corrupt == 'cache-missing-directory': | |
| shutil.rmtree(caches[0]) | |
| elif reuse_corrupt == 'cache-missing-sdk': | |
| shutil.rmtree(caches[0] / 'sdk') | |
| elif reuse_corrupt == 'cache-jar': | |
| (caches[0] / 'sdk' / manifest['artifacts'][-1]['path']).write_bytes(b'changed cached SDK') | |
| fixture_env['FIXTURE_DIR'] = str(root / 'producer-warm') | |
| warm = subprocess.run(['bash', str(helpers / 'fixture.sh')], env=fixture_env, | |
| capture_output=True, text=True, timeout=20) | |
| if reuse_corrupt in ('cache-missing-directory', 'cache-missing-sdk', 'cache-jar'): | |
| self.assertNotEqual(0, warm.returncode) | |
| self.assertEqual(['install'], (root / 'installer-count').read_text().splitlines()) | |
| return warm, {}, {} | |
| self.assertEqual(0, warm.returncode, warm.stderr) | |
| if expected_installs == 1: | |
| self.assertIn('reactor builds: 0', warm.stdout) | |
| self.assertEqual(['install'] * expected_installs, (root / 'installer-count').read_text().splitlines()) | |
| payload = root / 'producer-warm' | |
| # Go/other Server-only consumers retain their existing manifest contract. | |
| server_only = root / 'server-only' | |
| server_only.mkdir() | |
| for name in ('server.tar.gz', 'manifest.json'): | |
| shutil.copyfile(payload / name, server_only / name) | |
| server_env = dict(fixture_env, FIXTURE_DIR=str(server_only), FIXTURE_MODE='start', | |
| FIXTURE_EXPORT_CANDIDATE_SDK='false') | |
| server = subprocess.run(['bash', str(helpers / 'fixture.sh')], env=server_env, | |
| cwd=root, capture_output=True, text=True, timeout=20) | |
| self.assertEqual(0, server.returncode, server.stderr) | |
| env['MAVEN_OPTS'] = ('-Xmx3g -XX:MaxMetaspaceSize=768m -Dmaven.repo.local=/consumer/m2 ' | |
| '-Dhugegraph.version=' + revision + ' -Dsdk.validation.mode=candidate') | |
| if reuse_corrupt == 'archive': | |
| (payload / 'server.tar.gz').write_bytes(b'tampered') | |
| elif reuse_corrupt == 'jar': | |
| (payload / 'sdk' / manifest['artifacts'][-1]['path']).write_bytes(b'tampered') | |
| elif reuse_corrupt == 'origin': | |
| target = payload / 'sdk' / manifest['artifacts'][0]['path'] | |
| (target.parent / '_remote.repositories').unlink() | |
| elif reuse_corrupt == 'missing-sdk': | |
| shutil.rmtree(payload / 'sdk') | |
| elif reuse_corrupt == 'inputs': | |
| action_input.write_text(action + '\n# changed installer inputs\n') | |
| elif reuse_corrupt == 'vendor': | |
| env['SDK_TEST_VENDOR'] = 'Different JDK' | |
| elif reuse_corrupt == 'jdk-build': | |
| env['SDK_TEST_RUNTIME'] = '17.0.17+11' | |
| elif reuse_corrupt == 'architecture': | |
| env['SDK_TEST_ARCH'] = 'aarch64' | |
| elif reuse_corrupt == 'maven': | |
| env['SDK_TEST_MAVEN'] = '3.9.12' | |
| elif reuse_corrupt == 'jvm-option': | |
| env['MAVEN_OPTS'] += ' --add-opens=java.base/java.lang=ALL-UNNAMED' | |
| elif reuse_corrupt == 'source': | |
| env['SERVER_COMMIT'] = 'c' * 40 | |
| env['SDK_FIXTURE_ARTIFACT'] = 'current-fixture-this-run' | |
| repositories = [] | |
| for index in range(consumers): | |
| consumer = root / ('consumer-' + str(index)) | |
| consumer.mkdir() | |
| if reuse_corrupt != 'missing-artifact': | |
| shutil.copytree(payload, consumer / 'candidate-sdk-fixture') | |
| env['RUNNER_TEMP'] = str(consumer) | |
| environment.unlink(missing_ok=True) | |
| output.unlink(missing_ok=True) | |
| result = subprocess.run(['bash', '-c', script], env=env, | |
| capture_output=True, text=True, timeout=20) | |
| if result.returncode: | |
| break | |
| current = consumer / 'candidate-sdk-fixture/sdk' | |
| self.assertFalse((current / 'consumer-private').exists()) | |
| (current / 'consumer-private').write_text('isolated consumer state') | |
| repositories.append(str(current)) | |
| (root / 'consumer-repositories.json').write_text(json.dumps(repositories)) | |
| else: | |
| result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True, timeout=10) | |
| variables = dict(line.split("=", 1) for line in environment.read_text().splitlines()) if environment.exists() else {} | |
| outputs = dict(line.split("=", 1) for line in output.read_text().splitlines()) if output.exists() else {} | |
| return result, variables, outputs | |
| def test_action_verifies_pinned_and_floating_sdk_before_selecting_consumer_coordinates(self): | |
| for revision in ("1.8.0", "1.9.0", "1.9.0-SNAPSHOT"): | |
| with self.subTest(revision=revision), tempfile.TemporaryDirectory() as temporary: | |
| result, variables, outputs = self.run_action(Path(temporary), revision) | |
| self.assertEqual(0, result.returncode, result.stdout + result.stderr) | |
| for key in ("MAVEN_ARGS", "MAVEN_OPTS"): | |
| self.assertIn("-Dmaven.repo.local=" + outputs["maven-repo"], variables[key]) | |
| self.assertIn("-Dhugegraph.version=" + revision, variables[key]) | |
| self.assertIn("-Dsdk.validation.mode=candidate", variables[key]) | |
| self.assertEqual(revision, variables["CANDIDATE_SDK_VERSION"]) | |
| manifest = Path(outputs["manifest"]) | |
| self.assertTrue(manifest.is_file()) | |
| self.assertEqual(revision, json.loads(manifest.read_text())["source_revision"]) | |
| self.assertEqual(manifest.parent, Path(outputs["maven-repo"])) | |
| # The distribution uses exactly the SDK version configured by this action. | |
| with patch.dict(os.environ, variables): | |
| sdk.validate_sdk(outputs["maven-repo"]) | |
| self.assertEqual(sdk.digest(Path(outputs["archive"])), outputs["archive-sha256"]) | |
| with self.assertRaises(RuntimeError): | |
| sdk.validate_release_sdk(outputs["maven-repo"], revision) | |
| def test_action_rejects_unresolved_version_or_unverified_source_without_exporting_environment(self): | |
| for revision, corrupt in (("${revision}", None), ("1.8.0", "source"), ("1.8.0", "module"), | |
| ("1.9.0-SNAPSHOT", "hash"), ("1.9.0-SNAPSHOT", "origin")): | |
| with self.subTest(revision=revision, corrupt=corrupt), tempfile.TemporaryDirectory() as temporary: | |
| result, variables, _ = self.run_action(Path(temporary), revision, corrupt) | |
| self.assertNotEqual(0, result.returncode) | |
| self.assertEqual({}, variables) | |
| def test_complete_fixture_cache_and_two_consumers_never_repeat_the_sdk_reactor(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| result, variables, outputs = self.run_action(root, '1.8.0', reuse=True, consumers=2) | |
| self.assertEqual(0, result.returncode, result.stdout + result.stderr) | |
| self.assertEqual(['install'], (root / 'installer-count').read_text().splitlines()) | |
| repositories = json.loads((root / 'consumer-repositories.json').read_text()) | |
| self.assertEqual(2, len(set(repositories))) | |
| self.assertEqual('1.8.0', variables['CANDIDATE_SDK_VERSION']) | |
| self.assertEqual(Path(outputs['fixture-directory']) / 'sdk', Path(outputs['maven-repo'])) | |
| original = (root / 'fixture/m2/candidate-sdk-manifest.json').read_bytes() | |
| self.assertEqual(original, Path(outputs['manifest']).read_bytes()) | |
| self.assertEqual('/original/producer/source', json.loads(original)['source_dir']) | |
| self.assertEqual(sdk.digest(outputs['archive']), outputs['archive-sha256']) | |
| def test_server_only_cache_is_not_an_sdk_cache_hit(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| result, _, _ = self.run_action(root, '1.8.0', reuse=True, reuse_corrupt='server-only-cache') | |
| self.assertEqual(0, result.returncode, result.stderr) | |
| self.assertEqual(['install', 'install'], (root / 'installer-count').read_text().splitlines()) | |
| def test_broken_exact_sdk_cache_fails_without_rebuilding(self): | |
| for corrupt in ('cache-missing-directory', 'cache-missing-sdk', 'cache-jar'): | |
| with self.subTest(corrupt=corrupt), tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| result, variables, _ = self.run_action(root, '1.8.0', reuse=True, reuse_corrupt=corrupt) | |
| self.assertNotEqual(0, result.returncode) | |
| self.assertEqual({}, variables) | |
| self.assertEqual(['install'], (root / 'installer-count').read_text().splitlines()) | |
| def test_explicit_reuse_rejects_wrong_inputs_or_payload_without_fresh_build_fallback(self): | |
| for corrupt in ('missing-artifact', 'missing-sdk', 'archive', 'jar', 'origin', | |
| 'source', 'inputs', 'vendor', 'jdk-build', 'architecture', 'maven', 'jvm-option'): | |
| with self.subTest(corrupt=corrupt), tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| result, variables, outputs = self.run_action(root, '1.8.0', reuse=True, reuse_corrupt=corrupt) | |
| self.assertNotEqual(0, result.returncode, corrupt) | |
| self.assertEqual({}, variables) | |
| self.assertEqual({}, outputs) | |
| self.assertEqual(['install'], (root / 'installer-count').read_text().splitlines()) | |
| def test_sdk_only_file_changes_do_not_invalidate_server_only_fingerprints(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| result, _, _ = self.run_action(root, '1.8.0', reuse=True) | |
| self.assertEqual(0, result.returncode, result.stderr) | |
| workspace = root / 'workspace' | |
| helper = workspace / 'hugegraph-client/assembly/travis/manifest.py' | |
| env = dict(os.environ, JAVA_HOME=str(root / 'jdk'), GITHUB_WORKSPACE=str(workspace), | |
| PATH=str(root / 'jdk/bin') + os.pathsep + os.environ['PATH']) | |
| def fingerprint(command): | |
| return subprocess.check_output([sys.executable, str(helper), command], env=env, text=True).strip() | |
| server, candidate = fingerprint('inputs'), fingerprint('sdk-identity') | |
| sdk_only = ('.github/actions/setup-candidate-sdk/action.yml', | |
| '.github/actions/get-hugegraph-commit/action.yml', | |
| '.github/actions/get-hugegraph-commit/resolve.cjs', | |
| 'hugegraph-client/assembly/travis/verify_candidate_image_sdk.py') | |
| for relative in sdk_only + ('hugegraph-client/assembly/travis/install-candidate-sdk.sh', | |
| 'hugegraph-client/assembly/travis/checkout-server.sh'): | |
| path = workspace / relative | |
| original = path.read_bytes() if path.exists() else None | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_bytes((original or b'') + b'\n# input mutation\n') | |
| self.assertEqual(relative in sdk_only, fingerprint('inputs') == server, relative) | |
| self.assertNotEqual(candidate, fingerprint('sdk-identity'), relative) | |
| if original is None: | |
| path.unlink() | |
| else: | |
| path.write_bytes(original) | |
| def test_requested_reuse_with_empty_name_stops_before_the_installer(self): | |
| with tempfile.TemporaryDirectory() as temporary: | |
| root = Path(temporary) | |
| result, variables, outputs = self.run_action(root, '1.8.0', empty_reuse=True) | |
| self.assertNotEqual(0, result.returncode) | |
| self.assertIn('SDK reuse requires current-artifact-name', result.stderr) | |
| self.assertEqual({}, variables) | |
| self.assertEqual({}, outputs) | |
| self.assertFalse((root / 'installer-count').exists()) | |
| if __name__ == "__main__": | |
| unittest.main() |