From b371e527f1dd0aae5b7762c5d07a6b3687755519 Mon Sep 17 00:00:00 2001 From: Wenyuan Huang <157926136+whuang369@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:30:27 -0500 Subject: [PATCH] Fix #205: drop problem 23's unchecked accept for Tid % 100 == 0 check_Tid() returned "ok Test 0." for any Tid divisible by 100 before the submitted A=B program ran, so even an empty program scored 100 on such a test. At c7ec558 the only tests were Tid 0, 100 and 200, which made the whole problem free. None of the 16 released test ids on main is a multiple of 100, so current scores are not affected. Tid % 100 picks the test set. dict maps 0 to 20 to themselves and anything else to 0, but work() has only 17 generators, and generator 0 is empty. So 0 got the free accept, 17 to 20 indexed past the end of vf (undefined behavior), and 21 to 99 ran no test strings and printed "Ratio: nan". - Remove the shortcut. - work() fails with _fail (a data error) when Tid % 100 selects no generator. - validate_test() fails with _fail when a test generates no test strings, so no test can pass without running the program. - Remove the Scoring paragraph in statement.txt that documented the shortcut. The released tests (Tid % 100 in 1 to 16) are unchanged: the old and new checkers give byte-identical output on all 16 of them for 8 programs (empty, "a=b", the reference program from the .ans files and 5 archived solutions). Tested with g++ 15.2 (-O2 -std=gnu++17): all 33 tests in the new algorithmic/tests/test_problem23.py pass. On main, the 17 that cover Tid % 100 == 0 and the ids without a test set fail, and the 16 released-test cases pass. Fixes #205. Co-Authored-By: Claude Opus 5.5 --- algorithmic/problems/23/check.cpp | 7 +++- algorithmic/problems/23/statement.txt | 2 - algorithmic/tests/test_problem23.py | 58 +++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 algorithmic/tests/test_problem23.py diff --git a/algorithmic/problems/23/check.cpp b/algorithmic/problems/23/check.cpp index 49d9140b9..fd4a73fb2 100644 --- a/algorithmic/problems/23/check.cpp +++ b/algorithmic/problems/23/check.cpp @@ -351,6 +351,8 @@ namespace test_gen{ } + if (op<0 || op>=(int)vf.size()) + quitf(_fail, "Tid %d selects no test set. Please fix the data !!!",Tid); (vf[op])(); } @@ -368,7 +370,6 @@ namespace test_gen{ } void check_Tid(){ test_gen::Tid=inf.readInteger(); - if (test_gen::Tid%100==0) quitf(_ok,"Test 0."); } void read_test(){ test_gen::gao(); @@ -387,6 +388,10 @@ void read_test(){ */ } void validate_test(){ + // Every test must run the program. Test set 0 is empty, and a Tid with + // Tid%100==0 used to be accepted without running it (#205). + if (Tcase==0) + quitf(_fail, "Tid %d generates no test strings. Please fix the data !!!",test_gen::Tid); pattern tp1("[abcS]{3,1000}"); pattern tp2("[abc]{1,1000}S[abc]{1,1000}"); for (int T=1;T<=Tcase;T++){ diff --git a/algorithmic/problems/23/statement.txt b/algorithmic/problems/23/statement.txt index 122510f0b..c8d983452 100644 --- a/algorithmic/problems/23/statement.txt +++ b/algorithmic/problems/23/statement.txt @@ -321,8 +321,6 @@ b=1 ## Scoring -For Tid divisible by 100, the checker validates the submitted program syntax and then accepts it for 100 points without executing generated test strings. No currently released test case uses Tid divisible by 100. The execution-based rules below apply to the other test identifiers. - Let x be E, the number of executed instructions in a test string. C=max(2*l*l,50), where l is the initial string length. The execution limit remains 2*C; program-length and correctness requirements still apply. The physical reference (the 60-point level) is a=C. Define the lower-band continuation: L(x) = 60*max(0,(2*C-x)/C). diff --git a/algorithmic/tests/test_problem23.py b/algorithmic/tests/test_problem23.py new file mode 100644 index 000000000..f9387d164 --- /dev/null +++ b/algorithmic/tests/test_problem23.py @@ -0,0 +1,58 @@ +"""Every test must run the submitted A=B program; Tid % 100 == 0 used to be accepted unchecked (#205).""" +from pathlib import Path +import re +import subprocess +import pytest + +ROOT = Path(__file__).resolve().parents[2] +DATA = ROOT / 'algorithmic/problems/23/testdata' +RELEASED = sorted(int(p.stem) for p in DATA.glob('*.in')) +# Every N.ans holds the same correct A=B program for the substring task. +REFERENCE = (DATA / '1.ans').read_text() +EMPTY, ONE_RULE = '', 'a=b\n' + + +@pytest.fixture(scope='module') +def checker(tmp_path_factory): + executable = tmp_path_factory.mktemp('checker23') / 'checker' + subprocess.run(['g++', '-std=gnu++17', '-O2', '-I' + str(ROOT / 'algorithmic/judge/include'), + str(ROOT / 'algorithmic/problems/23/check.cpp'), '-o', str(executable)], check=True) + return executable + + +def check(checker, tmp_path, data, program): + inf, out = tmp_path / 'in', tmp_path / 'out' + inf.write_text(data) + out.write_text(program) + return subprocess.run([str(checker), str(inf), str(out), str(DATA / '1.ans')], + text=True, capture_output=True, timeout=30) + + +# Tid % 100 picks the test set, and set 0 is empty. The ids at the pinned commit +# c7ec558 were 0, 100 and 200, and each accepted any parseable program as "ok Test 0.". +@pytest.mark.parametrize('tid', [0, 100, 200, 2000000000]) +@pytest.mark.parametrize('program', [EMPTY, ONE_RULE, REFERENCE], ids=['empty', 'one-rule', 'reference']) +def test_tid_divisible_by_100_is_never_accepted(checker, tmp_path, tid, program): + result = check(checker, tmp_path, '%d\n' % tid, program) + assert result.returncode == 3, result.stderr + assert 'generates no test strings' in result.stderr + + +# 17 to 20 indexed past the 17 generators and crashed; 21 to 99 fell back to the +# empty set 0 and scored nan. +@pytest.mark.parametrize('tid', [17, 20, 21, 50, 1234567899]) +def test_tid_without_a_test_set_is_a_data_error(checker, tmp_path, tid): + result = check(checker, tmp_path, '%d\n' % tid, REFERENCE) + assert result.returncode == 3, result.stderr + assert result.stderr.startswith('FAIL Tid %d ' % tid), result.stderr + + +@pytest.mark.parametrize('test', RELEASED) +def test_released_tests_run_the_program(checker, tmp_path, test): + data = (DATA / ('%d.in' % test)).read_text() + for program in (EMPTY, ONE_RULE): + result = check(checker, tmp_path, data, program) + assert result.returncode == 1 and 'WA on Test case' in result.stderr, result.stderr + result = check(checker, tmp_path, data, REFERENCE) + assert result.returncode == 7, result.stderr + assert 0 < float(re.search(r'Ratio: ([0-9.]+)', result.stderr).group(1)) <= 1