diff options
Diffstat (limited to 'tests/builder')
| -rw-r--r-- | tests/builder/cache.test.py | 58 | ||||
| -rw-r--r-- | tests/builder/cold_warm.test.py | 69 | ||||
| -rw-r--r-- | tests/builder/flags.test.py | 31 | ||||
| -rw-r--r-- | tests/builder/lookbehind.test.py | 6 | ||||
| -rw-r--r-- | tests/builder/ordering.test.py (renamed from tests/builder/insertion_order.test.py) | 0 | ||||
| -rw-r--r-- | tests/builder/passthrough.test.py | 7 | ||||
| -rw-r--r-- | tests/builder/validation.test.py | 8 |
7 files changed, 84 insertions, 95 deletions
diff --git a/tests/builder/cache.test.py b/tests/builder/cache.test.py index 700a8e4..045e8e9 100644 --- a/tests/builder/cache.test.py +++ b/tests/builder/cache.test.py @@ -1,8 +1,33 @@ -"""Tests for the per-instance lazy compile cache.""" +"""Tests for the per-instance lazy compile cache — semantics and cold/warm ratio.""" + +import time from edify import Pattern, RegexBuilder +def _hex_number_builder(): + return ( + RegexBuilder() + .start_of_input() + .assert_ahead() + .any_of("0x", "0o", "0b") + .end() + .capture() + .any_of("0x", "0o", "0b") + .end() + .between(1, 16) + .any_of_chars("0123456789abcdefABCDEF") + .end_of_input() + ) + + +def _measure_call_wall_clock(action, iterations): + start = time.perf_counter() + for _ in range(iterations): + action() + return time.perf_counter() - start + + def test_two_no_kwargs_to_regex_calls_return_the_same_instance(): builder = RegexBuilder().one_or_more().digit() first = builder.to_regex() @@ -61,3 +86,34 @@ def test_chain_step_yields_a_fresh_cache_slot(): def test_pattern_also_caches_across_repeat_to_regex_calls(): pattern = Pattern().string("hi") assert pattern.to_regex() is pattern.to_regex() + + +def test_warm_to_regex_is_at_least_ten_times_cheaper_than_cold_to_regex(): + warm_iterations = 200 + cold_iterations = 200 + + cold_total = 0.0 + for _ in range(cold_iterations): + builder = _hex_number_builder() + cold_start = time.perf_counter() + builder.to_regex() + cold_total += time.perf_counter() - cold_start + + warm_builder = _hex_number_builder() + warm_builder.to_regex() + warm_total = _measure_call_wall_clock(warm_builder.to_regex, warm_iterations) + + average_cold = cold_total / cold_iterations + average_warm = warm_total / warm_iterations + + assert average_warm * 10 < average_cold, ( + f"cache ratio too weak: warm={average_warm * 1e6:.2f}µs, " + f"cold={average_cold * 1e6:.2f}µs — warm should be < cold / 10." + ) + + +def test_repeat_to_regex_returns_the_same_regex_instance(): + warm_builder = _hex_number_builder() + first = warm_builder.to_regex() + second = warm_builder.to_regex() + assert first is second diff --git a/tests/builder/cold_warm.test.py b/tests/builder/cold_warm.test.py deleted file mode 100644 index 8777f90..0000000 --- a/tests/builder/cold_warm.test.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Deterministic cold/warm ratio gate for the per-instance lazy compile cache. - -Measures the wall-clock cost of the first ``.to_regex()`` call on a fresh -builder (``cold``) against the cost of a repeat call on the same instance -(``warm``). The cache promise from :issue:`141` is that the warm path is -an order of magnitude cheaper than the cold path — a hardware-independent -ratio that gates the cache contract in a way absolute milliseconds cannot. -""" - -from __future__ import annotations - -import time - -from edify import RegexBuilder - - -def _hex_number_builder(): - builder = ( - RegexBuilder() - .start_of_input() - .assert_ahead() - .any_of("0x", "0o", "0b") - .end() - .capture() - .any_of("0x", "0o", "0b") - .end() - .between(1, 16) - .any_of_chars("0123456789abcdefABCDEF") - .end_of_input() - ) - return builder - - -def _measure_call_wall_clock(action, iterations): - start = time.perf_counter() - for _ in range(iterations): - action() - return time.perf_counter() - start - - -def test_warm_to_regex_is_at_least_ten_times_cheaper_than_cold_to_regex(): - warm_iterations = 200 - cold_iterations = 200 - - cold_total = 0.0 - for _ in range(cold_iterations): - builder = _hex_number_builder() - cold_start = time.perf_counter() - builder.to_regex() - cold_total += time.perf_counter() - cold_start - - warm_builder = _hex_number_builder() - warm_builder.to_regex() - warm_total = _measure_call_wall_clock(warm_builder.to_regex, warm_iterations) - - average_cold = cold_total / cold_iterations - average_warm = warm_total / warm_iterations - - assert average_warm * 10 < average_cold, ( - f"cache ratio too weak: warm={average_warm * 1e6:.2f}µs, " - f"cold={average_cold * 1e6:.2f}µs — warm should be < cold / 10." - ) - - -def test_repeat_to_regex_returns_the_same_regex_instance(): - warm_builder = _hex_number_builder() - first = warm_builder.to_regex() - second = warm_builder.to_regex() - assert first is second diff --git a/tests/builder/flags.test.py b/tests/builder/flags.test.py index 02e875e..704065f 100644 --- a/tests/builder/flags.test.py +++ b/tests/builder/flags.test.py @@ -4,6 +4,7 @@ import re import sys import pytest +import regex as regex_module from edify import Pattern, RegexBuilder @@ -72,38 +73,32 @@ def test_multiple_kwargs_combine(): assert compiled.compiled.flags & re.S == re.S -def _regex_module(): - import regex - - return regex - - def test_regex_engine_ignore_case_kwarg_enables_the_flag(): - regex = _regex_module() + regex = regex_module compiled = RegexBuilder().string("hi").to_regex(engine="regex", ignore_case=True) assert compiled.compiled.flags & regex.I == regex.I def test_regex_engine_multiline_kwarg_enables_the_flag(): - regex = _regex_module() + regex = regex_module compiled = RegexBuilder().string("hi").to_regex(engine="regex", multiline=True) assert compiled.compiled.flags & regex.M == regex.M def test_regex_engine_dotall_kwarg_enables_the_flag(): - regex = _regex_module() + regex = regex_module compiled = RegexBuilder().string("hi").to_regex(engine="regex", dotall=True) assert compiled.compiled.flags & regex.S == regex.S def test_regex_engine_ascii_only_kwarg_enables_the_flag(): - regex = _regex_module() + regex = regex_module compiled = RegexBuilder().string("hi").to_regex(engine="regex", ascii_only=True) assert compiled.compiled.flags & regex.A == regex.A def test_regex_engine_verbose_kwarg_enables_the_flag(): - regex = _regex_module() + regex = regex_module compiled = RegexBuilder().string("hi").to_regex(engine="regex", verbose=True) assert compiled.compiled.flags & regex.X == regex.X @@ -115,3 +110,17 @@ def test_regex_engine_verbose_kwarg_enables_the_flag(): def test_regex_engine_debug_kwarg_compiles_without_error(): compiled = RegexBuilder().string("hi").to_regex(engine="regex", debug=True) assert compiled.source == "hi" + + +def test_regex_engine_debug_flag_is_forwarded_to_regex_module_bitmask(monkeypatch): + captured_flags: list[int] = [] + original_compile = regex_module.compile + + def capturing_compile(pattern, flags=0): + captured_flags.append(flags) + return original_compile(pattern) + + monkeypatch.setattr(regex_module, "compile", capturing_compile) + RegexBuilder().string("hi").to_regex(engine="regex", debug=True) + assert captured_flags + assert captured_flags[0] & regex_module.DEBUG == regex_module.DEBUG diff --git a/tests/builder/lookbehind.test.py b/tests/builder/lookbehind.test.py index f16a6cc..5e101f3 100644 --- a/tests/builder/lookbehind.test.py +++ b/tests/builder/lookbehind.test.py @@ -1,5 +1,7 @@ """Tests for lookbehind width behavior across the ``re`` and ``regex`` engines.""" +import re + import pytest from edify import RegexBuilder @@ -42,14 +44,10 @@ def test_fixed_width_lookbehind_still_works_under_re(): def test_variable_width_lookbehind_error_chains_the_underlying_pattern_error(): with pytest.raises(VariableWidthLookbehindNotSupportedError) as excinfo: _variable_width_lookbehind_builder().to_regex(engine="re") - import re - assert isinstance(excinfo.value.__cause__, re.error) def test_re_engine_still_surfaces_other_pattern_errors_unchanged(monkeypatch): - import re - def raise_other_error(_pattern, flags=0): raise re.error("some other syntax error") diff --git a/tests/builder/insertion_order.test.py b/tests/builder/ordering.test.py index 4feae65..4feae65 100644 --- a/tests/builder/insertion_order.test.py +++ b/tests/builder/ordering.test.py diff --git a/tests/builder/passthrough.test.py b/tests/builder/passthrough.test.py index a2e77b5..e149052 100644 --- a/tests/builder/passthrough.test.py +++ b/tests/builder/passthrough.test.py @@ -1,6 +1,9 @@ """Tests for the pass-through branches when subexpression anchors merge without conflict.""" +import pytest + from edify import RegexBuilder +from edify.errors.structure import CannotCallSubexpressionError def test_start_of_input_merges_into_parent_without_existing_start(): @@ -18,10 +21,6 @@ def test_end_of_input_merges_into_parent_without_existing_end(): def test_subexpression_called_with_unfinished_expression_raises(): - import pytest - - from edify.errors.structure import CannotCallSubexpressionError - unfinished_sub = RegexBuilder().capture().digit() parent = RegexBuilder() with pytest.raises(CannotCallSubexpressionError): diff --git a/tests/builder/validation.test.py b/tests/builder/validation.test.py index 53128c5..a1b57d7 100644 --- a/tests/builder/validation.test.py +++ b/tests/builder/validation.test.py @@ -14,6 +14,7 @@ from edify.errors.anchors import ( ) from edify.errors.input import ( MustBeAStringError, + MustBeInstanceError, MustBeIntegerGreaterThanZeroError, MustBeLessThanError, MustBeOneCharacterError, @@ -22,6 +23,7 @@ from edify.errors.input import ( MustHaveASmallerValueError, ) from edify.errors.naming import NamedGroupDoesNotExistError +from edify.errors.structure import CannotCallSubexpressionError def test_start_of_input_twice_raises(): @@ -140,23 +142,17 @@ def test_named_back_reference_undeclared_raises(): def test_to_regex_string_with_open_frame_raises(): - from edify.errors.structure import CannotCallSubexpressionError - unfinished = RegexBuilder().capture().digit() with pytest.raises(CannotCallSubexpressionError): unfinished.to_regex_string() def test_to_regex_with_open_frame_raises(): - from edify.errors.structure import CannotCallSubexpressionError - unfinished = RegexBuilder().capture().digit() with pytest.raises(CannotCallSubexpressionError): unfinished.to_regex() def test_subexpression_non_builder_raises(): - from edify.errors.input import MustBeInstanceError - with pytest.raises(MustBeInstanceError): RegexBuilder().subexpression("not a builder") |
