ortools review on some design issues and fixed flaky test
All checks were successful
Build, test and publish / verify (push) Successful in 1m34s

This commit is contained in:
2026-10-30 15:14:27 +01:00
parent 1b4c7f7f2f
commit 38247d0705
25 changed files with 177 additions and 76 deletions

View File

@@ -1,10 +1,10 @@
package nl.agilecc.agiledancers;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
@@ -15,6 +15,31 @@ class ArchitectureTest {
private static final String ROOT = "nl.agilecc.agiledancers";
private static final String API_MODEL = ROOT + ".rest.apimodel";
// Exclude Lombok builders and compiler-generated enum-switch helper classes.
@ArchTest
static final ArchRule handwritten_types_are_top_level = classes()
.that().areNotAnnotatedWith("lombok.Generated")
.and().doNotHaveModifier(JavaModifier.SYNTHETIC)
.should().beTopLevelClasses();
@ArchTest
static final ArchRule service_records_reside_in_service_model = classes()
.that().resideInAPackage(ROOT + ".service..")
.and().areRecords()
.should().resideInAPackage(ROOT + ".service.model");
@ArchTest
static final ArchRule service_interfaces_reside_in_service_port = classes()
.that().resideInAPackage(ROOT + ".service..")
.and().areInterfaces()
.should().resideInAPackage(ROOT + ".service.port");
@ArchTest
static final ArchRule scheduling_port_uses_domain_types = classes()
.that().resideInAPackage(ROOT + ".service.port..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage(ROOT + ".service.port..", ROOT + ".model..", "java..");
// API model classes and records use the Request, Response, View, Dto or DTO suffix.
@ArchTest
static final ArchRule api_models_reside_in_the_api_model_package = classes()

View File

@@ -1,5 +1,8 @@
package nl.agilecc.agiledancers;
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
import nl.agilecc.agiledancers.service.model.*;
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
import nl.agilecc.agiledancers.model.*;
import nl.agilecc.agiledancers.model.specifications.*;
import nl.agilecc.agiledancers.service.*;
@@ -80,11 +83,11 @@ class SchedulingTest {
service.endScheduling(show.getId());
var fresh = service.beginScheduling(school, show.getId());
assertThrows(ManagementException.class, () -> service.finishScheduling(school, fresh,
new SchoolManagementService.SchedulingResult(false, List.of(List.of(), List.of()))));
new SchedulingResult(false, List.of(List.of(), List.of()))));
assertEquals(before, show.getDayParts().stream().map(DayPart::getDanceGroups).toList());
// Feasible results are accepted without requiring proof of optimality.
var valid = new OrToolsSchedulingEngine().solve(fresh);
assertDoesNotThrow(() -> service.finishScheduling(school, fresh, new SchoolManagementService.SchedulingResult(false, valid.assignments())));
assertDoesNotThrow(() -> service.finishScheduling(school, fresh, new SchedulingResult(false, valid.assignments())));
service.endScheduling(show.getId());
}
@Test void staleDaypartCountChangesAndDeletedShowsAreRejected() {

View File

@@ -3,9 +3,7 @@ package nl.agilecc.agiledancers;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.response.ValidatableResponse;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@@ -171,7 +169,7 @@ class SchoolManagementResourceTest {
String exported = given().get(school + "/export").then().statusCode(200).extract().asString();
given().contentType("application/json").body(exported).post(SCHOOLS + "/import?replaceId=" + school.substring(school.lastIndexOf('/') + 1))
.then().statusCode(200);
given().get(school + "/export").then().body(equalTo(exported));
given().get(school + "/export").then().body("$", equalTo(io.restassured.path.json.JsonPath.from(exported).getMap("$")));
} finally { given().delete(school); }
}

View File

@@ -1,5 +1,6 @@
package nl.agilecc.agiledancers;
import nl.agilecc.agiledancers.service.model.*;
import nl.agilecc.agiledancers.service.*;
import nl.agilecc.agiledancers.model.specifications.*;
import org.junit.jupiter.api.Test;
@@ -9,12 +10,12 @@ import static org.junit.jupiter.api.Assertions.*;
class SchoolSnapshotTest {
private SchoolSnapshot complete() {
return new SchoolSnapshot(2, UUID.randomUUID(), "Studio", List.of(new SchoolSnapshot.Person("a", "Sam", LocalDate.of(2010,1,1))),
List.of(new SchoolSnapshot.Trainer("c", "Alex")),
List.of(new SchoolSnapshot.Group("g", "Hiphop", DanceGroupType.REGULAR, 0, 99, List.of(DanceGroupStyle.HIPHOP), List.of("a"), "c"),
new SchoolSnapshot.Group("h", "Jazz", DanceGroupType.REGULAR, 0, 99, List.of(), List.of("a"), "c")),
List.of(new SchoolSnapshot.Performance("s", "Show",
List.of(new SchoolSnapshot.Part(LocalDate.of(2027,1,1), DayPartType.MORNING, List.of("g", "h"))), ShowConfiguration.defaults(1))));
return new SchoolSnapshot(2, UUID.randomUUID(), "Studio", List.of(new Person("a", "Sam", LocalDate.of(2010,1,1))),
List.of(new Trainer("c", "Alex")),
List.of(new Group("g", "Hiphop", DanceGroupType.REGULAR, 0, 99, List.of(DanceGroupStyle.HIPHOP), List.of("a"), "c"),
new Group("h", "Jazz", DanceGroupType.REGULAR, 0, 99, List.of(), List.of("a"), "c")),
List.of(new Performance("s", "Show",
List.of(new Part(LocalDate.of(2027,1,1), DayPartType.MORNING, List.of("g", "h"))), ShowConfiguration.defaults(1))));
}
@Test void roundTripAfterRestartPreservesAllFieldsAndSharedRelationships() {
var snapshot = complete();

View File

@@ -1,5 +1,6 @@
package nl.agilecc.agiledancers.service;
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
import nl.agilecc.agiledancers.model.*;
import nl.agilecc.agiledancers.model.specifications.*;
import nl.agilecc.agiledancers.solver.OrToolsSchedulingEngine;
@@ -23,7 +24,7 @@ class SchedulingServiceTest {
var before = schools.exportSchool(school);
for (SchedulingEngine failure : List.<SchedulingEngine>of(
input -> { throw new IllegalStateException("Native failure"); },
input -> { throw new ManagementException(ManagementException.Kind.INVALID_INPUT, "No valid result before the limit"); },
input -> { throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, "No valid result before the limit"); },
input -> null)) {
scheduling.engine = failure;
assertThrows(ManagementException.class, () -> scheduling.schedule(school, show.getId()));
@@ -51,7 +52,7 @@ class SchedulingServiceTest {
var first = executor.submit(() -> scheduling.schedule(school, show.getId()));
try {
assertTrue(entered.await(10, TimeUnit.SECONDS));
assertEquals(ManagementException.Kind.CONFLICT, assertThrows(ManagementException.class,
assertEquals(ManagementExceptionKind.CONFLICT, assertThrows(ManagementException.class,
() -> scheduling.schedule(school, show.getId())).getKind());
schools.createGroup(school, "New group", DanceGroupType.REGULAR, 0, 99, Set.of());
} finally { release.countDown(); }