Compare commits
2 Commits
7a6111b75e
...
651cc4459e
| Author | SHA1 | Date | |
|---|---|---|---|
| 651cc4459e | |||
| ba37b199de |
30
.gitea/workflows/ci.yaml
Normal file
30
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK 16
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '16'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Build and test with coverage
|
||||||
|
run: mvn verify --batch-mode
|
||||||
|
|
||||||
|
- name: Upload coverage report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: jacoco-report
|
||||||
|
path: target/site/jacoco/
|
||||||
34
pom.xml
34
pom.xml
@@ -25,6 +25,40 @@
|
|||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<version>3.2.5</version>
|
<version>3.2.5</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.11</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
<goal>report</goal>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<!-- UI classes require a display and cannot be unit-tested -->
|
||||||
|
<exclude>ovh/gasser/newshapes/App.class</exclude>
|
||||||
|
<exclude>ovh/gasser/newshapes/ui/**</exclude>
|
||||||
|
<exclude>ovh/gasser/newshapes/Selection.class</exclude>
|
||||||
|
</excludes>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>LINE</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>0.50</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ public class HTMLDraftman implements ShapeVisitor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void visitTriangle(STriangle sTriangle) {
|
public void visitTriangle(STriangle sTriangle) {
|
||||||
htmlOutput.printf("<div class=\"triangle%d\"></div>\n", this.hashCode());
|
htmlOutput.printf("<div class=\"triangle%d\"></div>\n", sTriangle.hashCode());
|
||||||
var bounds = sTriangle.getBounds();
|
var bounds = sTriangle.getBounds();
|
||||||
ColorAttributes colAttrs = (ColorAttributes) sTriangle.getAttributes(ColorAttributes.ID);
|
ColorAttributes colAttrs = (ColorAttributes) sTriangle.getAttributes(ColorAttributes.ID);
|
||||||
String colorString = formatCSSColor(colAttrs.filledColor);
|
String colorString = formatCSSColor(colAttrs.filledColor);
|
||||||
StringJoiner joiner = new StringJoiner("\n", ".triangle%d{\n".formatted(this.hashCode()), "\n}\n");
|
StringJoiner joiner = new StringJoiner("\n", ".triangle%d{\n".formatted(sTriangle.hashCode()), "\n}\n");
|
||||||
joiner.add(" position: absolute;");
|
joiner.add(" position: absolute;");
|
||||||
joiner.add(" top: %dpx;".formatted(bounds.y));
|
joiner.add(" top: %dpx;".formatted(bounds.y));
|
||||||
joiner.add(" left: %dpx;".formatted(bounds.x));
|
joiner.add(" left: %dpx;".formatted(bounds.x));
|
||||||
|
|||||||
144
src/test/java/ovh/gasser/newshapes/SelectionTest.java
Normal file
144
src/test/java/ovh/gasser/newshapes/SelectionTest.java
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package ovh.gasser.newshapes;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||||
|
import ovh.gasser.newshapes.shapes.SCircle;
|
||||||
|
import ovh.gasser.newshapes.shapes.SRectangle;
|
||||||
|
import ovh.gasser.newshapes.shapes.STriangle;
|
||||||
|
import ovh.gasser.newshapes.ui.listeners.SelectionListener;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class SelectionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsEmptyInitially() {
|
||||||
|
Selection selection = new Selection();
|
||||||
|
assertTrue(selection.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAdd() {
|
||||||
|
Selection selection = new Selection();
|
||||||
|
SCircle circle = SCircle.create(0, 0, 50);
|
||||||
|
|
||||||
|
selection.add(circle);
|
||||||
|
|
||||||
|
assertFalse(selection.isEmpty());
|
||||||
|
assertEquals(1, selection.getSelectedShapes().size());
|
||||||
|
assertTrue(selection.getSelectedShapes().contains(circle));
|
||||||
|
|
||||||
|
// Verify shape is marked as selected
|
||||||
|
SelectionAttributes attrs = (SelectionAttributes) circle.getAttributes(SelectionAttributes.ID);
|
||||||
|
assertNotNull(attrs);
|
||||||
|
assertTrue(attrs.selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddAll() {
|
||||||
|
Selection selection = new Selection();
|
||||||
|
SCircle circle1 = SCircle.create(0, 0, 50);
|
||||||
|
SRectangle rect = SRectangle.create(10, 10, 100, 50);
|
||||||
|
STriangle triangle = STriangle.create(50, 50, 30, java.awt.Color.BLACK, java.awt.Color.BLACK);
|
||||||
|
|
||||||
|
List<ovh.gasser.newshapes.shapes.Shape> shapes = Arrays.asList(circle1, rect, triangle);
|
||||||
|
selection.addAll(shapes);
|
||||||
|
|
||||||
|
assertEquals(3, selection.getSelectedShapes().size());
|
||||||
|
assertTrue(selection.getSelectedShapes().contains(circle1));
|
||||||
|
assertTrue(selection.getSelectedShapes().contains(rect));
|
||||||
|
assertTrue(selection.getSelectedShapes().contains(triangle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testClear() {
|
||||||
|
Selection selection = new Selection();
|
||||||
|
SCircle circle = SCircle.create(0, 0, 50);
|
||||||
|
SRectangle rect = SRectangle.create(10, 10, 100, 50);
|
||||||
|
|
||||||
|
selection.add(circle);
|
||||||
|
selection.add(rect);
|
||||||
|
|
||||||
|
assertFalse(selection.isEmpty());
|
||||||
|
assertEquals(2, selection.getSelectedShapes().size());
|
||||||
|
|
||||||
|
selection.clear();
|
||||||
|
|
||||||
|
assertTrue(selection.isEmpty());
|
||||||
|
assertEquals(0, selection.getSelectedShapes().size());
|
||||||
|
|
||||||
|
// Verify shapes are marked as unselected
|
||||||
|
SelectionAttributes circleAttrs = (SelectionAttributes) circle.getAttributes(SelectionAttributes.ID);
|
||||||
|
SelectionAttributes rectAttrs = (SelectionAttributes) rect.getAttributes(SelectionAttributes.ID);
|
||||||
|
|
||||||
|
assertNotNull(circleAttrs);
|
||||||
|
assertNotNull(rectAttrs);
|
||||||
|
assertFalse(circleAttrs.selected);
|
||||||
|
assertFalse(rectAttrs.selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetSelectedShapesReturnsCopy() {
|
||||||
|
Selection selection = new Selection();
|
||||||
|
SCircle circle = SCircle.create(0, 0, 50);
|
||||||
|
selection.add(circle);
|
||||||
|
|
||||||
|
List<ovh.gasser.newshapes.shapes.Shape> copy = selection.getSelectedShapes();
|
||||||
|
|
||||||
|
// Try to modify the copy
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> copy.add(SCircle.create(10, 10, 20)));
|
||||||
|
|
||||||
|
// Original should be unchanged
|
||||||
|
assertEquals(1, selection.getSelectedShapes().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddListener() {
|
||||||
|
Selection selection = new Selection();
|
||||||
|
AtomicBoolean listenerNotified = new AtomicBoolean(false);
|
||||||
|
AtomicInteger notificationCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
SelectionListener listener = (selectedShapes) -> {
|
||||||
|
listenerNotified.set(true);
|
||||||
|
notificationCount.incrementAndGet();
|
||||||
|
};
|
||||||
|
|
||||||
|
selection.addListener(listener);
|
||||||
|
|
||||||
|
// Adding a shape should notify listener
|
||||||
|
SCircle circle = SCircle.create(0, 0, 50);
|
||||||
|
selection.add(circle);
|
||||||
|
|
||||||
|
assertTrue(listenerNotified.get());
|
||||||
|
assertEquals(1, notificationCount.get());
|
||||||
|
|
||||||
|
// Adding another shape should notify listener again
|
||||||
|
SRectangle rect = SRectangle.create(10, 10, 100, 50);
|
||||||
|
selection.add(rect);
|
||||||
|
|
||||||
|
assertEquals(2, notificationCount.get());
|
||||||
|
|
||||||
|
// Clearing should notify listener
|
||||||
|
selection.clear();
|
||||||
|
|
||||||
|
assertEquals(3, notificationCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNullAddAllDoesNothing() {
|
||||||
|
Selection selection = new Selection();
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
assertDoesNotThrow(() -> selection.addAll(null));
|
||||||
|
|
||||||
|
// Selection should still be empty
|
||||||
|
assertTrue(selection.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
176
src/test/java/ovh/gasser/newshapes/shapes/SCollectionTest.java
Normal file
176
src/test/java/ovh/gasser/newshapes/shapes/SCollectionTest.java
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package ovh.gasser.newshapes.shapes;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import ovh.gasser.newshapes.App;
|
||||||
|
import ovh.gasser.newshapes.attributes.ColorAttributes;
|
||||||
|
import ovh.gasser.newshapes.attributes.SelectionAttributes;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class SCollectionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateWithShapes() {
|
||||||
|
SRectangle rect = SRectangle.create(10, 20, 100, 50);
|
||||||
|
SCircle circle = SCircle.create(50, 50, 25);
|
||||||
|
|
||||||
|
SCollection collection = SCollection.of(rect, circle);
|
||||||
|
|
||||||
|
assertNotNull(collection);
|
||||||
|
assertEquals(2, collection.stream().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAdd() {
|
||||||
|
SCollection collection = SCollection.of();
|
||||||
|
SRectangle rect = SRectangle.create(10, 20, 100, 50);
|
||||||
|
|
||||||
|
collection.add(rect);
|
||||||
|
|
||||||
|
assertEquals(1, collection.stream().count());
|
||||||
|
assertSame(rect, collection.iterator().next());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRemove() {
|
||||||
|
SRectangle rect = SRectangle.create(10, 20, 100, 50);
|
||||||
|
SCollection collection = SCollection.of(rect);
|
||||||
|
|
||||||
|
collection.remove(rect);
|
||||||
|
|
||||||
|
assertEquals(0, collection.stream().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIterator() {
|
||||||
|
SRectangle rect1 = SRectangle.create(10, 20, 100, 50);
|
||||||
|
SRectangle rect2 = SRectangle.create(30, 40, 60, 70);
|
||||||
|
SCollection collection = SCollection.of(rect1, rect2);
|
||||||
|
|
||||||
|
Iterator<Shape> iterator = collection.iterator();
|
||||||
|
|
||||||
|
assertTrue(iterator.hasNext());
|
||||||
|
assertSame(rect1, iterator.next());
|
||||||
|
assertTrue(iterator.hasNext());
|
||||||
|
assertSame(rect2, iterator.next());
|
||||||
|
assertFalse(iterator.hasNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStream() {
|
||||||
|
SRectangle rect1 = SRectangle.create(10, 20, 100, 50);
|
||||||
|
SRectangle rect2 = SRectangle.create(30, 40, 60, 70);
|
||||||
|
SCircle circle = SCircle.create(50, 50, 25);
|
||||||
|
SCollection collection = SCollection.of(rect1, rect2, circle);
|
||||||
|
|
||||||
|
List<Shape> shapes = collection.stream().collect(Collectors.toList());
|
||||||
|
|
||||||
|
assertEquals(3, shapes.size());
|
||||||
|
assertTrue(shapes.contains(rect1));
|
||||||
|
assertTrue(shapes.contains(rect2));
|
||||||
|
assertTrue(shapes.contains(circle));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetBoundsEmptyCollection() {
|
||||||
|
SCollection collection = SCollection.of();
|
||||||
|
|
||||||
|
Rectangle bounds = collection.getBounds();
|
||||||
|
|
||||||
|
assertEquals(App.WIN_SIZE.width, bounds.width);
|
||||||
|
assertEquals(App.WIN_SIZE.height, bounds.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetBoundsWithChildren() {
|
||||||
|
SRectangle rect1 = SRectangle.create(10, 10, 50, 50);
|
||||||
|
SRectangle rect2 = SRectangle.create(100, 100, 80, 40);
|
||||||
|
SCollection collection = SCollection.of(rect1, rect2);
|
||||||
|
|
||||||
|
Rectangle bounds = collection.getBounds();
|
||||||
|
|
||||||
|
// Union should cover from (10,10) to (180,140)
|
||||||
|
assertEquals(10, bounds.x);
|
||||||
|
assertEquals(10, bounds.y);
|
||||||
|
assertEquals(170, bounds.width); // 100+80-10 = 170
|
||||||
|
assertEquals(130, bounds.height); // 100+40-10 = 130
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTranslate() {
|
||||||
|
SRectangle rect = SRectangle.create(10, 20, 100, 50);
|
||||||
|
SCollection collection = SCollection.of(rect);
|
||||||
|
|
||||||
|
collection.translate(5, 10);
|
||||||
|
|
||||||
|
Rectangle bounds = rect.getBounds();
|
||||||
|
assertEquals(15, bounds.x);
|
||||||
|
assertEquals(30, bounds.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testClone() {
|
||||||
|
SRectangle rect = SRectangle.create(10, 20, 100, 50);
|
||||||
|
SCollection original = SCollection.of(rect);
|
||||||
|
|
||||||
|
SCollection cloned = (SCollection) original.clone();
|
||||||
|
|
||||||
|
assertNotSame(original, cloned);
|
||||||
|
assertEquals(original.stream().count(), cloned.stream().count());
|
||||||
|
|
||||||
|
// Verify SelectionAttributes is added to clone
|
||||||
|
assertNotNull(cloned.getAttributes(SelectionAttributes.ID));
|
||||||
|
|
||||||
|
// Verify deep copy - modifying clone doesn't affect original
|
||||||
|
Iterator<Shape> clonedIterator = cloned.iterator();
|
||||||
|
Shape clonedChild = clonedIterator.next();
|
||||||
|
clonedChild.translate(100, 100);
|
||||||
|
|
||||||
|
Rectangle originalBounds = rect.getBounds();
|
||||||
|
assertEquals(10, originalBounds.x);
|
||||||
|
assertEquals(20, originalBounds.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testToString() {
|
||||||
|
SCollection collection = SCollection.of();
|
||||||
|
|
||||||
|
String str = collection.toString();
|
||||||
|
|
||||||
|
assertTrue(str.contains("SCollection"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddAttributesPropagatesToChildren() {
|
||||||
|
SRectangle rect = SRectangle.create(10, 20, 100, 50);
|
||||||
|
SCollection collection = SCollection.of(rect);
|
||||||
|
|
||||||
|
ColorAttributes attrs = new ColorAttributes(true, true, Color.RED, Color.BLUE);
|
||||||
|
collection.addAttributes(attrs);
|
||||||
|
|
||||||
|
ColorAttributes childAttrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID);
|
||||||
|
|
||||||
|
assertNotNull(childAttrs);
|
||||||
|
assertEquals(Color.RED, childAttrs.filledColor);
|
||||||
|
assertEquals(Color.BLUE, childAttrs.strokedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetAttributesReturnsChildColor() {
|
||||||
|
SRectangle rect = SRectangle.create(10, 20, 100, 50, Color.GREEN);
|
||||||
|
SCircle circle = SCircle.create(50, 50, 25);
|
||||||
|
SCollection collection = SCollection.of(rect, circle);
|
||||||
|
|
||||||
|
ColorAttributes attrs = (ColorAttributes) collection.getAttributes(ColorAttributes.ID);
|
||||||
|
|
||||||
|
assertNotNull(attrs);
|
||||||
|
// First child's color should be returned (strokedColor for SRectangle)
|
||||||
|
assertEquals(Color.GREEN, attrs.strokedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/test/java/ovh/gasser/newshapes/util/StreamableTest.java
Normal file
48
src/test/java/ovh/gasser/newshapes/util/StreamableTest.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package ovh.gasser.newshapes.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class StreamableTest {
|
||||||
|
|
||||||
|
private static class TestStreamable implements Streamable<String> {
|
||||||
|
private final List<String> elements;
|
||||||
|
|
||||||
|
TestStreamable(List<String> elements) {
|
||||||
|
this.elements = new ArrayList<>(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Iterator<String> iterator() {
|
||||||
|
return elements.iterator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStreamReturnsStreamOfElements() {
|
||||||
|
List<String> testData = Arrays.asList("a", "b", "c");
|
||||||
|
Streamable<String> streamable = new TestStreamable(testData);
|
||||||
|
|
||||||
|
Stream<String> result = streamable.stream();
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(testData, result.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStreamEmptyCollection() {
|
||||||
|
List<String> emptyData = new ArrayList<>();
|
||||||
|
Streamable<String> streamable = new TestStreamable(emptyData);
|
||||||
|
|
||||||
|
Stream<String> result = streamable.stream();
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.toList().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user