Stage 3 round 4 lock implementation: extend build_layout_css beyond
the horizontal-2-only dynamic path. Every layout_css return now
carries length-locked col-axis keys (widths_px, width_ratios,
dynamic_cols) matching the parsed css_areas grid (R rows, C cols),
so 2-D layouts (T / 2x2 in PR 2) and the unified
_compute_per_zone_geometry mapper can plug in without further
contract churn.
PR 1 scope:
- _parse_css_areas + _parse_fr_string + _compute_per_zone_geometry
(unified — 1-D and 2-D from the same code path)
- compute_zone_layout_cols (vertical-2 weight-only solver)
- _build_fr_default / _build_rows_dynamic / _build_cols_dynamic
(populate widths_px/heights_px on every return path)
- build_layout_css override branch keeps the warn-and-fallthrough
legacy for unsupported presets (PR 2 promotes to strict raise)
- retry gate in _attempt_zone_ratio_retry skips when dynamic_cols=True
or dynamic_rows=False, with explicit retry_skipped_reason
- Step 8 artifact gains zone_widths_px_planned /
zone_col_ratios_planned (top-level) + zone_width_px_planned /
zone_col_ratio_planned (per-zone)
- debug_zones width injection via _compute_per_zone_geometry
(replaces the legacy row-only zip)
Tests: tests/phase_z2/ — 47 new cases (parse / fr-string / cols solver /
per-zone geometry / build_layout_css contract / retry gate +
6 build_layout_css YAML fixtures + 3 retry_gate fixtures).
Verification: python -m pytest -q tests = 89 passed (was 42).
horizontal-2 grid CSS strings (areas/cols/rows) byte-identical to
legacy; only additive col-axis keys are introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
1.3 KiB
Python
50 lines
1.3 KiB
Python
"""IMP-09 PR 1 — _parse_fr_string tests.
|
|
|
|
Catalog presets only use `1fr` / `1fr 1fr` specs (verified
|
|
templates/phase_z2/layouts/layouts.yaml). The helper must reject
|
|
non-fr tokens and round to integer pixel sizes summing to `total`.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from src.phase_z2_pipeline import _parse_fr_string
|
|
|
|
|
|
def test_single_fr_returns_full_total():
|
|
assert _parse_fr_string("1fr", 585) == [585]
|
|
|
|
|
|
def test_two_equal_fr_splits_evenly():
|
|
result = _parse_fr_string("1fr 1fr", 1180)
|
|
assert result == [590, 590]
|
|
assert sum(result) == 1180
|
|
|
|
|
|
def test_unequal_fr_distributes_by_ratio():
|
|
result = _parse_fr_string("2fr 1fr", 300)
|
|
assert sum(result) == 300
|
|
assert result[0] > result[1]
|
|
|
|
|
|
def test_rounding_absorbed_by_last_track():
|
|
# 1fr 1fr 1fr / total=100 -> 33,33,33 + diff 1 absorbed by last.
|
|
result = _parse_fr_string("1fr 1fr 1fr", 100)
|
|
assert sum(result) == 100
|
|
assert result == [33, 33, 34]
|
|
|
|
|
|
def test_non_fr_token_raises():
|
|
with pytest.raises(ValueError, match="non-fr token"):
|
|
_parse_fr_string("200px 1fr", 1000)
|
|
|
|
|
|
def test_empty_spec_raises():
|
|
with pytest.raises(ValueError, match="empty spec"):
|
|
_parse_fr_string("", 1000)
|
|
|
|
|
|
def test_zero_fr_raises():
|
|
with pytest.raises(ValueError, match="total fr"):
|
|
_parse_fr_string("0fr 0fr", 1000)
|