Files
2026-08-27 11:12:22 +02:00

268 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail-closed, profile-aware verifier for S03-H1 runtime configurations."""
import argparse
import hashlib
import sys
from pathlib import Path
EXPECTED_PROFILE_HASHES = {
"TEST": "d037561b32760fbd0e2aec9f1eefc5efda27b1918eca4742c89092a8a2f7e92e",
"DIAGNOSTIC": "cd999ff249452298d650a22c8fe5b0b03bf48c0735f4630befdec8209cbb13a3",
}
EXPECTED_DISPLAY_CLASSES = {
"TEST": "GAMDataSource",
"DIAGNOSTIC": "LoggerDataSource",
}
EXPECTED_GAMDISPLAY_BLOCK_SHA256 = (
"13b14b504dac43df5febf230cd069c5093d951726382b1d5dee8edf43d6814d0"
)
EXPECTED_IOGAM_WRITER_BLOCK_SHA256 = (
"b3d54ad2fae08e6640bfa0be39ead9841906b4573a3b407e5ecf21340b5c5717"
)
EXPECTED_FILEWRITER_BLOCK_SHA256 = (
"0bb949b9d72fa758bdfd8b2039336bc1fda071ecf038304402032de63f4fc455"
)
PLACEHOLDER_LINE = ' Filename = "__MARTE_RUN_DIR__/s03_output.bin"'
def digest(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def extract_block(text, start_marker, end_marker):
if text.count(start_marker) != 1:
raise ValueError(f"START_MARKER_COUNT:{start_marker!r}:{text.count(start_marker)}")
start = text.index(start_marker)
try:
end = text.index(end_marker, start)
except ValueError as exc:
raise ValueError(f"END_MARKER_MISSING:{end_marker!r}") from exc
return text[start:end]
class Verification:
def __init__(self):
self.checks = {}
self.first_error = "NONE"
def check(self, name, condition, error_code):
self.checks[name] = "PASS" if condition else "FAIL"
if not condition and self.first_error == "NONE":
self.first_error = error_code
def emit(self):
for name in sorted(self.checks):
print(f"{name}={self.checks[name]}")
print(f"PROFILE_ERROR_CODE={self.first_error}")
def verify(mode, config_path, expected_filename):
report = Verification()
try:
text = config_path.read_text(encoding="utf-8")
except OSError as exc:
print("CONFIG_READ_STATUS=FAIL")
print(f"CONFIG_READ_ERROR={exc}")
print("PROFILE_ERROR_CODE=CONFIG_READ_FAILED")
print("CONFIG_PROFILE_VERIFICATION_STATUS=FAIL")
return 1
expected_line = f' Filename = "{expected_filename}"'
filename_count = sum(1 for line in text.splitlines() if line == expected_line)
placeholder_count = text.count("__MARTE_RUN_DIR__")
canonical = text
if filename_count == 1:
canonical = text.replace(expected_line, PLACEHOLDER_LINE, 1)
canonical_hash = digest(canonical)
expected_profile_hash = EXPECTED_PROFILE_HASHES[mode]
expected_display_class = EXPECTED_DISPLAY_CLASSES[mode]
print(f"VERIFIED_MODE={mode}")
print(f"EXPECTED_FILENAME={expected_filename}")
print(f"EXPECTED_FILENAME_COUNT={filename_count}")
print(f"PLACEHOLDER_COUNT={placeholder_count}")
print(f"CANONICAL_CONFIG_SHA256={canonical_hash}")
print(f"EXPECTED_PROFILE_CONFIG_SHA256={expected_profile_hash}")
report.check(
"RUNTIME_FILENAME_STATUS",
filename_count == 1 and placeholder_count == 0,
"RUNTIME_FILENAME_MISMATCH",
)
report.check(
"GAMDISPLAY_OBJECT_STATUS",
text.count("+GAMDisplay =") == 1,
"GAMDISPLAY_OBJECT_COUNT_MISMATCH",
)
report.check(
"THREAD_ORDER_STATUS",
text.count(
"Functions = {GAMTimer SentinelProducer GAMDisplay IOGAM_Writer}"
)
== 1,
"THREAD_ORDER_MISMATCH",
)
try:
gamdisplay = extract_block(
canonical,
" +GAMDisplay = {\n",
" +IOGAM_Writer = {\n",
)
except ValueError as exc:
print(f"GAMDISPLAY_EXTRACTION_ERROR={exc}")
gamdisplay = ""
display_checks = {
"GAMDISPLAY_CLASS_COUNT": gamdisplay.count("Class = IOGAM") == 1,
"GAMDISPLAY_COUNTER_COUNT": gamdisplay.count(
" Counter = {\n"
)
== 2,
"GAMDISPLAY_TIME_COUNT": gamdisplay.count(" Time = {\n") == 2,
"GAMDISPLAY_CYCLETIME_COUNT": gamdisplay.count(
"State1_Thread1_CycleTime ="
)
== 2,
"GAMDISPLAY_SCALAR_A_COUNT": gamdisplay.count("SentinelScalarA =") == 2,
"GAMDISPLAY_SCALAR_B_COUNT": gamdisplay.count("SentinelScalarB =") == 2,
"GAMDISPLAY_VECTOR_COUNT": gamdisplay.count("SentinelVector =") == 2,
"GAMDISPLAY_VECTOR_DIMENSION_COUNT": gamdisplay.count(
"NumberOfDimensions = 1"
)
== 2,
"GAMDISPLAY_VECTOR_ELEMENTS_COUNT": gamdisplay.count(
"NumberOfElements = 3"
)
== 2,
"GAMDISPLAY_OUTPUT_DATASOURCE_COUNT": gamdisplay.count(
"DataSource = Display"
)
== 6,
}
for name, passed in display_checks.items():
report.check(name, passed, f"{name}_MISMATCH")
gamdisplay_hash = digest(gamdisplay)
print(f"GAMDISPLAY_BLOCK_SHA256={gamdisplay_hash}")
print(
"EXPECTED_GAMDISPLAY_BLOCK_SHA256="
f"{EXPECTED_GAMDISPLAY_BLOCK_SHA256}"
)
report.check(
"GAMDISPLAY_BLOCK_STATUS",
gamdisplay_hash == EXPECTED_GAMDISPLAY_BLOCK_SHA256,
"GAMDISPLAY_BLOCK_HASH_MISMATCH",
)
try:
writer = extract_block(
canonical,
" +IOGAM_Writer = {\n",
" }\n +Data = {\n",
)
except ValueError as exc:
print(f"IOGAM_WRITER_EXTRACTION_ERROR={exc}")
writer = ""
writer_hash = digest(writer)
print(f"IOGAM_WRITER_BLOCK_SHA256={writer_hash}")
print(
"EXPECTED_IOGAM_WRITER_BLOCK_SHA256="
f"{EXPECTED_IOGAM_WRITER_BLOCK_SHA256}"
)
report.check(
"IOGAM_WRITER_INVARIANT_STATUS",
writer_hash == EXPECTED_IOGAM_WRITER_BLOCK_SHA256,
"IOGAM_WRITER_BLOCK_HASH_MISMATCH",
)
try:
filewriter = extract_block(
canonical,
" +FileWriter = {\n",
" }\n +States = {\n",
)
except ValueError as exc:
print(f"FILEWRITER_EXTRACTION_ERROR={exc}")
filewriter = ""
filewriter_hash = digest(filewriter)
print(f"FILEWRITER_BLOCK_SHA256={filewriter_hash}")
print(
"EXPECTED_FILEWRITER_BLOCK_SHA256="
f"{EXPECTED_FILEWRITER_BLOCK_SHA256}"
)
report.check(
"FILEWRITER_INVARIANT_STATUS",
filewriter_hash == EXPECTED_FILEWRITER_BLOCK_SHA256,
"FILEWRITER_BLOCK_HASH_MISMATCH",
)
structural_expectations = {
"CONSTANT_GAM_COUNT": (text.count("Class = ConstantGAM"), 1),
"SENTINEL_SCALAR_A_DECLARATION_COUNT": (
text.count("SentinelScalarA ="),
6,
),
"SENTINEL_SCALAR_B_DECLARATION_COUNT": (
text.count("SentinelScalarB ="),
6,
),
"SENTINEL_VECTOR_DECLARATION_COUNT": (
text.count("SentinelVector ="),
6,
),
"VECTOR_DIMENSION_COUNT": (text.count("NumberOfDimensions = 1"), 6),
"VECTOR_CARDINALITY_COUNT": (text.count("NumberOfElements = 3"), 6),
"FILEWRITER_CLASS_COUNT": (
text.count("Class = FileDataSource::FileWriter"),
1,
),
"PLAIN_FILEWRITER_CLASS_COUNT": (text.count("Class = FileWriter"), 0),
}
for name, (observed, expected) in structural_expectations.items():
print(f"{name}={observed}")
print(f"EXPECTED_{name}={expected}")
report.check(
f"{name}_STATUS",
observed == expected,
f"{name}_MISMATCH",
)
expected_display_block = (
" +Display = {\n"
f" Class = {expected_display_class}\n"
" }\n"
)
report.check(
"DISPLAY_DATASOURCE_CLASS_STATUS",
text.count(expected_display_block) == 1,
"DISPLAY_DATASOURCE_CLASS_MISMATCH",
)
report.check(
"PROFILE_CANONICAL_HASH_STATUS",
canonical_hash == expected_profile_hash,
"PROFILE_CANONICAL_HASH_MISMATCH",
)
report.emit()
passed = all(value == "PASS" for value in report.checks.values())
print(f"CONFIG_PROFILE_VERIFICATION_STATUS={'PASS' if passed else 'FAIL'}")
return 0 if passed else 1
def main():
parser = argparse.ArgumentParser()
parser.add_argument("mode", choices=sorted(EXPECTED_PROFILE_HASHES))
parser.add_argument("configuration", type=Path)
parser.add_argument("expected_filename")
args = parser.parse_args()
return verify(args.mode, args.configuration, args.expected_filename)
if __name__ == "__main__":
sys.exit(main())