Skip to content

codingame_tools.contribution_manager.test_cases_dir

test_cases_dir

Reads/writes the tests/ subdirectory of a contribution working directory, converting between it and the flat list[CgTestCase] used by the CodinGame API.

The API just presents a flat list of individual test cases, distinguished only by is_test/is_validator. The CodinGame web app (and, informally, this contribution's own findContribution responses observed so far) presents them as local/validator pairs--but nothing in the API schema enforces that convention, titles are independent and not guaranteed-unique even within a well-paired contribution, and only each side's own relative order (not the interleaving) is actually meaningful.

To make editing easy while surviving all of that, tests are stored as:

tests/
    <ordinal>/
        <normalized-title>/
            test.json          # {"title": "<the real, unnormalized title>"}
            local/
                input.txt
                output.txt
            validator/
                input.txt
                output.txt

<ordinal> is a sort key, not a guaranteed-stable index--see import_test_cases (always writes clean zero-padded numbers) and commit_test_cases (agnostic to naming convention, via natural sort, so a user is free to rename/insert directories, e.g. "05a"). Within one ordinal, the local and validator test are co-located under one <normalized-title>/ directory if (and only if) they share the exact same title; otherwise each side gets its own named directory (auto-suffixed, e.g. "-2", only if their normalized slugs happen to collide despite different true titles--collisions are otherwise irrelevant across different ordinals).

commit_test_cases reconstructs the flat list by walking ordinals in sorted order and emitting the local test (if present) then the validator test (if present)--this may reorder relative to the original API response, but preserves each side's own relative order, which is what actually matters (see module docstring above).

TESTS_SUBDIR_NAME module-attribute

TESTS_SUBDIR_NAME = 'tests'

Name of the contribution working directory's test-cases subdirectory.

TEST_META_FILE_NAME module-attribute

TEST_META_FILE_NAME = 'test.json'

Name of the metadata file within each named test directory.

LOCAL_SUBDIR_NAME module-attribute

LOCAL_SUBDIR_NAME = 'local'

Name of the subdirectory holding a local (is_test=True) test case's input/output.

VALIDATOR_SUBDIR_NAME module-attribute

VALIDATOR_SUBDIR_NAME = 'validator'

Name of the subdirectory holding a server-side validator (is_validator=True) test case's input/output.

CgContributionTestCaseError

Bases: Exception

Raised by commit_test_cases when tests/'s on-disk layout is malformed in a way that can't be interpreted--e.g. more than one local (or validator) test case directory under a single ordinal directory. Only possible via manual editing; import_test_cases never produces a layout that triggers this.

CgTestCaseFileMeta dataclass

CgTestCaseFileMeta(title, extra_data=dict())

Bases: JSONWizardX

The content of a single test case directory's test.json: just the test's real, unnormalized title (the directory name itself is a lossy filename-friendly slug--see normalize_test_title).

title instance-attribute

title

The test case's real title, exactly as it appears in CgTestCase.title.

CgContributionLocalTestCase dataclass

CgContributionLocalTestCase(ordinal, side, title, input_file, output_file, input_text, output_text)

One (ordinal, side) test case as read back from tests_dir by list_local_test_cases-- the local-execution counterpart to commit_test_cases's flat list[CgTestCase] (which is shaped for submission to updateContribution, not for running locally: it drops the ordinal, and--unlike CgTestCase--this carries file paths, not just content, so a caller can overwrite output_file in place, e.g. to accept a solution's current output as the new expected baseline).

ordinal instance-attribute

ordinal

