|
1 | 1 | import assert from 'node:assert/strict'; |
2 | 2 | import fs from 'node:fs'; |
| 3 | +import { spawnSync } from 'node:child_process'; |
3 | 4 | import path from 'node:path'; |
4 | 5 | import { fileURLToPath } from 'node:url'; |
5 | 6 | import { test } from 'vitest'; |
| 7 | +import { parse } from 'yaml'; |
| 8 | +import { mkdtempForTestSync } from '@agent-device/test-kit'; |
6 | 9 |
|
7 | 10 | // `.github/actions/setup-android-sdk` owns the pinned build-tools version and the sdkmanager |
8 | 11 | // install. The version decides the helper APK bytes, so a second copy can drift silently. |
@@ -39,3 +42,95 @@ test('setup-android-sdk lets a real licence failure stop the step', () => { |
39 | 42 | assert.doesNotMatch(action, /--licenses[^\n]*\|\|/); |
40 | 43 | assert.match(action, /AGENT_DEVICE_ANDROID_BUILD_TOOLS=\$BUILD_TOOLS_VERSION" >> "\$GITHUB_ENV"/); |
41 | 44 | }); |
| 45 | + |
| 46 | +function systemImageFixture(rootVariable: 'ANDROID_HOME' | 'ANDROID_SDK_ROOT') { |
| 47 | + const directory = mkdtempForTestSync('sdk-system-image-'); |
| 48 | + const sdkRoot = path.join(directory, 'sdk'); |
| 49 | + const image = path.join(sdkRoot, 'system-images/android-36/google_apis_playstore/x86_64'); |
| 50 | + const action = parse(fs.readFileSync(path.join(REPO_ROOT, OWNER_DIR, 'action.yml'), 'utf8')) as { |
| 51 | + inputs: { 'build-tools': { default: string } }; |
| 52 | + runs: { steps: { run: string }[] }; |
| 53 | + }; |
| 54 | + const version = action.inputs['build-tools'].default; |
| 55 | + const tools = path.join(sdkRoot, 'build-tools', version); |
| 56 | + fs.mkdirSync(tools, { recursive: true }); |
| 57 | + for (const tool of ['aapt', 'aapt2', 'd8', 'zipalign', 'apksigner']) |
| 58 | + fs.writeFileSync(path.join(tools, tool), '#!/bin/sh\nexit 0\n', { mode: 0o755 }); |
| 59 | + fs.mkdirSync(path.join(sdkRoot, 'platforms/android-36'), { recursive: true }); |
| 60 | + fs.writeFileSync(path.join(sdkRoot, 'platforms/android-36/android.jar'), ''); |
| 61 | + const manager = path.join(sdkRoot, 'cmdline-tools/latest/bin/sdkmanager'); |
| 62 | + fs.mkdirSync(path.dirname(manager), { recursive: true }); |
| 63 | + const calls = path.join(directory, 'calls'); |
| 64 | + fs.writeFileSync( |
| 65 | + manager, |
| 66 | + `#!/bin/bash |
| 67 | +set -euo pipefail |
| 68 | +printf '%s\\n' "$*" >> "$SDK_FIXTURE_CALLS" |
| 69 | +if [ "$1" = --install ]; then |
| 70 | + mkdir -p "$SDK_FIXTURE_IMAGE" |
| 71 | + printf 'Pkg.Revision=%s\\n' "$SDK_FIXTURE_REVISION" > "$SDK_FIXTURE_IMAGE/source.properties" |
| 72 | +fi |
| 73 | +`, |
| 74 | + { mode: 0o755 }, |
| 75 | + ); |
| 76 | + return { |
| 77 | + image, |
| 78 | + calls, |
| 79 | + run: (install: boolean, revision = '9') => { |
| 80 | + const output = path.join(directory, 'output'); |
| 81 | + fs.writeFileSync(output, ''); |
| 82 | + const result = spawnSync('bash', ['-c', action.runs.steps[0]!.run], { |
| 83 | + encoding: 'utf8', |
| 84 | + env: { |
| 85 | + ...process.env, |
| 86 | + ANDROID_HOME: rootVariable === 'ANDROID_HOME' ? sdkRoot : '', |
| 87 | + ANDROID_SDK_ROOT: |
| 88 | + rootVariable === 'ANDROID_SDK_ROOT' ? sdkRoot : path.join(directory, 'other'), |
| 89 | + BUILD_TOOLS_VERSION: version, |
| 90 | + INSTALL_SYSTEM_IMAGE: String(install), |
| 91 | + SDK_FIXTURE_CALLS: calls, |
| 92 | + SDK_FIXTURE_IMAGE: image, |
| 93 | + SDK_FIXTURE_REVISION: revision, |
| 94 | + GITHUB_OUTPUT: output, |
| 95 | + GITHUB_ENV: path.join(directory, 'env'), |
| 96 | + }, |
| 97 | + }); |
| 98 | + const outputs = new Map( |
| 99 | + fs |
| 100 | + .readFileSync(output, 'utf8') |
| 101 | + .trim() |
| 102 | + .split('\n') |
| 103 | + .map((line) => { |
| 104 | + const separator = line.indexOf('='); |
| 105 | + return [line.slice(0, separator), line.slice(separator + 1)] as const; |
| 106 | + }), |
| 107 | + ); |
| 108 | + return { ...result, outputs }; |
| 109 | + }, |
| 110 | + }; |
| 111 | +} |
| 112 | + |
| 113 | +test.each(['ANDROID_HOME', 'ANDROID_SDK_ROOT'] as const)( |
| 114 | + 'system image outputs follow %s and the installed revision', |
| 115 | + (rootVariable) => { |
| 116 | + const fixture = systemImageFixture(rootVariable); |
| 117 | + const initial = fixture.run(false); |
| 118 | + assert.equal(initial.status, 0, initial.stderr); |
| 119 | + assert.equal(initial.outputs.get('system-image-dir'), fixture.image); |
| 120 | + assert.equal(initial.outputs.has('system-image-revision'), false); |
| 121 | + assert.equal(fs.existsSync(fixture.calls), false); |
| 122 | + for (const revision of ['9', '10.1']) { |
| 123 | + const installed = fixture.run(true, revision); |
| 124 | + assert.equal(installed.status, 0, installed.stderr); |
| 125 | + assert.equal(installed.outputs.get('system-image-dir'), fixture.image); |
| 126 | + assert.equal(installed.outputs.get('system-image-revision'), revision); |
| 127 | + } |
| 128 | + }, |
| 129 | +); |
| 130 | + |
| 131 | +test('an invalid installed image revision cannot produce a cache identity', () => { |
| 132 | + const fixture = systemImageFixture('ANDROID_HOME'); |
| 133 | + const installed = fixture.run(true, 'invalid'); |
| 134 | + assert.equal(installed.status, 1); |
| 135 | + assert.equal(installed.outputs.has('system-image-revision'), false); |
| 136 | +}); |
0 commit comments