3Static ingress audit for PETSc option parsing in setup/io.
5This script scans the source files named by the manifest for PetscOptionsGet*/HasName
6calls, extracts option flags, and compares them against a maintained manifest.
8Two kinds of option name reach C. Most are string literals. A variable-arity
9configuration, such as the field-statistics window list, must construct its names
10instead, and a constructed name is invisible to a literal scan. Rather than let that
11become a hole, this audit also collects the format strings those names are built
12from, requires each to be declared as a family, and fails on any constructed name it
13cannot trace back to one. The invariant is that every option name reaching C is
14either a declared literal or a declared family.
16Reads that pass a private PetscOptions object rather than NULL are deliberately out
17of scope: they read checkpoint state, not user configuration.
20from __future__
import annotations
27from typing
import Iterable, Set
30_ACCESSOR =
r'PetscOptions(?:Get(?:Int|Real|Bool|String|IntArray|RealArray)|HasName)'
32OPTION_RE = re.compile(_ACCESSOR +
r'\s*\(\s*NULL\s*,\s*NULL\s*,\s*"(-[^"]+)"')
35CONSTRUCTED_RE = re.compile(
36 _ACCESSOR +
r'\s*\(\s*NULL\s*,\s*NULL\s*,\s*([A-Za-z_][A-Za-z0-9_]*)\s*,'
41SNPRINTF_RE = re.compile(
r'PetscSNPrintf\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,')
43LITERAL_RE = re.compile(
r'"((?:[^"\\]|\\.)*)"')
48 @brief Join the literal fragments of a constructed name into one stable pattern.
49 @details Each substituted value collapses to a bare `%`, so a name spliced around
50 `PetscInt_FMT` and one written with `%d` produce the same family.
51 @param[in] fragments Literal chunks of the format expression, in source order.
52 @return Value returned by `canonical_family()`.
54 joined =
"".join(fragments)
55 return re.sub(
r"%[-+ #0-9.]*[a-zA-Z]?",
"%", joined)
60 @brief Map each name variable built by PetscSNPrintf to every family it carries.
61 @details One buffer is normally reused for many names, so a variable maps to a
62 set rather than a single pattern; collapsing it to the last assignment
63 would hide every family but one.
64 @param[in] text Source text of one translation unit.
65 @return Value returned by `extract_families()`.
67 families:
"dict[str, Set[str]]" = {}
68 for match
in SNPRINTF_RE.finditer(text):
69 variable = match.group(1)
72 while index < len(text)
and depth:
73 if text[index] ==
"(":
75 elif text[index] ==
")":
78 fragments = LITERAL_RE.findall(text[match.end():index])
79 if not fragments
or not fragments[0].startswith(
"-"):
87 @brief Collect declared option families and any constructed name that lacks one.
88 @param[in] paths Source files to scan.
89 @return Value returned by `scan_option_families()`.
91 families: Set[str] = set()
92 untraceable:
"list[str]" = []
94 text = path.read_text(encoding=
"utf-8")
96 for match
in CONSTRUCTED_RE.finditer(text):
97 variable = match.group(1)
99 families.update(built[variable])
101 untraceable.append(f
"{path.name}: '{variable}'")
102 return families, untraceable
107 @brief Perform scan petsc options.
108 @param[in] paths Argument passed to `scan_petsc_options()`.
109 @return Value returned by `scan_petsc_options()`.
111 flags: Set[str] = set()
113 text = path.read_text(encoding=
"utf-8")
114 for match
in OPTION_RE.finditer(text):
115 flags.add(match.group(1))
121 @brief Read and validate the option names, families, and sources in the manifest.
122 @param[in] path Filesystem path argument passed to `load_manifest()`.
123 @return Value returned by `load_manifest()`.
125 data = json.loads(path.read_text(encoding=
"utf-8"))
127 for key
in (
"known_petsc_options",
"known_petsc_option_families"):
128 entries = data.get(key, [])
129 if not isinstance(entries, list):
130 raise ValueError(f
"Manifest key '{key}' must be a list.")
131 bad = [opt
for opt
in entries
if not isinstance(opt, str)
or not opt.startswith(
"-")]
133 raise ValueError(f
"Manifest key '{key}' has invalid entries: {bad}")
134 result[key] = set(entries)
135 sources = data.get(
"sources")
136 if not isinstance(sources, list)
or not all(isinstance(src, str)
for src
in sources):
137 raise ValueError(
"Manifest key 'sources' must be a list of paths.")
138 result[
"sources"] = sources
144 @brief Entry point for this script.
145 @return Value returned by `main()`.
147 parser = argparse.ArgumentParser(
149 "Scan PETSc option ingress in src/setup.c and src/io.c, then compare "
150 "against tests/tooling/audit_ingress_manifest.json."
152 formatter_class=argparse.RawDescriptionHelpFormatter,
155 " python3 tests/tooling/audit_ingress.py\n"
156 " python3 tests/tooling/audit_ingress.py --show-scanned\n"
157 " python3 tests/tooling/audit_ingress.py --manifest tests/tooling/audit_ingress_manifest.json\n"
162 default=
"tests/tooling/audit_ingress_manifest.json",
164 "Manifest JSON path, relative to repository root unless absolute "
165 "(default: tests/tooling/audit_ingress_manifest.json)."
171 help=
"Print discovered PETSc options before drift comparison.",
173 args = parser.parse_args()
175 repo_root = pathlib.Path(__file__).resolve().parents[2]
176 manifest_path = (repo_root / args.manifest).resolve()
178 if not manifest_path.exists():
179 print(f
"[ERROR] Manifest not found: {manifest_path}", file=sys.stderr)
185 scan_paths = [repo_root / src
for src
in manifest[
"sources"]]
186 absent = [str(path)
for path
in scan_paths
if not path.exists()]
188 print(f
"[ERROR] Manifest names sources that do not exist: {absent}", file=sys.stderr)
192 expected = manifest[
"known_petsc_options"]
194 expected_families = manifest[
"known_petsc_option_families"]
196 missing_in_manifest = sorted(scanned - expected)
197 stale_in_manifest = sorted(expected - scanned)
198 missing_families = sorted(scanned_families - expected_families)
199 stale_families = sorted(expected_families - scanned_families)
201 if args.show_scanned:
202 print(
"[INFO] Scanned PETSc options:")
203 for flag
in sorted(scanned):
207 print(f
"[INFO] Scanned options: {len(scanned)}")
208 print(f
"[INFO] Manifest options: {len(expected)}")
209 print(f
"[INFO] Scanned option families: {len(scanned_families)}")
210 print(f
"[INFO] Manifest option families: {len(expected_families)}")
212 if missing_in_manifest:
213 print(
"[ERROR] New PETSc ingress options missing in manifest:")
214 for flag
in missing_in_manifest:
217 if stale_in_manifest:
218 print(
"[ERROR] Manifest options no longer present in setup/io scan:")
219 for flag
in stale_in_manifest:
223 print(
"[ERROR] Constructed option families missing in manifest:")
224 for family
in missing_families:
225 print(f
" - {family}")
228 print(
"[ERROR] Manifest option families no longer present in the scan:")
229 for family
in stale_families:
230 print(f
" - {family}")
233 print(
"[ERROR] Option names built from a variable this audit cannot trace to a family:")
234 for entry
in untraceable:
237 " Build the name with PetscSNPrintf from a literal format beginning with '-', "
238 "so the audit can see it."
241 if missing_in_manifest
or stale_in_manifest
or missing_families
or stale_families
or untraceable:
243 "[FAIL] Ingress drift detected. Update tests/tooling/audit_ingress_manifest.json and docs mapping.",
248 print(
"[OK] Ingress manifest matches the PETSc option scan of its declared sources.")
252if __name__ ==
"__main__":
253 raise SystemExit(
main())
Set[str] scan_petsc_options(Iterable[pathlib.Path] paths)
Perform scan petsc options.
"dict[str, Set[str]]" extract_families(str text)
Map each name variable built by PetscSNPrintf to every family it carries.
"tuple[Set[str], list[str]]" scan_option_families(Iterable[pathlib.Path] paths)
Collect declared option families and any constructed name that lacks one.
int main()
Entry point for this script.
str canonical_family(Iterable[str] fragments)
Join the literal fragments of a constructed name into one stable pattern.
dict load_manifest(pathlib.Path path)
Read and validate the option names, families, and sources in the manifest.