Files
Michał Pierzchała 5676d5ff8a refactor(apple): drop dead runner code and collapse duplicated helpers (#1936)
* refactor(apple): drop dead runner code and collapse duplicated helpers

Removes declarations with no consumers (findScopeElement, interactiveTypes,
two unused PresentedNode convenience inits) and collapses copy-pasted logic:
DataPayload now relies on the synthesized memberwise init, TvRemoteButton is
String-raw-valued, point-hit sorting shares smallestElementFirst, command-id
trim-or-nil lives once on RunnerCommandJournal, scroll/desktopScroll share
direction and durationMs validators, and the seven inline NSError refusals use
unsupportedOperationError. elementTypeName reads a table pinned by the
visibility-fold parity test.

The packager now skips files whose unit-test blocks were their whole body, so
10 test-only files stop shipping (and stop compiling on user machines) as
empty translation units.

Packaged Swift: 432.3 kB -> 427.8 kB; 56 files instead of 66. Net -144 lines.

* test(apple): pin skeleton-file exclusion in the packaging guard

The strip fixture always kept runtime content, so reverting the
skeleton-skip branch left every guard green. The new fixture's whole body is
unit-test blocks; the packaged path must be absent while a non-skeleton
sibling still ships. Observed red with the skip branch disabled before
re-enabling it.

* refactor(apple): tighten runner cleanup boundaries
2026-08-21 16:42:17 +02:00

98 lines
4.2 KiB
TypeScript

// What the runner's XCTest target declares, and on which platforms — the scan behind
// "which lane reaches what" (`check-xctest-selection.ts`), kept separate because it answers a
// different question: this module reads Swift sources, that one reads workflow manifests.
//
// The per-platform attribution is the whole point. A `func test…` is not simply "declared":
// it is declared *for the platforms whose `#if` guards let it compile*, which is what makes a
// test's guard its lane classification (#1781 A7).
import fs from 'node:fs';
import path from 'node:path';
import { activeSource, PLATFORMS, type Platform } from './swift-conditional-compilation.ts';
/** The XCTest target directory; its basename is the target name the identifiers use. */
export const RUNNER_TESTS_DIR = 'apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests';
// Every .swift file below the target directory is a member: the Xcode project uses a
// PBXFileSystemSynchronizedRootGroup, so membership is the directory, not a file list. A
// `RunnerTests*` name filter would miss RunnerTapPointPolicy.swift, which declares a real
// addressable test inside `extension RunnerTests`.
const SWIFT_SOURCE = /\.swift$/;
// One ordered pass over the source. A column-0 type declaration moves the enclosing type;
// a `func test…` indented exactly one level binds to it. Position carries the meaning
// rather than brace counting, which would have to know which `{` sits inside a string
// literal. It is also the more precise rule: only a method declared directly in a
// top-level `class`/`extension` block is addressable as `Target/Class/method`, so a
// helper type nested inside a test body (`final class ResultBox` — several of these
// exist) contributes no test identifiers, and neither does a closure-local `func test…`.
const DECLARATION =
/^(?:[\w@]+[ \t]+)*(?:class|extension|struct|enum|actor|protocol)[ \t]+([A-Za-z_]\w*)|^ {2}(?:[\w@]+[ \t]+)*func[ \t]+(test\w*)[ \t]*\(/gm;
export type SwiftSource = { readonly file: string; readonly text: string };
/** A declared method and the platforms its `#if` guards let it compile for. */
export type DeclaredTest = {
readonly identifier: string;
readonly platforms: readonly Platform[];
};
export function readSwiftSources(directory: string): SwiftSource[] {
const files: string[] = [];
collectSwiftSourcePaths(directory, '', files);
return files.sort().map((file) => ({
file,
text: fs.readFileSync(path.join(directory, file), 'utf8'),
}));
}
function collectSwiftSourcePaths(directory: string, relativeDirectory: string, files: string[]) {
const currentDirectory = path.join(directory, relativeDirectory);
for (const entry of fs.readdirSync(currentDirectory, { withFileTypes: true })) {
const relativePath = path.join(relativeDirectory, entry.name);
if (entry.isDirectory()) {
collectSwiftSourcePaths(directory, relativePath, files);
} else if (entry.isFile() && SWIFT_SOURCE.test(entry.name)) {
files.push(relativePath);
}
}
}
/** Every `Target/Class/method` identifier the sources declare, sorted, guards ignored. */
export function parseDeclaredTests(target: string, sources: readonly SwiftSource[]): string[] {
const declared = new Set<string>();
for (const source of sources) {
let enclosing = '';
for (const [, type, method] of source.text.matchAll(DECLARATION)) {
if (type !== undefined) enclosing = type;
else if (method !== undefined && enclosing) declared.add(`${target}/${enclosing}/${method}`);
}
}
return [...declared].sort();
}
/** The declared identifiers, each with the platforms whose unit-test build compiles it. */
export function parseDeclaredTestsByPlatform(
target: string,
sources: readonly SwiftSource[],
): DeclaredTest[] {
const compiled = new Map<Platform, Set<string>>(
PLATFORMS.map((platform) => [
platform,
new Set(
parseDeclaredTests(
target,
sources.map((source) => ({
file: source.file,
text: activeSource(source.text, platform, source.file),
})),
),
),
]),
);
return parseDeclaredTests(target, sources).map((identifier) => ({
identifier,
platforms: PLATFORMS.filter((platform) => compiled.get(platform)?.has(identifier)),
}));
}