Files
NX2navi/tests/cfg_parse_check.py
chrg6 8c9011d56f Read low_end_gpu's and shader_carry's off words the same in the low-end mode and in make_game.py as in the game's other readers.
The review of the commit that taught nx2navi.ini's readers off, false and no found three readers it left behind. cfg.c
lowend_of reads low_end_gpu through raw_get, which skips switch_word, and still took anything but "0" as on: with
"low_end_gpu = off" cfg_get, cfg_bool and the menu said off while cfg_low_end_active() was 1, so the mode's defaults
and its LOWEND_ONLY keys stayed in effect, and F-7's check, which now accepts the word, logged nothing. lowend_of now
takes the value through switch_of as cfg_bool does: 0 and the off words are off, anything else set is on (an
environment value "0 ; x" stays on, as cfg_bool reads it). tools/make_game.py read the file on its own too:
shader_carry took only "0" as off and kept a comment in the value, so with "shader_carry = off" or "shader_carry = 0 ;
note" an update skipped the full shader preparation and left the shaders to a game that no longer carries them over;
guest_opt_ini took low_end_gpu as on for anything but "" and "0". shader_carry now reads the switch as the game does
(the environment wins over the file and is taken as written, the file's last line with a value wins, a comment after
a blank is cut unless the value is in double quotes, make_game.py ini_choice, and 0, off, false and no in any case are
off); guest_opt_ini keeps its cut and takes the same words as off.

tests/cfg_parse_check gains a lowend case (low_end_gpu = Off, = 0 ; note and = yes from the file, FALSE and "0 ; x"
from the environment; two of them fail on the old lowend_of), and launcher_test's MakeGameTest two tests of
shader_carry's and guest_opt_ini's reading, which fail on the old make_game.py. cfg_rows_check reports 0 problems, and
the -Wall compile of cfg.c shows no warning.

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

