mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-22 06:09:35 +08:00
Preserve the current selector, frozen-input inventory, result codec, coverage collection, runner filtering, and isolated Windows venue work. Component-level regression proofs are recorded in the PR handover. The live classified provider, public execution integration, artifact admission, fresh oracle/shadow evidence, and full platform gates remain incomplete. This checkpoint does not enable or claim verified narrowing. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
650 lines
29 KiB
Python
650 lines
29 KiB
Python
"""Experimental, bounded POSIX native transport for unadmitted profile-map v2.
|
|
|
|
Only actual observations are transported. Registry/image hashes are custody
|
|
facts, not evidence that a registry, interval, runtime or image set is complete.
|
|
No RowEvidence is created here, so this bundle cannot authorize negative
|
|
coverage evidence or test narrowing. Raw evidence is retained, including on
|
|
failure. Process groups cannot prove closure of children that leave the group.
|
|
"""
|
|
|
|
import errno
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import selectors
|
|
import signal
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from profile_map_v2 import (
|
|
ExpectedSuite, ObservationRole, ProfileMapError, ProfileMapLimits,
|
|
ProfileMapLimitExceededError, ProfileMapCancelledError,
|
|
ProfileObservation, SuiteOutcome, build_profile_map_v2,
|
|
)
|
|
from profile_text_parser import (
|
|
ParseLimits, ProfileTextError, LimitExceededError, ParseCancelledError, parse_profile_text,
|
|
)
|
|
|
|
|
|
REGISTRY_CAP = 2 * 1024 * 1024
|
|
ROW_CAP = 65536
|
|
RECORD_CAP = 4000000
|
|
COUNTER_CAP = 32000000
|
|
NAME_CAP = 128 * 1024 * 1024
|
|
GRACE = 2.0
|
|
PROFILE = re.compile(r"^(.+?)\.(parent|[0-9]+)\.profraw$")
|
|
SETUP = re.compile(r"^_setup\.(?:[0-9]+|child\.[0-9]+)\.profraw$")
|
|
MARKER = re.compile(r"^(.+?)\.([0-9]+)\.forked$")
|
|
|
|
|
|
class CollectionError(ValueError):
|
|
def __init__(self, kind, message):
|
|
super().__init__(message)
|
|
self.kind = kind
|
|
|
|
|
|
def _error(kind, message):
|
|
raise CollectionError(kind, message)
|
|
|
|
|
|
def _directory(path):
|
|
"""Open each absolute component without following symlinks; caller closes."""
|
|
path = os.path.abspath(path)
|
|
fd = os.open(os.sep, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
for part in path.split(os.sep):
|
|
if not part:
|
|
continue
|
|
next_fd = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
|
dir_fd=fd)
|
|
os.close(fd)
|
|
fd = next_fd
|
|
return fd
|
|
except BaseException:
|
|
os.close(fd)
|
|
raise
|
|
|
|
|
|
def _regular(path):
|
|
parent, name = os.path.split(os.path.abspath(path))
|
|
directory = _directory(parent)
|
|
try:
|
|
fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK,
|
|
dir_fd=directory)
|
|
finally:
|
|
os.close(directory)
|
|
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
os.close(fd)
|
|
_error("input", "input is not a regular file")
|
|
return fd
|
|
|
|
|
|
def _signature(info):
|
|
return (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
|
|
|
|
|
|
def _read(path, limit):
|
|
fd = _regular(path)
|
|
try:
|
|
before = os.fstat(fd)
|
|
if before.st_size > limit:
|
|
_error("resource_limit", "file byte limit exceeded")
|
|
data = bytearray()
|
|
while True:
|
|
part = os.read(fd, min(65536, limit + 1 - len(data)))
|
|
if not part:
|
|
break
|
|
data.extend(part)
|
|
if len(data) > limit:
|
|
_error("resource_limit", "file byte limit exceeded")
|
|
if _signature(before) != _signature(os.fstat(fd)):
|
|
_error("identity", "input changed during capture")
|
|
return bytes(data)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
|
|
def _identity(path):
|
|
fd = _regular(path)
|
|
try:
|
|
before = os.fstat(fd)
|
|
if before.st_size > 1024 * 1024 * 1024:
|
|
_error("resource_limit", "executable exceeds 1GiB identity limit")
|
|
digest = hashlib.sha256()
|
|
size = 0
|
|
while True:
|
|
part = os.read(fd, 65536)
|
|
if not part:
|
|
break
|
|
size += len(part)
|
|
if size > 1024 * 1024 * 1024:
|
|
_error("resource_limit", "executable grew beyond identity limit")
|
|
digest.update(part)
|
|
if _signature(before) != _signature(os.fstat(fd)) or size != before.st_size:
|
|
_error("identity", "executable changed while hashing")
|
|
if not os.access(path, os.X_OK):
|
|
_error("input", "executable is not executable")
|
|
return {"path": path, "sha256": digest.hexdigest(), "bytes": size}
|
|
finally:
|
|
os.close(fd)
|
|
|
|
|
|
def _pairs(values):
|
|
result = {}
|
|
for key, value in values:
|
|
if key in result:
|
|
_error("input", "duplicate registry key")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _identifier(value, suite=False):
|
|
if type(value) is not str or not value:
|
|
_error("input", "registry identifier must be a nonempty string")
|
|
try:
|
|
data = value.encode("utf-8", "strict")
|
|
except UnicodeError:
|
|
_error("input", "registry identifier is not valid UTF-8")
|
|
if any(char in data for char in (0, 9, 10, 13)) or (suite and b":" in data):
|
|
_error("input", "registry identifier violates producer grammar")
|
|
if not suite and value == "*":
|
|
_error("input", "setup test name is reserved")
|
|
# Native routing uses each identifier as one filesystem component. The
|
|
# pure producer accepts a wider byte grammar; those routes are unsupported.
|
|
if value in (".", "..") or "/" in value or "\\" in value:
|
|
_error("unsupported", "registry identifier cannot be a native path component")
|
|
if not suite and value in ("_setup", "_setup.child"):
|
|
_error("unsupported", "registry test collides with native setup routing")
|
|
return data
|
|
|
|
|
|
def _registry(path, image):
|
|
raw = _read(path, REGISTRY_CAP)
|
|
try:
|
|
value = json.loads(raw.decode("utf-8", "strict"), object_pairs_hook=_pairs,
|
|
parse_constant=lambda _: _error("input", "nonfinite JSON number"))
|
|
except (UnicodeError, json.JSONDecodeError, RecursionError):
|
|
_error("input", "invalid registry JSON")
|
|
if type(value) is not dict or set(value) != {"format", "image_sha256", "suites"}:
|
|
_error("input", "registry keys do not match v0 schema")
|
|
if value["format"] != "cbm.coverage.registry.v0":
|
|
_error("input", "unsupported registry format")
|
|
declared = value["image_sha256"]
|
|
if type(declared) is not str or not re.fullmatch(r"[0-9a-f]{64}", declared):
|
|
_error("input", "registry image digest must be lowercase SHA256")
|
|
if declared != image["sha256"]:
|
|
_error("identity", "registry does not match runner image")
|
|
suites = value["suites"]
|
|
if type(suites) is not list or not suites:
|
|
_error("input", "registry suites must be nonempty")
|
|
result = {}
|
|
rows = 0
|
|
for item in suites:
|
|
if type(item) is not dict or set(item) != {"name", "tests"}:
|
|
_error("input", "registry suite keys do not match schema")
|
|
name = item["name"]
|
|
_identifier(name, suite=True)
|
|
if name in result:
|
|
_error("input", "duplicate registry suite")
|
|
tests = item["tests"]
|
|
if type(tests) is not list or not tests:
|
|
_error("input", "registry tests must be nonempty")
|
|
seen = set()
|
|
for test in tests:
|
|
_identifier(test)
|
|
if test in seen:
|
|
_error("input", "duplicate registry test")
|
|
seen.add(test)
|
|
rows += 1 + len(tests)
|
|
if rows > ROW_CAP:
|
|
_error("resource_limit", "registry row limit exceeded")
|
|
result[name] = tuple(tests)
|
|
return result, hashlib.sha256(raw).hexdigest(), raw
|
|
|
|
|
|
class Evidence:
|
|
def __init__(self, path, args):
|
|
self.path = os.path.abspath(path)
|
|
self.args = args
|
|
self.records = []
|
|
self.uncertainty = []
|
|
self.converted_bytes = 0
|
|
self.captured_bytes = 0
|
|
self.allowed_suites = set()
|
|
self.directory = None
|
|
parent, name = os.path.split(self.path)
|
|
if not name:
|
|
_error("input", "output must name a fresh directory")
|
|
fd = _directory(parent)
|
|
try:
|
|
os.mkdir(name, 0o700, dir_fd=fd)
|
|
self.directory = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
|
|
dir_fd=fd)
|
|
finally:
|
|
os.close(fd)
|
|
try:
|
|
self.directory_identity = os.fstat(self.directory)
|
|
for name in ("evidence", "raw"):
|
|
os.mkdir(name, 0o700, dir_fd=self.directory)
|
|
except BaseException:
|
|
self.close()
|
|
raise
|
|
|
|
def close(self):
|
|
if self.directory is not None:
|
|
os.close(self.directory)
|
|
self.directory = None
|
|
|
|
def check(self):
|
|
fd = _directory(self.path)
|
|
try:
|
|
actual = os.fstat(fd)
|
|
if (actual.st_dev, actual.st_ino) != (self.directory_identity.st_dev,
|
|
self.directory_identity.st_ino):
|
|
_error("identity", "owned output directory was replaced")
|
|
finally:
|
|
os.close(fd)
|
|
|
|
def write(self, relative, data):
|
|
self.check()
|
|
parent, name = os.path.split(relative)
|
|
fd = _directory(os.path.join(self.path, parent))
|
|
try:
|
|
target = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
|
0o600, dir_fd=fd)
|
|
try:
|
|
with os.fdopen(target, "wb") as stream:
|
|
stream.write(data)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
except BaseException:
|
|
# Partial evidence remains; no marker is created on this path.
|
|
raise
|
|
finally:
|
|
os.close(fd)
|
|
return os.path.join(self.path, relative)
|
|
|
|
def json(self, relative, value):
|
|
data = (json.dumps(value, sort_keys=True, indent=2,
|
|
ensure_ascii=True) + "\n").encode("ascii")
|
|
if len(data) > 64 * 1024 * 1024:
|
|
_error("resource_limit", "metadata exceeds 64MiB bound")
|
|
return self.write(relative, data)
|
|
|
|
def publish(self, meta):
|
|
# A partial JSON file is never a commit marker. The pending complete
|
|
# file remains evidence, and hard-link creation cannot overwrite a
|
|
# preexisting or symlink destination.
|
|
self.json("meta.pending.json", meta)
|
|
for name in ("profiles", "tests"):
|
|
data = _read(os.path.join(self.path, name + ".tsv"), self.args.max_map_bytes)
|
|
if hashlib.sha256(data).hexdigest() != meta[name + "_sha256"]:
|
|
_error("identity", "wire changed before publication")
|
|
self.check()
|
|
os.fsync(self.directory)
|
|
os.link("meta.pending.json", "meta.json", src_dir_fd=self.directory,
|
|
dst_dir_fd=self.directory, follow_symlinks=False)
|
|
|
|
def inventory(self):
|
|
"""Bound scans while children run; never follow raw tree symlinks."""
|
|
self.check()
|
|
root = os.path.join(self.path, "raw")
|
|
pending = [root]
|
|
files, total, count = [], 0, 1 # listing counts as one profile slot
|
|
try:
|
|
info = os.stat("listing.profraw", dir_fd=self.directory, follow_symlinks=False)
|
|
except FileNotFoundError:
|
|
info = None
|
|
if info is not None:
|
|
if not stat.S_ISREG(info.st_mode):
|
|
_error("input", "listing profile is not a regular file")
|
|
if info.st_size > self.args.max_profile_bytes:
|
|
_error("resource_limit", "listing profile byte limit exceeded")
|
|
total = info.st_size
|
|
if total > self.args.max_total_profile_bytes:
|
|
_error("resource_limit", "aggregate raw byte limit exceeded")
|
|
while pending:
|
|
directory = pending.pop()
|
|
fd = _directory(directory)
|
|
try:
|
|
with os.scandir(fd) as entries:
|
|
for entry in entries:
|
|
count += 1
|
|
if count > self.args.max_profile_files:
|
|
_error("resource_limit", "raw entry count limit exceeded")
|
|
info = entry.stat(follow_symlinks=False)
|
|
path = os.path.join(directory, entry.name)
|
|
if stat.S_ISDIR(info.st_mode) and directory == root:
|
|
if entry.name not in self.allowed_suites:
|
|
self.uncertainty.append({"path": path, "reason": "unknown_suite_destination"})
|
|
_error("input", "raw suite directory is outside requested scope")
|
|
pending.append(path)
|
|
elif stat.S_ISREG(info.st_mode):
|
|
if info.st_size > self.args.max_profile_bytes:
|
|
_error("resource_limit", "raw file byte limit exceeded")
|
|
total += info.st_size
|
|
if total > self.args.max_total_profile_bytes:
|
|
_error("resource_limit", "aggregate raw byte limit exceeded")
|
|
files.append(path)
|
|
else:
|
|
self.uncertainty.append({"path": path, "reason": "unsupported_raw_entry"})
|
|
_error("input", "unsupported raw tree entry")
|
|
finally:
|
|
os.close(fd)
|
|
return sorted(files)
|
|
|
|
|
|
def _group_exists(pid):
|
|
try:
|
|
os.killpg(pid, 0)
|
|
return True
|
|
except ProcessLookupError:
|
|
return False
|
|
|
|
|
|
def _quiesce(proc):
|
|
sent = []
|
|
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
if proc.poll() is not None and not _group_exists(proc.pid):
|
|
return True, sent
|
|
try:
|
|
os.killpg(proc.pid, sig)
|
|
sent.append(signal.Signals(sig).name)
|
|
except ProcessLookupError:
|
|
pass
|
|
until = time.monotonic() + GRACE
|
|
while time.monotonic() < until:
|
|
if proc.poll() is not None and not _group_exists(proc.pid):
|
|
return True, sent
|
|
time.sleep(0.02)
|
|
return proc.poll() is not None and not _group_exists(proc.pid), sent
|
|
|
|
|
|
def _run(evidence, label, argv, environment, timeout, cwd, watch_raw=False):
|
|
selector = selectors.DefaultSelector()
|
|
streams = {"stdout": bytearray(), "stderr": bytearray()}
|
|
started = time.monotonic()
|
|
proc = None
|
|
failure = None
|
|
quiescent, cleanup = False, []
|
|
try:
|
|
proc = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, env=environment, cwd=cwd,
|
|
start_new_session=True, bufsize=0)
|
|
for name, pipe in (("stdout", proc.stdout), ("stderr", proc.stderr)):
|
|
os.set_blocking(pipe.fileno(), False)
|
|
selector.register(pipe, selectors.EVENT_READ, name)
|
|
while True:
|
|
if watch_raw:
|
|
evidence.inventory()
|
|
left = timeout - (time.monotonic() - started)
|
|
if left <= 0:
|
|
failure = "timeout"
|
|
break
|
|
for key, _ in selector.select(min(left, 0.05)):
|
|
part = os.read(key.fd, 65536)
|
|
if not part:
|
|
selector.unregister(key.fileobj)
|
|
continue
|
|
used = sum(len(data) for data in streams.values())
|
|
allowed = min(evidence.args.max_output_bytes - used,
|
|
evidence.args.max_total_output_bytes - evidence.captured_bytes - used)
|
|
streams[key.data].extend(part[:max(0, allowed)])
|
|
if len(part) > allowed:
|
|
failure = "resource_limit"
|
|
break
|
|
if failure:
|
|
break
|
|
if proc.poll() is not None:
|
|
if _group_exists(proc.pid):
|
|
failure = "quiescence"
|
|
break
|
|
if not selector.get_map():
|
|
break
|
|
except CollectionError as error:
|
|
failure = error.kind
|
|
except (OSError, ValueError):
|
|
failure = "process"
|
|
finally:
|
|
if proc is not None:
|
|
quiescent, cleanup = _quiesce(proc)
|
|
if not quiescent:
|
|
failure = "quiescence"
|
|
if cleanup and failure is None:
|
|
failure = "quiescence"
|
|
proc.stdout.close()
|
|
proc.stderr.close()
|
|
selector.close()
|
|
record = {"label": label, "argv": argv, "returncode": proc.returncode if proc else None,
|
|
"failure": failure, "quiescent": quiescent, "cleanup": cleanup,
|
|
"wall_s": round(time.monotonic() - started, 6), "timeout_s": timeout}
|
|
evidence.captured_bytes += sum(len(data) for data in streams.values())
|
|
for name, data in streams.items():
|
|
path = evidence.write("evidence/" + label + "." + name, bytes(data))
|
|
record[name] = {"path": path, "bytes": len(data),
|
|
"sha256": hashlib.sha256(data).hexdigest(),
|
|
"capture": "complete" if failure is None else "bounded_prefix"}
|
|
evidence.json("evidence/" + label + ".process.json", record)
|
|
evidence.records.append(record)
|
|
return record, bytes(streams["stdout"])
|
|
|
|
|
|
def _required(record):
|
|
if record["failure"] or record["returncode"] != 0 or not record["quiescent"]:
|
|
_error(record["failure"] or "tool", "required native command failed: " + record["label"])
|
|
|
|
|
|
def _environment():
|
|
result = dict(os.environ)
|
|
for name in ("CBM_TEST_COVERAGE_DIR", "CBM_TEST_ONLY", "CBM_TEST_ONLY_FILE"):
|
|
result.pop(name, None)
|
|
return result
|
|
|
|
|
|
def _convert(evidence, tool, path, label, environment, root, unavailable=False):
|
|
try:
|
|
raw = _read(path, evidence.args.max_profile_bytes)
|
|
except OSError as error:
|
|
if unavailable and error.errno in (errno.EACCES, errno.EPERM, errno.ENOENT):
|
|
evidence.uncertainty.append({"path": path, "reason": "profile_unreadable"})
|
|
return None
|
|
raise
|
|
snapshot = evidence.write("evidence/" + label + ".profraw", raw)
|
|
raw_digest = hashlib.sha256(raw).hexdigest()
|
|
record, text = _run(evidence, label, [tool, "merge", "--instr", "--text", "--failure-mode=any",
|
|
"--num-threads=1", "--output=-", snapshot],
|
|
environment, evidence.args.conversion_timeout, root)
|
|
record["input"] = {"path": path, "snapshot": snapshot, "sha256": raw_digest,
|
|
"bytes": len(raw)}
|
|
_required(record)
|
|
if hashlib.sha256(_read(snapshot, evidence.args.max_profile_bytes)).hexdigest() != raw_digest:
|
|
_error("identity", "conversion input changed")
|
|
evidence.converted_bytes += len(text)
|
|
if evidence.converted_bytes > evidence.args.max_total_profile_bytes:
|
|
_error("resource_limit", "aggregate converted byte limit exceeded")
|
|
# Reject malformed transport, even though the pure producer can represent
|
|
# unavailable/malformed observations conservatively. Unknown identities and
|
|
# counter-shape incompatibilities are rejected by the producer's full join.
|
|
deadline = time.monotonic() + evidence.args.reduction_timeout
|
|
parse_profile_text(text, ParseLimits(evidence.args.max_output_bytes, RECORD_CAP,
|
|
COUNTER_CAP, NAME_CAP),
|
|
cancel=lambda _: time.monotonic() >= deadline)
|
|
return text
|
|
|
|
|
|
def _scope(registry, requested, available):
|
|
selected = requested or list(registry)
|
|
if len(set(selected)) != len(selected):
|
|
_error("input", "duplicate requested suite")
|
|
for name in selected:
|
|
if name not in registry or name not in available:
|
|
_error("input", "requested suite is absent from registry or native list")
|
|
return selected
|
|
|
|
|
|
def _observations(evidence, tool, registry, selected, environment, root):
|
|
observations = []
|
|
for index, path in enumerate(evidence.inventory()):
|
|
relative = os.path.relpath(path, os.path.join(evidence.path, "raw"))
|
|
parts = relative.split(os.sep)
|
|
if len(parts) != 2 or parts[0] not in selected:
|
|
evidence.uncertainty.append({"path": path, "reason": "unknown_profile_destination"})
|
|
_error("input", "raw profile destination is outside requested scope")
|
|
suite, name = parts
|
|
marker = MARKER.fullmatch(name)
|
|
setup = SETUP.fullmatch(name)
|
|
profile = PROFILE.fullmatch(name)
|
|
if setup:
|
|
test = "*"
|
|
role = ObservationRole.CHILD if name.startswith("_setup.child.") else ObservationRole.SETUP
|
|
elif marker:
|
|
test = "*" if marker.group(1) == "_setup.child" else marker.group(1)
|
|
role = ObservationRole.CHILD
|
|
elif profile:
|
|
test = profile.group(1)
|
|
role = ObservationRole.PARENT if profile.group(2) == "parent" else ObservationRole.CHILD
|
|
else:
|
|
evidence.uncertainty.append({"path": path, "reason": "unaccounted_raw_entry"})
|
|
_error("input", "unaccounted raw profile entry")
|
|
if test != "*" and test not in registry[suite]:
|
|
evidence.uncertainty.append({"path": path, "reason": "unknown_test_identity"})
|
|
_error("input", "raw test identifier is outside expected registry")
|
|
if marker:
|
|
evidence.uncertainty.append({"path": path, "reason": "unaccounted_fork"})
|
|
text = None
|
|
else:
|
|
text = _convert(evidence, tool, path, "profile-%06d" % index,
|
|
environment, root, unavailable=True)
|
|
observations.append(ProfileObservation(suite.encode("utf-8"), test.encode("utf-8"),
|
|
name.encode("utf-8"), role, text))
|
|
return tuple(observations)
|
|
|
|
|
|
def collect(args):
|
|
evidence = None
|
|
try:
|
|
if os.name != "posix" or not all(hasattr(os, name) for name in
|
|
("O_NOFOLLOW", "O_DIRECTORY", "killpg", "set_blocking")):
|
|
_error("unsupported", "format2 requires POSIX owned process-group custody")
|
|
for name in ("timeout", "max_output_bytes", "max_total_output_bytes", "max_profile_bytes", "max_total_profile_bytes",
|
|
"max_profile_files", "conversion_timeout", "reduction_timeout", "max_map_bytes"):
|
|
value = getattr(args, name)
|
|
if type(value) is not int or value <= 0 or value > sys.maxsize // 2:
|
|
_error("input", "resource limits must be positive bounded integers")
|
|
if not args.registry or not args.llvm_bin:
|
|
_error("input", "format2 requires --registry and explicit --llvm-bin")
|
|
runner = os.path.realpath(os.path.abspath(args.runner))
|
|
tool = os.path.realpath(os.path.join(os.path.abspath(args.llvm_bin), "llvm-profdata"))
|
|
root = os.path.abspath(args.root or os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
root_fd = _directory(root)
|
|
os.close(root_fd)
|
|
image = _identity(runner)
|
|
tool_identity = _identity(tool)
|
|
registry_path = os.path.abspath(args.registry)
|
|
registry, registry_digest, registry_bytes = _registry(registry_path, image)
|
|
# Reject explicit scope errors before any runner/tool action.
|
|
_scope(registry, args.suite, set(registry))
|
|
evidence = Evidence(args.out, args)
|
|
registry_snapshot = evidence.write("evidence/registry.json", registry_bytes)
|
|
environment = _environment()
|
|
environment["LLVM_PROFILE_FILE"] = os.devnull
|
|
version_record, version = _run(evidence, "tool-version", [tool, "--version"], environment,
|
|
args.conversion_timeout, root)
|
|
_required(version_record)
|
|
if not re.search(rb"LLVM version 22\.1\.8(?:\s|$)", version):
|
|
_error("unsupported", "only observed LLVM 22.1.8 proftext transport is supported")
|
|
listing_path = os.path.join(evidence.path, "listing.profraw")
|
|
listing_env = dict(environment, LLVM_PROFILE_FILE=listing_path)
|
|
listed, listing = _run(evidence, "list-suites", [runner, "--list-suites"], listing_env,
|
|
args.timeout, root, watch_raw=True)
|
|
_required(listed)
|
|
try:
|
|
available = listing.decode("utf-8", "strict").splitlines()
|
|
except UnicodeError:
|
|
_error("input", "native suite list is not UTF-8")
|
|
if not available or any(not name for name in available) or len(set(available)) != len(available):
|
|
_error("input", "native suite list is empty or ambiguous")
|
|
for name in available:
|
|
_identifier(name, suite=True)
|
|
selected = _scope(registry, args.suite, set(available))
|
|
evidence.allowed_suites = set(selected)
|
|
enumeration = _convert(evidence, tool, listing_path, "enumeration", environment, root)
|
|
evidence.inventory()
|
|
suite_env = dict(environment, CBM_TEST_COVERAGE_DIR=os.path.join(evidence.path, "raw"))
|
|
outcomes = []
|
|
expected = []
|
|
failed = False
|
|
for index, suite in enumerate(selected):
|
|
record, _ = _run(evidence, "suite-%06d" % index, [runner, suite], suite_env,
|
|
args.timeout, root, watch_raw=True)
|
|
if record["failure"] not in (None, "timeout") or not record["quiescent"]:
|
|
_error(record["failure"] or "quiescence", "native suite capture failed")
|
|
outcome = (SuiteOutcome.TIMED_OUT if record["failure"] == "timeout" else
|
|
SuiteOutcome.PASSED if record["returncode"] == 0 else SuiteOutcome.FAILED)
|
|
failed = failed or outcome is not SuiteOutcome.PASSED
|
|
expected.append(ExpectedSuite(suite.encode("utf-8"),
|
|
tuple(test.encode("utf-8") for test in registry[suite]), outcome))
|
|
outcomes.append({"suite": suite, "tests": list(registry[suite]),
|
|
"outcome": outcome.value, "exit": record["returncode"], "process": record})
|
|
observations = _observations(evidence, tool, registry, selected, environment, root)
|
|
if _identity(runner) != image or _identity(tool) != tool_identity:
|
|
_error("identity", "runner or conversion tool changed during collection")
|
|
# No affirmative runtime/interval/image/child assertions are supplied.
|
|
limits = ProfileMapLimits(args.max_total_profile_bytes + REGISTRY_CAP * 2, ROW_CAP,
|
|
args.max_profile_files, RECORD_CAP, RECORD_CAP, COUNTER_CAP,
|
|
NAME_CAP, COUNTER_CAP, args.max_map_bytes)
|
|
deadline = time.monotonic() + args.reduction_timeout
|
|
produced = build_profile_map_v2(bytes.fromhex(image["sha256"]), enumeration,
|
|
tuple(expected), observations, (), limits,
|
|
cancel=lambda _: time.monotonic() >= deadline)
|
|
profiles_path = evidence.write("profiles.tsv", produced.profiles_tsv)
|
|
tests_path = evidence.write("tests.tsv", produced.tests_tsv)
|
|
meta = {"format": 2, "admission": "unverified", "image_sha256": image["sha256"],
|
|
"registry_sha256": registry_digest, "profiles_sha256": produced.profiles_sha256.hex(),
|
|
"tests_sha256": produced.tests_sha256.hex(), "registry": {
|
|
"path": registry_path, "snapshot": registry_snapshot, "sha256": registry_digest,
|
|
"format": "cbm.coverage.registry.v0", "completeness": "unverified"},
|
|
"scope": selected,
|
|
"image": image, "llvm_profdata": dict(tool_identity, version=version.decode("utf-8", "replace")),
|
|
"profiles": {"path": profiles_path, "sha256": produced.profiles_sha256.hex()},
|
|
"tests": {"path": tests_path, "sha256": produced.tests_sha256.hex()},
|
|
"suites": outcomes, "processes": evidence.records, "uncertainty": evidence.uncertainty,
|
|
"evidence_root": os.path.join(evidence.path, "evidence"),
|
|
"raw_root": os.path.join(evidence.path, "raw"),
|
|
"row_evidence": "absent", "platform": sys.platform,
|
|
"raw_retained": True, "captured_bytes": evidence.captured_bytes,
|
|
"converted_bytes": evidence.converted_bytes,
|
|
"custody": {"method": "POSIX session and process group",
|
|
"descendant_closure": "unproved",
|
|
"compiler_runtime_compatibility": "unverified"},
|
|
"limits": {name: getattr(args, name) for name in (
|
|
"timeout", "max_output_bytes", "max_total_output_bytes", "max_profile_bytes",
|
|
"max_total_profile_bytes", "max_profile_files", "conversion_timeout",
|
|
"reduction_timeout", "max_map_bytes")},
|
|
"selection_environment": {"CBM_SKIP_PERF": environment.get("CBM_SKIP_PERF")}}
|
|
# Consumer acceptance requires this final marker AND both wire digests.
|
|
evidence.publish(meta)
|
|
return 1 if failed else 0
|
|
except (CollectionError, ProfileMapError, ProfileTextError, OSError, UnicodeError,
|
|
ValueError, MemoryError) as error:
|
|
kind = getattr(error, "kind", "input_or_conversion")
|
|
if isinstance(error, (ProfileMapLimitExceededError, LimitExceededError, MemoryError)):
|
|
kind = "resource_limit"
|
|
if isinstance(error, (ProfileMapCancelledError, ParseCancelledError)):
|
|
kind = "reduction_timeout"
|
|
print("coverage-map: format2 %s: %s" % (kind, str(error)), file=sys.stderr)
|
|
if evidence is not None:
|
|
try:
|
|
evidence.json("failure.json", {"format": 2, "error": kind,
|
|
"message": str(error), "processes": evidence.records,
|
|
"uncertainty": evidence.uncertainty})
|
|
except (OSError, ValueError, MemoryError):
|
|
pass
|
|
return 2
|
|
finally:
|
|
if evidence is not None:
|
|
evidence.close()
|