Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions test/integration/android-emulator-e2e/live-harness.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { execFile } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { promisify } from 'node:util';

import {
createLiveDeviceContext,
Expand Down Expand Up @@ -37,9 +39,68 @@ export function createContext(): LiveContext {
};
}

const execFileAsync = promisify(execFile);

/**
* What the OS says about rotation when a step fails: the two settings `orientation` writes, the
* display's current rotation, and every WindowManager rotation decision logcat still holds (with
* the reason it gives). Read through adb, not agent-device, so it stands even when the CLI path
* is what failed.
*/
async function readAndroidRotationEvidence(context: LiveContext): Promise<string> {
const probes: readonly [string, string[]][] = [
['accelerometer_rotation', ['shell', 'settings', 'get', 'system', 'accelerometer_rotation']],
['user_rotation', ['shell', 'settings', 'get', 'system', 'user_rotation']],
['display rotation', ['shell', 'dumpsys', 'display']],
['logcat rotation decisions', ['logcat', '-d', '-v', 'time']],
];
const sections: string[] = [];
for (const [title, args] of probes) {
try {
const { stdout } = await execFileAsync('adb', ['-s', context.serial, ...args], {
maxBuffer: 64 * 1024 * 1024,
timeout: 20_000,
});
sections.push(`## ${title}\n${selectRotationLines(title, stdout)}`);
} catch (error) {
sections.push(
`## ${title}\n(failed: ${error instanceof Error ? error.message : String(error)})`,
);
}
}
return `${sections.join('\n\n')}\n`;
}

function selectRotationLines(title: string, output: string): string {
if (title === 'display rotation') {
return output
.split('\n')
.filter((line) =>
/mCurrentOrientation|mRotation=|installOrientation|\brotation \d/.test(line),
)
.map((line) => line.trim().slice(0, 200))
.slice(0, 8)
.join('\n');
}
if (title === 'logcat rotation decisions') {
return output
.split('\n')
.filter(
(line) =>
/(WindowManager|DisplayRotation|WindowOrientationListener|RotationResolver|DisplayContent|SensorService)/.test(
line,
) && /rotat|orient/i.test(line),
)
.slice(-60)
.join('\n');
}
return output.trim();
}

const harness = createLiveDeviceHarness<LiveContext, AndroidEmulatorBehaviorId>({
behaviorsForScenario: liveBehaviorsForScenario,
commandsForScenario: liveCommandsForScenario,
deviceEvidence: readAndroidRotationEvidence,
commonFlags: (context, args) => [
...args,
'--platform',
Expand Down
21 changes: 19 additions & 2 deletions test/integration/live-device-e2e/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ type HarnessOptions<Context, BehaviorId extends string> = {
env: NodeJS.ProcessEnv,
options?: { timeoutMs?: number },
) => Promise<CliJsonResult>;
/**
* Platform-owned device facts for a failed step (rotation state, system logs), read outside
* agent-device so they describe the device even when the CLI path is what failed. Best-effort:
* a throw or undefined records nothing.
*/
deviceEvidence?: (context: Context) => Promise<string | undefined>;
writeCoverageReport: (context: Context) => void;
};

Expand Down Expand Up @@ -176,6 +182,7 @@ export function createLiveDeviceHarness<
`artifacts: ${context.artifactDir}`,
`screenshot: ${evidence.screenshotPath ?? '(capture failed)'}`,
`snapshot: ${evidence.snapshotPath ?? '(capture failed)'}`,
`device: ${evidence.devicePath ?? '(not collected)'}`,
].join('\n');
fs.writeFileSync(path.join(context.artifactDir, 'failed-step.txt'), message);
assert.fail(message);
Expand All @@ -191,12 +198,22 @@ export function createLiveDeviceHarness<
*/
async function captureFailedStepEvidence(
context: Context,
): Promise<{ screenshotPath?: string; snapshotPath?: string }> {
): Promise<{ screenshotPath?: string; snapshotPath?: string; devicePath?: string }> {
const stem = path.join(context.artifactDir, `failed-step-${context.stepHistory.length}`);
const screenshotPath = `${stem}.png`;
const snapshotPath = `${stem}-snapshot.json`;
const devicePath = `${stem}-device.txt`;
const runCli = options.runCli ?? runBuiltCliJson;
const evidence: { screenshotPath?: string; snapshotPath?: string } = {};
const evidence: { screenshotPath?: string; snapshotPath?: string; devicePath?: string } = {};
try {
const facts = await options.deviceEvidence?.(context);
if (facts !== undefined) {
fs.writeFileSync(devicePath, facts);
evidence.devicePath = devicePath;
}
} catch {
// evidence only
}
try {
const screenshot = await runCli(
options.commonFlags(context, ['screenshot', screenshotPath]),
Expand Down
Loading