import itertools import json import os from pathlib import Path import shlex import shutil import subprocess import tempfile import unittest import yaml ROOT = Path(__file__).resolve().parents[2] class CiChecksTest(unittest.TestCase): def test_pr_cache_writes_and_source_timestamp_policy(self): jobs = yaml.safe_load((ROOT / '.github/workflows/build.yml').read_text())['jobs'] cache = next(step for step in jobs['build']['steps'] if step.get('uses') == 'gradle/actions/setup-gradle@v4')['with'] self.assertEqual(cache['cache-read-only'], "${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}") gradle = (ROOT / 'app/build.gradle').read_text() self.assertIn("gitValue(['show', '-s', '--format=%ct', '--end-of-options', sourceCommit])", gradle) def test_report_comment_lookup_ignores_human_marker(self): if not shutil.which('jq'): self.skipTest('jq required to execute comment selection') script = (ROOT / 'tools/release/report-build.sh').read_text() line = next(line for line in script.splitlines() if line.startswith('ID=')) arguments = shlex.split(line[len('ID=$('):].split(' | head', 1)[0]) query = arguments[arguments.index('--jq') + 1].replace('$MARK', '') comments = [dict(id=1, user=dict(login='someone'), body=' forged'), dict(id=2, user=dict(login='github-actions[bot]'), body=' result')] result = subprocess.check_output(['jq', '-r', query], input=json.dumps(comments), text=True) self.assertEqual(result.strip(), '2') def test_build_and_tests_use_identical_generated_metadata(self): jobs = yaml.safe_load((ROOT / '.github/workflows/build.yml').read_text())['jobs'] steps = {step.get('id'): step for step in jobs['build']['steps']} self.assertEqual(steps['build']['env'], steps['unit-tests']['env']) self.assertIn('DROIDDECK_BUILD_LABEL', steps['unit-tests']['env']) self.assertIn('SGDB_API_KEY', steps['unit-tests']['env']) gradle = (ROOT / 'app/build.gradle').read_text() self.assertIn("outputs.doNotCacheIf('CI executes the test suite on every run') { true }", gradle) self.assertIn('outputs.upToDateWhen { false }', gradle) def test_independent_jobs_wait_only_for_the_pinned_source(self): jobs = yaml.safe_load((ROOT / '.github/workflows/build.yml').read_text())['jobs'] for name in ('xbox', 'components-runner'): self.assertEqual(jobs[name]['needs'], 'source') self.assertEqual(jobs['python-tests']['needs'], 'source') self.assertEqual(jobs['python-tests']['runs-on'], 'ubuntu-24.04') node = [step for step in jobs['python-tests']['steps'] if step.get('uses', '').startswith('actions/setup-node@')] self.assertEqual(len(node), 1) self.assertEqual(set(jobs['components']['needs']), {'source', 'components-runner'}) self.assertEqual(jobs['build-runner']['needs'], ['components']) self.assertEqual(set(jobs['build']['needs']), {'source', 'components', 'xbox', 'build-runner'}) def test_dependency_update_errors_block_installation(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) sudo = root / 'sudo' sudo.write_text('#!/bin/bash\nprintf "%s\\n" "$*" >> "$APT_LOG"\n' 'for arg; do case "$arg" in Dir::Etc::sourcelist=*) ' 'grep -h "^URIs:" "${arg#*=}" >> "$APT_LOG.mirrors";; esac; done\n' 'exit "${APT_RESULT:-0}"\n') sudo.chmod(0o755) query = root / 'dpkg-query' query.write_text('#!/bin/sh\nexit 1\n') query.chmod(0o755) dpkg = root / 'dpkg' dpkg.write_text('#!/bin/sh\necho "${DPKG_ARCH:-amd64}"\n') dpkg.chmod(0o755) log = root / 'apt.log' sources = root / 'ubuntu.sources' sources.write_text('URIs: http://mirror.invalid/ubuntu\n') env = dict(os.environ, PATH=str(root) + os.pathsep + os.environ['PATH'], APT_LOG=str(log), UBUNTU_SOURCES_FILE=str(sources)) installer = ROOT / 'tools/ci/install-ubuntu-packages.sh' result = subprocess.run(['bash', str(installer), 'qemu-user'], env=dict(env, APT_RESULT='17')) self.assertEqual(result.returncode, 17) self.assertEqual(len(log.read_text().splitlines()), 3) mirrors = Path(str(log) + '.mirrors') self.assertEqual(mirrors.read_text().splitlines(), [ 'URIs: http://mirror.invalid/ubuntu', 'URIs: https://archive.ubuntu.com/ubuntu', 'URIs: https://mirrors.edge.kernel.org/ubuntu', ]) log.unlink() mirrors.unlink() result = subprocess.run(['bash', str(installer), 'qemu-user'], env=dict(env, APT_RESULT='17', DPKG_ARCH='arm64')) self.assertEqual(result.returncode, 17) self.assertEqual(mirrors.read_text().splitlines()[1:], [ 'URIs: https://ports.ubuntu.com/ubuntu-ports', 'URIs: https://mirrors.ocf.berkeley.edu/ubuntu-ports', ]) log.unlink() mirrors.unlink() subprocess.run(['bash', str(installer), 'qemu-user'], env=env, check=True) update, install = log.read_text().splitlines() for command in (update, install): self.assertIn('Dir::Etc::sourceparts=-', command) self.assertIn(f'Dir::Etc::sourcelist={sources}', command) self.assertIn('Acquire::https::Timeout=15', command) self.assertIn('Acquire::Retries=1', command) self.assertIn('APT::Update::Error-Mode=any', update) self.assertIn('install -y -q --no-install-recommends qemu-user', install) log.unlink() absent = dict(env, UBUNTU_SOURCES_FILE=str(root / 'missing.sources'), APT_RESULT='17') result = subprocess.run(['bash', str(installer), 'qemu-user'], env=absent) self.assertEqual(result.returncode, 1) self.assertEqual(len(log.read_text().splitlines()), 1) self.assertNotIn('Dir::Etc::sourceparts', log.read_text()) def test_required_status_rejects_failed_cancelled_or_skipped_test_jobs(self): jobs = yaml.safe_load((ROOT / '.github/workflows/build.yml').read_text())['jobs'] gate = jobs['checks'] self.assertEqual(gate['name'], 'Compile and test APK') self.assertEqual(set(gate['needs']), {'build', 'python-tests'}) self.assertIn('!cancelled()', gate['if']) for build, python in itertools.product(('success', 'failure', 'cancelled', 'skipped'), repeat=2): with self.subTest(build=build, python=python): result = subprocess.run( ['bash', '-c', gate['steps'][0]['run']], env=dict(os.environ, BUILD_RESULT=build, PYTHON_RESULT=python), capture_output=True) self.assertEqual(result.returncode == 0, build == python == 'success') for publisher in ('sign', 'packs'): self.assertIn('checks', jobs[publisher]['needs'])