Skip to content

codingame_tools.puzzle_manager.test_cases_dir

test_cases_dir

Downloads a puzzle's test case input/output files into the .meta/tests/ subdirectory of a puzzle working directory--server-derived reference data, not user-editable state (see codingame_tools.puzzle_manager.manager's module docstring for the .meta/ trust model), so it's gitignored and always fully regenerated by import_()/repair(), never read back or diffed.

Deliberately much simpler than codingame_tools.contribution_manager.test_cases_dir's tests/ layout, which this is visually modeled on: a contribution's test cases are a flat, unordered list[CgTestCase] split into independently-numbered local/validator sides that have to be paired up by convention. A puzzle's test cases (CgTestSessionTestCase) are simpler on both counts--already individually indexed by the server (.index, the same 1-based number CgPuzzleManager.play(test_index=...) takes), and there's only one side, not two--so each test case gets exactly one directory, named after its own index rather than a locally-assigned ordinal:

.meta/tests/
    <index>/
        <normalized-label>/
            test.json      # {"label": "<the real, unnormalized label>"}
            input.txt
            output.txt

Not shared with contribution_manager.test_cases_dir even though the shapes rhyme--see codingame_tools.puzzle_manager.layout's module docstring for why the two packages are kept fully independent.

TESTS_SUBDIR_NAME module-attribute

TESTS_SUBDIR_NAME = 'tests'

Name of the puzzle working directory's .meta/-relative test-cases subdirectory.

TEST_META_FILE_NAME module-attribute

TEST_META_FILE_NAME = 'test.json'

Name of the metadata file within each test case's own directory.

CgPuzzleTestCasesDownloadError

Bases: Exception

