Flush queued IDE writes and restart validation after host revision changes
This commit is contained in:
@@ -45,6 +45,8 @@ Qualify the registered editor and loaded JavaFX natives in real disposable IDE w
|
|||||||
python3 scripts/check-ide-host.py --java-home "$JAVA_HOME" --target-java-home "$TARGET_JAVA_HOME" --idea-home "$IDEA_HOME"
|
python3 scripts/check-ide-host.py --java-home "$JAVA_HOME" --target-java-home "$TARGET_JAVA_HOME" --idea-home "$IDEA_HOME"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The [save/startup reliability follow-up](docs/ide-host-reliability.md) covers queued IntelliJ VFS writes and host-revision/validation races, including repeated registered module-path qualification. Add `--mode modulepath --repeat 3` to the IDE runner to repeat that gate.
|
||||||
|
|
||||||
Worker startup cancellation and ready-preview cleanup are covered by the [native lifecycle regression](docs/native-worker-lifecycle.md); host checks also reject new native JVM crash reports.
|
Worker startup cancellation and ready-preview cleanup are covered by the [native lifecycle regression](docs/native-worker-lifecycle.md); host checks also reject new native JVM crash reports.
|
||||||
|
|
||||||
These graphical checks require a macOS desktop session; other platforms are not qualified. See [workflow limits](docs/first-connected-workflow.md#limits-and-next-stage) before using arbitrary application documents.
|
These graphical checks require a macOS desktop session; other platforms are not qualified. See [workflow limits](docs/first-connected-workflow.md#limits-and-next-stage) before using arbitrary application documents.
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
|||||||
com.intellij.openapi.fileEditor.FileDocumentManagerListener.TOPIC,
|
com.intellij.openapi.fileEditor.FileDocumentManagerListener.TOPIC,
|
||||||
new com.intellij.openapi.fileEditor.FileDocumentManagerListener() {
|
new com.intellij.openapi.fileEditor.FileDocumentManagerListener() {
|
||||||
public void afterDocumentSaved(Document document) {
|
public void afterDocumentSaved(Document document) {
|
||||||
|
// A native save can queue physical I/O. Record the accepted version now;
|
||||||
|
// subsequent NIO reads must flush that file before comparing disk contents.
|
||||||
|
for (var state : states.values())
|
||||||
|
if (state.document == document) state.disk = document.getText();
|
||||||
var file = FileDocumentManager.getInstance().getFile(document);
|
var file = FileDocumentManager.getInstance().getFile(document);
|
||||||
if (file != null && file.isInLocalFileSystem())
|
if (file != null && file.isInLocalFileSystem())
|
||||||
saves.forEach(listener -> listener.accept(canonicalUri(file)));
|
saves.forEach(listener -> listener.accept(canonicalUri(file)));
|
||||||
@@ -112,6 +116,8 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
|||||||
if (state.file == null) throw new IllegalArgumentException("Missing file: " + path);
|
if (state.file == null) throw new IllegalArgumentException("Missing file: " + path);
|
||||||
state.document = FileDocumentManager.getInstance().getDocument(state.file);
|
state.document = FileDocumentManager.getInstance().getDocument(state.file);
|
||||||
if (state.document == null) throw new IllegalArgumentException("Not a text document");
|
if (state.document == null) throw new IllegalArgumentException("Not a text document");
|
||||||
|
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance()
|
||||||
|
.flushPendingUpdates(state.file);
|
||||||
state.disk = Files.readString(path);
|
state.disk = Files.readString(path);
|
||||||
var observed = state;
|
var observed = state;
|
||||||
state.document.addDocumentListener(
|
state.document.addDocumentListener(
|
||||||
@@ -172,6 +178,7 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
|||||||
private void refreshClean(State s) throws java.io.IOException {
|
private void refreshClean(State s) throws java.io.IOException {
|
||||||
var manager = FileDocumentManager.getInstance();
|
var manager = FileDocumentManager.getInstance();
|
||||||
if (manager.isDocumentUnsaved(s.document)) return;
|
if (manager.isDocumentUnsaved(s.document)) return;
|
||||||
|
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates(s.file);
|
||||||
String disk = Files.readString(Path.of(s.file.getPath()));
|
String disk = Files.readString(Path.of(s.file.getPath()));
|
||||||
if (disk.equals(s.document.getText())) {
|
if (disk.equals(s.document.getText())) {
|
||||||
s.disk = disk; // Native autosave already persisted this revision; do not reload it.
|
s.disk = disk; // Native autosave already persisted this revision; do not reload it.
|
||||||
@@ -270,6 +277,7 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
|||||||
if (!s.document.isWritable() || !s.file.isWritable())
|
if (!s.document.isWritable() || !s.file.isWritable())
|
||||||
throw new IllegalStateException("Read-only document");
|
throw new IllegalStateException("Read-only document");
|
||||||
Path file = Path.of(s.file.getPath());
|
Path file = Path.of(s.file.getPath());
|
||||||
|
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates(s.file);
|
||||||
String onDisk = Files.readString(file);
|
String onDisk = Files.readString(file);
|
||||||
if (!FileDocumentManager.getInstance().isDocumentUnsaved(s.document)
|
if (!FileDocumentManager.getInstance().isDocumentUnsaved(s.document)
|
||||||
&& onDisk.equals(s.document.getText())) {
|
&& onDisk.equals(s.document.getText())) {
|
||||||
@@ -281,6 +289,9 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
|||||||
FileDocumentManager.getInstance().saveDocument(s.document);
|
FileDocumentManager.getInstance().saveDocument(s.document);
|
||||||
if (FileDocumentManager.getInstance().isDocumentUnsaved(s.document))
|
if (FileDocumentManager.getInstance().isDocumentUnsaved(s.document))
|
||||||
throw new IllegalStateException("IDE did not save document");
|
throw new IllegalStateException("IDE did not save document");
|
||||||
|
// FileDocumentManager may mark the Document clean before its asynchronous
|
||||||
|
// VFS write reaches disk. Our completion promises persistence to NIO consumers.
|
||||||
|
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates(s.file);
|
||||||
s.disk = Files.readString(file);
|
s.disk = Files.readString(file);
|
||||||
return IntellijDocuments.this.snapshot(s);
|
return IntellijDocuments.this.snapshot(s);
|
||||||
});
|
});
|
||||||
|
|||||||
+90
@@ -6,6 +6,96 @@ import java.nio.file.*;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public final class IntellijDocumentsTest extends BasePlatformTestCase {
|
public final class IntellijDocumentsTest extends BasePlatformTestCase {
|
||||||
|
/** Keeps real VFS writes queued until its per-file flush executes them. */
|
||||||
|
public void testSaveWaitsForQueuedVfsWrites() throws Exception {
|
||||||
|
var fs = com.intellij.openapi.vfs.LocalFileSystem.getInstance();
|
||||||
|
var executorField = fs.getClass().getDeclaredField("ioTasksExecutor");
|
||||||
|
executorField.setAccessible(true);
|
||||||
|
var executor = executorField.get(fs);
|
||||||
|
var workerField = executor.getClass().getDeclaredField("pendingTasksExecutor");
|
||||||
|
workerField.setAccessible(true);
|
||||||
|
var originalWorker = workerField.get(executor);
|
||||||
|
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates();
|
||||||
|
var queuedTasks = new java.util.concurrent.atomic.AtomicInteger();
|
||||||
|
var queued =
|
||||||
|
new java.util.concurrent.AbstractExecutorService() {
|
||||||
|
public void execute(Runnable task) {
|
||||||
|
queuedTasks.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {}
|
||||||
|
|
||||||
|
public java.util.List<Runnable> shutdownNow() {
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isShutdown() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTerminated() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean awaitTermination(long time, java.util.concurrent.TimeUnit unit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Path file = Files.createTempFile("designer-queued-save-", ".fxml");
|
||||||
|
Files.writeString(file, "before");
|
||||||
|
com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess(
|
||||||
|
getTestRootDisposable(), file.toRealPath().toString());
|
||||||
|
try (var docs = new IntellijDocuments(getProject());
|
||||||
|
var d = docs.open(file.toUri()).toCompletableFuture().get()) {
|
||||||
|
workerField.set(executor, queued);
|
||||||
|
var changed =
|
||||||
|
d.apply(new EditBatch(0, "edit", List.of(new EditBatch.Replacement(0, 6, "after"))))
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get();
|
||||||
|
var saved = d.save(changed.revision()).toCompletableFuture().get();
|
||||||
|
assertFalse(saved.dirty());
|
||||||
|
assertEquals(
|
||||||
|
"A completed save must be visible to NIO readers", "after", Files.readString(file));
|
||||||
|
assertEquals("after", d.snapshot().toCompletableFuture().get().text());
|
||||||
|
assertTrue("The test must exercise asynchronous VFS writes", queuedTasks.get() > 0);
|
||||||
|
var nativeEdit =
|
||||||
|
d.apply(
|
||||||
|
new EditBatch(
|
||||||
|
saved.revision(),
|
||||||
|
"native save",
|
||||||
|
List.of(new EditBatch.Replacement(0, 5, "native"))))
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get();
|
||||||
|
var virtual = com.intellij.openapi.vfs.LocalFileSystem.getInstance().findFileByNioFile(file);
|
||||||
|
var manager = com.intellij.openapi.fileEditor.FileDocumentManager.getInstance();
|
||||||
|
manager.saveDocument(manager.getDocument(virtual));
|
||||||
|
assertEquals("Native autosave is queued", "after", Files.readString(file));
|
||||||
|
// Simulate editing in IntelliJ's text editor: no adapter apply/refresh in between.
|
||||||
|
com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction(
|
||||||
|
getProject(), () -> manager.getDocument(virtual).setText("followup"));
|
||||||
|
// A second designer host can open after native editing has already begun.
|
||||||
|
// Its initial disk baseline must include the queued native save as well.
|
||||||
|
try (var late = new IntellijDocuments(getProject());
|
||||||
|
var opened = late.open(file.toUri()).toCompletableFuture().get()) {
|
||||||
|
var current = opened.snapshot().toCompletableFuture().get();
|
||||||
|
opened.save(current.revision()).toCompletableFuture().get();
|
||||||
|
assertEquals("followup", Files.readString(file));
|
||||||
|
}
|
||||||
|
var followup = d.snapshot().toCompletableFuture().get();
|
||||||
|
assertTrue(followup.revision() > nativeEdit.revision());
|
||||||
|
d.save(followup.revision()).toCompletableFuture().get();
|
||||||
|
assertEquals("followup", Files.readString(file));
|
||||||
|
assertEquals("followup", d.snapshot().toCompletableFuture().get().text());
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
com.intellij.openapi.vfs.newvfs.ManagingFS.getInstance().flushPendingUpdates();
|
||||||
|
} finally {
|
||||||
|
workerField.set(executor, originalWorker);
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void testNativeDocumentTransactionUndoAndIndependentHandles() throws Exception {
|
public void testNativeDocumentTransactionUndoAndIndependentHandles() throws Exception {
|
||||||
Path file = Files.createTempFile("designer-document-", ".fxml");
|
Path file = Files.createTempFile("designer-document-", ".fxml");
|
||||||
Files.writeString(file, "abcdef");
|
Files.writeString(file, "abcdef");
|
||||||
|
|||||||
@@ -554,6 +554,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
|
|
||||||
private void load(DocumentSnapshot value) {
|
private void load(DocumentSnapshot value) {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
|
// A newer host revision can have identical text (edit/revert or native reload).
|
||||||
|
// Its predecessor's validation reply will be rejected by revision, so it must
|
||||||
|
// not leave an in-flight marker that absorbs every subsequent render request.
|
||||||
|
if (draft == null || draft.revision() != value.revision() || !draft.text().equals(value.text()))
|
||||||
|
invalidateBindings("Document synchronized; target validation outdated.");
|
||||||
loading = true;
|
loading = true;
|
||||||
layoutPasses = 0;
|
layoutPasses = 0;
|
||||||
canvasCommitQueued = false;
|
canvasCommitQueued = false;
|
||||||
@@ -635,6 +640,9 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
|
|||||||
report(error);
|
report(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
// The change debounce may already have fired against the previous draft.
|
||||||
|
// Render only after the authoritative revision and its projection agree.
|
||||||
|
if (projection != null && examples.getValue() != null) resize.playFromStart();
|
||||||
if (pendingSourceOffset != null) {
|
if (pendingSourceOffset != null) {
|
||||||
int offset = pendingSourceOffset;
|
int offset = pendingSourceOffset;
|
||||||
pendingSourceOffset = null;
|
pendingSourceOffset = null;
|
||||||
|
|||||||
@@ -1604,7 +1604,72 @@ public final class ProjectionHostChecks {
|
|||||||
await(() -> fx(() -> source(pane).getText().equals(original)));
|
await(() -> fx(() -> source(pane).getText().equals(original)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Object field(DesignerPane pane, String name) throws Exception {
|
||||||
|
var field = DesignerPane.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force a host revision to arrive after validation starts, with identical final text. */
|
||||||
|
public static void renderRevisionRace(DesignerPane pane, DocumentSession document)
|
||||||
|
throws Exception {
|
||||||
|
String original = document.snapshot().toCompletableFuture().get().text();
|
||||||
|
var poll = (javafx.animation.Timeline) field(pane, "poll");
|
||||||
|
var resize = (javafx.animation.PauseTransition) field(pane, "resize");
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
poll.stop();
|
||||||
|
resize.stop();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
set(document, original + "<!-- transient external edit -->");
|
||||||
|
set(document, original);
|
||||||
|
long revision = document.snapshot().toCompletableFuture().get().revision();
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
resize.stop();
|
||||||
|
pane.render();
|
||||||
|
check(original.equals(field(pane, "validatingSource")), "Validation was not in flight");
|
||||||
|
// Deliver the real host-poll callback before the worker validation reply.
|
||||||
|
poll.getKeyFrames().getFirst().getOnFinished().handle(new javafx.event.ActionEvent());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
await(() -> fx(() -> ((DocumentSnapshot) field(pane, "draft")).revision() == revision));
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
|
||||||
|
boolean rendered = false;
|
||||||
|
while (System.nanoTime() < deadline) {
|
||||||
|
rendered =
|
||||||
|
fx(
|
||||||
|
() ->
|
||||||
|
List.of("card", "second").stream()
|
||||||
|
.allMatch(
|
||||||
|
id -> {
|
||||||
|
var region = (SnapshotRegion) object(pane, id).getSceneGraphObject();
|
||||||
|
return region.image() != null
|
||||||
|
&& region.statusText().equals("Target runtime");
|
||||||
|
}));
|
||||||
|
if (rendered) break;
|
||||||
|
Thread.sleep(25);
|
||||||
|
}
|
||||||
|
check(rendered, "Host revision stranded target validation/rendering");
|
||||||
|
check(
|
||||||
|
document.snapshot().toCompletableFuture().get().text().equals(original),
|
||||||
|
"Render edited source");
|
||||||
|
System.out.println(
|
||||||
|
"PASS host revision during validation: identical text, fresh target pixels, no extra"
|
||||||
|
+ " action");
|
||||||
|
} finally {
|
||||||
|
fx(
|
||||||
|
() -> {
|
||||||
|
poll.playFromStart();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void run(DesignerPane pane, DocumentSession document) throws Exception {
|
public static void run(DesignerPane pane, DocumentSession document) throws Exception {
|
||||||
|
renderRevisionRace(pane, document);
|
||||||
String original = document.snapshot().toCompletableFuture().get().text();
|
String original = document.snapshot().toCompletableFuture().get().text();
|
||||||
// The same real application, now with nested, fully-qualified Component declarations.
|
// The same real application, now with nested, fully-qualified Component declarations.
|
||||||
String nested =
|
String nested =
|
||||||
|
|||||||
+11
@@ -111,6 +111,17 @@ public final class KitWorkflowSmoke {
|
|||||||
.toList()));
|
.toList()));
|
||||||
throw new AssertionError("Shared Kit did not render target pixels");
|
throw new AssertionError("Shared Kit did not render target pixels");
|
||||||
}
|
}
|
||||||
|
if (Boolean.getBoolean("designer.check.racesOnly")) {
|
||||||
|
try (var document =
|
||||||
|
host.services()
|
||||||
|
.documents()
|
||||||
|
.open(Path.of(args[1]).toUri())
|
||||||
|
.toCompletableFuture()
|
||||||
|
.get()) {
|
||||||
|
ProjectionHostChecks.renderRevisionRace(pane, document);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
fx(
|
fx(
|
||||||
() -> {
|
() -> {
|
||||||
var card = new FXOMFxIdIndex(pane.kit().editor().getFxomDocument()).lookup("card");
|
var card = new FXOMFxIdIndex(pane.kit().editor().getFxomDocument()).lookup("card");
|
||||||
|
|||||||
@@ -58,3 +58,7 @@ Evidence logs are `/tmp/generated-vm-bootstrap.log`, `/tmp/generated-vm-processo
|
|||||||
The first registered IntelliJ run passed the classpath workflow but stopped in module-path mode at the previously recorded immediate exact-save assertion, before generation checks. An unchanged module-path retry timed out at initial canvas rendering (`WorkflowProbe` line 438), also before generation checks. No assertion was weakened. Existing startup/persistence/preview-gate reliability work remains open; this slice does not claim to fix those intermittent failures.
|
The first registered IntelliJ run passed the classpath workflow but stopped in module-path mode at the previously recorded immediate exact-save assertion, before generation checks. An unchanged module-path retry timed out at initial canvas rendering (`WorkflowProbe` line 438), also before generation checks. No assertion was weakened. Existing startup/persistence/preview-gate reliability work remains open; this slice does not claim to fix those intermittent failures.
|
||||||
|
|
||||||
The final unchanged module-path retry hit the same initial-render timeout. Consequently, **registered IntelliJ module-path whole-host qualification remains open** for this slice. Registered IntelliJ classpath passed generated-registry discovery, full shared editing/history/diagnostic checks, exclusive JavaFX 26 native verification and worker shutdown. Standalone and real target-process module-path checks passed, including generated classes and registry behavior; these do not substitute for the missing registered IDE pass. Follow up on initial rendering and save synchronization before declaring that host gate complete.
|
The final unchanged module-path retry hit the same initial-render timeout. Consequently, **registered IntelliJ module-path whole-host qualification remains open** for this slice. Registered IntelliJ classpath passed generated-registry discovery, full shared editing/history/diagnostic checks, exclusive JavaFX 26 native verification and worker shutdown. Standalone and real target-process module-path checks passed, including generated classes and registry behavior; these do not substitute for the missing registered IDE pass. Follow up on initial rendering and save synchronization before declaring that host gate complete.
|
||||||
|
|
||||||
|
## Save/startup gate follow-up
|
||||||
|
|
||||||
|
The previously open module-path host gate is closed by the [IntelliJ save/startup reliability follow-up](ide-host-reliability.md): deterministic regressions reproduced and corrected queued VFS persistence and stale validation state, followed by three consecutive passing registered module-path workflows with the generated registry checks included. The failed attempts above remain historical evidence.
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# IntelliJ save completion and canvas startup
|
||||||
|
|
||||||
|
This follow-up addresses the save and initial-render failures recorded during [generated ViewModel qualification](generated-viewmodels.md). The unchanged initial module-path baseline passed once; that did not close the intermittent gate. Controlled scheduling regressions then reproduced both failures without manual input.
|
||||||
|
|
||||||
|
## Causes and fixes
|
||||||
|
|
||||||
|
**Save completion:** IntelliJ's `FileDocumentManager` is an asynchronous VFS write requestor. It can mark a Document clean before its bytes reach the filesystem. Our adapter completed its save future and read the old bytes through NIO. That also left a stale external-change baseline after a native autosave. The adapter now flushes pending updates for the affected file when establishing the initial disk baseline, before clean-disk comparisons and before completing an explicit save. Native save notifications record the accepted Document version. Actual external edits still fail with the draft retained; no competing file writer or undo store was introduced.
|
||||||
|
|
||||||
|
**Canvas startup:** the document-change render debounce and the host revision poll are independent. Validation can start against the old host revision, followed by loading a newer revision with identical text (edit/revert or native reload). Its reply is correctly rejected by revision, but the old in-flight marker previously remained. Later rendering joined that dead validation request indefinitely. Loading a new revision now invalidates obsolete validation state, and loading a usable projection schedules rendering again after synchronization. Stale replies remain rejected, including after a replacement validation starts.
|
||||||
|
|
||||||
|
Broader document-event filtering was considered but was not needed to reproduce or correct these failures. The fixes preserve the existing captured-draft and preview invalidation policy.
|
||||||
|
|
||||||
|
## Deterministic regressions
|
||||||
|
|
||||||
|
`IntellijDocumentsTest.testSaveWaitsForQueuedVfsWrites` runs against the real IntelliJ Document and asynchronous local VFS. Its test-only scheduling control holds the VFS executor queue; the real per-file flush must execute queued writes. Before the fix, the completed save returned clean while NIO read `before` instead of `after`. It additionally checks a queued native save followed by another edit/save, with no false external conflict. Opening a second designer adapter while the native save was queued reproduced `External change; draft retained` before the initial-baseline flush was added. The existing real external-conflict and draft-preservation tests remain required. Reflection into the executor is limited to this test and pinned to the qualified SDK; no host implementation depends on that internal field.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
JAVA_HOME="$JAVA_HOME" designer-intellij/gradlew -p designer-intellij --no-daemon \
|
||||||
|
-PideaHome="$IDEA_HOME" test --tests '*IntellijDocumentsTest'
|
||||||
|
```
|
||||||
|
|
||||||
|
`ProjectionHostChecks.renderRevisionRace` pauses only the test's document-poll and render timers, performs an edit/revert through the real host document service, starts target validation and delivers the real poll callback before its reply. It requires fresh target pixels on the newer revision without another user action and verifies unchanged source. Before the fix it failed with `Host revision stranded target validation/rendering`. This scenario runs in both standalone and registered IntelliJ workflows. A narrow native standalone entry point runs the same scenario against a real module-path application:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/check-render-race.py --java-home "$JAVA_HOME" \
|
||||||
|
--target-java-home "$TARGET_JAVA_HOME"
|
||||||
|
```
|
||||||
|
|
||||||
|
Build/install the modules and run the bootstrap once to prepare its test classpath. The narrow script creates a disposable fixture and closes the host and workers. It does not replace whole-host qualification.
|
||||||
|
|
||||||
|
## Whole-host qualification
|
||||||
|
|
||||||
|
The real IDE runner now accepts `--mode classpath|modulepath|all` and `--repeat N`. Every repetition starts a fresh isolated IDE/project; it retains per-run evidence under `target/ide-workflow/runs-*/<mode>-run-<N>`. Existing exact-source save, history, diagnostics, generated discovery, exclusive JavaFX 26 natives and worker-shutdown assertions and timeouts remain intact.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/check-ide-host.py --java-home "$JAVA_HOME" \
|
||||||
|
--target-java-home "$TARGET_JAVA_HOME" --idea-home "$IDEA_HOME" \
|
||||||
|
--mode modulepath --repeat 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Qualification on 2026-11-13: the full bootstrap passed 141 Maven tests, target linkage/bytecode/packaging checks, 38 target processes with 548 checks, both standalone native workflows, 48 lifecycle cycles and abrupt owner death. After extending the initial-baseline regression, all 11 IntelliJ unit/adapter tests and the plugin rebuild passed again (152 unit/adapter tests overall, zero failures/errors/skips). Three consecutive fresh registered IntelliJ module-path sessions and a final fresh classpath session passed with the final plugin, without retrying a failed run. Each includes the forced revision race, source-preserving edits and exact save assertions, Undo/Redo, native diagnostics/fixes, generated registry discovery, exclusive JavaFX 26 natives and worker shutdown.
|
||||||
|
|
||||||
|
The previous failures remain historical evidence. Red/green logs: `/tmp/ide-save-red.log`, `/tmp/ide-save-late-open.log`, `/tmp/ide-save-final.log`, `/tmp/render-race-red.log` and `/tmp/render-race-green.log`. Whole-suite evidence: `/tmp/ide-reliability-bootstrap.log`, `/tmp/ide-reliability-modulepath.log` `/tmp/ide-reliability-classpath.log`, `target/ide-workflow/runs-ip2002jp` (module-path) and `target/ide-workflow/runs-4_sojrex` (classpath). These are disposable local evidence, not published artifacts. This closes the bounded IntelliJ module-path save/startup gate on the pinned macOS/IDE/JavaFX setup; other platforms and IDE SDKs remain outside that qualification. No manual testing is part of this follow-up; previously skipped human tests remain skipped.
|
||||||
@@ -155,3 +155,7 @@ The [generated Model metadata slice](generated-model-metadata.md) adds an opt-in
|
|||||||
The [ViewModel generation slice](generated-viewmodels.md) extends the metadata processor with typed replaceable Java sources, checked-in Maven settings and declarative registry services. NeitzelLib supplies stored-property lookup and compatible shared identity/default selection. Both hosts discover generated/custom defaults through target workers and keep existing authoring/history behavior. Clean consumer builds, Lombok, generated inheritance/nesting and rejection cases have automated coverage. Dedicated configuration selection UI, broader cross-module/generic mappings and reviewed FXML form generation remain later work.
|
The [ViewModel generation slice](generated-viewmodels.md) extends the metadata processor with typed replaceable Java sources, checked-in Maven settings and declarative registry services. NeitzelLib supplies stored-property lookup and compatible shared identity/default selection. Both hosts discover generated/custom defaults through target workers and keep existing authoring/history behavior. Clean consumer builds, Lombok, generated inheritance/nesting and rejection cases have automated coverage. Dedicated configuration selection UI, broader cross-module/generic mappings and reviewed FXML form generation remain later work.
|
||||||
|
|
||||||
Qualification: automated standalone and registered IntelliJ classpath workflows pass. Real module-path consumer/runtime tests pass, but registered IntelliJ module-path remains open after the existing save assertion and initial-render timeouts; see the qualification record above. No manual testing was requested or substituted.
|
Qualification: automated standalone and registered IntelliJ classpath workflows pass. Real module-path consumer/runtime tests pass, but registered IntelliJ module-path remains open after the existing save assertion and initial-render timeouts; see the qualification record above. No manual testing was requested or substituted.
|
||||||
|
|
||||||
|
## IntelliJ save/startup reliability gate
|
||||||
|
|
||||||
|
The [reliability follow-up](ide-host-reliability.md) closes the generated-ViewModel slice's remaining IntelliJ module-path gate. Explicit saves now await per-file VFS persistence, native saves update the accepted baseline, and opening/refreshing a document accounts for queued writes. Host revision synchronization clears obsolete validation and schedules current rendering. Controlled red/green regressions and three consecutive fresh registered module-path runs pass without changing source/history assertions or timeouts. Previously skipped human checks remain skipped.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import signal
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from native_crashes import NativeCrashGuard, descendant_workers, await_worker_exit
|
from native_crashes import NativeCrashGuard, descendant_workers, await_worker_exit
|
||||||
|
|
||||||
@@ -19,16 +20,26 @@ p = argparse.ArgumentParser(description=__doc__)
|
|||||||
p.add_argument("--java-home", type=Path, required=True)
|
p.add_argument("--java-home", type=Path, required=True)
|
||||||
p.add_argument("--target-java-home", type=Path, required=True)
|
p.add_argument("--target-java-home", type=Path, required=True)
|
||||||
p.add_argument("--idea-home", type=Path, required=True)
|
p.add_argument("--idea-home", type=Path, required=True)
|
||||||
|
p.add_argument("--mode", choices=["all", "classpath", "modulepath"], default="all")
|
||||||
|
p.add_argument("--repeat", type=int, default=1, help="Consecutive independent runs per mode")
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
if args.repeat < 1:
|
||||||
|
p.error("--repeat must be positive")
|
||||||
output = ROOT / "target/ide-workflow"
|
output = ROOT / "target/ide-workflow"
|
||||||
output.mkdir(parents=True, exist_ok=True)
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
for mode in ("classpath", "modulepath"):
|
batch = Path(tempfile.mkdtemp(prefix="runs-", dir=output))
|
||||||
|
print("Host evidence: " + str(batch), flush=True)
|
||||||
|
modes = ("classpath", "modulepath") if args.mode == "all" else (args.mode,)
|
||||||
|
for run_number, mode in ((n, mode) for n in range(1, args.repeat + 1) for mode in modes):
|
||||||
crashes = NativeCrashGuard(ROOT)
|
crashes = NativeCrashGuard(ROOT)
|
||||||
launch = subprocess.run([sys.executable, ROOT / "scripts/run-fixture-host.py", "intellij",
|
launch = subprocess.run([sys.executable, ROOT / "scripts/run-fixture-host.py", "intellij",
|
||||||
"--check", "--mode", mode, "--java-home", args.java_home,
|
"--check", "--mode", mode, "--java-home", args.java_home,
|
||||||
"--target-java-home", args.target_java_home, "--idea-home", args.idea_home],
|
"--target-java-home", args.target_java_home, "--idea-home", args.idea_home],
|
||||||
cwd=ROOT, text=True, capture_output=True, check=True, timeout=60)
|
cwd=ROOT, text=True, capture_output=True, check=True, timeout=60)
|
||||||
|
evidence = batch / f"{mode}-run-{run_number}"
|
||||||
|
evidence.mkdir(exist_ok=True)
|
||||||
(output / (mode + "-launch.log")).write_text(launch.stdout + launch.stderr)
|
(output / (mode + "-launch.log")).write_text(launch.stdout + launch.stderr)
|
||||||
|
(evidence / "launch.log").write_text(launch.stdout + launch.stderr)
|
||||||
base = Path(re.search(r"Disposable host: (.+)", launch.stdout)[1])
|
base = Path(re.search(r"Disposable host: (.+)", launch.stdout)[1])
|
||||||
pid = int(re.search(r"Host PID: (\d+)", launch.stdout)[1])
|
pid = int(re.search(r"Host PID: (\d+)", launch.stdout)[1])
|
||||||
try:
|
try:
|
||||||
@@ -60,7 +71,11 @@ for mode in ("classpath", "modulepath"):
|
|||||||
"result": (base / "SUCCESS.txt").read_text()}
|
"result": (base / "SUCCESS.txt").read_text()}
|
||||||
(output / (mode + ".json")).write_text(json.dumps(proof, indent=2) + "\n")
|
(output / (mode + ".json")).write_text(json.dumps(proof, indent=2) + "\n")
|
||||||
shutil.copyfile(base / "intellij.png", output / (mode + ".png"))
|
shutil.copyfile(base / "intellij.png", output / (mode + ".png"))
|
||||||
|
(evidence / "proof.json").write_text(json.dumps(proof, indent=2) + "\n")
|
||||||
finally:
|
finally:
|
||||||
|
for name in ("SUCCESS.txt", "FAILURE.txt", "ui-state.txt", "launcher.log"):
|
||||||
|
if (base / name).exists():
|
||||||
|
shutil.copyfile(base / name, evidence / name)
|
||||||
workers = descendant_workers(pid)
|
workers = descendant_workers(pid)
|
||||||
try:
|
try:
|
||||||
os.kill(pid, signal.SIGTERM)
|
os.kill(pid, signal.SIGTERM)
|
||||||
@@ -68,4 +83,4 @@ for mode in ("classpath", "modulepath"):
|
|||||||
pass
|
pass
|
||||||
await_worker_exit(workers)
|
await_worker_exit(workers)
|
||||||
crashes.check()
|
crashes.check()
|
||||||
print("PASS registered IntelliJ " + mode + " editor, exclusive JavaFX 26 natives and worker shutdown", flush=True)
|
print("PASS registered IntelliJ " + mode + f" run {run_number}: editor, exclusive JavaFX 26 natives and worker shutdown", flush=True)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Force a host revision to overtake in-flight target validation in the shared Kit."""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
p = argparse.ArgumentParser(description=__doc__)
|
||||||
|
p.add_argument('--java-home', type=Path, required=True)
|
||||||
|
p.add_argument('--target-java-home', type=Path, required=True)
|
||||||
|
args = p.parse_args()
|
||||||
|
classpath = os.pathsep.join([
|
||||||
|
str(ROOT / 'designer-standalone/target/test-classes'),
|
||||||
|
str(ROOT / 'designer-standalone/target/classes'),
|
||||||
|
(ROOT / 'designer-standalone/target/smoke-classpath.txt').read_text().strip()])
|
||||||
|
with tempfile.TemporaryDirectory(prefix='designer-render-race-') as tmp:
|
||||||
|
fixture = Path(tmp) / 'project'
|
||||||
|
subprocess.run([sys.executable, ROOT / 'scripts/prepare-fixture-host.py',
|
||||||
|
'--destination', fixture, '--mode', 'modulepath',
|
||||||
|
'--target-java-home', args.target_java_home], check=True)
|
||||||
|
subprocess.run([args.java_home / 'bin/java', '--enable-native-access=ALL-UNNAMED',
|
||||||
|
'-Ddesigner.check.racesOnly=true', '-cp', classpath,
|
||||||
|
'de.neitzel.fxdesigner.standalone.KitWorkflowSmoke',
|
||||||
|
fixture / '.neitzel-fx-designer/host.properties',
|
||||||
|
fixture / 'app/src/main/resources/fixture/components/Person.fxml',
|
||||||
|
Path(tmp) / 'unused.png'], check=True, timeout=90, cwd=ROOT)
|
||||||
Reference in New Issue
Block a user