diff options
| author | Bobby <[email protected]> | 2026-07-17 14:43:56 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-17 14:43:56 +0530 |
| commit | d27e5f4145972990544975a033d03abdd3a5f596 (patch) | |
| tree | b0d069809b22ee040e3bd294b6b7a868d926fde8 /tests/library/algebra.test.py | |
| parent | 8bff9a0ec3c7b7207ecd994ace4c696b71fd84f5 (diff) | |
| parent | 5d71c2f3b7218895a10bf485ddd3a04df2c4f66e (diff) | |
| download | edify-main.tar.xz edify-main.zip | |
Multi-locale phone validator, expanded corpora, and all-validator round-trip + operator-algebra coverage (#288)HEADmain
Completes the remaining non-docs library items on the v1.0.0 milestone:
a genuinely multi-locale `phone` validator, real per-locale corpus
hardening, and an end-to-end coverage layer that exercises every shipped
validator and the operator algebra.
## Multi-locale phone (#154)
`phone` is rebuilt as a table-driven multi-locale model: an optional `+`
or `00` prefix, two to eight digit groups of one to four digits each,
single-character space/dot/dash separators, and any group optionally
parenthesised. It now accepts the display forms it previously missed —
leading and inline parenthesised area codes such as `(555) 123-4567` and
`+44 (0)20 7946 0958`, and variable national grouping from three-group
North American through five-group French — plus two-to-six-digit service
and short codes. Doubled separators, bare prefixes, and letters are
still rejected. The corpus grows from a seven-string smoke test to
twenty-one real numbers across eleven locales with ten adversarial
rejects.
## Postal locale coverage (#155)
Multi-locale postal codes are delivered by the existing `postal`
validator; this pins it down with a twenty-code corpus spanning North
America, Europe, Asia, and Oceania (including alphanumeric UK, Irish,
Canadian, and Dutch shapes) with ten adversarial rejects. `zip_code`
stays deliberately US-scoped, with `postal` as its locale-complete
counterpart.
## Validator snapshots (#191)
Every per-validator migration to a callable `Pattern` has landed, so
regenerating the built-in snapshot corpus is a no-op apart from
`phone`'s new emitted shape, which is refreshed here.
## End-to-end coverage of every construct (#258)
- Every registered library validator now round-trips through both dict
and JSON serialization with its emitted regex preserved — a property
test over the full validator set, alongside the existing compiled-source
invariant.
- The operator algebra (`+`, `|`, and `.use()`) is asserted identical to
its fluent-chain equivalent across every character-class constant, at
both the emitted-string and compiled-matcher level, so the two surfaces
can never drift.
- Hardening corpora expand beyond the smoke-test handful to `phone`,
`postal`, `semver`, `slug`, `hostname`, `port`, `isbn`, and `vin`, each
with real-world accepts and adversarial rejects.
Closes #154, Closes #155, Closes #191, Closes #258
Diffstat (limited to 'tests/library/algebra.test.py')
| -rw-r--r-- | tests/library/algebra.test.py | 77 |
1 files changed, 77 insertions, 0 deletions
diff --git a/tests/library/algebra.test.py b/tests/library/algebra.test.py new file mode 100644 index 0000000..ee4005d --- /dev/null +++ b/tests/library/algebra.test.py @@ -0,0 +1,77 @@ +"""Chain <-> operator-algebra equivalence: ``+``, ``|`` and ``.use()`` emit the fluent-chain regex. + +Every character-class module constant is driven through both surfaces and the two +emitted regex strings are asserted identical, so the operator algebra can never +drift from the fluent builder it stands in for. +""" + +import pytest + +from edify import ( + ALPHANUMERIC, + ANY_CHAR, + CARRIAGE_RETURN, + DIGIT, + LETTER, + LOWERCASE, + NEW_LINE, + NON_DIGIT, + NON_WHITESPACE, + NON_WORD, + NULL_BYTE, + TAB, + UPPERCASE, + WHITESPACE, + WORD, + Pattern, +) + +_CONSTANTS: dict[str, Pattern] = { + "ANY_CHAR": ANY_CHAR, + "WHITESPACE": WHITESPACE, + "NON_WHITESPACE": NON_WHITESPACE, + "DIGIT": DIGIT, + "NON_DIGIT": NON_DIGIT, + "WORD": WORD, + "NON_WORD": NON_WORD, + "NEW_LINE": NEW_LINE, + "CARRIAGE_RETURN": CARRIAGE_RETURN, + "TAB": TAB, + "NULL_BYTE": NULL_BYTE, + "LETTER": LETTER, + "UPPERCASE": UPPERCASE, + "LOWERCASE": LOWERCASE, + "ALPHANUMERIC": ALPHANUMERIC, +} +_IDS = list(_CONSTANTS) +_VALUES = list(_CONSTANTS.values()) + + [email protected]("constant", _VALUES, ids=_IDS) +def test_plus_matches_chain_subexpression(constant: Pattern): + operator_form = (constant + DIGIT).to_regex_string() + chain_form = Pattern().subexpression(constant).subexpression(DIGIT).to_regex_string() + assert operator_form == chain_form + + [email protected]("constant", _VALUES, ids=_IDS) +def test_or_matches_chain_any_of(constant: Pattern): + operator_form = (constant | DIGIT).to_regex_string() + chain_form = ( + Pattern().any_of().subexpression(constant).subexpression(DIGIT).end().to_regex_string() + ) + assert operator_form == chain_form + + [email protected]("constant", _VALUES, ids=_IDS) +def test_use_matches_chain_subexpression(constant: Pattern): + use_form = Pattern().use(constant).to_regex_string() + chain_form = Pattern().subexpression(constant).to_regex_string() + assert use_form == chain_form + + [email protected]("constant", _VALUES, ids=_IDS) +def test_operator_and_chain_compile_to_the_same_matcher(constant: Pattern): + operator_compiled = (constant + DIGIT).to_regex().source + chain_compiled = Pattern().subexpression(constant).subexpression(DIGIT).to_regex().source + assert operator_compiled == chain_compiled |
