Connect minimal hosts to snapshots, target workers and shared Kit editing

This commit is contained in:
2026-11-12 11:07:45 +01:00
parent 28c1e47c95
commit 7db7218a6e
52 changed files with 4236 additions and 29 deletions
+1
View File
@@ -42,6 +42,7 @@ run([wrapper, "-B", "-ntp", "-pl", "designer-standalone", "org.apache.maven.plug
smoke_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()])
run([Path(os.environ["JAVA_HOME"]) / "bin/java", "-cp", smoke_classpath, "de.neitzel.fxdesigner.build.BuildRefreshSmoke", ROOT, args.target_java_home])
run([ROOT / "designer-intellij" / ("gradlew.bat" if os.name == "nt" else "gradlew"), "-p", "designer-intellij", "--no-daemon", "-PideaHome=" + str(idea), "clean", "test", "buildPlugin", "verifyPluginProjectConfiguration"])
run([sys.executable, ROOT / "scripts/check-connected-workflow.py", "--idea-home", args.idea_home, "--target-java-home", args.target_java_home])
with zipfile.ZipFile(next((ROOT / "designer-intellij/build/distributions").glob("*.zip"))) as plugin:
names = plugin.namelist()
assert any("javafx-graphics-26-mac" in n for n in names), "Missing host JavaFX implementation"
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Real snapshot/socket/overlay checks against both host document adapters."""
from pathlib import Path
import argparse
import os
import shutil
import subprocess
import tempfile
import json
import time
ROOT = Path(__file__).resolve().parent.parent
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--target-java-home", required=True)
parser.add_argument("--idea-home", required=True)
args = parser.parse_args()
logs = ROOT / "target/connected-workflow"
logs.mkdir(parents=True, exist_ok=True)
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()])
def run(name, command):
with (logs / (name + ".log")).open("w") as output:
result = subprocess.run(list(map(str, command)), cwd=ROOT, stdout=output,
stderr=subprocess.STDOUT, timeout=420)
if result.returncode:
raise RuntimeError((logs / (name + ".log")).read_text()[-16000:])
print("PASS " + name, flush=True)
with tempfile.TemporaryDirectory(prefix="designer-connected-") as temporary:
for host in ("standalone", "intellij"):
base = Path(temporary) / host
for mode in ("classpath", "modulepath"):
fixture = base / mode
shutil.copytree(ROOT / "integration-fixtures/preview" / mode, fixture,
ignore=shutil.ignore_patterns("target"))
if host == "standalone":
run(host + "-" + mode, [Path(os.environ["JAVA_HOME"]) / "bin/java", "-cp", classpath,
"de.neitzel.fxdesigner.standalone.WorkflowSmoke", ROOT, args.target_java_home,
fixture, mode])
local = fixture / ".neitzel-fx-designer"
(local / "runtimes.json").write_text(json.dumps({"schemaVersion":"1.0","runtimes":{"java21":{"build":args.target_java_home,"target":args.target_java_home}}}))
tools = [ROOT / ("designer-" + name) / "target" / ("designer-" + name + "-0.1.0-SNAPSHOT.jar") for name in ("target-tools","contracts","preview-api")]
(local / "host.properties").write_text("configuration=../neitzel-fx-designer.json\ncontext=fixture\nsettings=runtimes.json\nmaven="+str(ROOT/"mvnw")+"\ntools="+os.pathsep.join(map(str,tools))+"\n")
run("native-kit-" + mode, [Path(os.environ["JAVA_HOME"]) / "bin/java", "--enable-native-access=ALL-UNNAMED", "-cp", classpath,
"de.neitzel.fxdesigner.standalone.KitWorkflowSmoke", local / "host.properties",
fixture / "app/src/main/resources/fixture/components/Person.fxml", logs / ("kit-" + mode + ".png")])
if mode == "classpath":
pidfile = base / "worker.pid"
owner = subprocess.Popen([str(Path(os.environ["JAVA_HOME"]) / "bin/java"),
"-cp", classpath, "de.neitzel.fxdesigner.standalone.WorkerOwnerProbe",
str(ROOT), str(fixture), str(pidfile)], cwd=ROOT)
try:
until = time.monotonic() + 20
while not pidfile.exists() and time.monotonic() < until:
if owner.poll() is not None:
raise RuntimeError("Owner probe exited before startup")
time.sleep(.05)
worker_pid = int(pidfile.read_text())
owner.kill()
owner.wait(timeout=5)
until = time.monotonic() + 10
while time.monotonic() < until:
try:
os.kill(worker_pid, 0)
except ProcessLookupError:
break
time.sleep(.05)
else:
raise RuntimeError("Worker survived abrupt owner death")
print("PASS abrupt owner death", flush=True)
finally:
if owner.poll() is None:
owner.kill()
owner.wait(timeout=5)
if host == "intellij":
run(host, [ROOT / "designer-intellij/gradlew", "-p", "designer-intellij", "--no-daemon",
"-PideaHome=" + args.idea_home, "-Ddesigner.workflow.fixtures=" + str(base),
"-Ddesigner.repository=" + str(ROOT), "-Ddesigner.target.java=" + args.target_java_home,
"test"])
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Run the disposable companion checks in real isolated IntelliJ windows (macOS)."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import signal
import subprocess
import sys
import time
import zipfile
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)
p.add_argument("--idea-home", type=Path, required=True)
args = p.parse_args()
output = ROOT / "target/ide-workflow"
output.mkdir(parents=True, exist_ok=True)
for mode in ("classpath", "modulepath"):
launch = subprocess.run([sys.executable, ROOT / "scripts/run-fixture-host.py", "intellij",
"--check", "--mode", mode, "--java-home", args.java_home,
"--target-java-home", args.target_java_home, "--idea-home", args.idea_home],
cwd=ROOT, text=True, capture_output=True, check=True, timeout=60)
(output / (mode + "-launch.log")).write_text(launch.stdout + launch.stderr)
base = Path(re.search(r"Disposable host: (.+)", launch.stdout)[1])
pid = int(re.search(r"Host PID: (\d+)", launch.stdout)[1])
try:
until = time.monotonic() + 240
while time.monotonic() < until:
if (base / "FAILURE.txt").exists():
raise RuntimeError((base / "FAILURE.txt").read_text())
if (base / "SUCCESS.txt").exists():
break
time.sleep(.25)
else:
raise RuntimeError("IDE qualification timed out: " + str(base))
natives = subprocess.run([args.java_home / "bin/jcmd", str(pid), "VM.dynlibs"],
capture_output=True, text=True, check=True, timeout=20).stdout
(output / (mode + "-natives.txt")).write_text(natives)
paths = sorted(set(re.findall(r"(/[^\r\n]*?libglass\.dylib)", natives)))
expected = {}
for artifact in (Path.home() / ".m2/repository/org/openjfx").glob("*/26/*-mac*.jar"):
with zipfile.ZipFile(artifact) as jar:
for name in jar.namelist():
if name.endswith(".dylib"):
expected.setdefault(Path(name).name, set()).add(hashlib.sha256(jar.read(name)).hexdigest())
actual = {path: hashlib.sha256(Path(path).read_bytes()).hexdigest() for path in paths}
assert len(actual) == 1 and next(iter(actual.values())) in expected["libglass.dylib"], actual
all_paths = sorted(set(re.findall(r"(/[^\r\n]*?/\.openjfx/[^\r\n]*?\.dylib)", natives)))
all_native = {path: hashlib.sha256(Path(path).read_bytes()).hexdigest() for path in all_paths}
assert all(digest in expected.get(Path(path).name, set()) for path, digest in all_native.items())
proof = {"mode": mode, "host": str(base), "glass": actual, "openjfx": all_native,
"result": (base / "SUCCESS.txt").read_text()}
(output / (mode + ".json")).write_text(json.dumps(proof, indent=2) + "\n")
shutil.copyfile(base / "intellij.png", output / (mode + ".png"))
print("PASS registered IntelliJ " + mode + " editor and exclusive JavaFX 26 natives", flush=True)
finally:
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Create a fresh editable development fixture and explicit local host setup."""
import argparse
import json
from pathlib import Path
import shutil
ROOT = Path(__file__).resolve().parent.parent
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--destination", type=Path, required=True)
parser.add_argument("--target-java-home", type=Path, required=True)
parser.add_argument("--mode", choices=["classpath", "modulepath"], default="classpath")
args = parser.parse_args()
destination = args.destination.resolve()
shutil.copytree(ROOT / "integration-fixtures/preview" / args.mode, destination,
ignore=shutil.ignore_patterns("target"))
local = destination / ".neitzel-fx-designer"
local.mkdir()
(local / "runtimes.json").write_text(json.dumps({"schemaVersion":"1.0","runtimes":{"java21":{"build":str(args.target_java_home.resolve()),"target":str(args.target_java_home.resolve())}}}))
tools = [ROOT / ("designer-" + name) / "target" / ("designer-" + name + "-0.1.0-SNAPSHOT.jar")
for name in ("target-tools","contracts","preview-api")]
# Properties separators in values do not need escaping; double backslashes do.
def prop(value): return str(value).replace("\\", "\\\\")
(local / "host.properties").write_text(
"configuration=../neitzel-fx-designer.json\ncontext=fixture\nsettings=runtimes.json\n"
+ "maven=" + prop(ROOT / "mvnw") + "\ntools=" + ":".join(map(prop, tools)) + "\n")
print(local / "host.properties")
print(destination / "app/src/main/resources/fixture/components/Person.fxml")
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Launch a disposable standalone or isolated IntelliJ fixture host (macOS development)."""
import argparse
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import zipfile
from xml.sax.saxutils import escape
ROOT = Path(__file__).resolve().parent.parent
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("host", choices=["standalone","intellij"])
p.add_argument("--java-home", type=Path, required=True)
p.add_argument("--target-java-home", type=Path, required=True)
p.add_argument("--idea-home", type=Path)
p.add_argument("--check", action="store_true", help="Install the disposable IDE qualification companion")
p.add_argument("--mode", choices=["classpath","modulepath"], default="classpath")
args = p.parse_args()
base = Path(tempfile.mkdtemp(prefix="neitzel-designer-host-")).resolve()
project = base / "project"
subprocess.run([sys.executable, ROOT / "scripts/prepare-fixture-host.py", "--destination", project,
"--target-java-home", args.target_java_home, "--mode", args.mode], check=True)
settings = project / ".neitzel-fx-designer/host.properties"
file = project / "app/src/main/resources/fixture/components/Person.fxml"
env = dict(os.environ)
if args.host == "standalone":
cp = os.pathsep.join([str(ROOT / "designer-standalone/target/classes"),
(ROOT / "designer-standalone/target/smoke-classpath.txt").read_text().strip()])
command = [str(args.java_home / "bin/java"), "--enable-native-access=ALL-UNNAMED", "-cp", cp,
"de.neitzel.fxdesigner.standalone.Launcher", str(settings), str(file)]
else:
if args.idea_home is None:
p.error("--idea-home is required for IntelliJ")
plugin = next((ROOT / "designer-intellij/build/distributions").glob("*.zip"))
with zipfile.ZipFile(plugin) as archive:
archive.extractall(base / "plugins")
if args.check:
classes = base / "check-classes"
classes.mkdir()
jars = [*(args.idea_home / "lib").glob("*.jar"), *(base / "plugins").rglob("*.jar")]
subprocess.run([str(args.java_home / "bin/javac"), "-cp", os.pathsep.join(map(str,jars)), "-d", str(classes),
str(ROOT / "integration-tests/idea/WorkflowProbe.java")], check=True)
meta = classes / "META-INF"
meta.mkdir()
(meta / "plugin.xml").write_text('<idea-plugin><id>de.neitzel.fxdesigner.qualification</id><name>Designer workflow qualification</name><version>1</version><vendor>Local tests</vendor><idea-version since-build="263" until-build="263.*"/><depends>com.intellij.modules.platform</depends><depends>de.neitzel.fxdesigner</depends><extensions defaultExtensionNs="com.intellij"><postStartupActivity implementation="qualification.WorkflowProbe"/></extensions></idea-plugin>')
jar = base / "plugins/qualification/lib/qualification.jar"
jar.parent.mkdir(parents=True)
subprocess.run([str(args.java_home / "bin/jar"), "cf", str(jar), "-C", str(classes), "."], check=True)
options = base / "config/options"
options.mkdir(parents=True)
(options / "ide.general.xml").write_text('<application><component name="Registry"><entry key="embed.scene.builder" value="false"/></component></application>')
(options / "trusted-paths.xml").write_text('<application><component name="Trusted.Paths"><option name="TRUSTED_PROJECT_PATHS"><map><entry key="'+escape(str(project))+'" value="true"/></map></option></component></application>')
props = base / "idea.properties"
props.write_text("\n".join("idea."+name+".path="+str(base/name) for name in ["config","system","plugins","log"])+"\nidea.initially.ask.config=false\n")
vm = base / "idea.vmoptions"
vm.write_text((args.idea_home / "bin/idea.vmoptions").read_text()+"\n--enable-native-access=ALL-UNNAMED\n-Dneitzel.designer.launch="+str(settings)+"\n-Ddesigner.check.output="+str(base)+"\n")
env.update(IDEA_PROPERTIES=str(props), IDEA_VM_OPTIONS=str(vm), IDEA_JDK=str(args.java_home))
command = [str(args.idea_home / "MacOS/idea"), str(project), str(file)]
with (base / "launcher.log").open("w") as log:
process = subprocess.Popen(command, env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
(base / "host.pid").write_text(str(process.pid))
print("Host PID:", process.pid)
print("Disposable host:", base)
print("FXML:", file)