268 lines
8.5 KiB
Python
Executable File
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())
|