Validate preview sessions against real classpath and module-path fixtures

This commit is contained in:
2026-11-12 09:47:59 +01:00
parent e97af99412
commit 28c1e47c95
52 changed files with 1867 additions and 9 deletions
@@ -0,0 +1,197 @@
package fixture.checks;
import de.neitzel.fxdesigner.preview.*;
import de.neitzel.fxdesigner.target.*;
import fixture.components.*;
import fixture.model.*;
import fixture.preview.Examples;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
public final class PreviewChecks {
static int checks;
static void check(boolean b, String message) {
if (!b) throw new AssertionError(message);
checks++;
}
static <T> T fx(Callable<T> action) throws Exception {
var result = new CompletableFuture<T>();
Platform.runLater(
() -> {
try {
result.complete(action.call());
} catch (Throwable e) {
result.completeExceptionally(e);
}
});
return result.get(10, TimeUnit.SECONDS);
}
static PreviewFailure failure(Throwable error) {
for (Throwable t = error; t != null; t = t.getCause())
if (t instanceof PreviewFailure f) return f;
throw new AssertionError("No structured preview diagnostic", error);
}
public static void main(String[] args) throws Exception {
String mode = args[0];
ClassLoader loader = PreviewChecks.class.getClassLoader();
var descriptors = ExampleCatalog.discover(loader);
check(descriptors.size() == 9, "Descriptor discovery");
check(
System.getProperty("fixture.provider.initialized") == null,
"Discovery initialized provider");
check(System.getProperty("fixture.models") == null, "Discovery constructed Model");
check(System.getProperty("fixture.controllers") == null, "Discovery constructed Controller");
if (mode.equals("discovery")) {
System.out.println("PASS discovery " + checks);
return;
}
var started = new CountDownLatch(1);
Platform.startup(
() -> {
Platform.setImplicitExit(false);
started.countDown();
});
check(started.await(10, TimeUnit.SECONDS), "FX startup");
TargetPreview preview = null;
try {
String id = mode.startsWith("missing-") ? "standard" : mode;
var descriptor =
descriptors.stream().filter(d -> d.id().equals(id)).findFirst().orElseThrow();
preview =
TargetPreview.open(
descriptor, loader, Duration.ofMillis(mode.equals("timeout") ? 2000 : 10000));
if (mode.equals("async") || mode.equals("cancel")) {
for (int i = 0; i < 100 && Examples.pending == null; i++) Thread.sleep(20);
check(Examples.pending != null, "Provider readiness created");
check(!preview.ready().toCompletableFuture().isDone(), "Premature readiness");
if (mode.equals("async")) Examples.pending.complete(null);
else preview.cancel();
}
if (java.util.Set.of(
"timeout",
"cancel",
"readiness-failure",
"wrong-model",
"invalid-attribute",
"factory-failure",
"missing-components",
"missing-model")
.contains(mode)) {
try {
preview.ready().toCompletableFuture().get(15, TimeUnit.SECONDS);
throw new AssertionError("Failure accepted: " + mode);
} catch (ExecutionException error) {
var f = failure(error);
var expected =
switch (mode) {
case "timeout" -> PreviewFailure.Code.TIMEOUT;
case "cancel" -> PreviewFailure.Code.CANCELLED;
case "readiness-failure" -> PreviewFailure.Code.READINESS;
case "wrong-model" -> PreviewFailure.Code.MODEL_TYPE;
case "missing-components", "missing-model" -> PreviewFailure.Code.MODULE_ACCESS;
default -> PreviewFailure.Code.LOAD;
};
check(f.code() == expected, "Wrong diagnostic: " + f.code() + " expected " + expected);
}
preview.cleanup().toCompletableFuture().get(5, TimeUnit.SECONDS);
if (!mode.equals("missing-components")) {
check(Examples.service.closes == 1, "Failure did not clean services once");
if (!mode.equals("factory-failure"))
check(Examples.closes == 1, "Session cleanup missing");
}
} else {
TargetPreview ready = preview.ready().toCompletableFuture().get(15, TimeUnit.SECONDS);
fx(
() -> {
var view = ready.loadedView();
check(view.getViewModel() instanceof PersonViewModel, "Named ViewModel not selected");
var controller = Examples.context.getComponent(PersonController.class);
check(controller.received == view.getViewModel(), "Controller ViewModel handoff");
check(controller.greeting == Examples.service, "Configured service not injected");
var card = (Card) view.getRoot().lookup("#card");
var second = (Card) view.getRoot().lookup("#second");
check(card != second, "Component instances reused");
check(
card.getModel() == Examples.person.getHome()
&& second.getModel() == card.getModel(),
"Nested Model identity not shared");
check(card.received == second.received, "Nested ViewModel identity changed");
check(
card.greeting == Examples.service && second.greeting == Examples.service,
"Component injection");
check(card.getCaption().equals("FXML caption"), "Custom caption lost");
check(
card.getPrefWidth() == 310 && card.getAlignment() == Pos.CENTER_LEFT,
"Inherited/coerced attributes lost");
check(card.injected, "Component initialization");
TextField name = (TextField) view.getRoot().lookup("#name");
check(name.getText().equals("Ada"), "Initial parent binding");
name.setText("Grace");
check(Examples.person.getName().equals("Grace"), "Parent write-back");
card.street().setText("Changed Street");
check(
Examples.person.getHome().getStreet().equals("Changed Street")
&& second.street().getText().equals("Changed Street"),
"Component write-back and shared Model");
check(
((Label) view.getRoot().lookup("#greeting")).getText().startsWith("configured-"),
"Computed controller binding");
byte[] rgba = ready.render(420, 240);
check(rgba.length == 420 * 240 * 4, "Render size");
boolean nonzero = false;
for (byte value : rgba)
if (value != 0) {
nonzero = true;
break;
}
check(nonzero, "Blank rendering");
try {
ready.render(8192, 8192);
throw new AssertionError("Oversized image allowed");
} catch (IllegalArgumentException expected) {
checks++;
}
return null;
});
System.out.println(
"SESSION "
+ Examples.service.identity
+ " models="
+ System.getProperty("fixture.models"));
preview.close();
try {
preview.cleanup().toCompletableFuture().get(5, TimeUnit.SECONDS);
check(!mode.equals("cleanup-failure"), "Cleanup failure hidden");
} catch (ExecutionException error) {
check(
mode.equals("cleanup-failure")
&& failure(error).code() == PreviewFailure.Code.CLEANUP,
"Wrong cleanup diagnostic");
}
check(
Examples.closes == 1 && Examples.service.closes == 1,
"Owned resources not cleaned exactly once");
preview.close();
check(Examples.closes == 1, "Repeated close cleaned twice");
}
try {
TargetPreview.open(descriptor, loader, Duration.ofSeconds(1));
throw new AssertionError("Process session reused");
} catch (PreviewFailure e) {
check(e.code() == PreviewFailure.Code.SESSION_REUSE, "Wrong reuse diagnostic");
}
System.out.println("PASS " + mode + " " + checks);
} finally {
if (preview != null) preview.close();
Platform.exit();
}
}
}
@@ -0,0 +1,10 @@
module fixture.checks {
requires fixture.app;
requires fixture.preview;
requires de.neitzel.fxdesigner.target;
requires de.neitzel.fxdesigner.preview;
requires fx;
requires injection;
requires javafx.controls;
requires javafx.fxml;
}