mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
- SetCapability named NEW_PIPELINE_STATE, but its ClipDistance0..7 arms write ClipDistanceEnabledMask (dynamic chunk D7) and deliberately do not BumpVersions, so that publisher does not fire at all for glEnable(GL_CLIP_DISTANCE0); SetStencilFunc named it too, while ++m_pipelineStateVersion there is conditional on Func moving, so a ref-only glStencilFunc does not move it either. Both are now NEW_RENDER_STATE, the answer that holds on every path - a row may now carry several publishers joined with '|', which is what lets the 18 setters that call BumpVersions on every path state both counters, and the patch trio state its own bit and the two render counters it also moves - --check no longer validates only row existence and answer vocabulary: it reads RenderState.cpp, derives per setter which of the two counters moves on EVERY path (BumpVersions moves both, a bare ++m_version only the first, a setter with both kinds of path only the first, a delegating setter inherits its callee's) and fails when a row claims a publisher that under-fires or omits one that always fires - two new self-test negative controls, one per direction, both built from the defects that were actually in the file - MarkProgram/MarkProgramPipeline/MarkShaderForDeletion answered kExplicitDestroy, a mechanism D13 does not build for them: Espryt 0b's explicit destroy is scoped to six object kinds that exclude programs, pipelines and shaders. They answer kUnpublishedDestroy now - a recorded hole rather than a mechanism that does not exist
507 lines
23 KiB
Python
507 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
# MobileGL - scripts/gen_pipe_dirty_surface.py
|
|
# Copyright (c) 2025-2026 MobileGL-Dev
|
|
# Licensed under the GNU Lesser General Public License v3.0:
|
|
# https://www.gnu.org/licenses/gpl-3.0.txt
|
|
# https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
# SPDX-License-Identifier: LGPL-3.0-only
|
|
# End of Source File Header
|
|
"""The dirty-surface scanner (plan B corollary 4, section 5.2).
|
|
|
|
MGPipe replaces "the backend rediscovers what changed" with "the frontend says what
|
|
changed", which only works if EVERY frontend mutation that a backend can observe bumps an
|
|
aggregate generation. The failure mode is silent and one-directional: a mutation that
|
|
forgets to bump renders stale, and no purity gate can see it.
|
|
|
|
So the mutation surface has to be enumerated mechanically rather than by memory. This
|
|
script reports every place in MG_Impl/GLImpl where a GL entry point BOTH mutates frontend
|
|
state through pGLContext AND reaches the backend in the same function - those are the
|
|
publish points, the ones that must map onto an aggregate generation.
|
|
|
|
P0 is the skeleton: it reports. P1 adds the mapping file and CI regenerates it with
|
|
`git diff --exit-code` and zero unmapped mutators, the same shape as gen_pipe.py's G6.
|
|
|
|
P2 adds the half a completeness gate cannot have: for the RenderState family the ANSWER is
|
|
derived from RenderState.cpp rather than believed, so a row that names a publisher which
|
|
fires on only some paths through the setter (or omits one that always fires) is red. Without
|
|
it a row could be wrong in exactly the direction ARCHITECTURE.md 13.2 calls dangerous while
|
|
--check stayed green, which is how two rows in this mapping were wrong for a whole review.
|
|
|
|
python3 scripts/gen_pipe_dirty_surface.py # human-readable report
|
|
python3 scripts/gen_pipe_dirty_surface.py --summary # counts only
|
|
python3 scripts/gen_pipe_dirty_surface.py --check # THE GATE: rc 1 on any hole
|
|
python3 scripts/gen_pipe_dirty_surface.py --self-test # the gate's own negative controls
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SCAN_ROOT = os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "GLImpl")
|
|
|
|
# The mutating half of GLContext's surface. Prefix-matched, per the plan's list.
|
|
MUTATOR_PREFIXES = ("Add", "Set", "Mark", "Bump", "Allocate", "Truncate", "Record", "Notify",
|
|
"Begin", "End")
|
|
|
|
MUTATOR_RE = re.compile(r"pGLContext->\s*((?:%s)\w*)\s*\(" % "|".join(MUTATOR_PREFIXES))
|
|
BACKEND_RE = re.compile(r"gBackendFunctionsTable\.GL\.(\w+)|pActiveBackendObject->\s*(\w+)")
|
|
FUNCTION_RE = re.compile(r"(?:^|\n)[ \t]*(?:[A-Za-z_][\w:<>,&*\s]*?)\b(\w+)\s*\([^;{}]*\)\s*"
|
|
r"(?:const\s*)?(?:noexcept\s*)?\{")
|
|
|
|
|
|
def mask_comments_and_strings(text):
|
|
"""Replace comment and string-literal bodies with spaces, keeping every offset and
|
|
newline, so the regexes below cannot match inside a comment or a literal."""
|
|
out = list(text)
|
|
i = 0
|
|
n = len(text)
|
|
while i < n:
|
|
c = text[i]
|
|
if c == "/" and i + 1 < n and text[i + 1] == "/":
|
|
while i < n and text[i] != "\n":
|
|
out[i] = " "
|
|
i += 1
|
|
elif c == "/" and i + 1 < n and text[i + 1] == "*":
|
|
out[i] = out[i + 1] = " "
|
|
i += 2
|
|
while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
|
|
if text[i] != "\n":
|
|
out[i] = " "
|
|
i += 1
|
|
if i < n:
|
|
out[i] = " "
|
|
if i + 1 < n:
|
|
out[i + 1] = " "
|
|
i += 2
|
|
elif c in "\"'":
|
|
quote = c
|
|
i += 1
|
|
while i < n and text[i] != quote:
|
|
if text[i] == "\\":
|
|
out[i] = " "
|
|
i += 1
|
|
if i < n and text[i] != "\n":
|
|
out[i] = " "
|
|
i += 1
|
|
if i < n:
|
|
out[i] = " "
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
return "".join(out)
|
|
|
|
|
|
def function_bodies(masked):
|
|
"""Yield (name, start_offset, end_offset) for every braced function body."""
|
|
for match in FUNCTION_RE.finditer(masked):
|
|
name = match.group(1)
|
|
start = masked.index("{", match.end() - 1) if masked[match.end() - 1] != "{" else match.end() - 1
|
|
depth = 0
|
|
i = start
|
|
while i < len(masked):
|
|
if masked[i] == "{":
|
|
depth += 1
|
|
elif masked[i] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
yield name, start, i
|
|
break
|
|
i += 1
|
|
|
|
|
|
def line_of(text, offset):
|
|
return text.count("\n", 0, offset) + 1
|
|
|
|
|
|
def scan_file(path):
|
|
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
text = handle.read()
|
|
masked = mask_comments_and_strings(text)
|
|
findings = []
|
|
# Every mutator in the file, whether or not it shares a function with a backend call.
|
|
# The difference between this and the publish points below is the whole point of the
|
|
# report: a mutation that does NOT reach the backend in the same function is published
|
|
# by the NEXT verb, and it is exactly those that need an aggregate generation rather
|
|
# than an inline push.
|
|
all_mutators = [(m.group(1), line_of(masked, m.start())) for m in MUTATOR_RE.finditer(masked)]
|
|
for name, start, end in function_bodies(masked):
|
|
body = masked[start:end]
|
|
mutators = [(m.group(1), line_of(masked, start + m.start())) for m in MUTATOR_RE.finditer(body)]
|
|
if not mutators:
|
|
continue
|
|
backend = sorted(set(m.group(1) or m.group(2) for m in BACKEND_RE.finditer(body)))
|
|
if not backend:
|
|
continue
|
|
findings.append({
|
|
"function": name,
|
|
"line": line_of(masked, start),
|
|
"mutators": mutators,
|
|
"backend": backend,
|
|
})
|
|
return findings, all_mutators
|
|
|
|
|
|
DEF_PATH = os.path.join(REPO_ROOT, "MobileGL", "MG_Pipe", "DirtySurface.def")
|
|
TRACKER_PATH = os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "Pipe", "Tracker.h")
|
|
RENDER_STATE_PATH = os.path.join(REPO_ROOT, "MobileGL", "MG_State", "GLState", "RenderState",
|
|
"RenderState.cpp")
|
|
|
|
# An answer is one or more publishers joined with "|" - every publisher that fires on EVERY
|
|
# path through the mutator (DirtySurface.def's header states the rule).
|
|
ROW_RE = re.compile(r"^[ \t]*X\((\w+),\s*([\w|]+)\)\s*\\?\s*$", re.M)
|
|
DIRTY_NAME_RE = re.compile(r'^\s*"(NEW_[A-Z0-9_]+)",\s*$', re.M)
|
|
|
|
# The answers that are not a dirty-bit name. Each one is documented in DirtySurface.def's
|
|
# header; a row that uses anything else is a typo, and a typo that read as "mapped" would be
|
|
# exactly the silent hole this gate exists to close.
|
|
NON_BIT_ANSWERS = ("kImmediate", "kReverseChannel", "kNoBackendRead", "kExplicitDestroy",
|
|
"kUnpublishedDestroy", "kPulledEveryVerb")
|
|
|
|
# ---- the render-state answers, DERIVED rather than believed -----------------------------
|
|
# The two RenderState counters are the one place in the mapping where "what publishes this"
|
|
# has a mechanical answer, and where getting it wrong is not a documentation slip: P3a builds
|
|
# its narrow shutters from this file, so a row that claims NEW_PIPELINE_STATE for a setter
|
|
# whose pipeline bump is conditional (SetStencilFunc) or absent (SetCapability's
|
|
# ClipDistance0..7 arms) encodes exactly the under-firing ARCHITECTURE.md 13.2 calls the
|
|
# dangerous direction. So the gate derives the answer from RenderState.cpp:
|
|
#
|
|
# BumpVersions() moves m_version AND m_pipelineStateVersion (RenderState.h);
|
|
# a bare ++m_version moves only the first;
|
|
# a setter that has BOTH kinds of path always-fires only NEW_RENDER_STATE.
|
|
#
|
|
# A setter whose body has no bump at all is resolved through the RenderState setter it
|
|
# delegates to (SetPolygonOffset -> SetPolygonOffsetClamped).
|
|
RENDER_STATE_BIT = "NEW_RENDER_STATE"
|
|
PIPELINE_STATE_BIT = "NEW_PIPELINE_STATE"
|
|
BUMP_VERSIONS_RE = re.compile(r"\bBumpVersions\s*\(\s*\)")
|
|
BARE_VERSION_RE = re.compile(r"\+\+\s*m_version\b")
|
|
BARE_PIPELINE_RE = re.compile(r"\+\+\s*m_pipelineStateVersion\b")
|
|
SETTER_CALL_RE = re.compile(r"\b(Set\w+)\s*\(")
|
|
|
|
|
|
def render_state_publishers():
|
|
"""{setter: set of always-firing render-state publishers} read out of RenderState.cpp.
|
|
|
|
A setter absent from the result is not a RenderState setter at all; a setter mapped to an
|
|
EMPTY set moves neither counter (SetPixelStoreParam)."""
|
|
with open(RENDER_STATE_PATH, "r", encoding="utf-8", errors="replace") as handle:
|
|
masked = mask_comments_and_strings(handle.read())
|
|
|
|
bodies = {}
|
|
for name, start, end in function_bodies(masked):
|
|
if name.startswith("Set"):
|
|
bodies.setdefault(name, []).append(masked[start:end])
|
|
|
|
def direct(name):
|
|
publishers = set()
|
|
for body in bodies[name]:
|
|
bump = BUMP_VERSIONS_RE.search(body) is not None
|
|
bare_version = BARE_VERSION_RE.search(body) is not None
|
|
bare_pipeline = BARE_PIPELINE_RE.search(body) is not None
|
|
if bump or bare_version:
|
|
publishers.add(RENDER_STATE_BIT)
|
|
if bump and not bare_version and not bare_pipeline:
|
|
publishers.add(PIPELINE_STATE_BIT)
|
|
return publishers
|
|
|
|
def resolve(name, seen):
|
|
if name in seen:
|
|
return set()
|
|
seen.add(name)
|
|
publishers = direct(name)
|
|
if publishers:
|
|
return publishers
|
|
# No bump of its own: whatever the setter it delegates to publishes.
|
|
for body in bodies[name]:
|
|
for match in SETTER_CALL_RE.finditer(body):
|
|
callee = match.group(1)
|
|
if callee != name and callee in bodies:
|
|
publishers |= resolve(callee, seen)
|
|
return publishers
|
|
|
|
return {name: resolve(name, set()) for name in bodies}
|
|
|
|
|
|
def dirty_bit_names():
|
|
"""The MGPipeDirty bit names, read out of Tracker.h's kMGPipeDirtyNames so a row cannot
|
|
name a bit that does not exist and a bit cannot be renamed out from under a row. Read
|
|
from the RAW text on purpose: the names are string literals, which is exactly what
|
|
mask_comments_and_strings blanks."""
|
|
with open(TRACKER_PATH, "r", encoding="utf-8", errors="replace") as handle:
|
|
return set(DIRTY_NAME_RE.findall(handle.read()))
|
|
|
|
|
|
def load_mapping(text=None):
|
|
"""{mutator: answer} from DirtySurface.def, or from `text` for the self-test. An answer
|
|
keeps its "|"-joined spelling; answer_set() below is what compares them."""
|
|
if text is None:
|
|
with open(DEF_PATH, "r", encoding="utf-8", errors="replace") as handle:
|
|
text = handle.read()
|
|
rows = {}
|
|
duplicates = []
|
|
for match in ROW_RE.finditer(mask_comments_and_strings(text)):
|
|
mutator, answer = match.group(1), match.group(2)
|
|
if mutator in rows:
|
|
duplicates.append(mutator)
|
|
rows[mutator] = answer
|
|
return rows, duplicates
|
|
|
|
|
|
def answer_set(answer):
|
|
return {part.strip() for part in answer.split("|") if part.strip()}
|
|
|
|
|
|
def check_mapping(mapping, duplicates, scanned, bits, publishers=None):
|
|
"""Every problem the gate fails on, as a list of human-readable lines. BOTH directions:
|
|
an unmapped mutator renders stale, and a row naming a mutator the scan no longer finds is
|
|
a stale row that would keep a real hole looking covered. `publishers` is
|
|
render_state_publishers()'s table; passing None checks only existence and vocabulary,
|
|
which is what the mutator-level negative controls want."""
|
|
problems = []
|
|
for mutator in sorted(set(scanned) - set(mapping)):
|
|
problems.append("UNMAPPED mutator %s - add a row to MG_Pipe/DirtySurface.def" % mutator)
|
|
for mutator in sorted(set(mapping) - set(scanned)):
|
|
problems.append("STALE row %s - the scan no longer finds this mutator; delete the row"
|
|
% mutator)
|
|
for mutator in sorted(duplicates):
|
|
problems.append("DUPLICATE row %s" % mutator)
|
|
for mutator in sorted(mapping):
|
|
answers = answer_set(mapping[mutator])
|
|
if not answers:
|
|
problems.append("BAD answer for %s - empty" % mutator)
|
|
continue
|
|
for answer in sorted(answers):
|
|
if answer in NON_BIT_ANSWERS or answer in bits:
|
|
continue
|
|
problems.append("BAD answer %s for %s - not a MGPipeDirty bit name and not one of %s"
|
|
% (answer, mutator, ", ".join(NON_BIT_ANSWERS)))
|
|
if len(answers) > 1 and answers & set(NON_BIT_ANSWERS):
|
|
problems.append("BAD answer %s for %s - a non-bit answer stands alone"
|
|
% (mapping[mutator], mutator))
|
|
|
|
if publishers is None:
|
|
return problems
|
|
|
|
# THE TRUTH HALF, and it is the half a row can be green and wrong without. For every
|
|
# mutator that is a RenderState setter, the render-state publishers the row claims must
|
|
# be exactly the ones RenderState.cpp always moves - a claimed publisher that does not
|
|
# always fire is an under-firing shutter waiting to be built from this file, and a
|
|
# publisher that always fires but is not claimed hides one.
|
|
render_bits = {RENDER_STATE_BIT, PIPELINE_STATE_BIT}
|
|
for mutator in sorted(mapping):
|
|
claimed = answer_set(mapping[mutator]) & render_bits
|
|
if mutator not in publishers:
|
|
if claimed:
|
|
problems.append(
|
|
"UNVERIFIABLE answer %s for %s - it claims a render-state publisher but "
|
|
"RenderState.cpp has no such setter to derive it from"
|
|
% (mapping[mutator], mutator))
|
|
continue
|
|
derived = publishers[mutator] & render_bits
|
|
if claimed == derived:
|
|
continue
|
|
for missing in sorted(derived - claimed):
|
|
problems.append(
|
|
"MISSING publisher %s for %s - RenderState.cpp moves it on every path, so the "
|
|
"row must name it (derived: %s)"
|
|
% (missing, mutator, "|".join(sorted(derived)) or "none"))
|
|
for extra in sorted(claimed - derived):
|
|
problems.append(
|
|
"UNDER-FIRING answer %s for %s - RenderState.cpp does NOT move it on every "
|
|
"path, so a shutter built on it would miss a mutation (derived: %s)"
|
|
% (extra, mutator, "|".join(sorted(derived)) or "none"))
|
|
return problems
|
|
|
|
|
|
SELF_TEST_WITHHELD = """
|
|
#define MGP_DIRTY_SURFACE_LIST(X) \\
|
|
X(RecordError, kReverseChannel)
|
|
"""
|
|
|
|
SELF_TEST_STALE = None # built from the real def at run time
|
|
|
|
|
|
def scan_all():
|
|
"""(findings-per-file, {mutator: call count}) over the whole scan root."""
|
|
sources = []
|
|
for root, _, files in os.walk(SCAN_ROOT):
|
|
for name in sorted(files):
|
|
if name.endswith((".cpp", ".h")):
|
|
sources.append(os.path.join(root, name))
|
|
sources.sort()
|
|
|
|
per_file = []
|
|
distinct_all = {}
|
|
for path in sources:
|
|
findings, all_mutators = scan_file(path)
|
|
for mutator, _ in all_mutators:
|
|
distinct_all[mutator] = distinct_all.get(mutator, 0) + 1
|
|
per_file.append((path, findings, all_mutators))
|
|
return sources, per_file, distinct_all
|
|
|
|
|
|
def self_test(scanned, bits, publishers):
|
|
"""Canned negative controls. Each MUST trip; trips == 0 is an error, which is the shape
|
|
check_include_closure.py and gen_pipe.py --self-test already use."""
|
|
trips = 0
|
|
failures = []
|
|
|
|
# 1. a mutator withheld from the def.
|
|
mapping, duplicates = load_mapping(SELF_TEST_WITHHELD)
|
|
problems = check_mapping(mapping, duplicates, scanned, bits)
|
|
if any(p.startswith("UNMAPPED") for p in problems):
|
|
trips += 1
|
|
else:
|
|
failures.append("negative control 1 (a withheld mutator) did NOT trip")
|
|
|
|
# 2. a row naming a mutator the scan does not find.
|
|
real, real_duplicates = load_mapping()
|
|
with_ghost = dict(real)
|
|
with_ghost["SetSomethingThatDoesNotExist"] = "kImmediate"
|
|
problems = check_mapping(with_ghost, real_duplicates, scanned, bits)
|
|
if any(p.startswith("STALE") for p in problems):
|
|
trips += 1
|
|
else:
|
|
failures.append("negative control 2 (a stale row) did NOT trip")
|
|
|
|
# 3. a row whose answer is neither a dirty bit nor one of the documented non-bit answers.
|
|
with_typo = dict(real)
|
|
with_typo["RecordError"] = "NEW_TYPO_THAT_IS_NOT_A_BIT"
|
|
problems = check_mapping(with_typo, real_duplicates, scanned, bits)
|
|
if any(p.startswith("BAD answer") for p in problems):
|
|
trips += 1
|
|
else:
|
|
failures.append("negative control 3 (a bad answer) did NOT trip")
|
|
|
|
# 4. THE CONTROL FOR THE TRUTH HALF, and it is the shape of the defect that was actually
|
|
# in this file: a row claiming a publisher that fires on only some paths through the
|
|
# setter. SetCapability's ClipDistance0..7 arms move m_version alone, so
|
|
# NEW_PIPELINE_STATE here must read as under-firing rather than as a valid answer.
|
|
with_under_firing = dict(real)
|
|
with_under_firing["SetCapability"] = PIPELINE_STATE_BIT
|
|
problems = check_mapping(with_under_firing, real_duplicates, scanned, bits, publishers)
|
|
if any(p.startswith("UNDER-FIRING") for p in problems):
|
|
trips += 1
|
|
else:
|
|
failures.append("negative control 4 (an under-firing render-state answer) did NOT trip")
|
|
|
|
# 5. the other direction: a row that drops a publisher which DOES always fire. Silent
|
|
# today, load-bearing the moment P3a builds a shutter from the file.
|
|
with_missing = dict(real)
|
|
with_missing["SetBlendEquation"] = RENDER_STATE_BIT
|
|
problems = check_mapping(with_missing, real_duplicates, scanned, bits, publishers)
|
|
if any(p.startswith("MISSING publisher") for p in problems):
|
|
trips += 1
|
|
else:
|
|
failures.append("negative control 5 (a dropped render-state publisher) did NOT trip")
|
|
|
|
for failure in failures:
|
|
print("dirty-surface self-test: %s" % failure)
|
|
if trips == 0:
|
|
print("dirty-surface self-test: NOTHING tripped - the gate cannot fail, which is worse "
|
|
"than a red gate")
|
|
return 1
|
|
if failures:
|
|
return 1
|
|
print("dirty-surface self-test: %d negative controls, all tripped" % trips)
|
|
return 0
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--summary", action="store_true", help="print the counts only")
|
|
parser.add_argument("--check", action="store_true",
|
|
help="fail when a scanned mutator has no row in DirtySurface.def, or a "
|
|
"row names a mutator the scan no longer finds")
|
|
parser.add_argument("--self-test", action="store_true",
|
|
help="run the canned negative controls; each must trip")
|
|
args = parser.parse_args()
|
|
|
|
if not os.path.isdir(SCAN_ROOT):
|
|
sys.exit("missing %s" % SCAN_ROOT)
|
|
if not os.path.isfile(DEF_PATH):
|
|
sys.exit("missing %s" % DEF_PATH)
|
|
if not os.path.isfile(RENDER_STATE_PATH):
|
|
sys.exit("missing %s" % RENDER_STATE_PATH)
|
|
|
|
sources, per_file, distinct_all = scan_all()
|
|
bits = dirty_bit_names()
|
|
if not bits:
|
|
sys.exit("could not read the MGPipeDirty bit names out of %s" % TRACKER_PATH)
|
|
publishers = render_state_publishers()
|
|
if not publishers:
|
|
sys.exit("could not derive any RenderState setter out of %s" % RENDER_STATE_PATH)
|
|
|
|
if args.self_test:
|
|
return self_test(distinct_all, bits, publishers)
|
|
|
|
mapping, duplicates = load_mapping()
|
|
|
|
if args.check:
|
|
problems = check_mapping(mapping, duplicates, distinct_all, bits, publishers)
|
|
for problem in problems:
|
|
print("dirty-surface: %s" % problem)
|
|
if problems:
|
|
print("dirty-surface: %d problem(s); the mapping must cover every mutator the scan "
|
|
"finds, in both directions, and every render-state answer must be the one "
|
|
"RenderState.cpp actually publishes" % len(problems))
|
|
return 1
|
|
derived = sum(1 for m in mapping if m in publishers)
|
|
print("dirty-surface: %d mutators, all mapped, no stale rows; %d render-state answers "
|
|
"derived from RenderState.cpp and matching" % (len(mapping), derived))
|
|
return 0
|
|
|
|
total_functions = 0
|
|
total_mutators = 0
|
|
deferred_mutators = 0
|
|
distinct_mutators = {}
|
|
for path, findings, all_mutators in per_file:
|
|
deferred_mutators += len(all_mutators)
|
|
if not findings:
|
|
continue
|
|
relative = os.path.relpath(path, REPO_ROOT).replace(os.sep, "/")
|
|
if not args.summary:
|
|
print("\n%s" % relative)
|
|
for finding in findings:
|
|
total_functions += 1
|
|
total_mutators += len(finding["mutators"])
|
|
for mutator, _ in finding["mutators"]:
|
|
distinct_mutators[mutator] = distinct_mutators.get(mutator, 0) + 1
|
|
if args.summary:
|
|
continue
|
|
print(" %s (line %d) -> backend: %s" % (finding["function"], finding["line"],
|
|
", ".join(finding["backend"][:4])))
|
|
for mutator, line in finding["mutators"]:
|
|
print(" %-44s :%d %s" % (mutator, line, mapping.get(mutator, "UNMAPPED")))
|
|
|
|
print("\ndirty-surface: %d files scanned under MG_Impl/GLImpl" % len(sources))
|
|
print("dirty-surface: %d mutator calls in total, %d distinct mutators" % (deferred_mutators,
|
|
len(distinct_all)))
|
|
print("dirty-surface: %d of them sit in %d IMMEDIATE PUBLISH POINTS - functions that also "
|
|
"reach the backend - across %d distinct mutators"
|
|
% (total_mutators, total_functions, len(distinct_mutators)))
|
|
print("dirty-surface: the remaining %d are DEFERRED: nothing reaches the backend in the same "
|
|
"function, so the next verb publishes them, and each one needs an aggregate generation"
|
|
% (deferred_mutators - total_mutators))
|
|
print("dirty-surface: distinct mutators, by call count, with what publishes each")
|
|
for mutator in sorted(distinct_all, key=lambda k: (-distinct_all[k], k)):
|
|
print(" %5d %-42s %s%s" % (distinct_all[mutator], mutator,
|
|
mapping.get(mutator, "UNMAPPED"),
|
|
" (immediate)" if mutator in distinct_mutators else ""))
|
|
unmapped = sorted(set(distinct_all) - set(mapping))
|
|
if unmapped:
|
|
print("dirty-surface: %d UNMAPPED - run --check, which is a gate since P2" % len(unmapped))
|
|
else:
|
|
print("dirty-surface: every mutator above is mapped (MG_Pipe/DirtySurface.def); --check "
|
|
"is a gate and --self-test proves it can fail")
|
|
print("dirty-surface: known limits of this scanner - it matches braced function bodies "
|
|
"textually, so a mutator inside a lambda is attributed to the enclosing function, and a "
|
|
"mutation published through a helper the entry point calls reads as deferred here.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|