initial commit
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record the tested local build without collecting user documents or paths."""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build', type=Path, default=root / 'build')
|
||||
parser.add_argument('--output', type=Path, default=root / 'docs/validation-record.json')
|
||||
parser.add_argument('--runtime-results', type=Path,
|
||||
help='Results from this build; older evidence remains hashed separately')
|
||||
parser.add_argument('--ctest-results', type=Path,
|
||||
help='Frozen full CTest directory containing tests.xml and LastTest.log')
|
||||
args = parser.parse_args()
|
||||
build = args.build.resolve()
|
||||
results = root / 'tests/results'
|
||||
runtime = args.runtime_results.resolve() if args.runtime_results else results
|
||||
if not runtime.is_dir() or not runtime.is_relative_to(results):
|
||||
raise SystemExit('--runtime-results must be an existing directory within tests/results')
|
||||
ctest_directory = args.ctest_results.resolve() if args.ctest_results else None
|
||||
if ctest_directory and (not ctest_directory.is_dir() or not ctest_directory.is_relative_to(results)):
|
||||
raise SystemExit('--ctest-results must be an existing directory within tests/results')
|
||||
junit_path = ctest_directory / 'tests.xml' if ctest_directory else build / 'tests.xml'
|
||||
test_log_path = ctest_directory / 'LastTest.log' if ctest_directory else build / 'Testing/Temporary/LastTest.log'
|
||||
tree = ET.parse(junit_path)
|
||||
cases = tree.findall('.//testcase')
|
||||
if not cases or any(case.find('failure') is not None or case.find('skipped') is not None for case in cases):
|
||||
raise SystemExit('A complete passing CTest JUnit file is required.')
|
||||
|
||||
def sha(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open('rb') as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b''):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
def compiled_source(name):
|
||||
# Debian desktop/profile/maintainer files are package inputs, separately
|
||||
# validated against the deb manifest; they are not compiled into the GUI.
|
||||
return (name.startswith(('src/', 'qml/', 'cmake/', 'resources/'))
|
||||
and not name.startswith('resources/linux/')) or name in ('CMakeLists.txt', 'resources.qrc')
|
||||
|
||||
def recorded_runner_matches(name, digest, frozen):
|
||||
return (sha(root / name) == digest or
|
||||
(name == 'tests/run_wayland_validation.py' and sha(frozen) == digest))
|
||||
|
||||
source_paths = sorted([p for folder in ['src', 'qml', 'resources', 'cmake', 'tools'] for p in (root / folder).rglob('*')
|
||||
if p.is_file() and '__pycache__' not in p.parts] + [root / 'CMakeLists.txt', root / 'resources.qrc'])
|
||||
sources = {str(p.relative_to(root)): sha(p) for p in source_paths}
|
||||
hardware = {'logicalCpus': os.cpu_count(), 'architecture': platform.machine()}
|
||||
if Path('/proc/cpuinfo').is_file():
|
||||
found = re.search(r'^model name\s*:\s*(.+)$', Path('/proc/cpuinfo').read_text(), re.M)
|
||||
if found:
|
||||
hardware['cpu'] = found.group(1)
|
||||
if Path('/proc/meminfo').is_file():
|
||||
found = re.search(r'MemTotal:\s+(\d+)', Path('/proc/meminfo').read_text())
|
||||
if found:
|
||||
hardware['physicalMemoryBytes'] = int(found.group(1)) * 1024
|
||||
record = {'recordedAtUtc': datetime.now(timezone.utc).isoformat(), 'os': platform.freedesktop_os_release(),
|
||||
'kernel': platform.release(), 'hardware': hardware,
|
||||
'compiler': subprocess.check_output(['c++', '--version'], text=True).splitlines()[0],
|
||||
'cmake': subprocess.check_output(['cmake', '--version'], text=True).splitlines()[0],
|
||||
'qt': subprocess.check_output(['pkg-config', '--modversion', 'Qt6Core'], text=True).strip(),
|
||||
'fontconfig': subprocess.check_output(['pkg-config', '--modversion', 'fontconfig'], text=True).strip(),
|
||||
'qpdf': subprocess.check_output(['pkg-config', '--modversion', 'libqpdf'], text=True).strip(),
|
||||
'pdfium': json.loads((root / 'cmake/pdfium.lock.json').read_text()),
|
||||
'display': 'Xvfb / xcb; Qt Quick software; WebEngine --disable-gpu; internal sandboxes enabled',
|
||||
'acceptance': 'Development evidence on Arch (including AMD OpenGL through private headless Wayland) and Ubuntu 24.04 KVM (X11 US/JP XKB and X11/Wayland IBus/Mozc over D-Bus); Windows native, target-OS GPU, physical input/display, reference hardware and release distribution gates incomplete',
|
||||
'windowsPort': {'status': 'Integrated source only; native MSVC/Qt build and execution unavailable',
|
||||
'unexecutedNativeSuites': ['windows_pipe', 'windows_resources', 'windows_worker'],
|
||||
'portableEvidence': ['runtime_manifest', 'runtime_staging'],
|
||||
'limits': 'Synthetic MZ and Wine-header syntax checks do not establish PE scanning, native ABI, LPAC protection or Windows rendering.'},
|
||||
'executables': {name: sha(build / name) for name in ['docview', 'docview-pdf-worker', 'docview-archive-worker']},
|
||||
'runtimeResults': str(runtime.relative_to(root)),
|
||||
'sourceSha256': sources,
|
||||
'ctest': {'suites': len(cases), 'failures': 0, 'skipped': 0,
|
||||
'cases': [{key: case.attrib.get(key) for key in ['name', 'time', 'status']} for case in cases]}}
|
||||
log = test_log_path.read_text()
|
||||
record['ctest']['evidenceDirectory'] = str((ctest_directory or build).relative_to(root))
|
||||
record['qtTestTotalsIncludingSetupAndCleanup'] = re.findall(r'Totals: .*', log)
|
||||
record['installedExecutableSha256'] = {name: sha(build / 'install/bin' / name) for name in
|
||||
['docview', 'docview-pdf-worker', 'docview-archive-worker']
|
||||
if (build / 'install/bin' / name).is_file()}
|
||||
record['testSourceSha256'] = {str(p.relative_to(root)): sha(p) for p in sorted((root / 'tests').rglob('*'))
|
||||
if p.is_file() and p.suffix in {'.cpp', '.h', '.py', '.qml', '.html'}
|
||||
and 'results' not in p.parts and '__pycache__' not in p.parts}
|
||||
record['fixtureSha256'] = {str(p.relative_to(root)): sha(p) for p in sorted((root / 'tests/fixtures').rglob('*'))
|
||||
if p.is_file() and '__pycache__' not in p.parts}
|
||||
record['designSha256'] = {str(p.relative_to(root)): sha(p) for p in sorted((root / 'docs/design').glob('*.md'))}
|
||||
record['documentationSha256'] = {str(p.relative_to(root)): sha(p) for p in
|
||||
sorted(list((root / 'docs').rglob('*.md')) + list((root / 'tests').glob('*VALIDATION.md')) + [root / 'README.md'])}
|
||||
record['evidenceSha256'] = {str(p.relative_to(root)): sha(p) for p in
|
||||
[junit_path, test_log_path]}
|
||||
if args.runtime_results:
|
||||
snapshot = runtime / 'build-record.json'
|
||||
if snapshot.is_file():
|
||||
record['canonicalBuildRecord'] = json.loads(snapshot.read_text())
|
||||
recorded_binaries = record['canonicalBuildRecord'].get('binaries', {})
|
||||
if any(recorded_binaries.get(name) != value for name, value in record['executables'].items()):
|
||||
raise SystemExit('Canonical CTest build differs from the current production executables')
|
||||
if (record['canonicalBuildRecord'].get('junitSha256') != sha(junit_path) or
|
||||
record['canonicalBuildRecord'].get('logSha256') != sha(test_log_path)):
|
||||
raise SystemExit('Canonical CTest evidence changed after its build record was captured')
|
||||
focused_scope = record.get('canonicalBuildRecord', {}).get('focusedTestScope')
|
||||
if focused_scope:
|
||||
record['focusedTestScope'] = focused_scope
|
||||
canvas_snapshot = results / 'canvas-scene-graph/build-record.json'
|
||||
if canvas_snapshot.is_file():
|
||||
value = json.loads(canvas_snapshot.read_text())
|
||||
frozen_runner = runtime / 'wayland/canvas-software/runner-at-start.py'
|
||||
if (any(value['binaries'].get(name) != digest for name, digest in record['executables'].items())
|
||||
or value['binaries'].get('test_pdf_canvas') != sha(build / 'test_pdf_canvas')
|
||||
or any(not recorded_runner_matches(name, digest, frozen_runner) for name, digest in value['sourceSha256'].items())
|
||||
or any(sha(root / name) != digest for name, digest in value['evidenceSha256'].items())):
|
||||
raise SystemExit('Supplemental Canvas evidence differs from the recorded files/build')
|
||||
value['currentSourceDifferences'] = [name for name, digest in value['sourceSha256'].items() if sha(root / name) != digest]
|
||||
value['frozenWaylandRunner'] = str(frozen_runner.relative_to(root))
|
||||
record['canvasFollowup'] = value
|
||||
for name in ['gui', 'gui-final', 'smoke', 'gpu-smoke', 'gpu-smoke-logged', 'gpu-gui', 'canvas-software']:
|
||||
wayland = runtime / 'wayland' / name / 'report.json'
|
||||
if wayland.is_file():
|
||||
value = json.loads(wayland.read_text())
|
||||
value['matchesRecordedBuild'] = all(
|
||||
value.get('executables', {}).get(binary) == digest
|
||||
for binary, digest in record['executables'].items())
|
||||
if value.get('mode') == 'gui':
|
||||
value['testExecutablesMatchCurrent'] = all(
|
||||
value.get('executables', {}).get(binary) == sha(build / binary)
|
||||
for binary in ['test_app', 'test_web_qml', 'test_pdf_canvas'])
|
||||
correction = wayland.parent / 'scope-correction.json'
|
||||
if correction.is_file():
|
||||
value['scopeCorrection'] = json.loads(correction.read_text())
|
||||
graphics = wayland.parent / 'graphics-evidence.json'
|
||||
if graphics.is_file():
|
||||
value['additionalGraphicsEvidence'] = json.loads(graphics.read_text())
|
||||
record.setdefault('wayland', {})[name] = value
|
||||
component_evidence = runtime / 'unchanged-component-evidence.json'
|
||||
if component_evidence.is_file():
|
||||
value = json.loads(component_evidence.read_text())
|
||||
for name in ['docview-pdf-worker', 'docview-archive-worker', 'test_pdf', 'test_core',
|
||||
'test_worker_deadlines', 'libdocview_common.a']:
|
||||
row = value.get('binaries', {}).get(name, {})
|
||||
if row.get('currentSha256') != sha(build / name):
|
||||
raise SystemExit('Unchanged-component record does not match the current build: ' + name)
|
||||
record['unchangedComponentEvidence'] = value
|
||||
ubuntu = runtime / 'ubuntu'
|
||||
if (ubuntu / 'build-record.json').is_file():
|
||||
guest = json.loads((ubuntu / 'build-record.json').read_text())
|
||||
guest_cases = ET.parse(ubuntu / 'ctest-font-final/tests.xml').findall('.//testcase')
|
||||
if (len(guest_cases) != guest['ctest']['groups'] or any(
|
||||
c.find('failure') is not None or c.find('skipped') is not None for c in guest_cases) or
|
||||
sha(ubuntu / 'ctest-font-final/tests.xml') != guest['ctest']['junitSha256'] or
|
||||
sha(ubuntu / 'ctest-font-final/LastTest.log') != guest['ctest']['logSha256']):
|
||||
raise SystemExit('Ubuntu CTest evidence does not match its build record')
|
||||
guest['productionSourceDifferencesFromCurrent'] = [name for name, value in sources.items()
|
||||
if compiled_source(name)
|
||||
and guest['sourceSha256'].get(name) != value]
|
||||
guest['productionSourceMatchesCurrent'] = not guest['productionSourceDifferencesFromCurrent']
|
||||
followup_path = ubuntu / 'canvas-build-record.json'
|
||||
if followup_path.is_file():
|
||||
followup = json.loads(followup_path.read_text())
|
||||
followup_ctest = ubuntu / 'canvas-ctest'
|
||||
selected = ET.parse(followup_ctest / 'tests.xml').findall('.//testcase')
|
||||
if (len(selected) != 2 or followup['ctest']['groups'] != 2 or any(
|
||||
c.find('failure') is not None or c.find('skipped') is not None for c in selected)
|
||||
or sha(followup_ctest / 'tests.xml') != followup['ctest']['junitSha256']
|
||||
or sha(followup_ctest / 'LastTest.log') != followup['ctest']['logSha256']):
|
||||
raise SystemExit('Ubuntu supplemental Canvas CTest evidence changed')
|
||||
followup['productionBinariesMatchFullCTest'] = all(
|
||||
followup['binaries'][n] == guest['binaries'][n] for n in record['executables'])
|
||||
followup['productionSourceMatchesCurrent'] = all(
|
||||
followup['sourceSha256'].get(name) == value for name, value in sources.items()
|
||||
if compiled_source(name))
|
||||
followup['canvasTestSourcesMatchCurrent'] = all(
|
||||
followup['sourceSha256'].get(name) == sha(root / name)
|
||||
for name in ['tests/test_pdf_canvas.cpp', 'tests/run_wayland_validation.py', 'CMakeLists.txt'])
|
||||
frozen_runner = ubuntu / 'canvas-wayland/runner-at-start.py'
|
||||
followup['canvasTestSourcesMatchRecordedRun'] = all(
|
||||
recorded_runner_matches(name, followup['sourceSha256'].get(name), frozen_runner)
|
||||
for name in ['tests/test_pdf_canvas.cpp', 'tests/run_wayland_validation.py', 'CMakeLists.txt'])
|
||||
followup['frozenWaylandRunner'] = str(frozen_runner.relative_to(root))
|
||||
wayland_followup = json.loads((ubuntu / 'canvas-wayland/report.json').read_text())
|
||||
followup['wayland'] = wayland_followup
|
||||
followup['waylandMatchesFollowupBuild'] = all(
|
||||
followup['binaries'].get(name) == digest for name, digest in wayland_followup['executables'].items())
|
||||
if not all(followup[name] for name in ['productionBinariesMatchFullCTest',
|
||||
'productionSourceMatchesCurrent', 'canvasTestSourcesMatchRecordedRun', 'waylandMatchesFollowupBuild']):
|
||||
raise SystemExit('Ubuntu Canvas follow-up does not match the current sources or baseline production binaries')
|
||||
guest['canvasFollowup'] = followup
|
||||
guest['wayland'] = {}
|
||||
for mode in ('gui', 'smoke'):
|
||||
value = json.loads((ubuntu / 'wayland' / mode / 'report.json').read_text())
|
||||
value['matchesGuestBuild'] = all(guest['binaries'].get(n) == digest
|
||||
for n, digest in value['executables'].items())
|
||||
correction = ubuntu / 'wayland' / mode / 'scope-correction.json'
|
||||
if correction.is_file():
|
||||
value['scopeCorrection'] = json.loads(correction.read_text())
|
||||
guest['wayland'][mode] = value
|
||||
installed = json.loads((ubuntu / 'installed-smoke/results.json').read_text())
|
||||
guest['installedSmoke'] = {'cases': len(installed),
|
||||
'allReady': all(x['state'] == 'Ready' for x in installed),
|
||||
'matchesInstalledBuild': all(x['binarySha256'] == guest['installedBinaries']['docview'] for x in installed)}
|
||||
inventory = json.loads((ubuntu / 'installed-runtime-final/runtime.json').read_text())
|
||||
indexed = {x['path']: x['sha256'] for x in inventory['files']}
|
||||
guest['installedRuntime'] = {
|
||||
'runtimeValidationSuccess': inventory['runtimeValidationSuccess'],
|
||||
'files': len(inventory['files']), 'systemPackages': len(inventory['systemPackages']),
|
||||
'noticesAndMetadata': len(inventory['notices']), 'noticeGaps': inventory['noticeGaps'],
|
||||
'matchesInstalledBuild': all(indexed.get('install:bin/' + n) == digest
|
||||
for n, digest in guest['installedBinaries'].items()),
|
||||
'completeChromiumNotices': inventory['completeChromiumNotices'],
|
||||
'completeCorrespondingSources': inventory['completeCorrespondingSources'],
|
||||
'runtimeBinariesCopied': inventory['runtimeBinariesCopied']}
|
||||
record['ubuntuValidation'] = guest
|
||||
heavy_root = results / 'heavy-pdf'
|
||||
for environment in ['final-arch', 'ubuntu']:
|
||||
report_path = heavy_root / environment / 'report.json'
|
||||
if not report_path.is_file():
|
||||
continue
|
||||
value = json.loads(report_path.read_text())
|
||||
measurement = json.loads((report_path.parent / 'measurement.json').read_text())
|
||||
if (not value['success'] or not measurement['success']
|
||||
or sha(report_path.parent / 'measurement.json') != value['measurementSha256']
|
||||
or sha(report_path.parent / 'heavy.pdf') != value['fixtureSha256']
|
||||
or not all(sha(root / name) == digest for name, digest in value['sourceSha256'].items())):
|
||||
raise SystemExit('Heavy-PDF supplemental source or evidence changed: ' + environment)
|
||||
value['recordedSourcesMatchCurrent'] = True
|
||||
if environment == 'final-arch':
|
||||
value['binariesMatchCurrent'] = all(sha(root / name) == digest for name, digest in value['binaries'].items())
|
||||
if not value['binariesMatchCurrent']:
|
||||
raise SystemExit('Heavy-PDF Arch libraries/workers/test executable changed')
|
||||
else:
|
||||
guest_binaries = record.get('ubuntuValidation', {}).get('binaries')
|
||||
value['productionBinariesMatchUbuntuBaseline'] = (all(
|
||||
value['binaries'].get('../docview-build/' + name) == guest_binaries[name]
|
||||
for name in record['executables']) if guest_binaries else None)
|
||||
if value['productionBinariesMatchUbuntuBaseline'] is False:
|
||||
raise SystemExit('Heavy-PDF Ubuntu production binaries differ from the selected baseline')
|
||||
value['measurement'] = measurement
|
||||
record.setdefault('heavyPdfFollowup', {})[environment] = value
|
||||
ime_root = results / 'ime'
|
||||
def record_ime(relative, summary, current):
|
||||
ime_path = ime_root / relative / 'report.json'
|
||||
value = json.loads(ime_path.read_text())
|
||||
if (not value['success'] or not value['inputsUnchanged'] or value['remainingOwnedProcesses']
|
||||
or sha(ime_path) != summary['reportSha256']
|
||||
or not all(sha(ime_path.parent / name) == digest for name, digest in summary['evidenceSha256'].items())):
|
||||
raise SystemExit('IME supplemental evidence is incomplete or changed')
|
||||
value['recordedSourcesMatchCurrent'] = all(sha(root / name) == digest
|
||||
for name, digest in value['inputs'].items() if not name.startswith('../'))
|
||||
value['productionSourcesMatchCurrent'] = all(sha(root / name) == digest
|
||||
for name, digest in value['inputs'].items() if name.startswith(('src/', 'qml/', 'resources/')) or name == 'resources.qrc')
|
||||
if not current:
|
||||
for name, digest in value['inputs'].items():
|
||||
if name.startswith('tests/ime/') and sha(ime_path.parent / 'harness-snapshot' / Path(name).name) != digest:
|
||||
raise SystemExit('Historical IME harness snapshot changed: ' + name)
|
||||
guest_binaries = record.get('ubuntuValidation', {}).get('binaries')
|
||||
value['productionBinariesMatchUbuntuBaseline'] = (all(
|
||||
value['inputs'].get('../docview-build/' + name) == guest_binaries[name]
|
||||
for name in record['executables']) if guest_binaries else None)
|
||||
if ((current and not value['recordedSourcesMatchCurrent']) or not value['productionSourcesMatchCurrent']
|
||||
or value['productionBinariesMatchUbuntuBaseline'] is False):
|
||||
raise SystemExit('IME supplemental sources or Ubuntu production binaries changed')
|
||||
platform = value.get('requestedPlatform', 'xcb')
|
||||
expected = ['us', 'jp'] if platform == 'xcb' else ['virtual-keysyms']
|
||||
if set(value['cases']) != set(expected):
|
||||
raise SystemExit('IME case set does not match the recorded platform')
|
||||
for layout in expected:
|
||||
case = json.loads((ime_path.parent / (layout + '.json')).read_text())
|
||||
if (not case['success'] or case != value['cases'][layout] or len(case['phases']) != 5
|
||||
or (current and case['qtPlatform'] != platform)):
|
||||
raise SystemExit('IME case does not match the successful final report: ' + layout)
|
||||
value['summary'] = summary
|
||||
return value
|
||||
if (ime_root / 'summary.json').is_file():
|
||||
record['imePriorX11'] = record_ime('ubuntu/final', json.loads((ime_root / 'summary.json').read_text()), False)
|
||||
if (ime_root / 'current-summary.json').is_file():
|
||||
current = json.loads((ime_root / 'current-summary.json').read_text())
|
||||
if set(current) != {'xcb', 'wayland'}:
|
||||
raise SystemExit('Current IME evidence must contain both tested platforms')
|
||||
runtime_fingerprint = json.loads((ime_root / 'ubuntu/wayland-runtime-record.json').read_text())
|
||||
if runtime_fingerprint['remainingValidationProcesses']:
|
||||
raise SystemExit('IME validation processes were not reclaimed')
|
||||
record['imeFollowup'] = {platform: record_ime(value['directory'], value, True) for platform, value in current.items()}
|
||||
record['imeRuntimeFingerprintAfterRun'] = runtime_fingerprint
|
||||
source_archives = results / 'source-archives'
|
||||
if (source_archives / 'qt-inspection/report.json').is_file():
|
||||
downloads = {name: json.loads((source_archives / 'download' / (name + '.json')).read_text())
|
||||
for name in ['qt-everywhere', 'tomlplusplus']}
|
||||
for name, value in downloads.items():
|
||||
archive = root / value['archive']
|
||||
if not value['success'] or archive.stat().st_size != value['bytes'] or sha(archive) != value['hashes']['sha256']:
|
||||
raise SystemExit('Collected source archive changed: ' + name)
|
||||
qt_source = json.loads((source_archives / 'qt-inspection/report.json').read_text())
|
||||
toml_source = json.loads((source_archives / 'tomlplusplus/verification.json').read_text())
|
||||
patches = json.loads((source_archives / 'arch-patch-recipe-mode/report.json').read_text())
|
||||
if (not qt_source['success'] or not patches['success'] or not patches['pristineFilesUnchanged']
|
||||
or not toml_source['allInstalledHeadersMatch'] or not toml_source['licenseMatchesInstalled']
|
||||
or sha(source_archives / 'qt-inspection/files.jsonl') != qt_source['inventorySha256']
|
||||
or sha(source_archives / 'qt-inspection/notices-index.json') != qt_source['noticesIndexSha256']
|
||||
or qt_source['productionBinaries'] != record['executables']):
|
||||
raise SystemExit('Source archive inspection evidence is incomplete or changed')
|
||||
for step in patches['steps']:
|
||||
if sha(root / step['patch']) != step['patchSha256']:
|
||||
raise SystemExit('Recorded Arch source patch changed')
|
||||
record['sourceArchiveCollection'] = {'status': 'partial', 'downloads': downloads,
|
||||
'qtInspection': qt_source, 'tomlVerification': toml_source, 'archPatchCheck': patches,
|
||||
'completeCorrespondingSources': False, 'completeChromiumNotices': False}
|
||||
pdfium_collection_path = source_archives / 'pdfium-git/report.json'
|
||||
if pdfium_collection_path.is_file():
|
||||
pdfium_collection = json.loads(pdfium_collection_path.read_text())
|
||||
pdfium_check_path = source_archives / 'pdfium-check/report.json'
|
||||
pdfium_check = json.loads(pdfium_check_path.read_text())
|
||||
pdfium_gitlink = json.loads((source_archives / 'pdfium-gitlink/report.json').read_text())
|
||||
pdfium_plan = json.loads((source_archives / 'pdfium-git/plan.json').read_text())
|
||||
if (not pdfium_collection['success'] or not pdfium_check['success'] or not pdfium_gitlink['success']
|
||||
or pdfium_collection['pinSha256'] != sha(results / 'source-correspondence/pdfium/dependency-pins.json')
|
||||
or pdfium_collection['planSha256'] != sha(source_archives / 'pdfium-git/plan.json')
|
||||
or pdfium_collection['collectorSha256'] != sha(root / 'tests/source_archives/collect_pdfium.py')
|
||||
or pdfium_check['checkerSha256'] != sha(root / 'tests/source_archives/check_pdfium.py')
|
||||
or pdfium_gitlink['collectorSha256'] != sha(root / 'tests/source_archives/collect_pdfium_gitlink.py')
|
||||
or pdfium_check['collectionReportSha256'] != sha(pdfium_collection_path)
|
||||
or pdfium_gitlink['parentCollectionSha256'] != sha(pdfium_collection_path)
|
||||
or pdfium_gitlink['gitmodulesSha256'] != sha(source_archives / 'pdfium-gitlink/freetype.gitmodules')
|
||||
or pdfium_check['productionBinaries'] != record['executables']
|
||||
or not pdfium_check['productionBinariesUnchanged']):
|
||||
raise SystemExit('PDFium source collection evidence is incomplete or changed')
|
||||
expected_pins = {(item['path'], item['revision']) for item in pdfium_plan['selectedGit']}
|
||||
actual_pins = {(item['path'], item['revision']) for item in pdfium_collection['repositories']}
|
||||
if expected_pins != actual_pins or len(actual_pins) != 28:
|
||||
raise SystemExit('Selected PDFium Git source inventory differs')
|
||||
nested_pins = {(item['path'] + '/' + link['path'], link['gitObject'])
|
||||
for item in pdfium_collection['repositories'] for link in item['gitlinks']}
|
||||
if nested_pins != {(item['path'], item['revision']) for item in pdfium_gitlink['repositories']}:
|
||||
raise SystemExit('Nested PDFium Git source inventory differs')
|
||||
for item in pdfium_collection['repositories'] + pdfium_gitlink['repositories']:
|
||||
if (not item['success'] or not item['gitFsckPassed'] or not item['archiveVerifiedAgainstGitBlobs']
|
||||
or sha(root / item['archive']) != item['archiveSha256']
|
||||
or (root / item['archive']).stat().st_size != item['archiveBytes']
|
||||
or sha(root / item['inventory']) != item['inventorySha256']):
|
||||
raise SystemExit('Collected PDFium source archive changed: ' + item['path'])
|
||||
for platform, checked in pdfium_check['platforms'].items():
|
||||
for step in checked['steps']:
|
||||
if (sha(root / step['patch']) != step['sha256'] or step['apply']['exitCode']
|
||||
or step['dryRun']['exitCode']):
|
||||
raise SystemExit('PDFium provider patch evidence changed')
|
||||
if len(checked['linuxPackagedHeaderComparisons']) != 24:
|
||||
raise SystemExit('PDFium public header inventory differs')
|
||||
for item in checked['linuxPackagedHeaderComparisons']:
|
||||
if (not item['match'] or item['sourceSha256'] != item['packagedSha256']
|
||||
or sha(root / '.deps/pdfium/include' / item['path']) != item['packagedSha256']):
|
||||
raise SystemExit('PDFium packaged public header changed')
|
||||
if len(pdfium_check['noticeComparisons']) != 15 or len(pdfium_check['gitilesSourceComparisons']) != 31:
|
||||
raise SystemExit('PDFium source comparison inventory differs')
|
||||
for item in pdfium_check['gitilesSourceComparisons']:
|
||||
if sha(results / 'source-correspondence/pdfium' / item['saved']) != item['sha256']:
|
||||
raise SystemExit('Previously retrieved PDFium source changed')
|
||||
for item in pdfium_check['noticeComparisons']:
|
||||
if not item['match'] or sha(root / '.deps/pdfium' / item['path']) != item['sha256']:
|
||||
raise SystemExit('PDFium packaged notice changed')
|
||||
record['sourceArchiveCollection']['pdfiumGit'] = {
|
||||
'collection': pdfium_collection, 'nestedGitlink': pdfium_gitlink, 'sourceCheck': pdfium_check,
|
||||
'completeCorrespondingSources': False, 'buildReproduced': False}
|
||||
sdk_notices_path = source_archives / 'qt-sdk-notices-final/report.json'
|
||||
if sdk_notices_path.is_file():
|
||||
sdk_notices = json.loads(sdk_notices_path.read_text())
|
||||
sdk_evidence = sdk_notices_path.parent
|
||||
archive = root / sdk_notices['sdkArchive']['path']
|
||||
if (not sdk_notices['success']
|
||||
or sdk_notices['collectorSha256'] != sha(root / 'tests/source_archives/collect_qt_sdk_notices.py')
|
||||
or sdk_notices['runtimeRecordSha256'] != sha(results / 'ui-final/ubuntu/installed-runtime-final/runtime.json')
|
||||
or sdk_notices['sdkArchive']['sha256'] != sha(archive)
|
||||
or sdk_notices['sdkArchive']['bytes'] != archive.stat().st_size
|
||||
or sdk_notices['sdkArchiveInventorySha256'] != sha(sdk_evidence / 'sdk-archive-inventory.json')
|
||||
or sdk_notices['noticeBundleSha256'] != sha(sdk_evidence / 'THIRD_PARTY_NOTICES.sdk-extract.txt')
|
||||
or sdk_notices['completeChromiumNotices'] or sdk_notices['completeCorrespondingSources']):
|
||||
raise SystemExit('Qt SDK notice evidence is incomplete or changed')
|
||||
if (len(sdk_notices['notices']) != 129 or len(sdk_notices['sbomFileComparisons']) != 67
|
||||
or len(sdk_notices['testedRuntimeComparisons']) != 83
|
||||
or sdk_notices['externalPackageReferencesValid']
|
||||
or len(sdk_notices['packagesWithoutEmbeddedNoticeText']) != 2):
|
||||
raise SystemExit('Qt SDK notice scope changed; reassess the recorded limitations')
|
||||
for entry in sdk_notices['metadata'].values():
|
||||
if sha(root / entry['localPath']) != entry['sha256']:
|
||||
raise SystemExit('Qt SDK SBOM input changed')
|
||||
qt_tree = root / qt_source['sourceTree']
|
||||
for entry in sdk_notices['notices']:
|
||||
if (sha(sdk_evidence / entry['file']) != entry['sha256']
|
||||
or sha(qt_tree / entry['source']) != entry['sha256']):
|
||||
raise SystemExit('Qt SDK notice or corresponding source changed')
|
||||
for name, digest in sdk_notices['generatorSources'].items():
|
||||
if sha(qt_tree / name) != digest:
|
||||
raise SystemExit('Qt SDK SBOM generator source changed')
|
||||
record['sourceArchiveCollection']['qtSdkNotices'] = sdk_notices
|
||||
package_root = results / 'ubuntu-package'
|
||||
if (package_root / 'lifecycle/report.json').is_file():
|
||||
package_build = json.loads((package_root / 'build/report.json').read_text())
|
||||
package_repeat = json.loads((package_root / 'repeat-report.json').read_text())
|
||||
package_verify = json.loads((package_root / 'build/verification.json').read_text())
|
||||
package_smoke = json.loads((package_root / 'smoke/report.json').read_text())
|
||||
package_lifecycle = json.loads((package_root / 'lifecycle/report.json').read_text())
|
||||
archive = root / 'build-ubuntu-vm/packages' / package_build['archive']
|
||||
if (not all(value['success'] for value in [package_build, package_repeat, package_verify, package_smoke, package_lifecycle])
|
||||
or package_build['archiveSha256'] != sha(archive)
|
||||
or package_build['archiveSha256'] != package_repeat['archiveSha256']
|
||||
or package_build['archiveSha256'] != package_verify['archiveSha256']
|
||||
or package_build['archiveBytes'] != archive.stat().st_size
|
||||
or package_build['manifestSha256'] != sha(package_root / 'build/package-manifest.json')
|
||||
or package_verify['packageManifestSha256'] != package_build['manifestSha256']
|
||||
or package_build['runtimeRecordSha256'] != sha(package_root / 'build/runtime/runtime.json')
|
||||
or package_build['packagerSha256'] != sha(root / 'tools/package_ubuntu.py')
|
||||
or package_build['collectorSha256'] != sha(root / 'tools/collect_ubuntu_validation_runtime.py')
|
||||
or package_build['executableSha256'] != record['ubuntuValidation']['installedBinaries']
|
||||
or package_build['executableSha256'] != package_smoke['executables']
|
||||
or package_smoke['runnerSha256'] != sha(root / 'tests/ubuntu_vm/package_smoke.py')
|
||||
or package_smoke['smokeSourceSha256'] != sha(root / 'tests/smoke.py')
|
||||
or package_smoke['waylandRunnerSha256'] != sha(root / 'tests/run_wayland_validation.py')
|
||||
or package_smoke['launcherSha256'] != sha(root / 'resources/linux/docview-launcher')
|
||||
or package_lifecycle['runnerSha256'] != sha(root / 'tests/ubuntu_vm/package_lifecycle.py')
|
||||
or package_lifecycle['smokeReportSha256'] != sha(package_root / 'smoke/report.json')
|
||||
or package_build['completeChromiumNotices'] or package_build['completeCorrespondingSources']
|
||||
or package_build['publicReleaseApproved']):
|
||||
raise SystemExit('Ubuntu package evidence changed or does not match the tested installation')
|
||||
package_manifest = json.loads((package_root / 'build/package-manifest.json').read_text())
|
||||
packaged_hashes = {row['path']: row['sha256'] for row in package_manifest['files']}
|
||||
for source, destination in [('docview-launcher', 'usr/bin/docview'),
|
||||
('docview.desktop', 'usr/share/applications/docview.desktop'),
|
||||
('docview.apparmor', 'etc/apparmor.d/docview'),
|
||||
('deb-postinst', 'DEBIAN/postinst'), ('deb-prerm', 'DEBIAN/prerm')]:
|
||||
if packaged_hashes[destination] != sha(root / 'resources/linux' / source):
|
||||
raise SystemExit('Ubuntu package launcher or lifecycle resources changed')
|
||||
for location in ['x11/results.json', 'wayland/smoke/results.json']:
|
||||
smoke_cases = json.loads((package_root / 'smoke' / location).read_text())
|
||||
if len(smoke_cases) != 6 or any(row['state'] != 'Ready' or row['binarySha256'] != package_smoke['launcherSha256'] for row in smoke_cases):
|
||||
raise SystemExit('Ubuntu installed launcher smoke failed or changed')
|
||||
package_wayland = json.loads((package_root / 'smoke/wayland/report.json').read_text())
|
||||
if (not package_wayland['success'] or package_wayland['executables'] != package_build['executableSha256']
|
||||
or not all(package_smoke['hiddenOriginalPrefixes'].values())
|
||||
or not package_smoke['callerRuntimeVariablesAbsent'] or not package_smoke['executableBytesUnchanged']
|
||||
or package_smoke['smokeConditions'] != 12
|
||||
or not all(package_lifecycle['remove'].values()) or not all(package_lifecycle['purge'].values())
|
||||
or not package_lifecycle['kernelUserNamespaceRestrictionUnchanged']):
|
||||
raise SystemExit('Ubuntu package isolation or remove/purge checks are incomplete')
|
||||
record['ubuntuPackage'] = {'scope': 'Local validation deb in existing dedicated VM; clean OS/public release incomplete',
|
||||
'build': package_build, 'verification': package_verify, 'repeatedArchiveIdentical': True,
|
||||
'smoke': package_smoke, 'lifecycle': package_lifecycle, 'archivePath': str(archive.relative_to(root))}
|
||||
clean_root = package_root / 'clean-vm'
|
||||
if (clean_root / 'remove/report.json').is_file():
|
||||
phases = {name: json.loads((clean_root / name / 'report.json').read_text()) for name in ['install', 'smoke', 'remove']}
|
||||
for name, phase in phases.items():
|
||||
if (not phase['success'] or phase['archiveSha256'] != package_build['archiveSha256']
|
||||
or phase['runnerSha256'] != sha(root / 'tests/ubuntu_vm/clean_package.py')
|
||||
or phase['originalSdkOrBuildPresent'] or phase['uid'] == 0):
|
||||
raise SystemExit('Fresh Ubuntu package phase failed or changed')
|
||||
for command in phase['commands']:
|
||||
if command['exitCode'] or sha(clean_root / name / (command['name'] + '.log')) != command['logSha256']:
|
||||
raise SystemExit('Fresh Ubuntu command evidence changed')
|
||||
for name in ['install', 'smoke']:
|
||||
if phases[name]['executables'] != package_build['executableSha256']:
|
||||
raise SystemExit('Fresh Ubuntu package uses different production binaries')
|
||||
if (phases['install']['installedFilesVerified'] != 2004
|
||||
or len(phases['install']['elfDependenciesBeforeTestHelpers']) != 131
|
||||
or phases['smoke']['smokeConditions'] != 12 or not phases['smoke']['executableBytesUnchanged']
|
||||
or not phases['smoke']['callerRuntimeVariablesAbsent']
|
||||
or phases['smoke']['launcherSha256'] != package_smoke['launcherSha256']
|
||||
or phases['smoke']['smokeSourceSha256'] != sha(root / 'tests/smoke.py')
|
||||
or phases['smoke']['waylandRunnerSha256'] != sha(root / 'tests/run_wayland_validation.py')
|
||||
or not all(phases['remove'][key] for key in ['payloadRemoved', 'profileUnloaded', 'conffilePurged', 'kernelRestrictionUnchanged'])
|
||||
or len(phases['remove']['userProbesPreserved']) != 4):
|
||||
raise SystemExit('Fresh Ubuntu package tests are incomplete')
|
||||
for location in ['x11/results.json', 'wayland/smoke/results.json']:
|
||||
smoke_cases = json.loads((clean_root / 'smoke' / location).read_text())
|
||||
if len(smoke_cases) != 6 or any(row['state'] != 'Ready' or row['binarySha256'] != package_smoke['launcherSha256'] for row in smoke_cases):
|
||||
raise SystemExit('Fresh Ubuntu smoke evidence changed')
|
||||
fresh_wayland = json.loads((clean_root / 'smoke/wayland/report.json').read_text())
|
||||
if not fresh_wayland['success'] or fresh_wayland['executables'] != package_build['executableSha256']:
|
||||
raise SystemExit('Fresh Ubuntu Wayland build changed')
|
||||
transferred = json.loads((clean_root / 'transferred-inputs.json').read_text())
|
||||
for item in transferred['files']:
|
||||
if sha(root / item['path']) != item['sha256']:
|
||||
raise SystemExit('Fresh Ubuntu transferred test input changed')
|
||||
base_provenance = json.loads((clean_root / 'base-provenance.json').read_text())
|
||||
original_image = root / 'build-ubuntu-vm/downloads/noble-server-cloudimg-amd64.img'
|
||||
if (base_provenance['baseImageSha256'] != sha(original_image) or not base_provenance['newOverlay']
|
||||
or not base_provenance['existingVerification']['ubuntuImageSignatureValid']
|
||||
or base_provenance['runtimeSdkCopied']):
|
||||
raise SystemExit('Fresh Ubuntu base image provenance changed')
|
||||
record['ubuntuPackage']['scope'] = 'Local validation deb on existing and fresh Ubuntu 24.04 cloud-image VMs; Xvfb/headless Wayland/software; public release and physical desktop incomplete'
|
||||
record['ubuntuPackage']['freshOs'] = {'base': base_provenance, 'phases': phases,
|
||||
'transferredTestInputs': len(transferred['files']), 'guiTestHelpersAddedAfterElfDependencyCheck': True}
|
||||
prior_stress = results / 'stress-final/report.json'
|
||||
if args.runtime_results and prior_stress.is_file():
|
||||
record['stressPdfPrior'] = json.loads(prior_stress.read_text())
|
||||
record['performance'] = {}
|
||||
for corpus in ['performance', 'performance-standard', 'performance-fonts']:
|
||||
record['performance'][corpus] = {}
|
||||
for path in sorted((runtime / corpus).glob('*.json')):
|
||||
values = json.loads(path.read_text())
|
||||
record['performance'][corpus][path.stem] = {key: values.get(key) for key in
|
||||
['success', 'summary', 'documentSha256', 'applicationRssPeakBytes', 'processGroupRssPeakBytes',
|
||||
'tileCachePeakChargedBytes', 'tileCacheBudgetBytes', 'staleRenderResponses', 'corpus', 'method',
|
||||
'executableSha256', 'schemaVersion', 'memoryStart', 'memoryAfterFinalClose',
|
||||
'operationSeries', 'pendingRenderPeak', 'queuedRenderPeak', 'activeRenderPeak',
|
||||
'discardedQueuedRequests', 'navigationCoverage', 'measurementChecks', 'proposedBudgetObservations',
|
||||
'coldProcessOpen', 'qtPlatform', 'sceneGraphBackend', 'timingSemantics', 'runnerSha256',
|
||||
'display', 'maximumMemoryPressureLevel']}
|
||||
record['performance'][corpus][path.stem]['matchesRecordedBuild'] = (
|
||||
values.get('executableSha256') == record['executables'])
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
for path in sorted((runtime / corpus / 'cold').rglob('*.json')):
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
run_log = runtime / (corpus + '-canonical.log')
|
||||
if run_log.is_file():
|
||||
record['evidenceSha256'][str(run_log.relative_to(root))] = sha(run_log)
|
||||
for name, relative in [('installedSmoke', 'smoke/results.json'),
|
||||
('stressPdf', 'stress/report.json' if args.runtime_results else 'stress-final/report.json'),
|
||||
('workerDeadlines', 'worker-deadlines/report.json'),
|
||||
('installedDependencies', 'installed-dependencies.json')]:
|
||||
path = runtime / relative
|
||||
if path.is_file():
|
||||
record[name] = json.loads(path.read_text())
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
for relative in ['worker-deadlines/junit.xml', 'worker-deadlines/test.txt']:
|
||||
path = runtime / relative
|
||||
if path.is_file():
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
for folder in ['recent-documents', 'web-epub-boundaries', 'resource-warnings', 'zip-entry-choice', 'epub-spreads',
|
||||
'link-borders', 'pdf-annotation-flags', 'pdf-comments', 'resource-classification', 'pdf-structure',
|
||||
'pdf', 'pdf-extended', 'pdf-fonts', 'font-collection', 'pdf-icc', 'smoke',
|
||||
'renderer-response', 'navigation-reflow', 'archive-ratio', 'linux-development-package',
|
||||
'performance-interaction', 'performance-before-xhtml-fix', 'xhtml-headings', 'benchmark-xhtml',
|
||||
'stress', 'stress-final', 'worker-deadlines', 'render-review', 'dependency-provenance',
|
||||
'completion-regressions', 'canvas-scene-graph', 'heavy-pdf', 'ime', 'source-archives', 'ubuntu-package']:
|
||||
for path in sorted((root / 'tests/results' / folder).rglob('*')):
|
||||
if path.is_file() and (path.suffix in {'.png', '.txt', '.log', '.md', '.xml', '.json', '.html', '.py'}
|
||||
or (folder == 'heavy-pdf' and path.suffix in {'.pdf', '.cpp'})
|
||||
or (folder == 'ime' and path.suffix in {'.cpp', '.xkb'}) or path.name.endswith('.tar.gz')):
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
if args.runtime_results:
|
||||
for path in sorted(runtime.rglob('*')):
|
||||
if path.is_file() and '__pycache__' not in path.parts:
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
interaction = runtime / ('performance-interaction/record.json' if args.runtime_results
|
||||
else 'performance-interaction/canonical-xhtml/record.json')
|
||||
for path in sorted((root / 'tests/results/source-correspondence').rglob('*')):
|
||||
if path.is_file() and '__pycache__' not in path.parts:
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
if interaction.is_file():
|
||||
record['interactionMeasurement'] = json.loads(interaction.read_text())
|
||||
sampled = {entry['path']: entry['sha256'] for entry in record['interactionMeasurement'].get('binaries', [])}
|
||||
record['interactionMeasurementBuildMatches'] = all(
|
||||
sampled.get(name) == record['executables'][name]
|
||||
for name in ['docview-pdf-worker', 'docview-archive-worker']) and sampled.get('test_app') == sha(build / 'test_app')
|
||||
record['interactionMeasurementScope'] = 'test_app with production Controller/QML and worker executables; docview main() is exercised separately by the application benchmarks and installed smoke tests.'
|
||||
package_folder = runtime / 'linux-development-package'
|
||||
package_name = 'docview-0.1.0-arch-x86_64-development.json'
|
||||
package = package_folder / ('package' if args.runtime_results else 'source-notices') / package_name
|
||||
prior_package = results / 'linux-development-package/source-notices' / package_name
|
||||
if prior_package.is_file():
|
||||
record['linuxDevelopmentPackagePrior'] = json.loads(prior_package.read_text())
|
||||
if package.is_file():
|
||||
record['linuxDevelopmentPackage'] = json.loads(package.read_text())
|
||||
packaged = record['linuxDevelopmentPackage'].get('executableSha256', {})
|
||||
record['linuxDevelopmentPackageInstalledBuildMatches'] = packaged == {
|
||||
'bin/' + name: digest for name, digest in record['installedExecutableSha256'].items()}
|
||||
record['linuxDevelopmentPackageUninstalledBuildMatches'] = {
|
||||
name: packaged.get('bin/' + name) == digest for name, digest in record['executables'].items()}
|
||||
record['linuxDevelopmentPackageMatchScope'] = ('Compared to cmake-installed executables. The PDF worker RUNPATH '
|
||||
'changes from its development PDFium path to $ORIGIN/../lib during installation, so its installed '
|
||||
'file hash differs from build/docview-pdf-worker. Both identities are retained.')
|
||||
package_smoke = package_folder / ('smoke' if args.runtime_results else 'source-notices-smoke') / 'package-smoke.json'
|
||||
if package_smoke.is_file():
|
||||
record['linuxDevelopmentPackageSmoke'] = json.loads(package_smoke.read_text())
|
||||
prior_smoke = results / 'linux-development-package/source-notices-smoke/package-smoke.json'
|
||||
if prior_smoke.is_file():
|
||||
record['linuxDevelopmentPackageSmokePrior'] = json.loads(prior_smoke.read_text())
|
||||
notice_record = results / 'linux-development-package/source-notices-record.json'
|
||||
if notice_record.is_file():
|
||||
record['linuxDevelopmentPackageNoticeUpdate'] = json.loads(notice_record.read_text())
|
||||
record['pdfComparisons'] = {}
|
||||
record['pdfComparisonBuildMatches'] = {}
|
||||
for name in ['pdf', 'pdf-extended', 'pdf-fonts', 'font-collection', 'link-borders', 'pdf-icc']:
|
||||
path = runtime / name / 'comparison.json'
|
||||
if path.is_file():
|
||||
record['pdfComparisons'][name] = json.loads(path.read_text())
|
||||
record['pdfComparisonBuildMatches'][name] = (
|
||||
record['pdfComparisons'][name].get('worker_sha256') == record['executables']['docview-pdf-worker'])
|
||||
record['evidenceSha256'][str(path.relative_to(root))] = sha(path)
|
||||
if 'workerDeadlines' in record:
|
||||
record['workerDeadlinesBuildMatches'] = {
|
||||
'testExecutable': record['workerDeadlines'].get('testExecutableSha256') == sha(build / 'test_worker_deadlines'),
|
||||
'commonLibrary': record['workerDeadlines'].get('commonLibrarySha256') == sha(build / 'libdocview_common.a')}
|
||||
if 'stressPdf' in record:
|
||||
record['stressPdfBuildMatches'] = record['stressPdf'].get('executableSha256') == record['executables']
|
||||
record['buildScope'] = ('Executable hashes identify the current production build; source hashes record the current tree. '
|
||||
'The frozen full CTest report is bound to its canonical build record. Later test-only Canvas/HiDPI changes '
|
||||
'and their focused results are identified separately; the full report is not relabeled as a rerun of those changes. '
|
||||
'Runtime results are selected explicitly; missing new evidence is not filled with an older success. '
|
||||
'Each performance record identifies its measured executables separately. '
|
||||
'Build-match flags distinguish earlier performance and PDF-comparison evidence. '
|
||||
'The stress record identifies all three measured executables. Historical reports and packages retain their own identities. '
|
||||
'The standalone real-time watchdog record identifies its own test executable and common library.')
|
||||
args.output.write_text(json.dumps(record, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
||||
print(f'{len(cases)} passing CTest suites recorded in {args.output}')
|
||||
Reference in New Issue
Block a user