"""Verify the background player-build flow: state machine + shallow git clone. Exercises the real functions against a local throwaway bare git repo so no network is needed, then confirms: * shallow clone produces a usable working tree * _run_git never blocks on credentials (GIT_TERMINAL_PROMPT=0) * a broken/partial checkout is detected and replaced, not reused * the background job moves idle -> running -> success and records the version * a bad repo URL ends in 'error', not a hang or a traceback Run: PYTHONPATH=$(pwd) ./.venv/bin/python docs/tools/test_player_build.py """ import os import shutil import subprocess import sys import tempfile import time TMP = tempfile.mkdtemp() os.environ['DATABASE_URL'] = f'sqlite:///{TMP}/pb.db' from app.app import create_app # noqa: E402 from app.extensions import db # noqa: E402 from app.utils import player_build as pb # noqa: E402 app = create_app('production') with app.app_context(): db.create_all() # ── Build a local origin repo (no network) ────────────────────────────────── ORIGIN = os.path.join(TMP, 'origin.git') SRC = os.path.join(TMP, 'src') subprocess.run(['git', 'init', '--bare', '-q', ORIGIN], check=True) subprocess.run(['git', 'init', '-q', SRC], check=True) for k, v in (('user.email', 't@t'), ('user.name', 'T')): subprocess.run(['git', '-C', SRC, 'config', k, v], check=True) os.makedirs(os.path.join(SRC, 'config'), exist_ok=True) open(os.path.join(SRC, 'config', 'app_config.json'), 'w').write('{}') open(os.path.join(SRC, 'main.py'), 'w').write('print("player")\n') subprocess.run(['git', '-C', SRC, 'add', '-A'], check=True) subprocess.run(['git', '-C', SRC, 'commit', '-qm', 'init'], check=True) subprocess.run(['git', '-C', SRC, 'branch', '-M', 'main'], check=True) subprocess.run(['git', '-C', SRC, 'remote', 'add', 'origin', ORIGIN], check=True) subprocess.run(['git', '-C', SRC, 'push', '-q', 'origin', 'main'], check=True) HEAD = subprocess.run(['git', '-C', SRC, 'rev-parse', '--short', 'HEAD'], capture_output=True, text=True).stdout.strip() # file:// forces a real transport so --depth is honoured (a local path clone # silently ignores it, which would make the shallow assertion meaningless). ORIGIN_URI = 'file://' + ORIGIN TARGET = os.path.join(TMP, 'staged') META = os.path.join(TMP, 'player_build.json') failures = [] def check(label, cond, detail=''): print(f" [{'PASS' if cond else 'FAIL'}] {label}" + (f' {detail}' if detail else '')) if not cond: failures.append(label) print('=' * 68) print('CASE 1 — fresh shallow clone') print('=' * 68) r = pb.build_player_files(TARGET, ORIGIN_URI, 'main') check('clone succeeded', r['success'], r['message']) check('version matches origin', r['version'] == HEAD, f"{r['version']} vs {HEAD}") check('working tree has files', os.path.isfile(os.path.join(TARGET, 'main.py'))) check('checkout reported usable', pb.is_valid_checkout(TARGET)) check('shallow (depth 1)', os.path.isfile(os.path.join(TARGET, '.git', 'shallow')), 'file:// transport honours --depth') print() print('=' * 68) print('CASE 2 — git never prompts for credentials') print('=' * 68) r = pb._run_git(['clone', '--depth', '1', 'https://127.0.0.1:1/nope/nope.git', os.path.join(TMP, 'nope')], timeout=20) check('unreachable repo returns fast (no hang)', r.returncode != 0, f'rc={r.returncode}') check('GIT_TERMINAL_PROMPT=0 is set', pb._git_env().get('GIT_TERMINAL_PROMPT') == '0') print() print('=' * 68) print('CASE 3 — broken/partial checkout is replaced, not reused') print('=' * 68) BROKEN = os.path.join(TMP, 'broken') shutil.rmtree(BROKEN, ignore_errors=True) os.makedirs(os.path.join(BROKEN, '.git'), exist_ok=True) # .git but no HEAD open(os.path.join(BROKEN, 'leftover.txt'), 'w').write('stale') check('broken dir is not considered usable', not pb.is_valid_checkout(BROKEN)) r = pb.build_player_files(BROKEN, ORIGIN_URI, 'main') check('build recovers from broken dir', r['success'], r['message']) check('stale file removed', not os.path.exists(os.path.join(BROKEN, 'leftover.txt'))) check('now a valid checkout', pb.is_valid_checkout(BROKEN)) print() print('=' * 68) print('CASE 4 — background job: idle -> running -> success') print('=' * 68) TARGET2 = os.path.join(TMP, 'staged2') pb._set_build_state(state='idle', step='', message='', version=None) check('starts idle', pb.get_build_state()['state'] == 'idle') with app.app_context(): started = pb.start_background_build( player_code_dir=TARGET2, repo_url=ORIGIN_URI, branch='main', config_payload={'server_ip': '192.168.0.152', 'port': '443', 'use_https': True, 'verify_ssl': False, 'orientation': 'Landscape', 'max_resolution': '1920x1080'}, meta_path=META, built_by='tester') check('build accepted', started is True) check('second start refused while running', pb.start_background_build(TARGET2, ORIGIN_URI, 'main', None, META, 'x') is False) for _ in range(120): if pb.get_build_state()['state'] in ('success', 'error'): break time.sleep(0.5) state = pb.get_build_state() check('finished successfully', state['state'] == 'success', state.get('message', '')) check('version recorded', state.get('version') == HEAD) check('config written to staged code', os.path.isfile(os.path.join(TARGET2, 'config', 'app_config.json'))) check('build settings persisted', os.path.isfile(META)) settings = pb.load_build_settings(META) or {} check('meta has server_ip', settings.get('server_ip') == '192.168.0.152', str(settings.get('server_ip'))) print() print('=' * 68) print('CASE 5 — bad repository URL ends in error (no hang, no traceback)') print('=' * 68) TARGET3 = os.path.join(TMP, 'staged3') pb._set_build_state(state='idle', step='', message='', version=None) with app.app_context(): pb.start_background_build(TARGET3, os.path.join(TMP, 'does-not-exist.git'), 'main', None, META, 'tester') for _ in range(120): if pb.get_build_state()['state'] in ('success', 'error'): break time.sleep(0.5) state = pb.get_build_state() check('reported as error', state['state'] == 'error') check('error has a message', bool(state.get('message')), state.get('message', '')[:70]) check('no partial dir left behind', not os.path.exists(TARGET3)) shutil.rmtree(TMP, ignore_errors=True) print() if failures: print(f'FAILED: {failures}') sys.exit(1) print('ALL PLAYER-BUILD CASES PASSED')