Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,15 @@ Entries land here as they merge.

### Tests

- Pinned the fail-loud guards on the new value types so a future refactor
cannot silently drop one: `PolygonNodeTest` (fewer than three points,
non-positive / `NaN` / ∞ box, defensive vertex-ring copy), `DocumentColorTest`
(`withOpacity` range + `NaN` rejection, boundary alpha rounding, `rgba`
alpha), `ShapeOutline.Path` cases in `ShapeOutlineTest` (segment-count /
`MoveTo`-first / null guards, defensive copy), and `PathBuilder` dashed-pattern
rejection plus the documented `build()`-snapshot contract. Extended
`PublicApiNoEngineLeakTest` to cover the new public `document.svg` package
(it is engine-clean today; the guard now keeps it that way).
- Chart geometry pinned without rendering: `NiceScaleTest` golden tables and
`ChartLayoutResolverTest` exact-position assertions on a font-independent
text-metrics fake; `ChartLayoutSnapshotTest` layout snapshots + a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,36 @@ void dashedFlowsThroughToTheNode() {
assertThat(node.dashPattern().segments()).containsExactly(4.0, 2.0);
}

@Test
void dashedRejectsAnEmptyOrNonPositivePattern() {
// dashed(double...) delegates to DocumentDashPattern.of, which throws
// eagerly at call time — pin both rejection paths.
assertThatThrownBy(() -> new PathBuilder().dashed())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("at least one segment");
assertThatThrownBy(() -> new PathBuilder().dashed(-1.0, 2.0))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("finite and strictly positive");
}

@Test
void buildSnapshotsTheSegmentsSoLaterMutationDoesNotLeakBack() {
// The documented build()-reuse contract: each build() copies the
// segments, so the builder may keep accumulating afterwards and the
// earlier node is unaffected.
PathBuilder builder = new PathBuilder()
.size(100, 40)
.moveTo(0.0, 0.5)
.lineTo(1.0, 0.5);

PathNode first = builder.build();
builder.lineTo(0.5, 1.0);
PathNode second = builder.build();

assertThat(first.segments()).hasSize(2);
assertThat(second.segments()).hasSize(3);
}

