#!/usr/bin/env python3 """Run the unchanged SDK2 server lifecycle test in isolated Linux ARM64.""" import argparse import hashlib import json import os from pathlib import Path import shutil import struct import subprocess import time import uuid import xml.etree.ElementTree as ET HERE = Path(__file__).resolve().parent ROOT = HERE.parents[2] PROJECT = ROOT / 'tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj' APPNAME = 'ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests' TARGET = 'Repeated_lifecycles_return_resources_to_a_steady_baseline' BASE = '67bc61236a7798e87deb9ae45e54bd29cf3d8ae9' def write(path, value): path.write_text(json.dumps(value, indent=2) + '\n') def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() def command(argv, log, timeout, **kwargs): with log.open('w') as stream: result = subprocess.run([str(x) for x in argv], stdout=stream, stderr=subprocess.STDOUT, timeout=timeout, **kwargs) return result.returncode def trx(path, count): doc = ET.parse(path) ns = {'t': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} counts = doc.find('.//t:Counters', ns) results = doc.findall('.//t:UnitTestResult', ns) if counts is None or any(int(counts.attrib[k]) != count for k in ('total', 'executed', 'passed')): raise RuntimeError('TRX expected exact passed/executed count ' + str(count) + ': ' + str(path)) if len(results) != count or any(r.attrib['outcome'] != 'Passed' for r in results): raise RuntimeError('TRX missing or non-passing result: ' + str(path)) for key in ('failed', 'error', 'timeout', 'aborted', 'notExecuted'): if int(counts.attrib.get(key, '0')): raise RuntimeError('TRX has nonzero ' + key) return {'counts': counts.attrib, 'tests': [r.attrib['testName'] for r in results]} def validate_evidence(task, seconds, full): rows = [json.loads(line) for line in (task / 'soak.jsonl').read_text().splitlines()] starts = [r for r in rows if r['kind'] == 'start'] complete = [r for r in rows if r['kind'] == 'complete'] if len(starts) != 1 or len(complete) != 1 or any(r['kind'] == 'failed' for r in rows): raise RuntimeError('Expected one start and one complete with no failed evidence row') start, done = starts[0], complete[0] if start['requestedSeconds'] != seconds or start['minimumFullSeconds'] != 7200: raise RuntimeError('Duration header mismatch') expected_mode = 'full' if full else 'diagnostic' if start['mode'] != expected_mode or done['mode'] != expected_mode or done['seconds'] < seconds: raise RuntimeError('Real duration/completion mode mismatch') if start['limits'] != {'retainedManagedGrowthBytes': 64 << 20, 'rssGrowthBytes': 128 << 20, 'fdGrowth': 32}: raise RuntimeError('Canonical test resource budgets changed') if done['satisfiesTwoHourDuration'] != full or (full and done['seconds'] < 7200): raise RuntimeError('Two-hour gate mismatch') samples = [r for r in rows if r['kind'] == 'quiescent'] active = [r for r in rows if r['kind'] == 'active'] stalls = [r for r in rows if r['kind'] == 'network-stall-recovered'] restarts = [r for r in rows if r['kind'] == 'restart'] if done['cycles'] < 4 or done['restarts'] != done['cycles'] // 4: raise RuntimeError('Missing actual lifecycle/restart coverage') if not (len(samples) == len(active) == len(stalls) == done['cycles']) or len(restarts) != done['restarts']: raise RuntimeError('Incomplete cycle/stall/restart evidence counts') if not start['sdk'].startswith('2.0.1+'): raise RuntimeError('Runtime SDK header mismatch') return {'start': start, 'complete': done, 'rowCounts': {k: sum(r['kind'] == k for r in rows) for k in sorted({r['kind'] for r in rows})}, 'firstResources': samples[0]['resources'], 'baselineResources': samples[2]['resources'], 'lastResources': samples[-1]['resources']} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output', required=True, type=Path, help='Fresh output including task private PKI; do not commit the whole directory') parser.add_argument('--duration', type=int, default=90) parser.add_argument('--full', action='store_true') parser.add_argument('--published-app', type=Path, help='Reuse immutable self-contained publish from this launcher/recorded matching build') parser.add_argument('--image', default='mcr.microsoft.com/dotnet/sdk:10.0.103-noble') args = parser.parse_args() if (args.full and args.duration < 7200) or (not args.full and not 60 <= args.duration <= 120): parser.error('full requires duration >=7200; diagnostic requires 60..120 seconds') output = args.output.resolve() output.mkdir(parents=True, exist_ok=False) os.chmod(output, 0o700) task = output / 'task' task.mkdir() revision = subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() production_diff = subprocess.check_output(['git', '-C', str(ROOT), 'diff', BASE, 'HEAD', '--', 'src', 'Directory.Packages.props', 'Directory.Build.props'], text=True) if production_diff: raise RuntimeError('Production/package source differs from reviewed ' + BASE[:8] + ' source; review required') image = json.loads(subprocess.check_output(['docker', 'image', 'inspect', args.image, '--format', '{{json .}}'], text=True)) if image['Architecture'] != 'arm64' or image['Os'] != 'linux': raise RuntimeError('Expected cached official Linux ARM64 image') app = args.published_app.resolve() if args.published_app else output / 'app' build_cmd = ['dotnet', 'publish', str(PROJECT), '-c', 'Release', '-r', 'linux-arm64', '--self-contained', 'true', '-p:RuntimeFrameworkVersion=10.0.14', '-p:PublishSingleFile=false', '-p:PublishTrimmed=false', '-p:UseAppHost=true', '--artifacts-path', str(output / 'build-artifacts'), '-o', str(app), '-m:1', '--nologo'] if not args.published_app: status = command(build_cmd, output / 'publish.log', 600) if status: raise RuntimeError('Self-contained Linux publish failed: ' + str(status)) receipt_path = app.parent / 'publish-receipt.json' receipt = json.loads(receipt_path.read_text()) if args.published_app else { 'sourceRevision': revision, 'command': build_cmd, 'hostDotnet': subprocess.check_output(['dotnet', '--info'], text=True), 'testSourceSha256': sha(PROJECT.parent / 'Sdk2LifecycleSoakTests.cs')} if receipt['testSourceSha256'] != sha(PROJECT.parent / 'Sdk2LifecycleSoakTests.cs'): raise RuntimeError('Reused publish test source differs from current canonical source') apphost = app / APPNAME magic = apphost.read_bytes()[:20] if magic[:4] != b'\x7fELF' or magic[4] != 2 or struct.unpack('