mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Add a static check of cfg.c's settings tables to the content guard, and list the 15 names it found missing from known[].
A // on a metas[] line hides the rows after it on that line, and the game then ends at start with "FATAL: cfg:
NX2NAVI_<X> has no row in the settings table"; that shipped in two test builds and nothing caught it before a
start. And 15 names the runtime reads were not in known[], so setting any of them in nx2navi.ini logged "unknown
key" and put it in the menu's list of unknown keys although it took effect: AUDIO_DUMP, DEFER_STAMP, DEPTH_TWIN,
DLAA_ZFIT, DUMP_SKIP_RUNS, DUMP_TEX, DUMP_UBO, EVENT_ORDER, GATHER_BIAS, PIN_SWEEP, REC_CHECK, REC_DELAY_US,
REG_ORDER, SHADOW and ZERO_LOCALS (GATHER_BIAS and ZERO_LOCALS through vk_caps.c env_u32).
tests/cfg_rows_check.py (standard library only) parses known[], metas[], sections[], DECK[] and LOWEND[] by
brace matching that skips string and character literals and comments (an apostrophe in a comment inside a table
would otherwise open a character literal and run the table on past its end), line by line with the first // outside
a string split off, and the first #if branch standing for the table. It fails on a row or name after //, a /* in a table, a name twice in known[], two metas[] rows for a
key, a metas[] row not in known[], a cfg_value/cfg_int/cfg_bool/cfg_double read (directly or through one level of
wrapper) with no row and no cfg_get, cfg_is_set or cfg_meta guard earlier in the same function (kernel.c SPIN and
SVC_WAITS, vk_headroom.c VRAM_HEADROOM pass that way), and a name read through cfg_get, cfg_is_set, cfg_meta,
rt_knob, a cfg_get or "NX2NAVI_%s" wrapper, or a patches-v111.txt set line that is not in known[] or a cheat.
--self-test puts each kind of fault into a copy and checks it is caught. The content guard workflow runs the
check after tools/check_content.py. The 15 names go into known[] on a line of their own with no //.
Reproduced before the change: the check reported the 15 names on stability-pass, as the audit's prototype did.
Now it reports 0 problems and its self-test catches all 15 injected faults; the scratchpad cfg rows check
("missing []"), cfg_parse_check and the compile checks pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1
.github/workflows/content-guard.yml
vendored
1
.github/workflows/content-guard.yml
vendored
@@ -13,3 +13,4 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: python tools/check_content.py
|
||||
- run: python tests/cfg_rows_check.py
|
||||
|
||||
@@ -88,6 +88,9 @@ static const char *const known[] = {
|
||||
"MEM_BUDGET_TEST_VRAM_MB", "MEM_BUDGET_TEST_RAM_MB",
|
||||
"TEX_COMPACT", "TEX_COMPACT_MIN_MB", "TEX_COMPACT_FRAME_MB", // texture compaction (vk_texture.c); no menu row
|
||||
"STANDIN_FILTER", // the filter of the scene upscaler's stand-in (vk_tonemap.c); no menu row
|
||||
// developer switches read with cfg_get, rt_knob or vk_caps.c env_u32, without a menu row (tests/cfg_rows_check.py)
|
||||
"AUDIO_DUMP", "DEFER_STAMP", "DEPTH_TWIN", "DLAA_ZFIT", "DUMP_SKIP_RUNS", "DUMP_TEX", "DUMP_UBO", "EVENT_ORDER",
|
||||
"GATHER_BIAS", "PIN_SWEEP", "REC_CHECK", "REC_DELAY_US", "REG_ORDER", "SHADOW", "ZERO_LOCALS",
|
||||
// retired: the settings of the co-op of versions up to 0.9.2; read by nothing, listed so that a file which still
|
||||
// sets them logs no unknown key
|
||||
"COOP", "COOP_BETA", "COOP_NAME", "COOP_SERVER", "COOP_PORT", "COOP_ROOM", "COOP_AUTO", "COOP_DROP",
|
||||
|
||||
382
tests/cfg_rows_check.py
Normal file
382
tests/cfg_rows_check.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""Static check of runtime/cfg.c's settings tables against what the runtime reads. Standard library only, so the
|
||||
content guard workflow runs it beside tools/check_content.py.
|
||||
|
||||
It fails on:
|
||||
- a row or name after a // on a line of known[], metas[], sections[], DECK[] or LOWEND[]: it is not compiled, and a
|
||||
hidden metas[] row ends every start with "FATAL: cfg: NX2NAVI_<X> has no row in the settings table" (this shipped
|
||||
in two test builds); a /* inside those tables, which this check does not read;
|
||||
- a name twice in known[], or two metas[] rows for one key in one #if branch (the first branch stands for the table,
|
||||
later ones repeat its keys);
|
||||
- a metas[] row whose key is not in known[];
|
||||
- a read with cfg_value, cfg_int, cfg_bool or cfg_double of "NX2NAVI_<KEY>" (directly, or through one level of a
|
||||
function that passes its const char * parameter on to one of them) whose key has no metas[] row, unless the same
|
||||
function asked cfg_get, cfg_is_set or cfg_meta about that key before it;
|
||||
- a name the runtime reads that is not in known[] and does not start with CHEAT_ (cfg.c is_known): cfg_get, cfg_is_set
|
||||
and cfg_meta of "NX2NAVI_<KEY>", rt_knob("<KEY>", ...), one level of functions whose const char * parameter reaches
|
||||
cfg_get or a "NX2NAVI_%s" of it, and the `set <name> NX2NAVI_<KEY>` lines of recomp/patches-v111.txt. Such a key set
|
||||
in nx2navi.ini logs "unknown key" and is listed as unknown in the menu.
|
||||
|
||||
Names read only with getenv, plat_setenv or cfg_setenv are environment-only and not checked. runtime/cfg_probe.c asks
|
||||
for an unknown key on purpose and is left out, as are third_party/ and the generated *_spv.h headers.
|
||||
|
||||
Usage: python tests/cfg_rows_check.py [repo] one line per problem with file:line; exit 1 if any
|
||||
python tests/cfg_rows_check.py --self-test inject each kind of fault into copies and check each is caught
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
KEY = r'[A-Z][A-Z0-9_]*'
|
||||
TABLES = { # declaration, what a row looks like on a line
|
||||
'known[]': ('static const char *const known[]', r'"(' + KEY + r')"'),
|
||||
'metas[]': ('static const CfgMeta metas[]', r'\{\s*"(' + KEY + r')"'),
|
||||
'sections[]': ('sections[] = {', r'\{\s*"(' + KEY + r')"'),
|
||||
'DECK[]': ('DECK[] = {', r'\{\s*"(' + KEY + r')"'),
|
||||
'LOWEND[]': ('LOWEND[] = {', r'\{\s*"(' + KEY + r')"'),
|
||||
}
|
||||
VALUE_FNS = ('cfg_value', 'cfg_int', 'cfg_bool', 'cfg_double')
|
||||
GUARD_FNS = ('cfg_get', 'cfg_is_set', 'cfg_meta')
|
||||
LIT = r'"NX2NAVI_(' + KEY + r')"'
|
||||
|
||||
|
||||
def matching(src, open_at):
|
||||
"""The index of the bracket that closes the one at open_at (brackets in string literals and comments left out)."""
|
||||
pair = {'{': '}', '(': ')'}[src[open_at]]
|
||||
depth, k, n = 0, open_at, len(src)
|
||||
while k < n:
|
||||
c = src[k]
|
||||
if src.startswith('//', k):
|
||||
k = src.find('\n', k)
|
||||
if k < 0:
|
||||
return n
|
||||
elif src.startswith('/*', k):
|
||||
k = src.find('*/', k + 2)
|
||||
if k < 0:
|
||||
return n
|
||||
k += 1
|
||||
elif c == '"' or c == "'":
|
||||
k += 1
|
||||
while k < n and src[k] != c:
|
||||
k += 2 if src[k] == '\\' else 1
|
||||
elif c == src[open_at]:
|
||||
depth += 1
|
||||
elif c == pair:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return k
|
||||
k += 1
|
||||
return n
|
||||
|
||||
|
||||
def split_comment(line):
|
||||
"""(code, comment) of one line: the comment starts at the first // outside a string literal."""
|
||||
i, n, quote = 0, len(line), None
|
||||
while i < n:
|
||||
c = line[i]
|
||||
if quote:
|
||||
if c == '\\':
|
||||
i += 2
|
||||
continue
|
||||
if c == quote:
|
||||
quote = None
|
||||
elif c in '"\'':
|
||||
quote = c
|
||||
elif line.startswith('//', i):
|
||||
return line[:i], line[i:]
|
||||
i += 1
|
||||
return line, ''
|
||||
|
||||
|
||||
def strip_comments(src):
|
||||
"""C source with its comments blanked out, line numbers and string literals kept."""
|
||||
out, i, n = [], 0, len(src)
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if c == '"' or c == "'":
|
||||
j = i + 1
|
||||
while j < n and src[j] != c and src[j] != '\n':
|
||||
j += 2 if src[j] == '\\' else 1
|
||||
out.append(src[i:j + 1])
|
||||
i = j + 1
|
||||
elif src.startswith('//', i):
|
||||
j = src.find('\n', i)
|
||||
i = n if j < 0 else j
|
||||
elif src.startswith('/*', i):
|
||||
j = src.find('*/', i + 2)
|
||||
j = n if j < 0 else j + 2
|
||||
out.append('\n' * src.count('\n', i, j))
|
||||
i = j
|
||||
else:
|
||||
out.append(c)
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def line_of(src, pos):
|
||||
return src.count('\n', 0, pos) + 1
|
||||
|
||||
|
||||
def tables(cfg, problems):
|
||||
"""The names of each table, the first #if branch of each line only, with the problems of hidden rows."""
|
||||
out = {}
|
||||
for name, (decl, row) in TABLES.items():
|
||||
i = cfg.find(decl)
|
||||
if i < 0:
|
||||
problems.append(f'runtime/cfg.c: no {name} table ("{decl}")')
|
||||
out[name] = []
|
||||
continue
|
||||
j = cfg.index('{', i + len(decl) - 1)
|
||||
body = cfg[j + 1:matching(cfg, j)]
|
||||
line0 = line_of(cfg, j)
|
||||
names, branch, seen_branch = [], [], {}
|
||||
for n, line in enumerate(body.split('\n')):
|
||||
st = line.strip()
|
||||
if st.startswith('#if'):
|
||||
branch.append(0)
|
||||
continue
|
||||
if st.startswith(('#else', '#elif')):
|
||||
if branch:
|
||||
branch[-1] += 1
|
||||
continue
|
||||
if st.startswith('#endif'):
|
||||
if branch:
|
||||
branch.pop()
|
||||
continue
|
||||
code, comment = split_comment(line)
|
||||
if '/*' in code:
|
||||
problems.append(f'runtime/cfg.c:{line0 + n}: a /* comment in {name}, which this check does not read')
|
||||
hidden = re.findall(row, comment)
|
||||
if hidden:
|
||||
problems.append(f'runtime/cfg.c:{line0 + n}: {name} rows after // are not compiled: {", ".join(hidden)}')
|
||||
if branch and branch[-1] > 0:
|
||||
continue # a later branch repeats the first one's keys
|
||||
for k in re.findall(row, code):
|
||||
names.append((k, line0 + n))
|
||||
out[name] = names
|
||||
return out
|
||||
|
||||
|
||||
def functions(src):
|
||||
"""(name, parameters, body start, body end) of every function definition in comment-stripped source."""
|
||||
out = []
|
||||
for m in re.finditer(r'\b([A-Za-z_]\w*)\s*\(([^;{}()]*)\)\s*(?:const\s*)?\{', src):
|
||||
name = m.group(1)
|
||||
if name in ('if', 'for', 'while', 'switch', 'return', 'sizeof', 'catch'):
|
||||
continue
|
||||
b = m.end() - 1
|
||||
out.append((name, m.group(2), b, matching(src, b)))
|
||||
return out
|
||||
|
||||
|
||||
def call_args(src, open_at):
|
||||
"""The arguments of the call whose ( is at open_at, as text."""
|
||||
end = matching(src, open_at)
|
||||
args, depth, cur, k = [], 0, '', open_at + 1
|
||||
while k < end:
|
||||
c = src[k]
|
||||
if c == '"':
|
||||
j = k + 1
|
||||
while j < end and src[j] != '"':
|
||||
j += 2 if src[j] == '\\' else 1
|
||||
cur += src[k:j + 1]
|
||||
k = j + 1
|
||||
continue
|
||||
if c in '([{':
|
||||
depth += 1
|
||||
elif c in ')]}':
|
||||
depth -= 1
|
||||
elif c == ',' and depth == 0:
|
||||
args.append(cur.strip())
|
||||
cur = ''
|
||||
k += 1
|
||||
continue
|
||||
cur += c
|
||||
k += 1
|
||||
args.append(cur.strip())
|
||||
return args
|
||||
|
||||
|
||||
def check(cfg_text, sources, patches):
|
||||
"""The problems, one line each. sources: {path as reported: text}; cfg_text is also among them as runtime/cfg.c."""
|
||||
problems = []
|
||||
tabs = tables(cfg_text, problems)
|
||||
known = [k for k, _ in tabs['known[]']]
|
||||
known_s = set(known)
|
||||
metas_s = {k for k, _ in tabs['metas[]']}
|
||||
for k in sorted({k for k in known if known.count(k) > 1}):
|
||||
problems.append(f'runtime/cfg.c: known[] lists {k} twice')
|
||||
seen = {}
|
||||
for k, n in tabs['metas[]']:
|
||||
if k in seen:
|
||||
problems.append(f'runtime/cfg.c:{n}: metas[] has a second row for {k} (the first at line {seen[k]})')
|
||||
seen.setdefault(k, n)
|
||||
for k, n in tabs['metas[]']:
|
||||
if k not in known_s:
|
||||
problems.append(f'runtime/cfg.c:{n}: metas[] row {k} is not in known[]')
|
||||
|
||||
stripped = {p: strip_comments(t) for p, t in sources.items()}
|
||||
# one level of wrappers: a const char * parameter passed on to a value read, or to cfg_get / "NX2NAVI_%s"
|
||||
wrappers = {} # name -> (argument index, 'value' | 'name', the literal carries the prefix)
|
||||
for p, s in stripped.items():
|
||||
for fname, params, b, e in functions(s):
|
||||
if fname in VALUE_FNS or fname in GUARD_FNS or fname == 'rt_knob':
|
||||
continue
|
||||
body = s[b:e]
|
||||
for idx, prm in enumerate(x.strip() for x in params.split(',')):
|
||||
pm = re.fullmatch(r'const\s+char\s*\*\s*(\w+)', prm)
|
||||
if not pm:
|
||||
continue
|
||||
v = pm.group(1)
|
||||
if re.search(r'\b(?:%s)\s*\(\s*%s\s*\)' % ('|'.join(VALUE_FNS), v), body):
|
||||
wrappers[fname] = (idx, 'value', True)
|
||||
elif re.search(r'\bcfg_get\s*\(\s*%s\s*\)' % v, body):
|
||||
wrappers.setdefault(fname, (idx, 'name', True))
|
||||
elif re.search(r'"NX2NAVI_%%s"\s*,\s*%s\b' % v, body):
|
||||
wrappers.setdefault(fname, (idx, 'name', False))
|
||||
|
||||
value_sites, name_sites = [], [] # (key, file, position, how)
|
||||
for p, s in stripped.items():
|
||||
start = s.index('static int is_known') if p.endswith('runtime/cfg.c') and 'static int is_known' in s else 0
|
||||
for m in re.finditer(r'\b(%s)\s*\(\s*' % '|'.join(VALUE_FNS) + LIT, s):
|
||||
if m.start() >= start:
|
||||
value_sites.append((m.group(2), p, m.start(), m.group(1)))
|
||||
for m in re.finditer(r'\b(%s)\s*\(\s*' % '|'.join(GUARD_FNS) + LIT, s):
|
||||
if m.start() >= start:
|
||||
name_sites.append((m.group(2), p, m.start(), m.group(1)))
|
||||
for m in re.finditer(r'\brt_knob\s*\(\s*"(' + KEY + r')"', s):
|
||||
name_sites.append((m.group(1), p, m.start(), 'rt_knob'))
|
||||
for w, (idx, kind, prefixed) in wrappers.items():
|
||||
for m in re.finditer(r'\b' + re.escape(w) + r'\s*\(', s):
|
||||
args = call_args(s, m.end() - 1)
|
||||
if idx >= len(args):
|
||||
continue
|
||||
lm = re.fullmatch(r'"(NX2NAVI_)?(' + KEY + r')"', args[idx])
|
||||
if not lm or bool(lm.group(1)) != prefixed:
|
||||
continue
|
||||
(value_sites if kind == 'value' else name_sites).append((lm.group(2), p, m.start(), w))
|
||||
for n, line in enumerate(patches.split('\n'), 1):
|
||||
m = re.match(r'\s*set\s+\S+\s+NX2NAVI_(' + KEY + r')\b', line)
|
||||
if m:
|
||||
name_sites.append((m.group(1), 'recomp/patches-v111.txt', -n, 'a patch set'))
|
||||
|
||||
def where(p, pos):
|
||||
return f'{p}:{-pos if pos < 0 else line_of(stripped[p], pos)}'
|
||||
|
||||
bodies = {p: functions(s) for p, s in stripped.items()}
|
||||
for k, p, pos, how in value_sites:
|
||||
if k in metas_s:
|
||||
continue
|
||||
body = next(((b, e) for _, _, b, e in bodies[p] if b < pos < e), None)
|
||||
text = stripped[p][body[0]:pos] if body else ''
|
||||
if re.search(r'\b(?:%s)\s*\(\s*"NX2NAVI_%s"' % ('|'.join(GUARD_FNS), k), text):
|
||||
continue # the function asked whether the key is set first (kernel.c SPIN, vk_headroom.c)
|
||||
problems.append(f'{where(p, pos)}: {how}("NX2NAVI_{k}") has no metas[] row and no cfg_get guard before it: '
|
||||
f'the game would end with "has no row in the settings table"')
|
||||
missing = {}
|
||||
for k, p, pos, how in name_sites + value_sites:
|
||||
if k not in known_s and not k.startswith('CHEAT_'):
|
||||
missing.setdefault(k, []).append(f'{where(p, pos)} {how}')
|
||||
for k, ws in sorted(missing.items()):
|
||||
problems.append(f'{ws[0].split()[0]}: {k} is read ({"; ".join(ws[:3])}{"; ..." if len(ws) > 3 else ""}) but '
|
||||
f'is not in cfg.c known[]: set in nx2navi.ini it logs "unknown key"')
|
||||
return problems, len(known), len(tabs['metas[]']), len(value_sites), len(name_sites), sorted(wrappers)
|
||||
|
||||
|
||||
def read_repo(repo):
|
||||
rt = repo / 'runtime'
|
||||
files = sorted(p for p in rt.rglob('*') if p.suffix in ('.c', '.cpp', '.h', '.inc') and 'third_party' not in p.parts
|
||||
and not p.name.endswith('_spv.h') and p.name != 'cfg_probe.c')
|
||||
sources = {p.relative_to(repo).as_posix(): p.read_text(encoding='utf-8-sig', errors='replace') for p in files}
|
||||
patches = repo / 'recomp' / 'patches-v111.txt'
|
||||
return sources['runtime/cfg.c'], sources, patches.read_text(encoding='utf-8') if patches.exists() else ''
|
||||
|
||||
|
||||
def self_test(repo):
|
||||
"""Each kind of fault, put into a copy of the tables or a made-up source file, must be reported."""
|
||||
cfg, sources, patches = read_repo(repo)
|
||||
base, *_ = check(cfg, sources, patches)
|
||||
bad = 0
|
||||
if base:
|
||||
print(f'self-test: the repository itself has {len(base)} problems; the cases below are judged on new ones')
|
||||
|
||||
def case(what, want, cfg_text=None, extra=None, patch_text=None):
|
||||
nonlocal bad
|
||||
src = dict(sources)
|
||||
src['runtime/cfg.c'] = cfg_text if cfg_text is not None else cfg
|
||||
src.update(extra or {})
|
||||
got, *_ = check(src['runtime/cfg.c'], src, patch_text if patch_text is not None else patches)
|
||||
new = [p for p in got if p not in base]
|
||||
ok = any(re.search(want, p) for p in new)
|
||||
print(f'self-test: {what}: {"caught" if ok else "MISSED"}')
|
||||
if not ok:
|
||||
bad += 1
|
||||
for p in new:
|
||||
print(' ', p)
|
||||
|
||||
def add_line(table_decl, line):
|
||||
"""cfg.c with line put in as the table's first line."""
|
||||
i = cfg.index(table_decl)
|
||||
j = cfg.index('\n', cfg.index('{', i + len(table_decl) - 1)) + 1
|
||||
return cfg[:j] + ' ' + line + '\n' + cfg[j:]
|
||||
|
||||
case('a metas[] row after //', r'metas\[\] rows after // are not compiled: HIDDEN_ROW',
|
||||
add_line(TABLES['metas[]'][0], '{"FULLSCREEN_X", "0", "0|1", LIVE}, // {"HIDDEN_ROW", "0", "0|1", LIVE},'))
|
||||
case('a known[] name after //', r'known\[\] rows after // are not compiled: HIDDEN_NAME',
|
||||
add_line(TABLES['known[]'][0], '"AB_X", // "HIDDEN_NAME",'))
|
||||
case('a sections[] entry after //', r'sections\[\] rows after // are not compiled: HIDDEN_SECTION',
|
||||
add_line(TABLES['sections[]'][0], '// {"HIDDEN_SECTION", "display"},'))
|
||||
case('a /* in a table', r'a /\* comment in known\[\]', add_line(TABLES['known[]'][0], '/* "X", */'))
|
||||
case('a name twice in known[]', r'known\[\] lists AA twice', add_line(TABLES['known[]'][0], '"AA",'))
|
||||
case('two metas[] rows for one key', r'second row for FULLSCREEN',
|
||||
add_line(TABLES['metas[]'][0], '{"FULLSCREEN", "0", "0|1", LIVE},'))
|
||||
case('a metas[] row not in known[]', r'metas\[\] row ROW_NOT_KNOWN is not in known\[\]',
|
||||
add_line(TABLES['metas[]'][0], '{"ROW_NOT_KNOWN", "0", "0|1", LIVE},'))
|
||||
# a row the game reads, hidden behind a // put before it on its line: its readers are left without a row
|
||||
at = cfg.index('{"FULLSCREEN"', cfg.index(TABLES['metas[]'][0]))
|
||||
case('a value read on a row hidden by // (FULLSCREEN)', r'FULLSCREEN"\) has no metas\[\] row',
|
||||
cfg[:at] + '// ' + cfg[at:])
|
||||
fake = '''
|
||||
static int my_opt(const char *key) { return cfg_int(key); }
|
||||
static unsigned my_env(const char *name) { return cfg_get(name) ? 1 : 0; }
|
||||
void fake(void) {
|
||||
int a = cfg_int("NX2NAVI_NO_ROW_INT");
|
||||
if (cfg_get("NX2NAVI_GUARDED_BOOL") && cfg_bool("NX2NAVI_GUARDED_BOOL")) a++;
|
||||
a += my_opt("NX2NAVI_WRAPPED_NO_ROW");
|
||||
a += rt_knob("KNOB_NOT_KNOWN", 0);
|
||||
a += (int)my_env("NX2NAVI_ENV_NOT_KNOWN");
|
||||
}
|
||||
'''
|
||||
extra = {'runtime/fake_check.c': fake}
|
||||
case('an unguarded cfg_int without a row', r'cfg_int\("NX2NAVI_NO_ROW_INT"\) has no metas', extra=extra)
|
||||
case('a value read through a wrapper without a row', r'my_opt\("NX2NAVI_WRAPPED_NO_ROW"\) has no metas', extra=extra)
|
||||
case('a guarded read without a row is not reported as such, its name is',
|
||||
r'GUARDED_BOOL is read .* not in cfg.c known', extra=extra)
|
||||
got, *_ = check(cfg, {**sources, **extra}, patches)
|
||||
guarded_flagged = any('GUARDED_BOOL") has no metas' in p for p in got)
|
||||
print(f'self-test: a read after a cfg_get guard in the same function: {"REPORTED" if guarded_flagged else "passes"}')
|
||||
bad += guarded_flagged
|
||||
case('an rt_knob name not in known[]', r'KNOB_NOT_KNOWN is read', extra=extra)
|
||||
case('a name read through a cfg_get wrapper', r'ENV_NOT_KNOWN is read', extra=extra)
|
||||
case('a patch set\'s name not in known[]', r'PATCH_SET_NOT_KNOWN is read',
|
||||
patch_text=patches + '\nset some_patch NX2NAVI_PATCH_SET_NOT_KNOWN 1\n')
|
||||
print(f'cfg_rows_check self-test: {"FAILED" if bad else "ok"}')
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
repo = Path(__file__).resolve().parents[1]
|
||||
if args and args[0] != '--self-test':
|
||||
repo = Path(args[0])
|
||||
if '--self-test' in args:
|
||||
return self_test(repo)
|
||||
problems, nk, nm, nv, nn, wr = check(*read_repo(repo))
|
||||
for p in problems:
|
||||
print(p)
|
||||
print(f'cfg_rows_check: known[] {nk} names, metas[] {nm} rows, {nv} value reads and {nn} name reads (wrappers: '
|
||||
f'{", ".join(wr)}): {len(problems)} problem{"" if len(problems) == 1 else "s"}')
|
||||
return 1 if problems else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user