@Test
void svgBridgeAppendsParsedSegments() {
PathNode node = new PathBuilder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.demcha.compose.document.node;

import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.ShapePoint;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Value semantics of {@link PolygonNode}: the fail-loud guards the author
* wrote (fewer than three points, non-positive / non-finite box) and the
* defensive copy of the vertex ring. The only production callers pass valid
* ring data, so without these tests a future refactor could silently drop a
* guard with nothing going red — the {@link PathNode} sibling already pins
* the equivalent contract.
*/
class PolygonNodeTest {

private static final DocumentStroke STROKE = DocumentStroke.of(DocumentColor.rgb(20, 60, 120), 1.5);
private static final List<ShapePoint> TRIANGLE =
List.of(new ShapePoint(0, 0), new ShapePoint(1, 0), new ShapePoint(0.5, 1));

@Test
void validTriangleBuildsAndReportsItsKind() {
PolygonNode node = node(TRIANGLE);

assertThat(node.points()).hasSize(3);
assertThat(node.nodeKind()).isEqualTo("Polygon");
}

@Test
void fewerThanThreePointsIsRejected() {
assertThatThrownBy(() -> node(List.of(new ShapePoint(0, 0), new ShapePoint(1, 1))))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("at least three points");
}

@Test
void nonPositiveOrNonFiniteWidthIsRejected() {
assertThatThrownBy(() -> new PolygonNode("P", 0, 40, TRIANGLE, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("width must be finite and positive");
assertThatThrownBy(() -> new PolygonNode("P", Double.NaN, 40, TRIANGLE, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("width must be finite and positive");
assertThatThrownBy(() -> new PolygonNode("P", Double.POSITIVE_INFINITY, 40, TRIANGLE, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("width must be finite and positive");
}

@Test
void nonPositiveOrNonFiniteHeightIsRejected() {
assertThatThrownBy(() -> new PolygonNode("P", 40, 0, TRIANGLE, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("height must be finite and positive");
assertThatThrownBy(() -> new PolygonNode("P", 40, Double.NaN, TRIANGLE, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("height must be finite and positive");
}

@Test
void nullPointsIsRejected() {
assertThatThrownBy(() -> new PolygonNode("P", 40, 40, null, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero()))
.isInstanceOf(NullPointerException.class)
.hasMessageContaining("points");
}

@Test
void vertexRingIsCopyProtectedAndImmutable() {
List<ShapePoint> source = new ArrayList<>(TRIANGLE);
PolygonNode node = node(source);
source.add(new ShapePoint(0.25, 0.25));

assertThat(node.points()).hasSize(3);
assertThatThrownBy(() -> node.points().add(new ShapePoint(0.5, 0.5)))
.isInstanceOf(UnsupportedOperationException.class);
}

private static PolygonNode node(List<ShapePoint> points) {
return new PolygonNode("P", 120, 60, points, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.demcha.compose.document.style;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Value-level contract of {@link DocumentColor}'s translucency API: the
* explicit {@code withOpacity} guard and rounding, and the alpha channel
* carried by {@code rgba}. These are public {@code @since 1.8.0} contracts
* that otherwise had no direct unit test.
*/
class DocumentColorTest {

@Test
void withOpacityRejectsOutOfRangeAndNaN() {
assertThatThrownBy(() -> DocumentColor.WHITE.withOpacity(-0.1))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("opacity must be in [0,1]");
assertThatThrownBy(() -> DocumentColor.WHITE.withOpacity(1.1))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("opacity must be in [0,1]");
assertThatThrownBy(() -> DocumentColor.WHITE.withOpacity(Double.NaN))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("opacity must be in [0,1]");
}

@Test
void withOpacityMapsTheBoundariesToAlphaZeroAndFull() {
assertThat(DocumentColor.BLACK.withOpacity(0.0).color().getAlpha()).isEqualTo(0);
assertThat(DocumentColor.BLACK.withOpacity(1.0).color().getAlpha()).isEqualTo(255);
// 0.5 rounds to 128 (Math.round of 127.5).
assertThat(DocumentColor.BLACK.withOpacity(0.5).color().getAlpha()).isEqualTo(128);
}

@Test
void withOpacityKeepsTheRgbChannelsAndReturnsAFreshInstance() {
DocumentColor base = DocumentColor.rgb(10, 20, 30);
DocumentColor faded = base.withOpacity(0.5);

assertThat(faded).isNotSameAs(base);
assertThat(faded.color().getRed()).isEqualTo(10);
assertThat(faded.color().getGreen()).isEqualTo(20);
assertThat(faded.color().getBlue()).isEqualTo(30);
// The source is unchanged (immutable value type).
assertThat(base.color().getAlpha()).isEqualTo(255);
}

@Test
void rgbaCarriesTheAlphaChannel() {
DocumentColor color = DocumentColor.rgba(10, 20, 30, 128);

assertThat(color.color().getAlpha()).isEqualTo(128);
assertThat(color.color().getRed()).isEqualTo(10);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,49 @@ void checkmarkAndArrowStyleOverloadsRejectNullStyle() {
assertThatThrownBy(() -> ShapeOutline.arrow(10, 10, ShapeOutline.Direction.RIGHT, null))
.isInstanceOf(NullPointerException.class);
}

@Test
void pathOutlineKeepsItsSizeAndSegments() {
ShapeOutline.Path outline = ShapeOutline.path(40, 20, List.of(
DocumentPathSegment.moveTo(0, 0),
DocumentPathSegment.lineTo(1, 0),
DocumentPathSegment.lineTo(0.5, 1),
DocumentPathSegment.close()));

assertThat(outline.width()).isEqualTo(40.0, within(EPS));
assertThat(outline.height()).isEqualTo(20.0, within(EPS));
assertThat(outline.segments()).hasSize(4);
}

@Test
void pathOutlineRejectsTooFewSegmentsAndNonMoveToStart() {
assertThatThrownBy(() -> ShapeOutline.path(10, 10,
List.of(DocumentPathSegment.moveTo(0, 0))))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("at least a MoveTo");
assertThatThrownBy(() -> ShapeOutline.path(10, 10,
List.of(DocumentPathSegment.lineTo(0, 0), DocumentPathSegment.lineTo(1, 1))))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("must start with a MoveTo");
}

@Test
void pathOutlineRejectsNullSegments() {
assertThatThrownBy(() -> ShapeOutline.path(10, 10, null))
.isInstanceOf(NullPointerException.class)
.hasMessageContaining("segments");
}

@Test
void pathOutlineCopiesItsSegmentsDefensively() {
List<DocumentPathSegment> mutable = new ArrayList<>(List.of(
DocumentPathSegment.moveTo(0, 0),
DocumentPathSegment.lineTo(1, 1)));
ShapeOutline.Path outline = ShapeOutline.path(10, 10, mutable);
mutable.clear();

assertThat(outline.segments()).hasSize(2);
assertThatThrownBy(() -> outline.segments().add(DocumentPathSegment.close()))
.isInstanceOf(UnsupportedOperationException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class PublicApiNoEngineLeakTest {
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/GraphCompose.java"),
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/api"),
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/chart"),
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/svg"),
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/dsl"),
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/node"),
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/document/style"),
Expand Down