Raised by download_test_cases if the server returns duplicate test case indices for the same puzzle (never observed--the whole per-index-directory scheme in this module's docstring assumes indices are unique--but checked explicitly rather than silently letting one clobber another on disk).

CgPuzzleTestCaseMeta dataclass

CgPuzzleTestCaseMeta(label, extra_data=dict())

Bases: JSONWizardX

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

label instance-attribute

label

The test case's real label, exactly as it appears in CgTestSessionTestCase.label.

CgPuzzleDownloadedTestCase dataclass

CgPuzzleDownloadedTestCase(index, label, input_file, output_file, input_text, output_text)

One test case as read back from .meta/tests/ by list_downloaded_test_cases--the local, already-downloaded counterpart to CgTestSessionTestCase.

index instance-attribute

index

Server-assigned test index--see CgTestSessionTestCase.index.

label instance-attribute

label

The test's real, unnormalized label (from test.json, or a best-effort guess from the directory name if test.json is missing--see CgPuzzleTestCaseMeta).

input_file instance-attribute

input_file

Path to the test case's input.txt.

output_file instance-attribute

output_file

Path to the test case's output.txt (the expected output).

input_text instance-attribute

input_text

Content of input_file, decoded as UTF-8.

output_text instance-attribute

output_text

Content of output_file, decoded as UTF-8.

normalize_test_label

normalize_test_label(label)

Convert a test case label 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 label that's pure punctuation).

Source code in codingame_tools/puzzle_manager/test_cases_dir.py
84
85
86
87
88
89
def normalize_test_label(label: str) -> str:
    """Convert a test case label 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 label that's pure punctuation)."""
    slug = _SLUG_INVALID_RUN_RE.sub("-", label).strip("-")
    return slug or "test"

download_test_cases async

download_test_cases(client, test_cases, tests_dir)

(Re)build tests_dir (a puzzle working directory's .meta/tests/) from the puzzle's test cases, entirely replacing any existing content there--one fileservlet download per input_binary_id/output_binary_id, written as raw bytes (test data isn't guaranteed to be UTF-8 text, so no decoding is attempted).

Deliberately not passed through common.text_files, unlike a contribution's test cases. These are already files server-side, downloaded byte-for-byte, read-only, and never pushed anywhere--so the bytes on disk are the same bytes CodinGame feeds the solution's stdin remotely, and local runs get exact parity for free. A contribution's test cases need the conversion for the opposite reason: there, the server holds a string and the file is this client's rendering of it.

CodinGame's runner does not append a terminator, so appending one here "to tidy the file up" would hand a local run one more byte of stdin than the same test gets remotely. Confirmed live (2026-08-03) rather than assumed: a probe solution reading sys.stdin.buffer.read() on a community puzzle whose stored input is the single unterminated byte "7" reported bytes=1 repr=b'7'.

Which means an unterminated final line of input is real and solutions have to cope with it-- but note it is a community-contribution phenomenon, not a universal one. Official CodinGame puzzles' test files are properly terminated (all 12 of Temperatures' are); community puzzles' mostly aren't, because their authors typed them into textareas. Whatever the origin, the bytes here are the bytes the server uses.

Raises:

Source code in codingame_tools/puzzle_manager/test_cases_dir.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
async def download_test_cases(
            client: CgClient,
            test_cases: list[CgTestSessionTestCase],
            tests_dir: Path,
        ) -> None:
    """(Re)build `tests_dir` (a puzzle working directory's `.meta/tests/`) from the puzzle's test
       cases, entirely replacing any existing content there--one `fileservlet` download per
       `input_binary_id`/`output_binary_id`, written as raw bytes (test data isn't guaranteed to
       be UTF-8 text, so no decoding is attempted).

       Deliberately *not* passed through `common.text_files`, unlike a contribution's test cases.
       These are already files server-side, downloaded byte-for-byte, read-only, and never pushed
       anywhere--so the bytes on disk are the same bytes CodinGame feeds the solution's stdin
       remotely, and local runs get exact parity for free. A contribution's test cases need the
       conversion for the opposite reason: there, the server holds a *string* and the file is this
       client's rendering of it.

       **CodinGame's runner does not append a terminator**, so appending one here "to tidy the file
       up" would hand a local run one more byte of stdin than the same test gets remotely.
       Confirmed live (2026-08-03) rather than assumed: a probe solution reading
       `sys.stdin.buffer.read()` on a community puzzle whose stored input is the single unterminated
       byte `"7"` reported `bytes=1 repr=b'7'`.

       Which means an unterminated final line of input is real and solutions have to cope with it--
       but note it is a *community-contribution* phenomenon, not a universal one. Official CodinGame
       puzzles' test files are properly terminated (all 12 of Temperatures' are); community puzzles'
       mostly aren't, because their authors typed them into textareas. Whatever the origin, the
       bytes here are the bytes the server uses.

    Raises:
        CgPuzzleTestCasesDownloadError: if two test cases report the same `index` (unexpected;
                                         see the class docstring).
    """
    if tests_dir.exists():
        shutil.rmtree(tests_dir)
    if not test_cases:
        return
    width = max(2, len(str(max(tc.index for tc in test_cases))))
    used_indices: set[int] = set()
    for test_case in test_cases:
        if test_case.index in used_indices:
            raise CgPuzzleTestCasesDownloadError(f"Duplicate test case index {test_case.index}")
        used_indices.add(test_case.index)
        named_dir = tests_dir / str(test_case.index).zfill(width) / normalize_test_label(test_case.label)
        named_dir.mkdir(parents=True, exist_ok=True)
        CgPuzzleTestCaseMeta(label=test_case.label).save(named_dir / TEST_META_FILE_NAME)
        input_download = await client.servlets.file_servlet(test_case.input_binary_id)
        (named_dir / _INPUT_FILE_NAME).write_bytes(input_download.content)
        output_download = await client.servlets.file_servlet(test_case.output_binary_id)
        (named_dir / _OUTPUT_FILE_NAME).write_bytes(output_download.content)

list_downloaded_test_cases

list_downloaded_test_cases(tests_dir)

Read tests_dir (a puzzle working directory's .meta/tests/, as written by download_test_cases) back into a list of downloaded test cases, sorted by index.

Tolerant of hand edits the same way contribution_manager.test_cases_dir is: a missing test.json falls back to guessing the label from the directory name rather than failing.

Returns:

Raises:

  • CgPuzzleTestCasesDownloadError

    if an index directory doesn't contain exactly one named test directory (only possible via manual editing-- download_test_cases never produces that).

Source code in codingame_tools/puzzle_manager/test_cases_dir.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def list_downloaded_test_cases(tests_dir: Path) -> list[CgPuzzleDownloadedTestCase]:
    """Read `tests_dir` (a puzzle working directory's `.meta/tests/`, as written by
       `download_test_cases`) back into a list of downloaded test cases, sorted by index.

       Tolerant of hand edits the same way `contribution_manager.test_cases_dir` is: a missing
       `test.json` falls back to guessing the label from the directory name rather than failing.

    Returns:
        An empty list if `tests_dir` doesn't exist (no test cases downloaded yet).

    Raises:
        CgPuzzleTestCasesDownloadError: if an index directory doesn't contain exactly one named
                                         test directory (only possible via manual editing--
                                         `download_test_cases` never produces that).
    """
    if not tests_dir.is_dir():
        return []
    result: list[CgPuzzleDownloadedTestCase] = []
    for index_dir in sorted((d for d in tests_dir.iterdir() if d.is_dir()), key=lambda d: d.name):
        named_dirs = sorted(d for d in index_dir.iterdir() if d.is_dir())
        if len(named_dirs) != 1:
            raise CgPuzzleTestCasesDownloadError(
                    f"Expected exactly one named test directory under {index_dir}, found {len(named_dirs)}")
        named_dir = named_dirs[0]
        meta_file = named_dir / TEST_META_FILE_NAME
        label = CgPuzzleTestCaseMeta.load(meta_file).label if meta_file.is_file() \
            else named_dir.name.replace("-", " ")
        input_file = named_dir / _INPUT_FILE_NAME
        output_file = named_dir / _OUTPUT_FILE_NAME
        result.append(CgPuzzleDownloadedTestCase(
                index=int(index_dir.name),
                label=label,
                input_file=input_file,
                output_file=output_file,
                input_text=input_file.read_text(encoding="utf-8"),
                output_text=output_file.read_text(encoding="utf-8"),
            ))
    return result