209 lines
11 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. What the readers get and what the log says about the values:
- a comment after a value (cfg.c cut_comment): cut at a blank followed by ';' or '#' for a key whose row has a set of
values, the line logged once with its line number, "key = ; note" left unset; kept whole in free text (a row
without a set, a key without a row), in a value in double quotes and in the environment, where the log still says
it is part of the value; at most 16 lines, then a count;
- switches written as words (cfg.c switch_word, cfg_bool): false, off and no in any case read as 0 and true, on and
yes as 1 through cfg_get, cfg_bool and cfg_int, from the file and from the environment, with no line about them;
the low-end mode's switch (cfg.c lowend_of, read apart from cfg_get) takes the same words, so "low_end_gpu = off"
leaves the mode and its keys off;
- 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, 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
cloth_realtime = disabled
window_size = 1280x720 # small
aspect = ; stretch some day
dlss_preset = "k ; quoted"
'''
READ = '''[display]
fullscreen = false
rtss = OFF
hdr_live = Yes
cloth_realtime = on ; the default
pace_smooth = No
vk_present = fifo\t; tear-free
fps_limit = 144 # my monitor
window_size = 1280x720 # small
aspect = ; nothing yet
mods_dir = D:\\My Mods #2
dlss_preset = "k ; quoted"
'''
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; a comment after a value of a row with a set: cut, and
# one line
rc, out = run(exe, env, 'load', BAD, work, {'NX2NAVI_SHARPNESS': '500', 'NX2NAVI_VK_PRESENT': 'fifo'})
want = [
"line 2: pace_smooth = '0', and '; off' after it is a comment",
"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', and '# my monitor' after it is a comment",
"line 9: cloth_realtime = 'disabled' is not 0, 1, on, off, true, false, yes or no",
"line 10: window_size = '1280x720 # small' a ';' or '#' after the value is part of it",
"line 11: aspect = '', and '; stretch some day' after it is a comment",
"line 12: dlss_preset = 'k ; quoted' is none of the menu's choices default|k|j; 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 '')
cfg_lines = [l for l in out.splitlines() if l.startswith('log: cfg: nx2navi.ini line') and 'unknown key' not in l]
check(len(cfg_lines) == 9, f'one line each for the nine lines, none for "fullscreen = false" ({len(cfg_lines)})',
out)
check(not any(' line 2: ' in l and 'is not 0' in l for l in cfg_lines), 'a cut value is checked as cut', out)
check('NX2NAVI_VK_PRESENT' not in out, 'an environment value that fits is not logged', out)
check("got: pace_smooth '0'" in out, 'the value is read without its comment', out)
# what the readers get: switches written as words, cut comments, free text kept whole, the environment as written
rc, out = run(exe, env, 'read', READ, work, {'NX2NAVI_DYN_RES': 'TRUE', 'NX2NAVI_SHADOW_PCF': '0 ; env note'})
got = {l.split()[1]: l.split(' ', 2)[2] for l in out.splitlines() if l.startswith('got: ')}
want = {'PACE_SMOOTH': "'0' bool 0 int 0", 'FULLSCREEN': "'0' bool 0 int 0", 'RTSS': "'0' bool 0 int 0",
'HDR_LIVE': "'1' bool 1 int 1", 'CLOTH_REALTIME': "'1' bool 1 int 1", 'FPS_LIMIT': "'144'",
'VK_PRESENT': "'fifo'", 'ASPECT': "'(unset)'", 'WINDOW_SIZE': "'1280x720 # small'",
'DLSS_PRESET': "'k ; quoted'", 'MODS_DIR': "'D:\\My Mods #2'", 'DYN_RES': "'1' bool 1 int 1",
'SHADOW_PCF': "'0 ; env note' bool 1 int 0"}
for k, w in want.items():
check(rc == 0 and got.get(k) == w, f'read: {k} {w}', f'got {got.get(k)!r}\n{out}' if got.get(k) != w else '')
cut = [l for l in out.splitlines() if 'after it is a comment' in l]
check(len(cut) == 4 and any("line 7: vk_present = 'fifo', and '; tear-free'" in l for l in cut),
f'four cut lines, a tab before the comment too ({len(cut)})', out)
check(not any(w in out for w in ('fullscreen =', 'rtss =', 'hdr_live =', 'pace_smooth =', 'NX2NAVI_DYN_RES =')),
'no line about switches written as words', out)
check("the environment's NX2NAVI_SHADOW_PCF = '0 ; env note' is not 0, 1, on, off, true, false, yes or no; a ';' "
"or '#' after the value is part of it" in out, "the environment's comment stays in its value", 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)
rc, out = run(exe, env, 'load', ''.join(f'vk_present = fifo ; note {i}\n' for i in range(20)), work)
listed = sum(1 for l in out.splitlines() if 'after it is a comment' in l)
check(rc == 0 and listed == 16 and '4 more comments after a value are not listed' in out,
f'20 comments after values: {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)
# the low-end mode's switch, read apart from cfg_get (cfg.c lowend_of): the same words as cfg_bool, from the file
# and from the environment; the mode is decided once per process, so each value is its own run
for text, extra, want in (
('low_end_gpu = Off\n', None, "low_end 0 bool 0 mesh_emulation '(unset)'"),
('low_end_gpu = 0 ; note\n', None, "low_end 0 bool 0 mesh_emulation '(unset)'"),
('low_end_gpu = yes\n', None, "low_end 1 bool 1 mesh_emulation '1'"),
('; nothing\n', {'NX2NAVI_LOW_END_GPU': 'FALSE'}, "low_end 0 bool 0 mesh_emulation '(unset)'"),
('; nothing\n', {'NX2NAVI_LOW_END_GPU': '0 ; x'}, "low_end 1 bool 1 mesh_emulation '1'")):
rc, out = run(exe, env, 'lowend', text, work, extra)
what = text.strip() if extra is None else f"the environment's {extra['NX2NAVI_LOW_END_GPU']!r}"
check(rc == 0 and f'got: {want}' in out, f'low end: {what}: {want}', 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())