From 8d591853af505f8dbb02e35f3fa1dbb522bbcc66 Mon Sep 17 00:00:00 2001 From: mariano Date: Thu, 30 Jul 2026 11:48:20 -0500 Subject: [PATCH 1/2] refactor(core,internal): one read-terminal normalization point + the law suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Item 3 — the eager read terminals (toList / toListIndexed / count / exists) now route through a single private visitFocuses normalization: a null root focuses nothing on every optic shape, a Lens (which includes every Iso-rooted telescope) has exactly one focus whose value may be null, an Affine has zero or one, and a composed traversal walks its visitor. The per-terminal Lens/Affine instanceof fast paths this replaces were each a hand-maintained copy of the null table — the two shipped divergences (#274's exists NPE, toListIndexed's [Indexed[0, null]] vs []) were both one terminal's copy drifting. read/find keep their fast paths: they pull a lazy head, and their shortcuts carry real dispatch savings on the codegen-holder hot path. toList now returns an unmodifiable list on every shape (previously mutable on the composed-traversal branch only). Item 1 — the laws become executable: - internal: FoldLaws.assertFoldLaws pins Fold's documented contract ("both primitives enumerate the same focuses in the same order" + short-circuit reporting) and OpticLawsTest applies it to every optic — all single-focus shapes (null and miss cases included), all four container traversals (null/empty included), eachOptional (null/empty/present), composed and filtered traversals. - core: ReadTerminalConsistencyTest asserts the cross-terminal laws (count == toList.size == toListIndexed.size; exists == count > 0; indexed mirrors toList positionally; find-present implies exists) over the full shape × input matrix — Iso roots, lens paths, traversals, affines, filters, and the split container form, each against null roots, null focuses, null containers, and empties. This suite would have caught both prior divergences before they shipped. Item 2, downsized honestly — the inspection proposed deleting firstHopName and deriving it from the trail, but the generated FieldOptics holder constants are bare Telescope.lens(Accessor) values whose stored name is load-bearing with an empty trail, and seeding the trail from lens() creates a Focus-vs-Traverse context knot only the caller can resolve (the codegen emitters append the correct node kind today). The field stays; the drift hazard is closed instead by a consistency pin: stored firstHopName must match the trail's first Focus/Traverse node (PropertyNames-normalized for bean paths) across record paths, container steps, filters, and fieldByName. --- .../github/eschizoid/telescope/Telescope.java | 78 ++++---- .../ReadTerminalConsistencyTest.java | 166 ++++++++++++++++++ .../telescope/internal/optics/FoldLaws.java | 51 ++++++ .../internal/optics/OpticLawsTest.java | 53 ++++++ 4 files changed, 310 insertions(+), 38 deletions(-) create mode 100644 core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java create mode 100644 internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java diff --git a/core/src/main/java/io/github/eschizoid/telescope/Telescope.java b/core/src/main/java/io/github/eschizoid/telescope/Telescope.java index f4d72d5f..55e26d2c 100644 --- a/core/src/main/java/io/github/eschizoid/telescope/Telescope.java +++ b/core/src/main/java/io/github/eschizoid/telescope/Telescope.java @@ -1729,14 +1729,34 @@ public ForwardMapper asForwardMapper(final Class sourceClass, final Cla *

See {@link #toListIndexed} to pair each value with its position. */ public List toList(final S source) { - if (optic instanceof final Lens lens) { - if (source == null) return List.of(); - return Collections.singletonList(lens.get(source)); - } + final var out = new ArrayList(); + visitFocuses(source, a -> { + out.add(a); + return true; + }); + return Collections.unmodifiableList(out); + } + + /** + * The single normalization point for the eager read terminals ({@code toList} / {@code + * toListIndexed} / {@code count} / {@code exists}): a null root focuses nothing, a {@link Lens} + * (which includes every Iso-rooted telescope — {@code Iso} IS-A {@code Lens}) has exactly one + * focus whose value may itself be null, an {@link Affine} has zero or one, and a composed + * traversal walks its visitor. Every terminal routing through here CANNOT disagree with its + * siblings — the divergences this replaces (a null root materializing as {@code [Indexed[0, + * null]]} on one terminal and {@code []} on another) were each terminal re-implementing this + * table by hand. {@code read} / {@code find} keep their dedicated fast paths: they pull a lazy + * head, not an eager fold, and their Lens/Affine shortcuts carry real dispatch savings on the + * codegen-holder hot path. + */ + private boolean visitFocuses(final S source, final Predicate visitor) { + if (source == null) return true; // a null root focuses nothing, on every optic shape + if (optic instanceof final Lens lens) return visitor.test(lens.get(source)); if (optic instanceof final Affine affine) { - return affine.getOption(source).map(List::of).orElseGet(List::of); + final var focus = affine.getOption(source); + return focus.isEmpty() || visitor.test(focus.get()); } - return optic.toList(source); + return optic.visitWhile(source, visitor); } /** @@ -1751,22 +1771,11 @@ public List toList(final S source) { * } */ public List> toListIndexed(final S source) { - // Mirror toList's normalization exactly: the two terminals must agree on every input. The - // Lens branch also catches Iso-rooted telescopes (Iso IS-A Lens), whose unguarded identity - // visit would otherwise materialize a null root as [Indexed[0, null]] while toList says []. - if (optic instanceof final Lens lens) { - if (source == null) return List.of(); - return Collections.singletonList(new Indexed<>(0, lens.get(source))); - } - if (optic instanceof final Affine affine) { - return affine - .getOption(source) - .map(a -> List.of(new Indexed<>(0, a))) - .orElseGet(List::of); - } final var out = new ArrayList>(); - final var i = new int[] { 0 }; - optic.forEach(source, a -> out.add(new Indexed<>(i[0]++, a))); + visitFocuses(source, a -> { + out.add(new Indexed<>(out.size(), a)); + return true; + }); return Collections.unmodifiableList(out); } @@ -1782,13 +1791,12 @@ public List> toListIndexed(final S source) { * nonzero (it stops at the first match). */ public long count(final S source) { - if (optic instanceof Lens) { - return source == null ? 0L : 1L; - } - if (optic instanceof final Affine affine) { - return affine.getOption(source).isPresent() ? 1L : 0L; - } - return optic.count(source); + final var c = new long[1]; + visitFocuses(source, a -> { + c[0]++; + return true; + }); + return c[0]; } /** @@ -1796,16 +1804,10 @@ public long count(final S source) { * #count}. */ public boolean exists(final S source) { - if (optic instanceof Lens) { - return source != null; - } - if (optic instanceof final Affine affine) { - return affine.getOption(source).isPresent(); - } - // A single false-returning visit stops at the first focus: visitWhile returns false iff at - // least one element was seen. Unlike Stream.findAny(), this tolerates a null focus (a null - // intermediate hop yields a one-element [null] traversal) instead of NPE-ing on Optional.of. - return !optic.visitWhile(source, a -> false); + // A single false-returning visit stops at the first focus: visitFocuses returns false iff at + // least one focus was seen — and, unlike Stream.findAny(), a null focus counts instead of + // NPE-ing on Optional.of. + return !visitFocuses(source, a -> false); } /** diff --git a/core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java b/core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java new file mode 100644 index 00000000..3c6f2933 --- /dev/null +++ b/core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java @@ -0,0 +1,166 @@ +package io.github.eschizoid.telescope; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.eschizoid.telescope.internal.pairing.PropertyNames; +import io.github.eschizoid.telescope.introspection.OpticNode; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * The suite that would have caught both prior read-terminal divergences before they shipped: every + * eager terminal must agree with every other on every telescope shape and every input shape. The + * laws, for any telescope {@code t} and source {@code s}: + * + *

+ */ +class ReadTerminalConsistencyTest { + + record Address(String city) {} + + record User(String name, Address address) {} + + record Team(String label, List users) {} + + private static void assertTerminalsAgree(final Telescope t, final S source) { + final var list = t.toList(source); + final var indexed = t.toListIndexed(source); + final var count = t.count(source); + final var exists = t.exists(source); + final var found = t.find(source); + + assertEquals(list.size(), count, "count must equal toList().size()"); + assertEquals(list.size(), indexed.size(), "toListIndexed must have toList's cardinality"); + assertEquals(exists, count > 0, "exists must equal count > 0"); + for (var i = 0; i < list.size(); i++) { + assertEquals(i, indexed.get(i).index(), "indexed positions are 0-based traversal order"); + assertEquals(list.get(i), indexed.get(i).value(), "indexed values mirror toList"); + } + if (found.isPresent()) { + assertTrue(exists, "a present find implies existence"); + assertEquals(list.get(0), found.get(), "find is the head of toList when present"); + } + } + + @Nested + @DisplayName("every telescope shape, every input shape") + class TheMatrix { + + @Test + @DisplayName("Iso root (Telescope.of) — null root, normal root") + void isoRoot() { + final var root = Telescope.of(User.class); + assertTerminalsAgree(root, null); + assertTerminalsAgree(root, new User("Ann", new Address("nyc"))); + } + + @Test + @DisplayName("Lens path — null root, normal, null focus, null intermediate") + void lensPath() { + final var name = Telescope.of(User.class).field(User::name); + assertTerminalsAgree(name, null); + assertTerminalsAgree(name, new User("Ann", null)); + assertTerminalsAgree(name, new User(null, null)); // null focus is still a focus + + final var city = Telescope.of(User.class).field(User::address).field(Address::city); + assertTerminalsAgree(city, new User("Ann", null)); // null intermediate propagates on reads + } + + @Test + @DisplayName("composed traversal — null root, empty container, null container, null elements") + void traversalPath() { + final var names = Telescope.of(Team.class).each(Team::users).field(User::name); + assertTerminalsAgree(names, null); + assertTerminalsAgree(names, new Team("t", List.of())); + assertTerminalsAgree(names, new Team("t", null)); + assertTerminalsAgree(names, new Team("t", List.of(new User(null, null), new User("Bo", null)))); + } + + @Test + @DisplayName("affine path (as / whenPresent) — hit, miss, null root") + void affinePath() { + record Box(Optional nick) {} + final var nick = Telescope.of(Box.class).whenPresent(Box::nick); + assertTerminalsAgree(nick, null); + assertTerminalsAgree(nick, new Box(Optional.of("n"))); + assertTerminalsAgree(nick, new Box(Optional.empty())); + assertTerminalsAgree(nick, new Box(null)); + } + + @Test + @DisplayName("filtered path — hit, miss, null root") + void filteredPath() { + final var longNames = Telescope.of(Team.class) + .each(Team::users) + .field(User::name) + .filter(n -> n != null && n.length() > 2); + assertTerminalsAgree(longNames, null); + assertTerminalsAgree(longNames, new Team("t", List.of(new User("Ann", null), new User("Bo", null)))); + assertTerminalsAgree(longNames, new Team("t", List.of(new User("Bo", null)))); + } + + @Test + @DisplayName("split container form — the typed steps agree too") + void splitContainerForm() { + final var names = Telescope.of(Team.class).list(Team::users).each().field(User::name); + assertTerminalsAgree(names, null); + assertTerminalsAgree(names, new Team("t", null)); + assertTerminalsAgree(names, new Team("t", List.of(new User("Ann", null)))); + } + } + + @Nested + @DisplayName("the stored first-hop name never drifts from the trail") + class FirstHopTrailAgreement { + + // firstHopName is stored per path (it survives because bare codegen-holder lenses have a name + // but no trail); this pin makes any skew between the stored value and the trail's first + // Focus/Traverse node a red test instead of a silent misroute. Bean paths store the raw getter + // name while the trail stores the property name — PropertyNames.property normalizes both. + private static void assertFirstHopMatchesTrail(final Telescope t) { + final var trailFirst = t + .explain() + .hops() + .stream() + .flatMap(h -> + h instanceof OpticNode.Focus f + ? Stream.of(f.path()) + : h instanceof OpticNode.Traverse tr + ? Stream.of(tr.path()) + : Stream.empty() + ) + .findFirst(); + trailFirst.ifPresent(expected -> + assertEquals(expected, PropertyNames.property(t.firstHopName()), "stored first hop must match the trail") + ); + } + + @Test + @DisplayName("record paths, container steps, filters, narrows, and bean paths all agree") + void allShapesAgree() { + assertFirstHopMatchesTrail(Telescope.of(User.class).field(User::name)); + assertFirstHopMatchesTrail(Telescope.of(User.class).field(User::address).field(Address::city)); + assertFirstHopMatchesTrail(Telescope.of(Team.class).each(Team::users).field(User::name)); + assertFirstHopMatchesTrail(Telescope.of(Team.class).list(Team::users).each().field(User::name)); + assertFirstHopMatchesTrail( + Telescope.of(Team.class) + .each(Team::users) + .filter(u -> true) + .field(User::name) + ); + assertFirstHopMatchesTrail(Telescope.of(Team.class).fieldByName("label")); + } + } +} diff --git a/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java new file mode 100644 index 00000000..2921a2af --- /dev/null +++ b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java @@ -0,0 +1,51 @@ +package io.github.eschizoid.telescope.internal.optics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; + +/** + * The executable form of {@link Fold}'s documented contract: "both primitives enumerate the same + * focuses in the same order." Apply it to every optic — a future override that lets {@code + * visitWhile} drift from {@code getAll} fails here instead of surfacing as a silent read-terminal + * divergence in the DSL. + */ +final class FoldLaws { + + private FoldLaws() {} + + /** + * Assert the fold laws for {@code fold} against one source: (1) {@code visitWhile} visits exactly + * the focuses {@code getAll} streams, in the same order; (2) a full visit reports completion; (3) + * a first-focus short-circuit stops after exactly one focus and reports the stop — or reports + * completion when there are no focuses at all. + */ + static void assertFoldLaws(final Fold fold, final S source) { + final var streamed = new ArrayList(); + fold.getAll(source).forEach(streamed::add); + + final var visited = new ArrayList(); + final var completed = fold.visitWhile(source, a -> { + visited.add(a); + return true; + }); + + assertEquals(streamed, visited, "visitWhile must enumerate exactly what getAll streams, in order"); + assertTrue(completed, "an all-true visit must report completion"); + + final var seen = new ArrayList(); + final var fullyVisited = fold.visitWhile(source, a -> { + seen.add(a); + return false; // stop at the first focus + }); + if (streamed.isEmpty()) { + assertTrue(fullyVisited, "no focuses: nothing to stop at, the visit completes"); + assertTrue(seen.isEmpty()); + } else { + assertEquals(1, seen.size(), "a false-returning visitor must stop after the first focus"); + assertEquals(streamed.get(0), seen.get(0), "the stopped-at focus is getAll's head"); + assertEquals(false, fullyVisited, "a short-circuited visit must report the stop"); + } + } +} diff --git a/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java index 39dcff98..9f4dd473 100644 --- a/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java +++ b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java @@ -233,6 +233,59 @@ void isoThenLensIsLens() { } } + @Nested + @DisplayName("fold laws hold for every optic (getAll/visitWhile lockstep)") + class FoldLawCoverage { + + record Wrapper(List items) {} + + @Test + @DisplayName("every single-focus optic obeys the fold laws") + void singleFocusOptics() { + FoldLaws.assertFoldLaws(userName, ALICE); // Lens, non-null + FoldLaws.assertFoldLaws(userName, null); // Lens, null source (focuses nothing) + FoldLaws.assertFoldLaws(userIso, ALICE); // Iso (one focus, always) + final Prism stringCase = Prism.downcast(String.class); + FoldLaws.assertFoldLaws(stringCase, "hit"); // Prism hit + FoldLaws.assertFoldLaws(stringCase, 42); // Prism miss (no focuses) + final Affine, String> head = Affine.of( + l -> l.isEmpty() ? Optional.empty() : Optional.of(l.get(0)), + (l, v) -> l + ); + FoldLaws.assertFoldLaws(head, List.of("a", "b")); // Affine present + FoldLaws.assertFoldLaws(head, List.of()); // Affine absent + } + + @Test + @DisplayName("every container traversal obeys the fold laws, including null and empty sources") + void containerTraversals() { + FoldLaws.assertFoldLaws(Traversals.eachList(), List.of("a", "b", "c")); + FoldLaws.assertFoldLaws(Traversals.eachList(), List.of()); + FoldLaws.assertFoldLaws(Traversals.eachList(), null); + FoldLaws.assertFoldLaws(Traversals.eachSet(), new java.util.LinkedHashSet<>(List.of("x", "y"))); + FoldLaws.assertFoldLaws(Traversals.eachSet(), null); + FoldLaws.assertFoldLaws(Traversals.eachMapValue(), java.util.Map.of("k", 1)); + FoldLaws.assertFoldLaws(Traversals.eachMapValue(), null); + FoldLaws.assertFoldLaws(Traversals., String>eachIterable(), List.of("a", "b")); + FoldLaws.assertFoldLaws(Traversals., String>eachIterable(), null); + FoldLaws.assertFoldLaws(Traversals.eachOptional(), Optional.of("v")); + FoldLaws.assertFoldLaws(Traversals.eachOptional(), Optional.empty()); + FoldLaws.assertFoldLaws(Traversals.eachOptional(), null); + } + + @Test + @DisplayName("composed and filtered traversals obey the fold laws") + void composedShapes() { + final Traversal items = Focus.>lens(Wrapper::items, (w, l) -> + new Wrapper(l) + ).then(Traversals.eachList()); + FoldLaws.assertFoldLaws(items, new Wrapper(List.of("a", "b", "c"))); + FoldLaws.assertFoldLaws(items, new Wrapper(List.of())); + FoldLaws.assertFoldLaws(items.filter(s -> s.compareTo("a") > 0), new Wrapper(List.of("a", "b", "c"))); + FoldLaws.assertFoldLaws(items.filter(s -> false), new Wrapper(List.of("a", "b"))); + } + } + @Nested @DisplayName("visitWhile enumerates the same focuses as getAll") class VisitWhileEquivalence { From c22bbc6aa49ff02644ffb0321593476ed305a212 Mon Sep 17 00:00:00 2001 From: mariano Date: Thu, 30 Jul 2026 17:59:57 -0500 Subject: [PATCH 2/2] =?UTF-8?q?test(core,internal):=20review=20polish=20?= =?UTF-8?q?=E2=80=94=20the=20Affine=20matrix=20cell,=20bean-path=20pin,=20?= =?UTF-8?q?honest=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review confirmed the normalization semantically equivalent by running the new suite against main's Telescope; the polish closes its findings: a genuine .as(...) Affine fixture (whenPresent composes into a Traversal, so the matrix's Affine branch was untested), a bean-path first-hop pin (the comment claimed bean normalization was covered; now it is), honest DisplayNames, assertFalse in FoldLaws, the Iso-null omission documented, two inline FQNs imported, and toList's unmodifiable guarantee stated in its javadoc. --- .../github/eschizoid/telescope/Telescope.java | 3 +- .../ReadTerminalConsistencyTest.java | 36 ++++++++++++++++++- .../telescope/internal/optics/FoldLaws.java | 3 +- .../internal/optics/OpticLawsTest.java | 9 +++-- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/io/github/eschizoid/telescope/Telescope.java b/core/src/main/java/io/github/eschizoid/telescope/Telescope.java index 55e26d2c..558dbca8 100644 --- a/core/src/main/java/io/github/eschizoid/telescope/Telescope.java +++ b/core/src/main/java/io/github/eschizoid/telescope/Telescope.java @@ -1726,7 +1726,8 @@ public ForwardMapper asForwardMapper(final Class sourceClass, final Cla * .toList(company); * } * - *

See {@link #toListIndexed} to pair each value with its position. + *

See {@link #toListIndexed} to pair each value with its position. The returned list is + * unmodifiable on every path shape. */ public List toList(final S source) { final var out = new ArrayList(); diff --git a/core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java b/core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java index 3c6f2933..60885f54 100644 --- a/core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java +++ b/core/src/test/java/io/github/eschizoid/telescope/ReadTerminalConsistencyTest.java @@ -34,6 +34,12 @@ record User(String name, Address address) {} record Team(String label, List users) {} + sealed interface Event permits Created, Updated {} + + record Created(String id) implements Event {} + + record Updated(String id, String diff) implements Event {} + private static void assertTerminalsAgree(final Telescope t, final S source) { final var list = t.toList(source); final var indexed = t.toListIndexed(source); @@ -99,6 +105,18 @@ record Box(Optional nick) {} assertTerminalsAgree(nick, new Box(null)); } + @Test + @DisplayName("genuine Affine path (.as narrow) — hit, miss, null root, null field") + void narrowPath() { + // whenPresent composes Lens.then(affine) into a Traversal; only .as(...) leaves a genuine + // Affine as the stored optic — this fixture is what exercises visitFocuses' Affine branch. + final var diff = Telescope.of(Event.class).as(Updated.class).field(Updated::diff); + assertTerminalsAgree(diff, null); + assertTerminalsAgree(diff, new Updated("e1", "d")); + assertTerminalsAgree(diff, new Created("e2")); + assertTerminalsAgree(diff, new Updated("e1", null)); + } + @Test @DisplayName("filtered path — hit, miss, null root") void filteredPath() { @@ -148,8 +166,9 @@ private static void assertFirstHopMatchesTrail(final Telescope t) { } @Test - @DisplayName("record paths, container steps, filters, narrows, and bean paths all agree") + @DisplayName("record paths, container steps, filters, bean paths, and fieldByName all agree") void allShapesAgree() { + assertFirstHopMatchesTrail(Telescope.ofBean(MutableUser.class).field(MutableUser::getName)); assertFirstHopMatchesTrail(Telescope.of(User.class).field(User::name)); assertFirstHopMatchesTrail(Telescope.of(User.class).field(User::address).field(Address::city)); assertFirstHopMatchesTrail(Telescope.of(Team.class).each(Team::users).field(User::name)); @@ -162,5 +181,20 @@ void allShapesAgree() { ); assertFirstHopMatchesTrail(Telescope.of(Team.class).fieldByName("label")); } + + public static class MutableUser { + + private String name; + + public MutableUser() {} + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + } } } diff --git a/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java index 2921a2af..ba781e12 100644 --- a/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java +++ b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/FoldLaws.java @@ -1,6 +1,7 @@ package io.github.eschizoid.telescope.internal.optics; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -45,7 +46,7 @@ static void assertFoldLaws(final Fold fold, final S source) { } else { assertEquals(1, seen.size(), "a false-returning visitor must stop after the first focus"); assertEquals(streamed.get(0), seen.get(0), "the stopped-at focus is getAll's head"); - assertEquals(false, fullyVisited, "a short-circuited visit must report the stop"); + assertFalse(fullyVisited, "a short-circuited visit must report the stop"); } } } diff --git a/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java index 9f4dd473..15da500e 100644 --- a/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java +++ b/internal/src/test/java/io/github/eschizoid/telescope/internal/optics/OpticLawsTest.java @@ -9,7 +9,9 @@ import io.github.eschizoid.telescope.internal.optics.collections.Traversals; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -244,7 +246,8 @@ record Wrapper(List items) {} void singleFocusOptics() { FoldLaws.assertFoldLaws(userName, ALICE); // Lens, non-null FoldLaws.assertFoldLaws(userName, null); // Lens, null source (focuses nothing) - FoldLaws.assertFoldLaws(userIso, ALICE); // Iso (one focus, always) + FoldLaws.assertFoldLaws(userIso, ALICE); // Iso — one focus, always; null source omitted: Iso.to(null) is deliberately + // unguarded final Prism stringCase = Prism.downcast(String.class); FoldLaws.assertFoldLaws(stringCase, "hit"); // Prism hit FoldLaws.assertFoldLaws(stringCase, 42); // Prism miss (no focuses) @@ -262,9 +265,9 @@ void containerTraversals() { FoldLaws.assertFoldLaws(Traversals.eachList(), List.of("a", "b", "c")); FoldLaws.assertFoldLaws(Traversals.eachList(), List.of()); FoldLaws.assertFoldLaws(Traversals.eachList(), null); - FoldLaws.assertFoldLaws(Traversals.eachSet(), new java.util.LinkedHashSet<>(List.of("x", "y"))); + FoldLaws.assertFoldLaws(Traversals.eachSet(), new LinkedHashSet<>(List.of("x", "y"))); FoldLaws.assertFoldLaws(Traversals.eachSet(), null); - FoldLaws.assertFoldLaws(Traversals.eachMapValue(), java.util.Map.of("k", 1)); + FoldLaws.assertFoldLaws(Traversals.eachMapValue(), Map.of("k", 1)); FoldLaws.assertFoldLaws(Traversals.eachMapValue(), null); FoldLaws.assertFoldLaws(Traversals., String>eachIterable(), List.of("a", "b")); FoldLaws.assertFoldLaws(Traversals., String>eachIterable(), null);