|
| 1 | +"""CompressKit test and benchmark metadata module. |
| 2 | +
|
| 3 | +Single source of truth for algorithm registry, corpus registry, Range corpus cap, |
| 4 | +benchmark job registry, and report schema. Conformance, smoke, test-data, and |
| 5 | +benchmark scripts are thin adapters over this module. |
| 6 | +
|
| 7 | +Design decision: docs/adr/0001-validation-metadata-module-shape.md |
| 8 | +Range corpus cap decision: docs/adr/0003-range-coder-corpus-cap-policy.md |
| 9 | +
|
| 10 | +Consumers must assert the version they depend on: |
| 11 | +
|
| 12 | + import metadata |
| 13 | + assert metadata.METADATA_VERSION == "1.0" |
| 14 | +""" |
| 15 | + |
| 16 | +from __future__ import annotations |
| 17 | + |
| 18 | +from dataclasses import dataclass, field |
| 19 | +from pathlib import Path |
| 20 | + |
| 21 | +# --------------------------------------------------------------------------- |
| 22 | +# Version |
| 23 | +# --------------------------------------------------------------------------- |
| 24 | + |
| 25 | +METADATA_VERSION = "1.0" |
| 26 | + |
| 27 | +# --------------------------------------------------------------------------- |
| 28 | +# Paths |
| 29 | +# --------------------------------------------------------------------------- |
| 30 | + |
| 31 | +ROOT = Path(__file__).resolve().parent.parent |
| 32 | +TESTS_DIR = ROOT / "tests" |
| 33 | +DATA_DIR = TESTS_DIR / "data" |
| 34 | +SCRIPTS_DIR = ROOT / "scripts" |
| 35 | +REPORTS_DIR = ROOT / "reports" |
| 36 | +DOCS_DATA_DIR = ROOT / "docs" / ".vitepress" / "data" |
| 37 | + |
| 38 | +# --------------------------------------------------------------------------- |
| 39 | +# Algorithm and binary registry |
| 40 | +# --------------------------------------------------------------------------- |
| 41 | + |
| 42 | + |
| 43 | +@dataclass(frozen=True) |
| 44 | +class BinaryEntry: |
| 45 | + language: str |
| 46 | + path: Path |
| 47 | + |
| 48 | + |
| 49 | +@dataclass(frozen=True) |
| 50 | +class AlgorithmEntry: |
| 51 | + """One algorithm supported by CompressKit.""" |
| 52 | + |
| 53 | + name: str |
| 54 | + """Canonical lower-case algorithm name (e.g. 'huffman').""" |
| 55 | + |
| 56 | + extension: str |
| 57 | + """File extension used for encoded output (e.g. 'huf').""" |
| 58 | + |
| 59 | + binaries: tuple[BinaryEntry, ...] |
| 60 | + """Shipped binaries, in display order (cpp).""" |
| 61 | + |
| 62 | + benchmark_input: str |
| 63 | + """Corpus file name used as the benchmark input for this algorithm.""" |
| 64 | + |
| 65 | + benchmark_driver: Path |
| 66 | + """Path to the per-algorithm benchmark driver script.""" |
| 67 | + |
| 68 | + |
| 69 | +ALGORITHMS: tuple[AlgorithmEntry, ...] = ( |
| 70 | + AlgorithmEntry( |
| 71 | + name="huffman", |
| 72 | + extension="huf", |
| 73 | + binaries=( |
| 74 | + BinaryEntry("cpp", ROOT / "algorithms/huffman/cpp/huffman_cpp"), |
| 75 | + BinaryEntry("go", ROOT / "algorithms/huffman/go/huffman_go"), |
| 76 | + BinaryEntry("rust", ROOT / "algorithms/huffman/rust/huffman_rust"), |
| 77 | + ), |
| 78 | + benchmark_input="textlike_10MiB.bin", |
| 79 | + benchmark_driver=ROOT / "algorithms/huffman/benchmark/bench.py", |
| 80 | + ), |
| 81 | + AlgorithmEntry( |
| 82 | + name="arithmetic", |
| 83 | + extension="aenc", |
| 84 | + binaries=( |
| 85 | + BinaryEntry("cpp", ROOT / "algorithms/arithmetic/cpp/arithmetic_cpp"), |
| 86 | + BinaryEntry("go", ROOT / "algorithms/arithmetic/go/arithmetic_go"), |
| 87 | + BinaryEntry("rust", ROOT / "algorithms/arithmetic/rust/arithmetic_rust"), |
| 88 | + ), |
| 89 | + benchmark_input="textlike_10MiB.bin", |
| 90 | + benchmark_driver=ROOT / "algorithms/arithmetic/benchmark/bench.py", |
| 91 | + ), |
| 92 | + AlgorithmEntry( |
| 93 | + name="range", |
| 94 | + extension="rcnc", |
| 95 | + binaries=( |
| 96 | + BinaryEntry("cpp", ROOT / "algorithms/range/cpp/rangecoder_cpp"), |
| 97 | + BinaryEntry("go", ROOT / "algorithms/range/go/rangecoder_go"), |
| 98 | + BinaryEntry("rust", ROOT / "algorithms/range/rust/target/release/rangecoder"), |
| 99 | + ), |
| 100 | + benchmark_input="small_dictionary_like.bin", |
| 101 | + benchmark_driver=ROOT / "algorithms/range/benchmark/bench.py", |
| 102 | + ), |
| 103 | + AlgorithmEntry( |
| 104 | + name="rle", |
| 105 | + extension="rle", |
| 106 | + binaries=( |
| 107 | + BinaryEntry("cpp", ROOT / "algorithms/rle/cpp/rle_cpp"), |
| 108 | + BinaryEntry("go", ROOT / "algorithms/rle/go/rle_go"), |
| 109 | + BinaryEntry("rust", ROOT / "algorithms/rle/rust/rle_rust"), |
| 110 | + ), |
| 111 | + benchmark_input="repetitive_10MiB.bin", |
| 112 | + benchmark_driver=ROOT / "algorithms/rle/benchmark/bench.py", |
| 113 | + ), |
| 114 | +) |
| 115 | + |
| 116 | +ALGORITHM_ORDER: tuple[str, ...] = tuple(a.name for a in ALGORITHMS) |
| 117 | +LANGUAGE_ORDER: tuple[str, ...] = ("cpp",) |
| 118 | + |
| 119 | +# --------------------------------------------------------------------------- |
| 120 | +# Corpus registry |
| 121 | +# --------------------------------------------------------------------------- |
| 122 | + |
| 123 | + |
| 124 | +@dataclass(frozen=True) |
| 125 | +class CorpusEntry: |
| 126 | + """A test corpus file under tests/data/.""" |
| 127 | + |
| 128 | + name: str |
| 129 | + """File name (e.g. 'empty.bin').""" |
| 130 | + |
| 131 | + is_large: bool = False |
| 132 | + """If True, only included when --include-large is passed to conformance.""" |
| 133 | + |
| 134 | + |
| 135 | +CORPUS: tuple[CorpusEntry, ...] = ( |
| 136 | + CorpusEntry("empty.bin"), |
| 137 | + CorpusEntry("single_byte.bin"), |
| 138 | + CorpusEntry("alternating.bin"), |
| 139 | + CorpusEntry("small_dictionary_like.bin"), |
| 140 | + CorpusEntry("random_1MiB.bin", is_large=True), |
| 141 | + CorpusEntry("random_10MiB.bin", is_large=True), |
| 142 | + CorpusEntry("repetitive_10MiB.bin", is_large=True), |
| 143 | + CorpusEntry("textlike_10MiB.bin", is_large=True), |
| 144 | +) |
| 145 | + |
| 146 | +DEFAULT_CORPUS: tuple[str, ...] = tuple(c.name for c in CORPUS if not c.is_large) |
| 147 | +LARGE_CORPUS: tuple[str, ...] = tuple(c.name for c in CORPUS if c.is_large) |
| 148 | + |
| 149 | +# --------------------------------------------------------------------------- |
| 150 | +# Range corpus cap |
| 151 | +# |
| 152 | +# See docs/adr/0003-range-coder-corpus-cap-policy.md for the policy decision. |
| 153 | +# Do NOT change this value without an OpenSpec change. |
| 154 | +# --------------------------------------------------------------------------- |
| 155 | + |
| 156 | +RANGE_CORPUS_CAP_BYTES: int = 100 * 1024 # 100 KiB |
| 157 | +RANGE_CORPUS_CAP_REASON: str = "range_coder_corpus_cap_100_kib" |
| 158 | + |
| 159 | +# --------------------------------------------------------------------------- |
| 160 | +# Benchmark report schema |
| 161 | +# |
| 162 | +# Field names used in benchmark JSON output. Changing these breaks the docs |
| 163 | +# page that consumes benchmarks.json. |
| 164 | +# --------------------------------------------------------------------------- |
| 165 | + |
| 166 | +BENCHMARK_REPORT_FIELDS: tuple[str, ...] = ( |
| 167 | + "algorithm", |
| 168 | + "language", |
| 169 | + "dataset", |
| 170 | + "encodeTime", |
| 171 | + "decodeTime", |
| 172 | + "encodeSpeed", |
| 173 | + "decodeSpeed", |
| 174 | + "compressionRatio", |
| 175 | + "throughput", |
| 176 | +) |
| 177 | + |
| 178 | +# --------------------------------------------------------------------------- |
| 179 | +# Convenience accessors |
| 180 | +# --------------------------------------------------------------------------- |
| 181 | + |
| 182 | + |
| 183 | +def algorithm_by_name(name: str) -> AlgorithmEntry: |
| 184 | + """Return the AlgorithmEntry for *name*, raising KeyError if not found.""" |
| 185 | + for algo in ALGORITHMS: |
| 186 | + if algo.name == name: |
| 187 | + return algo |
| 188 | + raise KeyError(f"unknown algorithm: {name!r}") |
| 189 | + |
| 190 | + |
| 191 | +def all_binaries() -> list[tuple[str, str, Path]]: |
| 192 | + """Return [(algorithm_name, language, binary_path)] for all shipped binaries.""" |
| 193 | + return [ |
| 194 | + (algo.name, binary.language, binary.path) |
| 195 | + for algo in ALGORITHMS |
| 196 | + for binary in algo.binaries |
| 197 | + ] |
| 198 | + |
| 199 | + |
| 200 | +def corpus_files(include_large: bool = False) -> tuple[str, ...]: |
| 201 | + """Return corpus file names eligible for conformance testing.""" |
| 202 | + if include_large: |
| 203 | + return tuple(c.name for c in CORPUS) |
| 204 | + return DEFAULT_CORPUS |
0 commit comments