Build-time test discovery + static native test codegen (experimental, opt-in) - #3197
Build-time test discovery + static native test codegen (experimental, opt-in)#3197piotruela wants to merge 22 commits into
Conversation
🚀 Preview Deployment Ready!Preview URL: https://pr-3197-patrol-docs.vercel.app Latest Deployment
This preview URL is stable and will be updated with each new commit to this PR. |
There was a problem hiding this comment.
Pull request overview
Adds opt-in build-time Patrol test discovery, static native test generation, and no-build test reruns for Android and iOS.
Changes:
- Generates manifests and native XCTest/JUnit tests.
- Adds configuration, selection, and
--no-buildCLI support. - Adds tests, documentation, and an end-to-end example.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
packages/patrol/lib/src/constants.dart |
Adds discovery-mode flag. |
packages/patrol/lib/src/common.dart |
Registers no-op tests during discovery. |
packages/patrol/darwin/patrol/Sources/PatrolImpl/PatrolTestManifest.swift |
Reads iOS manifests. |
packages/patrol/darwin/patrol/Sources/patrol/include/PatrolIntegrationTestIosRunner.h |
Adds static XCTest runner macros. |
packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolJUnitRunner.java |
Supports explicit skip values. |
packages/patrol_cli/test/general/pubspec_reader_test.dart |
Tests manifest configuration. |
packages/patrol_cli/test/crossplatform/test_manifest_test.dart |
Tests parsing and selectors. |
packages/patrol_cli/test/android/android_test_codegen_test.dart |
Tests JUnit generation. |
packages/patrol_cli/lib/src/test_bundler.dart |
Serializes discovery manifests. |
packages/patrol_cli/lib/src/runner/patrol_command.dart |
Defines new CLI options. |
packages/patrol_cli/lib/src/pubspec_reader.dart |
Reads the opt-in setting. |
packages/patrol_cli/lib/src/ios/xcode_test_codegen.dart |
Generates XCTest methods. |
packages/patrol_cli/lib/src/ios/ios_test_backend.dart |
Integrates iOS discovery and selection. |
packages/patrol_cli/lib/src/crossplatform/test_manifest.dart |
Models manifests and native names. |
packages/patrol_cli/lib/src/crossplatform/test_manifest_generator.dart |
Runs host-side discovery. |
packages/patrol_cli/lib/src/crossplatform/app_options.dart |
Builds discovery and filtered-run commands. |
packages/patrol_cli/lib/src/commands/test.dart |
Adds no-build execution flow. |
packages/patrol_cli/lib/src/commands/build_ios.dart |
Adds iOS opt-in support. |
packages/patrol_cli/lib/src/commands/build_android.dart |
Adds Android opt-in support. |
packages/patrol_cli/lib/src/android/android_test_codegen.dart |
Generates static JUnit classes. |
packages/patrol_cli/lib/src/android/android_test_backend.dart |
Integrates Android generation and reruns. |
docs/documentation/ci/meta.json |
Registers the documentation page. |
docs/documentation/ci/build-time-test-discovery.mdx |
Documents the feature. |
docs/cli-commands/test.mdx |
Documents no-build testing. |
docs/cli-commands/build.mdx |
Documents build-time discovery. |
dev/e2e_app/patrol_test/dynamic_test.dart |
Adds dynamic discovery examples. |
dev/e2e_app/ios/RunnerUITests/RunnerUITests.m |
Enables the static runner. |
dev/e2e_app/.gitignore |
Ignores generated native tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
packages/patrol_cli/lib/src/android/android_test_codegen.dart:194
- Although
_locateHostTestexplicitly accepts.kthosts, this regex only recognizes Java'sFooActivity.class. A Kotlin host usesFooActivity::class.java, so a supported custom activity is silently replaced withMainActivity, potentially producing uncompilable or incorrect generated tests. Parse both forms.
_HostActivity _parseActivity(String content) {
final match = RegExp(
r'setUp\(\s*([A-Za-z_][\w.]*)\.class',
).firstMatch(content);
final token = match?.group(1) ?? 'MainActivity';
Move iOS Dart test discovery out of the runtime +testInvocations
app-launch to build time, and generate real native XCTest methods from
the discovered list so they are statically discoverable (Xcode navigator,
test plans, per-test -only-testing sharding).
Pipeline (patrol build ios --emit-test-manifest):
- run the bundle on the host in discovery mode (flutter test +
PATROL_TEST_DISCOVERY) -> serialize the DartGroupEntry tree to a
manifest JSON. Same dart-defines as the build so names match.
- codegen reads the manifest and writes PatrolGeneratedTests.inc:
one - (void)test_<sanitized>_<index> per Dart test, with the verbatim
Dart name in the body -> [self patrolExecuteDartTest:@"..." skip:...].
Names stay byte-identical to runtime discovery (same listTestsFlat
flattening, incl. maxTestLength cropping).
- the .inc is #included between the new
PATROL_INTEGRATION_TEST_IOS_RUNNER_STATIC_BEGIN/END macros, so it
compiles into RunnerUITests without project.pbxproj changes.
Dart side: testDiscoveryEnabled guard in patrolTest/patrolSetUp/
patrolTearDown (register only, no PatrolBinding) so the bundle runs under
host flutter test; single-file bundle branches on the flag (the explorer
test writes the manifest and main() returns early to avoid a deadlock with
the automated binding).
Proven end-to-end on the simulator: all 20 generated selectors compile
into the .xctest binary; -only-testing runs an individual test in
isolation, mapping to the correct Dart test; skip flag is honored.
Notes / follow-ups:
- PatrolTestManifest.swift + the manifest-reading branch in the original
macro are the earlier "runtime manifest read" warm-up, now superseded
by static codegen and safe to drop.
- Parallel sharding on a single host (bluepill etc.) still needs
per-shard PatrolServer port isolation (see comment in ios_test_backend
_generateXcodeTests).
- dart:io/dart:convert in the shared bundle template still need a web guard.
Port the iOS build-time test discovery to Android: reuse the host
`flutter test` discovery run to produce a manifest, then generate static
JUnit test methods so each Dart test becomes a real, individually
selectable native test (and shows up under its own name in reports
instead of the parameterized `runDartTest[...]` wrapper).
Pipeline (patrol build/test android --emit-test-manifest):
- extract the manifest discovery (host `flutter test` +
PATROL_TEST_DISCOVERY) into a shared TestManifestGenerator, used by
both the iOS and Android backends.
- AndroidTestCodegen reads the manifest and writes PatrolGeneratedTests
.java into the app's androidTest source set (next to the host class,
reusing its package): one @test per Dart test, verbatim Dart name
embedded and handed to runDartTest(name, skip). Method names are
sanitized Java identifiers (also sidesteps the Orchestrator 1.5.0
whitespace limitation), disambiguated by index only on collision.
- the generated class is @RunWith(AndroidJUnit4.class) so
AndroidJUnitRunner enumerates its methods.
- execute() runs only the generated class via
-Pandroid.testInstrumentationRunnerArguments.class=<fqcn>, which also
stops the parameterized host class from doing its runtime-discovery
launch. Per-test selection (the Android analog of iOS -only-testing)
is then `...class=<fqcn>#<method>`.
PatrolJUnitRunner: add a skip-aware runDartTest(name, skip) overload used
by the generated code, and guard the original runDartTest(name) against a
null skip entry (it NPE'd when the skip map is empty, which is the case
in static mode where listDartTests() is never called).
Wiring: --emit-test-manifest moved to a shared usesEmitTestManifestOption
() and registered on build ios / build android / test; emitTestManifest
added to AndroidAppOptions.
Proven end-to-end on an emulator (example_test.dart): 3 tests discovered,
2 passed, 1 skip honored, reported under their own method names.
Follow-ups:
- androidx.test.runner.AndroidJUnit4 is deprecated; switch to
androidx.test.ext:junit once it's on the androidTest classpath.
- parallel sharding still needs per-shard PatrolServer port isolation
(same open item as iOS).
Add a shared TestManifest parser (crossplatform/test_manifest.dart) that flattens the build-time manifest byte-identically to the native runners and records each test's source file (its top-level, file-derived group). - TestManifestGenerator now prints a visible per-file discovery report (test count, source file paths, skip flags) instead of leaving it on the verbose-only channel; the file path is reconstructed from the top-level group and validated against disk. - Refactor XcodeTestCodegen and AndroidTestCodegen onto the shared parser, removing the duplicated _FlatTest/_flatten in each. - Raise the "Generated N static XCTest/JUnit method(s)" logs to the visible channel. - Add unit tests for TestManifest (flatten equivalence, skip, top-level group).
Make build-time test discovery configurable per-project and add a no-rebuild run path on top of the static native tests it generates. - pubspec: `patrol.emit_test_manifest: true` (PatrolPubspecConfig). The `--emit-test-manifest` flag is now negatable and overrides the config value (optionalBoolArg ?? config) on build ios/android and test. - iOS setup guardrail: when discovery is enabled, `patrol build ios` fails fast with instructions if RunnerUITests.m isn't switched to the STATIC macro + #include, instead of silently falling back to runtime discovery. - `patrol test --no-build [--only "<dart name>"]`: run already-built tests without rebuilding (gated on emit_test_manifest). iOS uses `xcodebuild test-without-building -only-testing`; Android uses `adb shell am instrument -e class <fqcn>#<method>` (a true no-rebuild path). `--only` maps exact Dart names to selectors via the manifest. - Move the selector/method-name generation into the shared crossplatform/test_manifest.dart (generateIosSelectors / generateAndroidMethodNames) so codegen and execution derive identical names from one source; both codegens now use it. - Tests: emit_test_manifest parsing + selector generation.
Add a Build-time test discovery page (how it works, setup incl. the iOS RunnerUITests.m static-macro change, running without rebuilding, sharding, trade-offs) under CI, wire it into the nav, and document the config option and `--no-build`/`--only` in the build/test CLI command pages.
scripts/generate_ios_testlist.sh derives the SauceLabs XCUITest testListFile (.sauce/ios_testlist.txt) from the static XCTest methods emitted by `patrol build ios --emit-test-manifest`, by extracting each generated selector from PatrolGeneratedTests.inc. Deriving from the compiled .inc keeps the list byte-identical to what XCTest discovers, with no duplicated sanitization. The generated list is gitignored (regenerate per build); referenced from the build-time discovery docs.
- Revert dev/e2e_app RunnerUITests.m to the runtime macro PATROL_INTEGRATION_TEST_IOS_RUNNER. The static macro requires the generated .inc, which only exists with --emit-test-manifest, so committing it broke the default (non-discovery) iOS pipeline. The static runner is now shown only in the docs as the opt-in step. - Docs: clarify the runtime macro is the default and that switching back from static requires restoring it (--no-emit-test-manifest alone isn't enough on iOS). - Fix: escape \r and \t in the generated Objective-C string literal (xcode_test_codegen), matching the Android generator, so test names with those characters don't break PatrolGeneratedTests.inc. - Fix: the static runner's +setUp passed a NULL NSError** to startAndReturnError:, so a server start failure was never detected; use &err/nil.
… tests - Static iOS runner's resetPermissions now resets the same resources as the runtime runner (HomeKit, MediaLibrary, KeyboardNetwork, Health, Focus, LocalNetwork), so --clear-permissions isn't weaker under static discovery. - Delete stale generated code before discovery and on opt-out: Android removes PatrolGeneratedTests.java (so the runtime host class and a stale generated class can't both run), iOS removes PatrolGeneratedTests.inc (so a failed discovery fails loudly at #include instead of compiling stale tests). - Add direct tests for the iOS XCTest codegen (selectors, skip, Obj-C escaping incl. \r/\t, duplicate-name uniqueness).
- executeWithoutBuilding installs the app + androidTest APKs via adb (glob build/app/outputs/apk/**, flavor-aware) before `am instrument`, so the documented `patrol build` → `patrol test --no-build` flow works on a clean device; errors clearly when APKs are missing. - Resolve the real instrumentation component from `adb shell pm list instrumentation` (match target==applicationId, tolerate flavor suffix, prefer a Patrol runner), falling back to the default `<appId>.test`/PatrolJUnitRunner — supports custom testApplicationId / BrowserStack runner. - Fatal (throwToolExit) when discovery is enabled but the manifest can't be produced, instead of silently skipping codegen. - Drop the `--no-build` opt-in flag gate; the backends already validate that the built artifacts exist (only the `--only`-requires-`--no-build` and Android/iOS platform checks remain).
- Port +uninstallApp into the STATIC runner macro and honor --full-isolation (uninstall the app between tests, mirroring the runtime runner); previously --emit-test-manifest + --full-isolation silently skipped isolation. - Fatal (throwToolExit) when discovery is enabled but the manifest can't be produced: the static macro has no runtime +testInvocations fallback, so a silent skip would leave the build to fail confusingly at #include.
The generated class hardcoded MainActivity.class, so host tests launching a different activity (e.g. FlutterActivity.class) produced an uncompilable source. Parse the activity token from the host's `setUp(<Class>.class)` (replicating its import when the reference is unqualified) and emit that; default to MainActivity. Adds a test covering a host that uses FlutterActivity.
The generated bundle imported `dart:io` to write the discovery manifest, which made it uncompilable for Flutter web even with discovery disabled. Add a conditionally-imported manifest writer in the patrol package (stub no-op for web, dart:io implementation elsewhere) and have the bundle call writeTestManifest(...) instead of File(...).writeAsStringSync(...).
In build-time discovery mode patrolTest registered a single default variant, so tests using a custom TestVariant (which testWidgets expands into several named cases) could be missing from the manifest or baked with names the on-device run never registers. Forward `variant` and `semanticsEnabled` to the discovery-mode testWidgets call so the discovered tree matches the device.
Note that --no-build reuses the previous build (re-run only when code is unchanged) and how the Android instrumentation component is resolved, including the custom testApplicationId / flavor applicationIdSuffix caveats.
- clang-format PatrolIntegrationTestIosRunner.h (the manual macro edits for full-isolation / permission parity / err fix left non-canonical backslash alignment that failed `clang-format --dry-run --Werror`). - Use a raw string for the JSON fixture in android_test_codegen_test.dart to satisfy the `use_raw_strings` lint (analyzer runs with warnings-as-errors).
Run `dart format` on the files touched by the build-time discovery / --no-build work so `dart format --set-exit-if-changed` passes (formatting only, no behavior change).
_locateHostTest matched any file referencing PatrolJUnitRunner, but the generated PatrolGeneratedTests.java references it too. Once generated, it became a second, ambiguous host candidate whose selection depended on listSync ordering (passing locally, failing in CI's `findGeneratedClassName` test). Skip the generated class by name so only the real host is ever matched.
SauceLabs expects a different test-identifier separator by device type: `TestTarget/TestClass/testMethod` on simulators but `TestTarget.TestClass/ testMethod` (a dot between target and class) on real devices. The generator only emitted the simulator form, so on real devices no test matched and the run hung on a blank/gray UITest runner. - generate_ios_testlist.sh: add SAUCE_DEVICE=simulator|real (default simulator) to pick the separator; also mkdir -p the output dir. - Document the real-vs-simulator format difference in the build-time discovery docs.
- Static iOS runner: reset the readiness flag before launching and wait for the app to report PatrolAppService readiness (bounded, with a clear failure) before sending runDartTest. Previously the request went out after the client's fixed 1s delay, so a slower first launch failed as if the app had crashed — the very case the client's own TODO warns about. - Android method names: never emit a Java keyword or reserved literal (a Dart test named `class` produced `public void class()`, which doesn't compile). - Stop promising a runtime-discovery fallback that no longer exists: both backends abort when discovery fails, so say discovery failed instead. - generate_ios_testlist.sh: let grep's no-match status through the pipeline, so the empty-list diagnostic is actually reachable under `set -euo pipefail`. - Docs: the generated native method names are sanitized and suffixed; it's the embedded Dart name that stays byte-identical. Clarify which to use where.
… lookup - Discovery now runs only the internal `patrol_test_explorer` (`flutter test --plain-name`). Declaring the tests is what builds the group tree, so the manifest stays complete, but the user's tests are never scheduled - so no setUp/tearDown/setUpAll/tearDownAll around them executes on the host. Previously only Patrol's own patrolSetUp/patrolTearDown were guarded, so plain hooks (or a project's own wrapper) still ran device-dependent fixtures and could fail the discovery run. Verified: with the filter the manifest keeps all tests and zero hooks run. - Android host lookup no longer treats any file mentioning PatrolJUnitRunner as the host: runner subclasses (the documented Allure `AllurePatrolJUnitRunner.kt` lives next to the host test) and the generated class are skipped, the host is recognized by how it uses the runner (getInstrumentation + setUp/listDartTests), and candidates are sorted by path so the choice is deterministic.
0742f95 to
cc63861
Compare
It was a scratch fixture from the POC, used to check that dart-define-derived and loop-generated test names survive build-time discovery. It isn't referenced anywhere and only added noise to the e2e suite.
android_test_codegen joined paths with `package:path`'s top-level `join`, which uses the *host* platform separator rather than the separator of the FileSystem it writes to. On Windows that mangled the generated file's path, the locator then mistook the generated class for the host test (it drives the runner the same way), and findGeneratedClassName looked in the wrong directory - failing `findGeneratedClassName returns the FQN only after generation` on windows-latest while passing on ubuntu. Use `_fs.path.join` so the host platform is irrelevant, and additionally skip our own output by its generated-code marker rather than by filename alone. Adds a regression test on a Windows-style MemoryFileSystem (fails without the fix).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (11)
packages/patrol_cli/lib/src/android/android_test_codegen.dart:224
- The host locator scans Kotlin files, but this expression only recognizes Java's
FooActivity.class. A Kotlin host normally callssetUp(FooActivity::class.java), so a custom activity is silently replaced withMainActivity; the import parser also requires Java's semicolon. The generated test then launches or compiles against the wrong activity. Parse both Java and Kotlin forms.
final match = RegExp(
r'setUp\(\s*([A-Za-z_][\w.]*)\.class',
).firstMatch(content);
packages/patrol_cli/lib/src/android/android_test_backend.dart:683
- When no flavor is supplied, this accepts every APK with the requested build-mode segment, including stale flavored APKs under paths such as
apk/dev/debug. SincelistSync()ordering is unspecified,--no-buildcan install a stale flavor (or pair app and test APKs from different variants) instead of the unflavored artifacts. Resolve the exact standard app/test paths for the requested flavor and mode, asBuildAndroidCommand.printApkPathsalready does, rather than taking the first recursive matches.
bool matches(File apk) {
final segments = apk.path.split(RegExp(r'[/\\]'));
if (!segments.contains(buildMode)) {
return false;
}
// When a flavor is set, its (case-sensitive) directory segment is present
// in both the app and androidTest APK paths; use it to disambiguate.
if (flavor != null && !segments.contains(flavor)) {
return false;
}
return true;
packages/patrol_cli/lib/src/android/android_test_backend.dart:773
- This prefix match can select an unrelated installed application (for example, configured package
com.fooalso matches targetcom.foobar). If no exact instrumentation exists,--no-buildmay invoke the wrong runner. Restrict fallback matching to the dot-delimitedapplicationIdSuffixform.
final prefixed = entries
.where((e) => e.target != null && e.target!.startsWith(packageName))
.toList();
packages/patrol_cli/lib/src/runner/patrol_command.dart:242
addMultiOptionsplits comma-separated values by default, but Dart test names can legitimately contain commas. For example,--only "opens menu, then closes it"is parsed as two requested names and will never match the manifest. Disable comma splitting so each repeated--onlyoccurrence remains an exact test name.
..addMultiOption(
'only',
help:
'With --no-build, run only the test(s) whose exact Dart name '
'(as printed during discovery) is given. Repeatable; omit to run '
'all discovered tests.',
);
packages/patrol_cli/lib/src/ios/ios_test_backend.dart:285
- The error is raised only when no requested name matches. With multiple
--onlyvalues, one valid name plus one typo silently runs the valid test and reports success without running the other requested test. Track matched names and fail if any distinct requested name is absent.
for (var i = 0; i < tests.length; i++) {
if (onlyTests.contains(tests[i].dartName)) {
out.add(selectors[i]);
}
}
if (out.isEmpty) {
throwToolExit(
'None of the requested --only test(s) were found in the manifest.\n'
'Available tests:\n${tests.map((t) => ' ${t.dartName}').join('\n')}',
);
packages/patrol_cli/lib/src/android/android_test_backend.dart:647
- The error is raised only when no requested name matches. With multiple
--onlyvalues, one valid name plus one typo silently runs the valid test and reports success without running the other requested test. Track matched names and fail if any distinct requested name is absent.
This issue also appears in the following locations of the same file:
- line 673
- line 771
for (var i = 0; i < tests.length; i++) {
if (onlyTests.contains(tests[i].dartName)) {
out.add('$fqcn#${methods[i]}');
}
}
if (out.isEmpty) {
throwToolExit(
'None of the requested --only test(s) were found in the manifest.\n'
'Available tests:\n${tests.map((t) => ' ${t.dartName}').join('\n')}',
);
packages/patrol_cli/lib/src/crossplatform/test_manifest_generator.dart:92
- An empty manifest is treated as a successful discovery, but Android codegen then emits a class with zero
@Testmethods and execution is restricted to that class. JUnit reportsNo runnable methods, so the build succeeds but the resulting test artifact is unusable. Reject an empty discovery before codegen, or generate an explicitly ignored placeholder test.
final tests = manifest.tests;
if (tests.isEmpty) {
_logger.info('No Dart tests discovered.');
return;
packages/patrol_cli/lib/src/crossplatform/app_options.dart:90
- The discovery-only defines are emitted before caller-provided
dartDefinesand--dart-define-from-filevalues. A project that already defines either reserved key can therefore overridePATROL_TEST_DISCOVERY=trueor redirectPATROL_MANIFEST_OUTPUT, causing discovery to run normally or report a missing manifest. Emit these two internal defines last so they are authoritative.
...['--dart-define', 'PATROL_TEST_DISCOVERY=true'],
...['--dart-define', 'PATROL_MANIFEST_OUTPUT=$manifestOutputPath'],
for (final dartDefine in dartDefines.entries) ...[
'--dart-define',
'${dartDefine.key}=${dartDefine.value}',
],
for (final dartDefineFromFilePath in dartDefineFromFilePaths) ...[
'--dart-define-from-file',
dartDefineFromFilePath,
packages/patrol_cli/lib/src/crossplatform/test_manifest_generator.dart:44
- This single global manifest is later used to map
--onlynames to methods in already-built platform artifacts, but every iOS or Android discovery overwrites it. Building Android after iOS (or rebuilding with different targets/defines) can therefore make a later iOS--no-build --onlyselect methods from the Android manifest, and vice versa. Non-manifest builds also leave this file stale. Persist/load a manifest keyed to the platform and concrete build artifact (or record and validate build identity) so selectors cannot be resolved against an unrelated binary.
final manifestFile = _rootDirectory
.childDirectory('build')
.childDirectory('patrol')
.childFile('patrol_test_manifest.json');
packages/patrol/lib/src/common.dart:117
- This early return bypasses the existing validation that rejects supplying both
nativeAutomatorConfigandplatformAutomatorConfig. Discovery therefore succeeds for a test bundle that will throw during normal on-device registration, producing static tests that cannot execute. Keep argument validation before the discovery branch so build-time and runtime registration enforce the same contract.
if (constants.testDiscoveryEnabled) {
// Build-time discovery (host `flutter test`): only register the test so it
// shows up in the group tree. Do NOT initialize PatrolBinding (Live binding,
// incompatible with the host automated binding) and do NOT run the body
// (it would block on waitForExecutionRequest()).
packages/patrol_cli/lib/src/android/android_test_backend.dart:505
- This new execution path has no automated coverage, although
AndroidTestBackendis unit-tested elsewhere. It contains artifact selection, manifest-to-method mapping, instrumentation parsing, process failure detection, and two APK installs; regressions here can silently run the wrong variant or report failed instrumentation as successful. Add backend tests for all/selected tests, missing artifacts, flavored APK resolution, custom instrumentation, and failingam instrumentoutput.
Future<void> executeWithoutBuilding(
What & why
Stock Patrol discovers tests at runtime (dynamic registration once the app
launches), so no static list of test identifiers exists before a run. On iOS —
where XCUITest has no built-in sharding — that blocks per-test sharding entirely.
This PR adds experimental, opt-in build-time test discovery: a host
flutter testserializes the Dart test tree to a manifest at build time, and wegenerate a real, statically-declared native test per Dart test. Every test becomes
individually addressable (
-only-testingon iOS,am instrument -e class#methodon Android).
This should unblock iOS test sharding on device farms that shard by an explicit
test list — SauceLabs (
testListFile) and BrowserStack — plus balanced Androidsharding and cleaner per-test report names.
What's included
patrol.emit_test_manifest: true(the--emit-test-manifestflag overrides it); iOS build fails fast if
RunnerUITests.misn't switched tothe static macro.
patrol test --no-build [--only "<dart name>"]: re-run built tests withoutrebuilding (iOS
xcodebuild test-without-building, Androidam instrument).Usage
iOS also needs the static macro in
RunnerUITests.m(see docs; the CLI prints theexact snippet if missing).
Verification
Unit tests (manifest/selector/codegen/config) +
dart analyzeclean. Verifiedend-to-end in
dev/e2e_app: iOS single test via-only-testing→Executed 1 test; Android viaam instrument→OK (1 test);--no-build --onlyruns onetest with no rebuild on both platforms.
Limitations
Experimental. Host-side discovery can diverge for tests registered conditionally
on the runtime env (
Platform.isX, dart-define-derived names). Tests must liveoutside
integration_test/(usepatrol_test/). Parallel simulators on one hostcollide on Patrol's fixed ports — shard across separate hosts/VMs (as farms do).
Initial build is slightly slower (paid once;
--no-buildre-runs skip building).