#!/usr/bin/env python3 """Qualify the actual relocated macOS app launcher using an external Java test agent.""" import argparse,hashlib,json,os,subprocess,sys,tempfile,time,zipfile from pathlib import Path from native_crashes import NativeCrashGuard ROOT=Path(__file__).resolve().parent.parent p=argparse.ArgumentParser(description=__doc__) p.add_argument('--app',type=Path,required=True) p.add_argument('--java-home',type=Path,required=True,help='Compiler for the external test agent only') p.add_argument('--target-java-home',type=Path,required=True) p.add_argument('--mode',choices=['all','classpath','modulepath','invalid-configuration','missing-runtime'],default='all') a=p.parse_args() manifest=json.loads((a.app.parent/'package-manifest.json').read_text()) for name,digest in manifest['files'].items(): assert hashlib.sha256((a.app/name).read_bytes()).hexdigest()==digest, name assert list((a.app/'Contents/runtime').rglob('legal')), 'Runtime legal notices missing' evidence=Path(tempfile.mkdtemp(prefix='designer-package-check-')).resolve() print('Evidence: '+str(evidence),flush=True) app=evidence/'relocated application with spaces'/'NeitzelFXDesigner.app' app.parent.mkdir() subprocess.run(['/usr/bin/ditto','-x','-k',a.app.parent/'NeitzelFXDesigner-macos.zip',app.parent],check=True) for name,digest in manifest['files'].items(): assert hashlib.sha256((app/name).read_bytes()).hexdigest()==digest, 'Archive differs: '+name launcher=app/'Contents/MacOS/NeitzelFXDesigner' env=dict(os.environ,JAVA_HOME='/does/not/exist',PATH='/usr/bin:/bin') for name in ('JAVA_TOOL_OPTIONS','JDK_JAVA_OPTIONS','_JAVA_OPTIONS','CLASSPATH'):env.pop(name,None) version=subprocess.run([launcher,'--version'],env=env,text=True,capture_output=True,check=True) assert str(app) in version.stdout,version assert 'Neitzel FX Designer' in subprocess.check_output([launcher,'--help'],env=env,text=True) invalid=subprocess.run([launcher,'--unknown'],env=env,text=True,capture_output=True) assert invalid.returncode==2 and 'Unknown' in invalid.stderr,invalid jars=list((app/'Contents/app').glob('*.jar')) checks=ROOT/'designer-kit-ui/target/designer-kit-ui-0.1.0-SNAPSHOT-tests.jar' classes=evidence/'agent-classes';classes.mkdir() subprocess.run([a.java_home/'bin/javac','-cp',os.pathsep.join(map(str,[*jars,checks])), '-d',classes, ROOT/'integration-tests/standalone/PackageProbe.java'],check=True) agent=evidence/'probe.jar' with zipfile.ZipFile(agent,'w') as out: out.writestr('META-INF/MANIFEST.MF','Manifest-Version: 1.0\nPremain-Class: qualification.PackageProbe\n\n') for file in classes.rglob('*.class'):out.write(file,str(file.relative_to(classes))) with zipfile.ZipFile(checks) as source: for name in source.namelist(): if name.endswith('.class'):out.writestr(name,source.read(name)) launcher_config=app/'Contents/app/NeitzelFXDesigner.cfg' base_config=launcher_config.read_text() crashes=NativeCrashGuard(ROOT) for mode in (('classpath','modulepath','invalid-configuration','missing-runtime') if a.mode=='all' else (a.mode,)): output=evidence/mode;output.mkdir() fixture=output/'project' subprocess.run([sys.executable,ROOT/'scripts/prepare-fixture-host.py','--destination',fixture, '--target-java-home',a.target_java_home,'--mode','modulepath' if mode=='modulepath' else 'classpath'],check=True) document=fixture/'app/src/main/resources/fixture/components/Person.fxml' if mode=='missing-runtime': settings=fixture/'.neitzel-fx-designer/runtimes.json' values=json.loads(settings.read_text());values['runtimes']['java21']['build']='/missing/build-jdk' settings.write_text(json.dumps(values)) # Only target runtime settings and Maven come from the project; worker tools come from the app. config=fixture/'neitzel-fx-designer.json' if mode=='invalid-configuration':config.write_text('{"schemaVersion":"999"}') args=[launcher,'--configuration',config,'--settings',fixture/'.neitzel-fx-designer/runtimes.json','--maven',ROOT/'mvnw',document] # Instrument only the packaged host. Environment-wide JVM options would also # inject the agent into Maven and target JVMs, violating the test boundary. options=[f'-javaagent:{agent}',f'-Dpackage.evidence={output}',f'-Dpackage.document={document}',f'-Djavafx.cachedir={output / "native-cache"}'] if mode=='invalid-configuration':options.append('-Dpackage.sourceOnly=true') if mode=='missing-runtime':options.append('-Dpackage.missingRuntime=true') launcher_config.write_text(base_config+'\n'+''.join('java-options='+v+'\n' for v in options)) with (output/'launcher.log').open('w') as log: process=subprocess.Popen(list(map(str,args)),env=env,cwd=output,stdout=log,stderr=subprocess.STDOUT) try:code=process.wait(timeout=300) except subprocess.TimeoutExpired: process.terminate();process.wait(timeout=15);raise assert code==0 and (output/'SUCCESS.txt').exists(), (output,(output/'launcher.log').read_text()[-6000:]) for pid in (output/'workers.txt').read_text().splitlines(): until=time.monotonic()+10 while time.monotonic()