Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/sdk1-comparator/build.py
T
Joseph Doherty 2e9cccbdef
v2-ci / unit-tests (push) Has been cancelled
test(soak): #799 SDK 1.5.378.182 comparator for the late-slope workload
Copies the reviewed baseline-canonical SDK1 port and replaces its 1..4-cycle smoke entry with the late-slope
loop (same cycle body, every-fourth-cycle active restart, 30/25/120 s bounds, 7200 s phase, cycle-3 cold
baseline with unchanged +64 MiB managed / +128 MiB RSS / +32 descriptor caps, latched first rejection,
bounded shutdown checks). Run against the immutable efb9197a publication (#807 fix included), never rebuilt.
The held-reference closed-session list assertion is recorded instead (SDK1 Close keeps lists until Dispose,
per OWNERSHIP.md) and the managed-monotonic guard is latched, so the comparator yields numbers for the whole
phase. acceptance=false by construction. Windows descriptor observer shared byte-for-byte with the SDK2 test.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 12:48:44 -05:00

81 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Publish the SDK 1.5.378.182 comparator (lmxopcua#799) self-contained against an immutable SDK1 publication.
Like ../baseline-canonical/build.py: never rebuilds product code. References come from the given RID-specific
framework-dependent publications of the SDK1 host and client (host wins duplicate names; native DLLs are skipped as
references and copied beside the output), plus Shouldly and its assertion dependencies. The comparator is published
self-contained for the requested RID on the same pinned runtime as the SDK2 run (10.0.14).
"""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import xml.etree.ElementTree as ET
RUNTIME = "10.0.14"
SOURCES = ["Program.cs", "BaselineRig.cs", "BaselineMetrics.cs", "CanonicalOperations.cs", "WindowsDescriptors.cs"]
def managed(path):
data = path.read_bytes()
if data[:2] != b"MZ":
return False
pe = int.from_bytes(data[0x3C:0x40], "little")
magic = int.from_bytes(data[pe + 24:pe + 26], "little")
directories = pe + 24 + (112 if magic == 0x20B else 96)
clr = directories + 14 * 8
return int.from_bytes(data[clr:clr + 4], "little") != 0
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--host", type=Path, required=True)
parser.add_argument("--client", type=Path, required=True)
parser.add_argument("--assertions", type=Path, required=True)
parser.add_argument("--rid", default="win-x64")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
root = args.output.resolve()
root.mkdir(parents=True, exist_ok=False)
source = Path(__file__).resolve().parent
references, natives = {}, {}
for directory in [args.host, args.client]:
for dll in sorted(directory.resolve(strict=True).glob("*.dll")):
(references if managed(dll) else natives).setdefault(dll.name, dll)
for name in ["Shouldly.dll", "DiffEngine.dll", "EmptyFiles.dll", "EmptyFiles.Xml.dll"]:
path = args.assertions.resolve(strict=True) / name
if path.exists():
references.setdefault(name, path)
project = ET.Element("Project", Sdk="Microsoft.NET.Sdk")
properties = ET.SubElement(project, "PropertyGroup")
for key, value in {"OutputType": "Exe", "TargetFramework": "net10.0", "Nullable": "enable", "ImplicitUsings": "enable",
"TreatWarningsAsErrors": "true", "EnableDefaultCompileItems": "false", "AssemblyName": "sdk1-comparator",
"RuntimeIdentifier": args.rid, "SelfContained": "true", "RuntimeFrameworkVersion": RUNTIME,
"NoWarn": "CA1416"}.items():
ET.SubElement(properties, key).text = value
items = ET.SubElement(project, "ItemGroup")
ET.SubElement(items, "FrameworkReference", Include="Microsoft.AspNetCore.App")
for name, path in references.items():
reference = ET.SubElement(items, "Reference", Include=path.stem)
ET.SubElement(reference, "HintPath").text = str(path)
for name in SOURCES:
ET.SubElement(items, "Compile", Include=str(source / name))
path = root / "comparator.csproj"
ET.indent(project)
ET.ElementTree(project).write(path, encoding="unicode")
publish = root / "publish"
with (root / "build.log").open("w") as log:
result = subprocess.run(["dotnet", "publish", str(path), "-c", "Release", "-o", str(publish), "-m:2", "--nologo"],
stdout=log, stderr=subprocess.STDOUT)
if result.returncode == 0:
for name, native in natives.items():
if not (publish / name).exists():
shutil.copy2(native, publish / name)
manifest = {"diagnosticOnly": True, "productRebuilt": False, "rid": args.rid, "runtime": RUNTIME,
"references": {n: hashlib.sha256(p.read_bytes()).hexdigest() for n, p in references.items()},
"natives": {n: hashlib.sha256(p.read_bytes()).hexdigest() for n, p in natives.items()},
"sources": {n: hashlib.sha256((source / n).read_bytes()).hexdigest() for n in SOURCES}}
(root / "provenance.json").write_text(json.dumps(manifest, indent=2) + "\n")
raise SystemExit(result.returncode)