Skip to content

Layout prop tests - #213

Merged
rorygraves merged 1 commit into
mainfrom
task/layout-prop-tests
Jun 15, 2026
Merged

Layout prop tests#213
rorygraves merged 1 commit into
mainfrom
task/layout-prop-tests

Conversation

@rorygraves

Copy link
Copy Markdown
Contributor

DECISIONS — layout-prop-tests (#142)

Test-only change. No production behaviour touched.

What changed

  • project/Dependencies.scala: added scalacheck 1.18.1 and the
    scalatest+scalacheck bridge scalacheck-1-18 3.2.19.0 (version tracks
    scalatest 3.2.19).
  • build.sbt: added both as % Test deps on termflowScreen only (the
    module that owns Layout). Matches the existing per-module test-dep style.
  • New modules/termflow-screen/src/test/scala/termflow/tui/LayoutPropSpec.scala:
    Gen[Layout] over the real DSL (Elem/Row/Column/Spacer/Fill/Zone/ Grid/Border), a custom Shrink[Layout] (replace container with a child,
    then drop one child) for minimal failing trees, and 5 properties
    (minSuccessful = 300).

Brief vs reality

The brief's invariant names referenced constructors that don't exist in the
v1 DSL (Pad/Flex/Sized/Clip/Scroll/Overlay). I read Layout.scala and
mapped each idea to a real, holding invariant rather than inventing API:

  • P1 measure total + non-negative (brief: "measure is total").
  • P2 resolve/resolveTo/resolveTracked never throw for any tree/budget
    (incl. zero/negative). (brief: totality)
  • P3 flow layouts never place a node before the origin — origin containment
    (brief: "bounds contained within requested bounds", scoped to where it
    actually holds; see below).
  • P4 sum of Fill children's main-axis sizes ≤ parent main-axis budget
    (brief: "sum of flex children's main-axis sizes ≤ parent main-axis").
    Tested with all-Fill Row/Column so every resolved BoxNode's main size
    is an observable flex allocation.
  • P5 Zone wrapping is transparent to measure and resolve — the
    wrapper-identity analogue of the brief's "nested Pad inverts" (Pad doesn't
    exist; Zone is the only pure pass-through wrapper).

Key finding: containment does NOT hold universally (P3 scope)

P3 initially asserted containment for any generated tree. ScalaCheck
shrank two counterexamples:

  1. Border right/bottom-edge pinning under a starving budget. Border
    places its right zone at leftX + budgetW - rightActualW. When the width
    budget is smaller than the right zone's natural width, this is negative
    relative to the origin
    (node drawn left of where it was placed).
  2. Grid spanning-cell starvation feeds (1) even with no top-level budget.
    A colSpan > 1 cell contributes 0 to per-column natural widths (Grid does
    no column compaction — explicitly documented out of scope), so it can be
    allotted width ≈ colGap. That starving budget flows into a nested
    Border and reproduces (1) — e.g. a TextNode resolved at x = -7 from
    origin 0.

Both stem from documented v1 limitations (Layout.scala: "Clipping / overflow
handling" and "automatic column-width compaction" are out of scope). So
asserting containment there is a test-side over-reach, not a resolver bug
I scoped P3 to the flow primitives (Row/Column/Fill/Zone/Spacer/Elem), which
only ever advance cursors forward and genuinely guarantee containment under
any budget. Did not weaken a property to hide a real bug, and did not park
NEEDS_HUMAN, because the behaviour is documented-out-of-scope, not a
regression.

Risk / follow-up: the Grid+Border combo emitting negative coordinates is
arguably worth a real fix (right-pin should clamp to >= leftX; spanning
cells could get a min width). Out of scope for this test-only task — flagged
here for a maintainer to triage. A reviewer who disagrees can promote P3 back
to all trees to make the failure reproduce.

Rejected

  • Custom Shrink via deprecated Stream — used Shrink.withLazyList
    (LazyList) instead to avoid deprecation warnings.
  • Mixing fixed + flex children in P4 — can't attribute resolved BoxNodes to
    the flex subset, so used all-Fill containers for a clean, honest check.

Add ScalaCheck + scalatest bridge to the termflow-screen test classpath
and a LayoutPropSpec with 5 invariants over arbitrary Layout trees:

- measure is total and non-negative
- resolve/resolveTo/resolveTracked never throw for any tree/budget
- flow layouts (Row/Column/Fill/Zone) never place a node before the origin
- sum of Fill children's main-axis sizes <= parent main-axis budget
- Zone wrapping is transparent to measure and resolve

Includes a Gen[Layout] over the real DSL and a custom Shrink[Layout] for
minimal failing trees. P3 is scoped to flow primitives: Grid (no column
compaction for spanning cells) and Border (right/bottom-edge pinning) can
emit coordinates before the origin under a starving budget, which is
documented out-of-scope overflow territory (see .multidev/DECISIONS.md).
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@rorygraves
rorygraves merged commit 52fb28f into main Jun 15, 2026
2 checks passed
@rorygraves
rorygraves deleted the task/layout-prop-tests branch June 15, 2026 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant