Files
NX2navi/tests/cfg_parse_check.py
chrg6 153295b64c Check cfg_meta's result before reading a row's default in default_number and gfx_cfg_window_size.
cfg.c default_number, which cfg_int and cfg_double use for a set value that does not parse, read cfg_meta(name)->def
without a check, so a key without a row whose value was set and unparsable crashed there (the verifier's probe: a
segmentation fault inside default_number). gfx_present.c gfx_cfg_window_size did the same for WINDOW_SIZE. Neither
is reachable today: every unguarded value read has a row (tests/cfg_rows_check.py holds it so), the three guarded
ones use cfg_bool, and WINDOW_SIZE has a row.

default_number gives 0 for a key without a row; gfx_cfg_window_size falls back to 1920x1080 when the row or its
default is missing. Taking the device profile's default there (1280x800 on a Steam Deck) would change what players
get and is left for approval.

Reproduced before the change with the new case in tests/cfg_parse_check.py (cfg_int and cfg_double of an unset row's
key set to "abc" crashed the program); with the change both give 0 and the check passes, as do the cfg rows checks
and the compile checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:34 -05:00

141 lines
6.1 KiB
Python

"""Build and run tests/cfg_parse_check.c: runtime/cfg.c's reading of nx2navi.ini and the environment, against stand-ins
for what it links with. The values stay as written; what is checked is what the log says about them:
- values outside their rows' sets (cfg.c check_value): a 0|1 switch with another value, a number outside its range, a
word that is none of the menu's choices, and a ';' or '#' comment kept in a value, each named with its line; none
for a file whose values fit, nor for a list with numbers in it (the menu's presets); at most 16 lines, then a count;
- a full settings table (cfg.c CFG_MAX_KEYS): one line at load naming the first key not read, one at the first menu
change not kept;
- a number read of a key without a row whose set value does not parse gives 0 (cfg.c default_number), not a crash.
No game files.
Usage: cfg_parse_check.py
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
def compiler():
if os.environ.get('CC'):
return os.environ['CC']
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
CLEAN = '''[frame rate]
pace_smooth = 1
fps_limit = 144
; a comment line
# another
[display]
sharpness = 80
color_gamma = 1.1
vk_present = FIFO
render_scale = 300
dlss_preset = k
pad_layout = xbox_prompts
monitor = 2
'''
BAD = '''[frame rate]
pace_smooth = 0 ; off
fullscreen = false
sharpness = 250
color_gamma = 2
vk_present = vsync
fps_limit = 144 # my monitor
sharpness_typo = 3
'''
def run(exe, env, case, text, work, extra_env=None):
ini = work / f'{case}_{len(text)}.ini'
ini.write_text(text, encoding='utf-8')
e = dict(env)
for k in list(e):
if k.upper().startswith('NX2NAVI_'):
del e[k]
e.update(extra_env or {})
p = subprocess.run([str(exe), case, str(ini)], env=e, capture_output=True, text=True, errors='replace', timeout=60)
if p.returncode:
print(p.stdout, p.stderr)
return p.returncode, p.stdout
def main():
work = ROOT / 'build' / 'test' / 'cfg_parse'
work.mkdir(parents=True, exist_ok=True)
exe = work / ('cfg_parse_check.exe' if os.name == 'nt' else 'cfg_parse_check')
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
rt = ROOT / 'runtime'
subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-g', '-Wall', '-Wno-unused-function', '-I', str(rt),
str(ROOT / 'tests' / 'cfg_parse_check.c'), str(rt / 'cfg.c'), '-o', str(exe)], env=env)
bad = 0
def check(ok, what, out=''):
nonlocal bad
print(f'{what}: {"ok" if ok else "FAIL"}', flush=True)
if not ok:
bad += 1
print(out)
# values that fit: no line about them
rc, out = run(exe, env, 'load', CLEAN, work)
lines = [l for l in out.splitlines() if l.startswith('log: cfg:') and '=' in l and 'line' in l]
check(rc == 0 and not lines, 'a file whose values fit logs nothing about them', out)
# values that do not: one line each, with the line number
rc, out = run(exe, env, 'load', BAD, work, {'NX2NAVI_SHARPNESS': '500', 'NX2NAVI_VK_PRESENT': 'fifo'})
want = [
"line 2: pace_smooth = '0 ; off' is not 0 or 1; a ';' or '#' after the value is part of it",
"line 3: fullscreen = 'false' is not 0 or 1",
"line 4: sharpness = '250' is not a number in 0..100",
"line 5: color_gamma = '2' is not a number in 0.8..1.2",
"line 6: vk_present = 'vsync' is none of the menu's choices mailbox|fifo|immediate",
"line 7: fps_limit = '144 # my monitor' a ';' or '#' after the value is part of it",
"the environment's NX2NAVI_SHARPNESS = '500' is not a number in 0..100",
]
for w in want:
check(rc == 0 and w in out, f'logged: {w}', out if w not in out else '')
check('NX2NAVI_VK_PRESENT' not in out, 'an environment value that fits is not logged', out)
check("pace_smooth '0 ; off'" in out, 'the value itself is read as written', out)
# at most 16 lines, then a count
many = '\n'.join(f'sharpness = {200 + i}' if i % 2 else f'fullscreen = no{i}' for i in range(2)) + '\n'
many = ''.join(f'[s{i}]\nsharpness = {200 + i}\n' for i in range(1)) + ''.join(
f'{k} = 7\n' for k in ('fullscreen', 'pace_smooth', 'vk_present', 'sharpness', 'color_gamma', 'perf_overlay',
'upscaler', 'standin_filter', 'dlss', 'dlss_preset', 'framegen', 'aa', 'aspect',
'texture_detail', 'color_preset', 'device_profile', 'mods_rebuild', 'operation_mode',
'text_speed', 'sprint_mode'))
rc, out = run(exe, env, 'load', many, work)
listed = sum(1 for l in out.splitlines() if l.startswith('log: cfg: nx2navi.ini line') and "= '7'" in l)
check(rc == 0 and listed == 16 and 'more values outside their settings' in out,
f'20 values outside their sets: {listed} lines and a count', out)
# more keys than the table holds: one line at load, one for the menu's changes
full = ''.join(f'cheat_{0x1000 + i:08x} = 1\n' for i in range(600)) + 'low_latency = 1\n'
rc, out = run(exe, env, 'full', full, work)
load_lines = [l for l in out.splitlines() if 'has more than 512 keys' in l]
set_lines = [l for l in out.splitlines() if 'settings table is full' in l]
check(rc == 0 and len(load_lines) == 1 and 'line 513 (CHEAT_' in load_lines[0] and 'got: low_latency (unset)' in out,
'a file with 601 keys: one line at load from line 513 on', out)
check(rc == 0 and len(set_lines) == 1 and 'VK_PRESENT is not kept' in set_lines[0],
'two menu changes past a full table: one line', out)
rc, out = run(exe, env, 'norow', CLEAN, work, {'NX2NAVI_NO_ROW_CHECK': 'abc'})
check(rc == 0 and 'got: cfg_int 0 cfg_double 0' in out, 'cfg_int of a set, unparsable key without a row: 0', out)
print(f'cfg_parse_check: {"FAILED" if bad else "ok"}')
return 1 if bad else 0
if __name__ == '__main__':
sys.exit(main())