blob: 221e00a91288e2129c0e13b39b9e2f0f23a49769 [file]
#!/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()