-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
ezicode takes testing seriously because Unicode is the kind of domain where being wrong is detectable down to the codepoint. Every algorithm that has an official conformance test corpus published by the Unicode Consortium is verified against the full corpus on every build. Several algorithms also have hostile adversarial inputs beyond the standard's own tests.
This page documents what's tested, how it's tested, and how to run the suite.
- 486 test blocks as of writing
- ~9,200 lines of test code across inline tests in source files and dedicated test files
-
~26 seconds to run the full suite in
ReleaseSafemode - Six UCD conformance corpora validated row-by-row on every build
- Every-codepoint walks for properties, scripts, normalization quick-check, and others
The test code is about 46% of the hand-written codebase by lines. That ratio is in the normal range for a library with this much conformance work, and substantially less than rigorous test-heavy projects like SQLite. Each test block is short because most of the assertions live inside while (cp <= 0x10FFFF) loops, which produce millions of effective comparisons per test block.
These six files from the official Unicode test suite are checked row-by-row on every test run:
| Corpus | Algorithm | Rows |
|---|---|---|
| GraphemeBreakTest.txt | UAX #29 grapheme clusters | ~600 |
| WordBreakTest.txt | UAX #29 words | ~1,800 |
| SentenceBreakTest.txt | UAX #29 sentences | ~500 |
| LineBreakTest.txt | UAX #14 line breaks | ~7,500 |
| NormalizationTest.txt | UAX #15 normalization | ~18,000 |
| ScriptExtensions.txt | UAX #24 script extensions | every codepoint |
The tests parse each file at test time, drive each row through ezicode, and compare the result against the expected value. If a single row fails, the test fails with the exact codepoint sequence and the diff.
NormalizationTest.txt is the largest. Each row produces eight assertions (NFC/NFD/NFKC/NFKD × batch/streaming), so the file contributes ~144,000 effective comparisons in one test block.
For properties that should be defined over the entire codespace, the tests walk every codepoint from U+0000 to U+10FFFF and check:
- Property lookups return values within the expected enum domain.
- The output of derived-property functions matches the UCD-provided derived data file directly.
- Round-trip properties (decompose then recompose) produce the original codepoint, for every codepoint in the recomposable subset.
- Hangul syllables compose and decompose correctly across the full Hangul syllable range.
- Encoding (UTF-8 / UTF-16 / UTF-32) round-trips every valid scalar.
Each of these walks is 1,114,112 iterations per test, with one or more assertions per iteration. The test count of 486 understates the effective assertion count by orders of magnitude; the actual number of equality checks per test run is in the tens of millions.
The standard's tests cover correct behavior on well-formed input. They don't cover hostile input by design — that's not what conformance is for. ezicode adds a separate adversarial test layer:
Zalgo combining-mark stress — six inputs at increasing severity:
- light: 25 combining marks across 5 base characters (Stream-Safe)
- medium: 84 marks across 8 bases (Stream-Safe)
- heavy: 210 marks across 7 bases (at the Stream-Safe boundary)
- savage: 744 marks across 14 bases (decisively non-Stream-Safe)
- ascii-punct: 120 marks across punctuation characters (tests grapheme rules for non-letter bases)
- single: 150 marks on a single base character (worst-case single-run input)
These exercise canonical reordering, grapheme cluster collapse, and the Stream-Safe boundary handling in the streaming Normalizer.
Malformed UTF-8 — inputs with overlong sequences, lone continuation bytes, truncated sequences, surrogates encoded as UTF-8. The decoder is expected to either reject (strict mode) or substitute U+FFFD (lossy mode) per WHATWG spec.
Empty and minimum-sized inputs — empty strings, single-codepoint strings, single-byte strings, single-grapheme strings, and so on.
Boundary codepoints — U+0000, U+007F, U+0080, U+07FF, U+0800, U+FFFF, U+10000, U+10FFFF. These tend to surface off-by-one bugs in encoding boundaries.
# full conformance: every UCD corpus, every-codepoint walks, adversarial inputs
zig build test
# release-mode tests with safety checks on
zig build test -Doptimize=ReleaseSafe
# verbose summary
zig build test --summary all
A typical run in ReleaseSafe looks like:
Build Summary: 11/11 steps succeeded; 486/486 tests passed
test success
├─ run test 1 pass (1 total) 6ms
├─ run test 56 pass (56 total) 15ms
├─ run test 141 pass (141 total) 20ms
├─ run test 26 pass (26 total) 4ms
└─ run test 262 pass (262 total) 26s
The 262-test bucket is the heavy one; it includes the every-codepoint walks and full NormalizationTest.txt sweeps. Most of the time goes there.
Honesty about gaps:
- No fuzzing in CI as of this writing. A simple fuzzer harness exists but is not yet wired into continuous testing. Adversarial fuzzing for the encoding layer specifically is a planned addition.
-
No memory leak testing across the full suite. Individual normalization tests verify allocator behavior, but a coverage sweep against
std.testing.allocatorfor every allocating function is on the to-do list. - No multithreaded stress testing. ezicode's tables are immutable and the iterators don't share state, so thread safety should hold by construction. It hasn't been adversarially tested.
- No long-running soak tests. The suite is bounded; no test runs longer than 30 seconds. Long-soak runs over hours of input are not part of CI.
For a new corner case you've found:
test "<descriptive name>" {
const input = "..."; // the problematic input
const expected = ...; // what the library should produce
const actual = try myFunction(input);
try std.testing.expectEqual(expected, actual);
}
Inline tests live next to the code they test, in the same .zig file. Conformance tests against external corpora live in dedicated test files under src/ and tests/. The convention is: spot tests inline; corpus-driven tests in dedicated files.