#!/usr/bin/env python3 """Exercise the actual installer with real Compose parsing and failing Docker transports.""" import json import os from pathlib import Path import shutil import subprocess import tempfile root = Path(__file__).resolve().parent.parent real_docker = shutil.which('docker') with tempfile.TemporaryDirectory(prefix='easyai-install-failures-') as temporary: base = Path(temporary) for phase in ('storage', 'manifest', 'pull', 'health'): project = base / phase project.mkdir() for name in ('scripts', 'system-update'): shutil.copytree(root / name, project / name, ignore=shutil.ignore_patterns('__pycache__')) for name in ('start.sh', 'docker-compose.yml', 'easyai-proxy.conf.sample'): shutil.copy2(root / name, project / name) for sample in root.glob('.env*.sample'): shutil.copy2(sample, project / sample.name) sample = project / '.env.sample' sample.write_text(sample.read_text().replace('UPDATE_MANIFEST_SOURCE=remote', 'UPDATE_MANIFEST_SOURCE=local')) binaries = project / 'bin' binaries.mkdir() commands = project / 'commands.jsonl' docker = binaries / 'docker' docker.write_text('''#!/usr/bin/env python3 import json,os,subprocess,sys args=sys.argv[1:] with open(os.environ['TEST_COMMANDS'],'a') as log: log.write(json.dumps(args)+'\\n') if args[:1]==['--config']: args=args[2:] if args[:2]==['compose','config'] or args[:2]==['compose','version']: sys.exit(subprocess.call([os.environ['REAL_DOCKER'],*args])) if args[:2]==['manifest','inspect']: if os.environ['TEST_PHASE']=='manifest': print('manifest unknown',file=sys.stderr);sys.exit(1) print(json.dumps({'Descriptor':{'platform':{'os':'linux','architecture':'amd64'}}}));sys.exit(0) if args[:2]==['compose','pull']: sys.exit(18 if os.environ['TEST_PHASE']=='pull' else 0) if args[:3]==['compose','ps','-q']: print('test-'+args[-1]);sys.exit(0) if args[:1]==['inspect']: if '--format' in args: print('true|unhealthy');sys.exit(0) sys.exit(1) if args[:2]==['container','inspect']: if os.environ['TEST_PHASE']=='storage' and args[-1]=='easyai-server': print(json.dumps([{'Config':{'Labels':{'com.docker.compose.project':'unrelated-project'}},'Mounts':[]}])) sys.exit(0) sys.exit(1) sys.exit(0) ''') docker.chmod(0o755) for name, code in [('sudo', '#!/bin/sh\nexec "$@"\n'), ('curl', '#!/bin/sh\nexit 0\n')]: path = binaries / name path.write_text(code) path.chmod(0o755) env = {k: v for k, v in os.environ.items() if not k.startswith(('UPDATE_', 'DEPLOY_', 'COMPOSE_'))} env.update({'PATH': str(binaries) + os.pathsep + env['PATH'], 'REAL_DOCKER': real_docker, 'TEST_COMMANDS': str(commands), 'TEST_PHASE': phase, 'CI': 'true', 'DEPLOY_ACCESS': 'ip', 'DEPLOY_IP': '127.0.0.1', 'DEPLOY_NON_INTERACTIVE': '1', 'DEPLOY_HEALTH_TIMEOUT_SECONDS': '1'}) result = subprocess.run(['bash', 'start.sh'], cwd=project, env=env, capture_output=True, text=True, errors='replace', timeout=120) assert result.returncode != 0, (phase, result.stdout, result.stderr) assert '应用启动成功' not in result.stdout and '部署完成' not in result.stdout, result.stdout calls = [json.loads(line) for line in commands.read_text().splitlines()] if phase in ('storage', 'manifest'): assert not any(call[:2] in (['compose', 'pull'], ['compose', 'up']) for call in calls) if phase == 'pull': assert ['compose', 'pull'] in calls, (result.stdout, result.stderr, calls) assert not any(call[:2] == ['compose', 'up'] for call in calls) if phase == 'health': assert ['compose', 'up', '-d'] in calls assert '部署未完成' in result.stdout + result.stderr print(phase + ': installer failed without reporting success or continuing to the next stage')