mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
d07b837621
Every declared AgentDeviceRunnerUITests method now belongs to a lane, and the #if guard is the classification: AGENT_DEVICE_RUNNER_UNIT_TESTS alone means a pure runner decision (runs on the macOS host on every PR — ci.yml's existing compile job now executes the bundle it builds), '&& os(iOS)' means runner/XCTest semantics (simulator lanes only). check:xctest-selection evaluates the guards per platform, derives each lane's reach, and fails on a flagged identifier that is undeclared or uncompiled on that lane, on a declared test no lane reaches (found the two tvOS-only tests, dark since birth — widened to os(tvOS) || os(macOS)), and on testCommand reaching any lane. The host and nightly lanes assert executed == derived reach, so a missing -D flag or a guard that compiles a file out reads red, not as a smaller green. One duplicate test deleted (sparse-verdict assertions folded into its twin).
88 lines
3.7 KiB
TypeScript
88 lines
3.7 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 in 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[] {
|
|
return fs
|
|
.readdirSync(directory)
|
|
.filter((entry) => SWIFT_SOURCE.test(entry))
|
|
.sort()
|
|
.map((entry) => ({
|
|
file: entry,
|
|
text: fs.readFileSync(path.join(directory, entry), 'utf8'),
|
|
}));
|
|
}
|
|
|
|
/** 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)),
|
|
}));
|
|
}
|