Skip to content

Commit 7475415

Browse files
authored
build: strip runner unit-test blocks from package (#1128)
* build: strip runner unit-test blocks from package * ci: fix package size and fallow checks * fix: resolve packaged recording scripts * fix: keep recording script resolver internal
1 parent ea69dc3 commit 7475415

10 files changed

Lines changed: 425 additions & 33 deletions

File tree

package.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"build:android-snapshot-helper": "sh ./scripts/build-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
9191
"package:android-snapshot-helper": "sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
9292
"package:android-snapshot-helper:npm": "rm -rf android-snapshot-helper/dist && sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android-snapshot-helper/dist",
93+
"package:apple-runner:npm": "node scripts/package-apple-runner-source.mjs",
9394
"build:android-multitouch-helper": "sh ./scripts/build-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper",
9495
"package:android-multitouch-helper": "sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper",
9596
"package:android-multitouch-helper:npm": "rm -rf android-multitouch-helper/dist && sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") android-multitouch-helper/dist",
@@ -116,7 +117,7 @@
116117
"check:tooling": "pnpm lint && pnpm typecheck && pnpm check:layering && pnpm check:mcp-metadata && pnpm build",
117118
"check:unit": "pnpm test:unit && pnpm test:smoke",
118119
"check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
119-
"prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm",
120+
"prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:apple-runner:npm && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm",
120121
"typecheck": "tsgo -p tsconfig.json",
121122
"test-app:install": "pnpm install --dir examples/test-app --ignore-workspace",
122123
"test-app:start": "pnpm --dir examples/test-app start",
@@ -149,13 +150,6 @@
149150
"files": [
150151
"bin",
151152
"dist",
152-
"apple-runner",
153-
"!apple-runner/**/.build",
154-
"!apple-runner/**/.swiftpm",
155-
"!apple-runner/**/xcuserdata",
156-
"!apple-runner/**/*.xcuserstate",
157-
"!apple-runner/README.md",
158-
"!apple-runner/RUNNER_PROTOCOL.md",
159153
"macos-helper",
160154
"!macos-helper/**/.build",
161155
"android-snapshot-helper/dist",
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env node
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
const UNIT_TEST_CONDITION = 'AGENT_DEVICE_RUNNER_UNIT_TESTS';
7+
const SOURCE_DIR = path.join('apple-runner');
8+
const OUTPUT_DIR = path.join('dist', 'apple-runner');
9+
const SKIPPED_DIR_NAMES = new Set(['.build', '.swiftpm', 'xcuserdata']);
10+
const SKIPPED_ROOT_FILES = new Set(['README.md', 'RUNNER_PROTOCOL.md']);
11+
12+
export function packageAppleRunnerSource(options = {}) {
13+
const root = path.resolve(options.root ?? process.cwd());
14+
const sourceRoot = path.join(root, SOURCE_DIR);
15+
const outputRoot = path.join(root, OUTPUT_DIR);
16+
if (!fs.existsSync(sourceRoot)) {
17+
throw new Error(`Apple runner source not found at ${sourceRoot}`);
18+
}
19+
20+
fs.rmSync(outputRoot, { recursive: true, force: true });
21+
const summary = {
22+
outputRoot,
23+
copiedFiles: 0,
24+
strippedFiles: 0,
25+
strippedBlocks: 0,
26+
};
27+
28+
copyDirectory(sourceRoot, outputRoot, '', summary);
29+
return summary;
30+
}
31+
32+
export function stripRunnerUnitTestBlocks(source, filePath = '<swift source>') {
33+
const lines = source.match(/[^\n]*\n|[^\n]+/g) ?? [];
34+
const state = {
35+
output: [],
36+
strippedBlocks: 0,
37+
skippedDepth: 0,
38+
};
39+
40+
for (const line of lines) {
41+
consumeSwiftLine(state, line);
42+
}
43+
44+
if (state.skippedDepth !== 0) {
45+
throw new Error(`Unterminated ${UNIT_TEST_CONDITION} block in ${filePath}`);
46+
}
47+
48+
return {
49+
contents: state.output.join(''),
50+
strippedBlocks: state.strippedBlocks,
51+
};
52+
}
53+
54+
function consumeSwiftLine(state, line) {
55+
if (state.skippedDepth > 0) {
56+
consumeSkippedConditionalLine(state, line);
57+
return;
58+
}
59+
if (isRunnerUnitTestBlockStart(line)) {
60+
state.skippedDepth = 1;
61+
state.strippedBlocks += 1;
62+
return;
63+
}
64+
state.output.push(line);
65+
}
66+
67+
function consumeSkippedConditionalLine(state, line) {
68+
if (isConditionalStart(line)) {
69+
state.skippedDepth += 1;
70+
}
71+
if (isConditionalEnd(line)) {
72+
state.skippedDepth -= 1;
73+
}
74+
}
75+
76+
function copyDirectory(sourceDir, outputDir, relativeDir, summary) {
77+
fs.mkdirSync(outputDir, { recursive: true });
78+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
79+
80+
for (const entry of entries) {
81+
copyDirectoryEntry(entry, sourceDir, outputDir, relativeDir, summary);
82+
}
83+
}
84+
85+
function copyDirectoryEntry(entry, sourceDir, outputDir, relativeDir, summary) {
86+
const relativePath = path.join(relativeDir, entry.name);
87+
if (shouldSkipEntry(entry, relativePath)) {
88+
return;
89+
}
90+
91+
const sourcePath = path.join(sourceDir, entry.name);
92+
const outputPath = path.join(outputDir, entry.name);
93+
if (entry.isDirectory()) {
94+
copyDirectory(sourcePath, outputPath, relativePath, summary);
95+
return;
96+
}
97+
if (entry.isFile()) {
98+
copyFile(sourcePath, outputPath, summary);
99+
}
100+
}
101+
102+
function copyFile(sourcePath, outputPath, summary) {
103+
if (path.extname(sourcePath) !== '.swift') {
104+
fs.copyFileSync(sourcePath, outputPath);
105+
summary.copiedFiles += 1;
106+
return;
107+
}
108+
109+
const source = fs.readFileSync(sourcePath, 'utf8');
110+
const stripped = stripRunnerUnitTestBlocks(source, sourcePath);
111+
fs.writeFileSync(outputPath, stripped.contents);
112+
summary.copiedFiles += 1;
113+
if (stripped.strippedBlocks > 0) {
114+
summary.strippedFiles += 1;
115+
summary.strippedBlocks += stripped.strippedBlocks;
116+
}
117+
}
118+
119+
function shouldSkipEntry(entry, relativePath) {
120+
return shouldSkipDirectory(entry) || shouldSkipFile(entry, relativePath);
121+
}
122+
123+
function shouldSkipDirectory(entry) {
124+
return entry.isDirectory() && SKIPPED_DIR_NAMES.has(entry.name);
125+
}
126+
127+
function shouldSkipFile(entry, relativePath) {
128+
return entry.isFile() && (isXcodeUserStateFile(entry) || isSkippedRootFile(entry, relativePath));
129+
}
130+
131+
function isXcodeUserStateFile(entry) {
132+
return entry.name.endsWith('.xcuserstate');
133+
}
134+
135+
function isSkippedRootFile(entry, relativePath) {
136+
return !relativePath.includes(path.sep) && SKIPPED_ROOT_FILES.has(entry.name);
137+
}
138+
139+
function isRunnerUnitTestBlockStart(line) {
140+
return new RegExp(`^\\s*#if\\s+${UNIT_TEST_CONDITION}(?:\\b|$)`).test(line);
141+
}
142+
143+
function isConditionalStart(line) {
144+
return /^\s*#if\b/.test(line);
145+
}
146+
147+
function isConditionalEnd(line) {
148+
return /^\s*#endif\b/.test(line);
149+
}
150+
151+
function parseArgs(argv) {
152+
const parsed = { root: process.cwd(), quiet: false };
153+
let index = 0;
154+
while (index < argv.length) {
155+
index = parseArg(argv, index, parsed);
156+
}
157+
return parsed;
158+
}
159+
160+
function parseArg(argv, index, parsed) {
161+
const arg = argv[index];
162+
if (arg === '--quiet') {
163+
parsed.quiet = true;
164+
return index + 1;
165+
}
166+
if (arg === '--root') {
167+
return parseRootArg(argv, index, parsed);
168+
}
169+
throw new Error(`Unknown argument: ${arg}`);
170+
}
171+
172+
function parseRootArg(argv, index, parsed) {
173+
const value = argv[index + 1];
174+
if (!value || value.startsWith('--')) {
175+
throw new Error('--root requires a path');
176+
}
177+
parsed.root = value;
178+
return index + 2;
179+
}
180+
181+
function isMainModule() {
182+
return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
183+
}
184+
185+
if (isMainModule()) {
186+
const options = parseArgs(process.argv.slice(2));
187+
const summary = packageAppleRunnerSource(options);
188+
if (!options.quiet) {
189+
const relativeOutput = path.relative(path.resolve(options.root), summary.outputRoot);
190+
console.log(
191+
`Packaged Apple runner source at ${relativeOutput} ` +
192+
`(${summary.copiedFiles} files, stripped ${summary.strippedBlocks} unit-test blocks).`,
193+
);
194+
}
195+
}

scripts/size-report.mjs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ function collectReport(root, options) {
105105
if (jsFiles.length === 0) {
106106
throw new Error('No dist/src JavaScript files found. Run `pnpm build` before measuring size.');
107107
}
108+
prepareGeneratedPackageAssets(root);
108109

109110
const chunks = jsFiles
110111
.map((file) => {
@@ -132,11 +133,24 @@ function collectReport(root, options) {
132133
generatedAt: new Date().toISOString(),
133134
js,
134135
npmPack: collectNpmPack(root),
135-
...(options.startupRuns > 0 ? { startup: collectStartupBenchmarks(root, options.startupRuns) } : {}),
136+
...(options.startupRuns > 0
137+
? { startup: collectStartupBenchmarks(root, options.startupRuns) }
138+
: {}),
136139
chunks: chunks.slice(0, 20),
137140
};
138141
}
139142

143+
function prepareGeneratedPackageAssets(root) {
144+
const packageAppleRunnerScript = path.join(root, 'scripts', 'package-apple-runner-source.mjs');
145+
if (!fs.existsSync(packageAppleRunnerScript)) {
146+
return;
147+
}
148+
execFileSync(process.execPath, [packageAppleRunnerScript, '--quiet'], {
149+
cwd: root,
150+
stdio: ['ignore', 'ignore', 'inherit'],
151+
});
152+
}
153+
140154
function collectStartupBenchmarks(root, runs) {
141155
return {
142156
runs,
@@ -297,7 +311,9 @@ function formatDiff(base, current) {
297311

298312
function formatStartupBenchmarks(startup, baseStartup) {
299313
if (!startup) return '';
300-
const baseByName = new Map((baseStartup?.benchmarks ?? []).map((benchmark) => [benchmark.name, benchmark]));
314+
const baseByName = new Map(
315+
(baseStartup?.benchmarks ?? []).map((benchmark) => [benchmark.name, benchmark]),
316+
);
301317
const rows = startup.benchmarks.map((benchmark) => {
302318
const base = baseByName.get(benchmark.name);
303319
return `| ${benchmark.name} | ${formatMaybeMs(base?.medianMs)} | ${formatMs(benchmark.medianMs)} | ${formatMsDiff(base?.medianMs, benchmark.medianMs)} |`;
@@ -369,7 +385,9 @@ function readGitHubCommentConfig(explicitPrNumber) {
369385
function assertGitHubCommentConfig(token, repository, prNumber) {
370386
for (const value of [token, repository, prNumber]) {
371387
if (!value) {
372-
throw new Error('GITHUB_TOKEN, GITHUB_REPOSITORY, and PR number are required to post a comment.');
388+
throw new Error(
389+
'GITHUB_TOKEN, GITHUB_REPOSITORY, and PR number are required to post a comment.',
390+
);
373391
}
374392
}
375393
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import assert from 'node:assert/strict';
2+
import fs from 'node:fs';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
import { onTestFinished, test } from 'vitest';
7+
import { runCmd } from '../utils/exec.ts';
8+
9+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
10+
const packageScript = path.join(repoRoot, 'scripts', 'package-apple-runner-source.mjs');
11+
12+
test('package apple runner source strips unit-test blocks without mutating checkout source', async () => {
13+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-package-'));
14+
onTestFinished(() => fs.rmSync(root, { recursive: true, force: true }));
15+
16+
writeFixtureFile(root, 'apple-runner/README.md', 'developer docs\n');
17+
writeFixtureFile(root, 'apple-runner/.build/cache.txt', 'cache\n');
18+
writeFixtureFile(
19+
root,
20+
'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj',
21+
'',
22+
);
23+
writeFixtureFile(
24+
root,
25+
'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate',
26+
'state\n',
27+
);
28+
writeFixtureFile(
29+
root,
30+
'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
31+
[
32+
'extension RunnerTests {',
33+
' func runtimeHelper() {}',
34+
'#if AGENT_DEVICE_RUNNER_UNIT_TESTS',
35+
' func unitOnlyHelper() {',
36+
' #if os(iOS)',
37+
' print("nested platform guard should disappear with the unit-test block")',
38+
' #endif',
39+
' }',
40+
'#endif',
41+
' #if os(macOS)',
42+
' func macOnlyRuntimeHelper() {}',
43+
' #endif',
44+
'}',
45+
'',
46+
].join('\n'),
47+
);
48+
49+
await runCmd(process.execPath, [packageScript, '--root', root, '--quiet']);
50+
51+
const sourceSwiftPath = path.join(
52+
root,
53+
'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
54+
);
55+
const packagedSwiftPath = path.join(
56+
root,
57+
'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift',
58+
);
59+
const sourceSwift = fs.readFileSync(sourceSwiftPath, 'utf8');
60+
const packagedSwift = fs.readFileSync(packagedSwiftPath, 'utf8');
61+
62+
assert.match(sourceSwift, /AGENT_DEVICE_RUNNER_UNIT_TESTS/);
63+
assert.doesNotMatch(packagedSwift, /AGENT_DEVICE_RUNNER_UNIT_TESTS/);
64+
assert.doesNotMatch(packagedSwift, /unitOnlyHelper/);
65+
assert.match(packagedSwift, /runtimeHelper/);
66+
assert.match(packagedSwift, /#if os\(macOS\)/);
67+
assert.ok(
68+
fs.existsSync(
69+
path.join(root, 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj'),
70+
),
71+
);
72+
assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/README.md')), false);
73+
assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/.build/cache.txt')), false);
74+
assert.equal(
75+
fs.existsSync(
76+
path.join(
77+
root,
78+
'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate',
79+
),
80+
),
81+
false,
82+
);
83+
});
84+
85+
function writeFixtureFile(root: string, relativePath: string, contents: string): void {
86+
const filePath = path.join(root, relativePath);
87+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
88+
fs.writeFileSync(filePath, contents);
89+
}

0 commit comments

Comments
 (0)