The ordinal directory name this test case lives under (e.g. "03")--not necessarily a clean integer (see the module docstring's "05a" example), but comparable via natural sort like everything else here.

side instance-attribute

side

Either "local" or "validator"--which of tests_dir's two subdirectories (see LOCAL_SUBDIR_NAME/VALIDATOR_SUBDIR_NAME) this test case came from.

title instance-attribute

title

The test case's real title (see CgTestCaseFileMeta.title).

input_file instance-attribute

input_file

Path to this test case's input.txt.

output_file instance-attribute

output_file

Path to this test case's output.txt--the expected/known-good output. A caller running this test case locally may overwrite this file with the solution's actual output to accept it as the new baseline (see codingame_tools.test_runner.debug_stdin's --update-expected, and CgContributionManager.run_local_test).

input_text instance-attribute

input_text

input_file's content as the server would hold it--decoded as UTF-8, then passed through file_to_server_text to drop the file's line terminator.

This is deliberately the server-side value rather than the file's raw bytes: it's what gets fed to a solution's stdin locally, and CodinGame feeds the stored string itself (confirmed by downloading published puzzles' test-case files, which are byte-identical to the contribution strings they came from). Feeding the raw file would silently give a local run one more byte of stdin than the same test gets on the server.

output_text instance-attribute

output_text

output_file's content as the server would hold it (see input_text), as of when this was read--stale the moment output_file is overwritten by an update, so re-list_local_test_cases after updating rather than reusing an old CgContributionLocalTestCase.

normalize_test_title

normalize_test_title(title)

Convert a test case title into a filename-friendly slug: runs of whitespace/punctuation become a single dash, and leading/trailing dashes are stripped. Falls back to "test" if nothing alphanumeric remains (e.g. a title that's pure punctuation).

Source code in codingame_tools/contribution_manager/test_cases_dir.py
103
104
105
106
107
108
def normalize_test_title(title: str) -> str:
    """Convert a test case title into a filename-friendly slug: runs of whitespace/punctuation
       become a single dash, and leading/trailing dashes are stripped. Falls back to "test" if
       nothing alphanumeric remains (e.g. a title that's pure punctuation)."""
    slug = _SLUG_INVALID_RUN_RE.sub("-", title).strip("-")
    return slug or "test"

import_test_cases

import_test_cases(test_cases, tests_dir)

(Re)build tests_dir from a flat list of test cases (as returned by findContribution), entirely replacing any existing content there. Local (is_test=True) and validator (is_validator=True) test cases are each numbered separately, in their existing relative order, and paired by that number into ordinal directories--see the module docstring.

Ordinal directories are always written as zero-padded numbers (width based on the total count, minimum 2 digits, e.g. "01"); commit_test_cases is agnostic to this and tolerates any renaming.

Source code in codingame_tools/contribution_manager/test_cases_dir.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def import_test_cases(test_cases: list[CgTestCase], tests_dir: Path) -> None:
    """(Re)build `tests_dir` from a flat list of test cases (as returned by `findContribution`),
       entirely replacing any existing content there. Local (`is_test=True`) and validator
       (`is_validator=True`) test cases are each numbered separately, in their existing relative
       order, and paired by that number into ordinal directories--see the module docstring.

       Ordinal directories are always written as zero-padded numbers (width based on the total
       count, minimum 2 digits, e.g. "01"); `commit_test_cases` is agnostic to this and tolerates
       any renaming.
    """
    locals_ = [tc for tc in test_cases if tc.is_test]
    validators = [tc for tc in test_cases if tc.is_validator]
    if tests_dir.exists():
        shutil.rmtree(tests_dir)
    count = max(len(locals_), len(validators))
    if count == 0:
        return
    width = max(2, len(str(count)))
    tests_dir.mkdir(parents=True, exist_ok=True)
    for i in range(count):
        local = locals_[i] if i < len(locals_) else None
        validator = validators[i] if i < len(validators) else None
        ordinal_dir = tests_dir / str(i + 1).zfill(width)
        _place_ordinal(ordinal_dir, local, validator)

commit_test_cases

commit_test_cases(tests_dir)

Read tests_dir back into a flat list[CgTestCase], in the order updateContribution should submit them: ordinal directories in natural-sort order, and within each, the local test case (if present) followed by the validator test case (if present)--see the module docstring for why this ordering is safe (each side's own relative order is preserved, even though the two may be interleaved differently than in the original API response).

Returns an empty list if tests_dir doesn't exist (no test cases yet).

Source code in codingame_tools/contribution_manager/test_cases_dir.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def commit_test_cases(tests_dir: Path) -> list[CgTestCase]:
    """Read `tests_dir` back into a flat `list[CgTestCase]`, in the order `updateContribution`
       should submit them: ordinal directories in natural-sort order, and within each, the local
       test case (if present) followed by the validator test case (if present)--see the module
       docstring for why this ordering is safe (each side's own relative order is preserved, even
       though the two may be interleaved differently than in the original API response).

       Returns an empty list if `tests_dir` doesn't exist (no test cases yet).
    """
    if not tests_dir.is_dir():
        return []
    ordinal_dirs = sorted(
            (d for d in tests_dir.iterdir() if d.is_dir()),
            key=lambda d: _natural_sort_key(d.name),
        )
    result: list[CgTestCase] = []
    for ordinal_dir in ordinal_dirs:
        local_entry: CgTestCase | None = None
        validator_entry: CgTestCase | None = None
        named_dirs = sorted((d for d in ordinal_dir.iterdir() if d.is_dir()), key=lambda d: d.name)
        for named_dir in named_dirs:
            title = _read_test_meta_title(named_dir)
            local_side = named_dir / LOCAL_SUBDIR_NAME
            if local_side.is_dir():
                if local_entry is not None:
                    raise CgContributionTestCaseError(
                            f"Multiple local test case directories found under {ordinal_dir}")
                local_entry = _read_test_side(local_side, title, is_test=True, is_validator=False)
            validator_side = named_dir / VALIDATOR_SUBDIR_NAME
            if validator_side.is_dir():
                if validator_entry is not None:
                    raise CgContributionTestCaseError(
                            f"Multiple validator test case directories found under {ordinal_dir}")
                validator_entry = _read_test_side(validator_side, title, is_test=False, is_validator=True)
        if local_entry is not None:
            result.append(local_entry)
        if validator_entry is not None:
            result.append(validator_entry)
    return result

renormalize_test_case_dirs

renormalize_test_case_dirs(tests_dir)

Rewrite tests_dir's ordinal directory names as a clean, sequential, zero-padded sort key (matching what import_test_cases would produce), preserving relative order via the same natural-sort comparator commit_test_cases uses. Named/local/validator subdirectory content is untouched--only the ordinal directories themselves are renamed. A no-op if tests_dir doesn't exist or is empty.

Source code in codingame_tools/contribution_manager/test_cases_dir.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
def renormalize_test_case_dirs(tests_dir: Path) -> None:
    """Rewrite `tests_dir`'s ordinal directory names as a clean, sequential, zero-padded sort key
       (matching what `import_test_cases` would produce), preserving relative order via the same
       natural-sort comparator `commit_test_cases` uses. Named/local/validator subdirectory
       content is untouched--only the ordinal directories themselves are renamed. A no-op if
       `tests_dir` doesn't exist or is empty.
    """
    if not tests_dir.is_dir():
        return
    ordinal_dirs = sorted(
            (d for d in tests_dir.iterdir() if d.is_dir()),
            key=lambda d: _natural_sort_key(d.name),
        )
    if not ordinal_dirs:
        return
    width = max(2, len(str(len(ordinal_dirs))))
    # Two-phase rename (via temporary names) so that renumbering never collides with another
    # ordinal directory's current (pre-renumbering) name.
    staged: list[tuple[Path, str]] = []
    for i, d in enumerate(ordinal_dirs):
        temp_path = tests_dir / f".renormalize-tmp-{i}"
        d.rename(temp_path)
        staged.append((temp_path, str(i + 1).zfill(width)))
    for temp_path, target_name in staged:
        temp_path.rename(tests_dir / target_name)

list_local_test_cases

list_local_test_cases(tests_dir)

Read tests_dir back into a flat list of individually-runnable local test cases--one per (ordinal, side) pair actually present on disk, in the same ordinal natural-sort order as commit_test_cases, local before validator within an ordinal. Unlike commit_test_cases, both sides of an ordinal are kept as separate entries here (rather than being merged into API submission order) since each is independently runnable, and file paths are included (rather than just content) since a caller may want to update output_file in place.

Returns:

Source code in codingame_tools/contribution_manager/test_cases_dir.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def list_local_test_cases(tests_dir: Path) -> list[CgContributionLocalTestCase]:
    """Read `tests_dir` back into a flat list of individually-runnable local test cases--one per
       (ordinal, side) pair actually present on disk, in the same ordinal natural-sort order as
       `commit_test_cases`, local before validator within an ordinal. Unlike `commit_test_cases`,
       both sides of an ordinal are kept as separate entries here (rather than being merged into
       API submission order) since each is independently runnable, and file paths are included
       (rather than just content) since a caller may want to update `output_file` in place.

    Returns:
        An empty list if `tests_dir` doesn't exist (no test cases yet).
    """
    if not tests_dir.is_dir():
        return []
    ordinal_dirs = sorted(
            (d for d in tests_dir.iterdir() if d.is_dir()),
            key=lambda d: _natural_sort_key(d.name),
        )
    result: list[CgContributionLocalTestCase] = []
    for ordinal_dir in ordinal_dirs:
        named_dirs = sorted((d for d in ordinal_dir.iterdir() if d.is_dir()), key=lambda d: d.name)
        for named_dir in named_dirs:
            title = _read_test_meta_title(named_dir)
            for side, subdir_name in (("local", LOCAL_SUBDIR_NAME), ("validator", VALIDATOR_SUBDIR_NAME)):
                side_dir = named_dir / subdir_name
                if not side_dir.is_dir():
                    continue
                input_file = side_dir / _INPUT_FILE_NAME
                output_file = side_dir / _OUTPUT_FILE_NAME
                result.append(CgContributionLocalTestCase(
                        ordinal=ordinal_dir.name, side=side, title=title,
                        input_file=input_file, output_file=output_file,
                        input_text=file_to_server_text(input_file.read_text(encoding="utf-8")),
                        output_text=file_to_server_text(output_file.read_text(encoding="utf-8")),
                    ))
    return result