Skip to content

Commit 2ccfc63

Browse files
committed
TEMPORARY, dropped before review: round E, value three, the app must come from the Expo workflow of this commit
1 parent c5ec65b commit 2ccfc63

3 files changed

Lines changed: 421 additions & 0 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { execFileSync } from 'node:child_process';
2+
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3+
import { join } from 'node:path';
4+
import { isRunning } from '../src/core/exec.ts';
5+
import type {
6+
BuildKey,
7+
DeviceBackend,
8+
HostEntry,
9+
LocalLease,
10+
Platform,
11+
RuntimeProcess,
12+
ScratchPath,
13+
} from '../src/core/types.ts';
14+
import { APP_ID } from '../src/fixture.ts';
15+
import { host, metroPort } from '../src/host.ts';
16+
import { sdkTool } from '../src/platform/android/sdk.ts';
17+
18+
const SKILL = join(import.meta.dirname, '..');
19+
const WORKTREE = join(SKILL, '..', '..', '..');
20+
const STATE = join(SKILL, '.verify', 'ci-device-proof.json');
21+
22+
interface State {
23+
readonly lease: LocalLease;
24+
readonly entry: HostEntry;
25+
readonly processes: readonly RuntimeProcess[];
26+
}
27+
28+
const log = (line: string) => console.log(line);
29+
const seconds = (since: number) => `${Math.round((Date.now() - since) / 1000)}s`;
30+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
31+
32+
function backendFor(platform: Platform): DeviceBackend<LocalLease> {
33+
const backend = host.backends.find(b => b.kind === 'local' && b.platform === platform);
34+
if (backend === undefined) throw new Error(`no local ${platform} backend`);
35+
return backend as unknown as DeviceBackend<LocalLease>;
36+
}
37+
38+
const readState = (): State => JSON.parse(readFileSync(STATE, 'utf8')) as State;
39+
40+
async function metroStatus(lease: LocalLease): Promise<string> {
41+
const response = await fetch(`http://127.0.0.1:${metroPort(lease)}/status`, { signal: AbortSignal.timeout(5000) });
42+
return response.text();
43+
}
44+
45+
async function up(platform: Platform): Promise<void> {
46+
const backend = backendFor(platform);
47+
const availability = backend.availability();
48+
log(`local usable=${availability.usable} ${availability.why}`);
49+
if (!availability.usable) process.exit(1);
50+
const into = join(SKILL, '.verify', 'builds', 'proof') as ScratchPath;
51+
mkdirSync(into, { recursive: true });
52+
let started = Date.now();
53+
const app = await host.build(platform, `${platform}-proof0000000` as BuildKey, into, log);
54+
if (app.path === null) throw new Error('the build has no path');
55+
log(`build ${seconds(started)} ${app.source} ${app.path}`);
56+
started = Date.now();
57+
const acquired = await backend.acquire({
58+
platform,
59+
worktree: WORKTREE,
60+
waitSeconds: 0,
61+
app,
62+
progress: log,
63+
retryWith: 'proof',
64+
});
65+
log(`lease ${seconds(started)} ${acquired.deviceName} (${acquired.deviceId})`);
66+
mkdirSync(join(SKILL, '.verify', 'leases'), { recursive: true });
67+
const leaseFile = join(SKILL, '.verify', 'leases', `${platform}.json`);
68+
writeFileSync(leaseFile, JSON.stringify(acquired, null, 2));
69+
started = Date.now();
70+
const lease = await backend.install(acquired, app, log);
71+
writeFileSync(leaseFile, JSON.stringify(lease, null, 2));
72+
log(`install ${seconds(started)}`);
73+
log(`check ${await backend.check(lease)}`);
74+
if (host.runtime === undefined) throw new Error('the host has no runtime');
75+
started = Date.now();
76+
const runtime = await host.runtime(lease, log);
77+
log(`runtime ${seconds(started)} ${runtime.processes.map(p => `${p.what} ${p.pid}`).join(', ')}`);
78+
log(`entry ${JSON.stringify(runtime.entry)}`);
79+
writeFileSync(STATE, JSON.stringify({ lease, entry: runtime.entry, processes: runtime.processes }, null, 2));
80+
if (runtime.entry.kind === 'dev-client') log(`metro :${metroPort(lease)} ${(await metroStatus(lease)).trim()}`);
81+
}
82+
83+
async function alive(platform: Platform): Promise<void> {
84+
const state = readState();
85+
const held = await backendFor(platform).check(state.lease);
86+
log(`check ${held}`);
87+
let ok = held === 'held';
88+
for (const ref of state.processes) {
89+
const running = isRunning(ref);
90+
log(`${ref.what.padEnd(7)} pid ${ref.pid} ${running ? 'still running' : 'GONE'}`);
91+
ok &&= running;
92+
}
93+
if (state.entry.kind === 'dev-client') {
94+
const status = (await metroStatus(state.lease)).trim();
95+
log(`metro :${metroPort(state.lease)} ${status}`);
96+
ok &&= status.includes('packager-status:running');
97+
}
98+
if (!ok) {
99+
console.error('the device or the runtime did not outlive the step that started it');
100+
process.exit(1);
101+
}
102+
}
103+
104+
async function launch(platform: Platform, shot: string): Promise<void> {
105+
const { lease, entry } = readState();
106+
if (platform === 'ios') {
107+
const args = entry.kind === 'binary' ? ['-verifyScreen', 'home'] : entry.launchArguments;
108+
execFileSync('xcrun', ['simctl', 'launch', lease.deviceId, APP_ID, ...args], { stdio: 'inherit' });
109+
} else {
110+
const start =
111+
entry.kind === 'binary'
112+
? `am start -W -n ${APP_ID}/.MainActivity --es verifyScreen home`
113+
: `am start -a android.intent.action.VIEW -d '${entry.openLink}' -n ${APP_ID}/${entry.androidActivity}`;
114+
execFileSync(sdkTool('adb'), ['-s', lease.deviceId, 'shell', start], { stdio: 'inherit' });
115+
}
116+
for (const n of [1, 2]) {
117+
await sleep(entry.kind === 'binary' ? 20_000 : 45_000);
118+
const file = shot.replace(/\.png$/, `-${n}.png`);
119+
if (platform === 'ios') execFileSync('xcrun', ['simctl', 'io', lease.deviceId, 'screenshot', file], { stdio: 'inherit' });
120+
else
121+
writeFileSync(
122+
file,
123+
execFileSync(sdkTool('adb'), ['-s', lease.deviceId, 'exec-out', 'screencap', '-p'], {
124+
maxBuffer: 64 * 1024 * 1024,
125+
}),
126+
);
127+
log(`shot ${file} ${statSync(file).size} bytes`);
128+
}
129+
if (platform === 'android') {
130+
const logcat = execFileSync(sdkTool('adb'), ['-s', lease.deviceId, 'logcat', '-d', '-s', 'ReactNativeJS:*'], {
131+
encoding: 'utf8',
132+
maxBuffer: 64 * 1024 * 1024,
133+
});
134+
log('---- ReactNativeJS lines in logcat that name the missing key');
135+
log(logcat.split('\n').filter(line => line.includes('EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY')).slice(0, 3).join('\n'));
136+
}
137+
if (entry.kind === 'binary') return;
138+
const metroLog = join(SKILL, '.verify', 'runtime', `metro-${metroPort(lease)}.log`);
139+
log(`---- tail of ${metroLog}`);
140+
log(readFileSync(metroLog, 'utf8').split('\n').slice(-25).join('\n'));
141+
}
142+
143+
async function down(platform: Platform): Promise<void> {
144+
const state = readState();
145+
await backendFor(platform).release(state.lease);
146+
log(`released ${state.lease.deviceName}`);
147+
for (const ref of state.processes) {
148+
if (isRunning(ref)) process.kill(ref.pid, 'SIGTERM');
149+
log(`stopped ${ref.what} ${ref.pid}`);
150+
}
151+
}
152+
153+
const [verb, platformArg, extra] = process.argv.slice(2);
154+
if (platformArg !== 'ios' && platformArg !== 'android') {
155+
console.error('usage: ci-device-proof.ts up|alive|launch|down ios|android [screenshot.png]');
156+
process.exit(2);
157+
}
158+
try {
159+
if (verb === 'up') await up(platformArg);
160+
else if (verb === 'alive') await alive(platformArg);
161+
else if (verb === 'launch') await launch(platformArg, extra ?? 'launch.png');
162+
else if (verb === 'down') await down(platformArg);
163+
else throw new Error(`unknown verb ${verb}`);
164+
} catch (error) {
165+
console.error(error);
166+
process.exit(1);
167+
}

0 commit comments

Comments
 (0)