test: add immutable Linux late-slope diagnostic tooling

This commit is contained in:
Joseph Doherty
2026-11-10 17:59:15 -05:00
parent 7023285a3e
commit 613369fc54
7 changed files with 1288 additions and 0 deletions
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Publish and record a fresh immutable Linux ARM64 bundle without running the workload."""
import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
sys.dont_write_bytecode = True
from analyze import APP, RUNTIME, SETTINGS, digest, require
from bundle import CANONICAL_SOURCES, TOOLS, check_application, context_identity, image_identity, manifest, verify_bundle
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[3]
PROJECT = REPO / "tests/Server" / APP / (APP + ".csproj")
SUPPORT = PROJECT.with_name("Sdk2LifecycleLateSlopeTests.cs")
MODULE_SOURCE = HERE.parent / "ArtifactModuleInfo.cs"
DESIGN = HERE.parent / "LATE_SLOPE.md"
def check_source(revision):
current = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=REPO, text=True).strip()
require(revision == current, "Publish the specified committed source revision")
require(not subprocess.check_output(["git", "status", "--porcelain"], cwd=REPO, text=True).strip(),
"The publication checkout must be clean")
require('"late-slope-result"' in SUPPORT.read_text(encoding="utf-8"), "The committed canonical test lacks late-slope mode")
sources = [*(REPO / name for name in CANONICAL_SOURCES), MODULE_SOURCE, DESIGN,
HERE / "prepare.py", HERE / "test_offline.py", *(HERE / name for name in TOOLS)]
return {path.relative_to(REPO).as_posix(): digest(path) for path in sources}
def managed_modules(build, root):
project = ET.Element("Project", Sdk="Microsoft.NET.Sdk")
settings = ET.SubElement(project, "PropertyGroup")
for key, value in {"OutputType": "Exe", "TargetFramework": "net10.0", "ImplicitUsings": "enable",
"EnableDefaultCompileItems": "false", "TreatWarningsAsErrors": "true"}.items():
ET.SubElement(settings, key).text = value
ET.SubElement(ET.SubElement(project, "ItemGroup"), "Compile", Include=str(MODULE_SOURCE))
path = build / "module-info.csproj"
ET.ElementTree(project).write(path, encoding="unicode")
with (root / "module-info-build.log").open("w", encoding="utf-8") as log:
subprocess.run(["dotnet", "build", str(path), "-c", "Release", "-m:2", "--nologo"],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=300)
command = ["dotnet", str(build / "bin/Release/net10.0/module-info.dll"), str(root), str(root / "app")]
return json.loads(subprocess.check_output(command, text=True, timeout=60))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--source-revision", required=True)
parser.add_argument("--image", required=True)
parser.add_argument("--docker-context", required=True)
options = parser.parse_args()
sources = check_source(options.source_revision)
context = context_identity(options.docker_context)
image = image_identity(options.image, context)
root = options.output.resolve()
root.mkdir(mode=0o700, parents=True, exist_ok=False)
with tempfile.TemporaryDirectory(prefix="opcua-late-slope-publish-") as scratch:
build = Path(scratch)
command = ["dotnet", "publish", str(PROJECT), "-c", "Release", "-r", "linux-arm64",
"--self-contained", "true", f"-p:RuntimeFrameworkVersion={RUNTIME}",
"-p:PublishSingleFile=false", "-p:PublishTrimmed=false", "-p:UseAppHost=true",
"--artifacts-path", str(build / "artifacts"), "-o", str(root / "app"), "-m:2", "--nologo"]
with (root / "publish.log").open("w", encoding="utf-8") as log:
subprocess.run(command, cwd=REPO, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=900)
sdk, gateway = check_application(root / "app")
modules = managed_modules(build, root)
tools = root / "tools"
tools.mkdir()
for name in TOOLS:
shutil.copy2(HERE / name, tools / name)
require(check_source(options.source_revision) == sources, "Committed inputs changed during publication")
require(context_identity(options.docker_context) == context and image_identity(options.image, context) == image,
"The local Docker context or image changed during publication")
receipt = {**SETTINGS, "kind": "bundle", "sourceRevision": options.source_revision, "sourceFiles": sources, "dockerContext": context,
"image": options.image, "imageIdentity": image, "preparedUtc": datetime.now(timezone.utc).isoformat(),
"publishCommand": command, "sdkLibraries": sdk, "gatewayLibraries": gateway,
"managedModules": modules, "files": manifest(root)}
path = root / "receipt.json"
path.write_text(json.dumps(receipt, indent=2, allow_nan=False) + "\n", encoding="utf-8")
sha = digest(path)
verify_bundle(root, sha)
print(json.dumps({"bundle": str(root), "sourceRevision": options.source_revision,
"receiptSha256": sha, "acceptance": False, "workloadLaunched": False}, indent=2))
if __name__ == "__main__":
main()