91 lines
5.9 KiB
Python
91 lines
5.9 KiB
Python
#!/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()<until:
|
|
try:os.kill(int(pid),0)
|
|
except ProcessLookupError:break
|
|
time.sleep(.1)
|
|
else:raise AssertionError('Worker survived packaged host: '+pid)
|
|
expected={}
|
|
for jar in jars:
|
|
with zipfile.ZipFile(jar) as archive:
|
|
for name in archive.namelist():
|
|
if name.endswith('.dylib'):expected.setdefault(Path(name).name,set()).add(hashlib.sha256(archive.read(name)).hexdigest())
|
|
natives=list((output/'native-cache').rglob('*.dylib'))
|
|
assert any(f.name=='libglass.dylib' for f in natives), 'Packaged JavaFX natives were not extracted'
|
|
for file in natives:assert hashlib.sha256(file.read_bytes()).hexdigest() in expected.get(file.name,set()), file
|
|
crashes.check()
|
|
print('PASS packaged '+mode,flush=True)
|
|
print('Evidence: '+str(evidence),flush=True)
|