Repository navigation
Conversation
PR HealthPublish ✔️
Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. This check can be disabled by tagging the PR with Changelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with |
…cleanup grace period
…ption catch blocks
|
@natebosch are you ok with the PR as it stands? |
…to script, and clean up exports
…r root-relative check
- Reflow long lines - Remove a src import - Remove an unnecessary type on the left
|
This is still on my radar and I should be able to get back to land it soon - I wanted to get some of the other test changes published before landing it. |
| if (mainFn is FutureOr<Object?> Function(List<String>, SendPort)) { | ||
| return await mainFn(args, sendPort); | ||
| } else if (mainFn is FutureOr<Object?> Function(List<String>)) { | ||
| return await mainFn(args); | ||
| } else if (mainFn is FutureOr<Object?> Function()) { | ||
| return await mainFn(); | ||
| } else { | ||
| throw ArgumentError( | ||
| 'The global setup script must define a top-level `setUp` function ' | ||
| 'with zero arguments, one List<String> argument, ' | ||
| 'or (List<String>, SendPort).', | ||
| ); | ||
| } |
There was a problem hiding this comment.
The docs specify a zero-argument setUp() function, so let's only accept FutureOr<Object?> Function() here rather than supporting (List<String>, SendPort) and (List<String>). args is always [], sendPort exposes the runner's internal bootstrap response port, and once published we wouldn't be able to drop those extra signatures without risking a breaking change.
There was a problem hiding this comment.
Done — restricted internalBootstrapVmHook to zero-argument FutureOr<Object?> Function() and removed the unused args parameter.
| try { | ||
| json.encode(result); | ||
| } on Object catch (error) { | ||
| throw ArgumentError( | ||
| 'The global setup script returned a value that is not JSON-encodable: ' | ||
| '$error', | ||
| ); | ||
| } | ||
|
|
||
| sendPort.send({ | ||
| 'success': true, | ||
| 'result': result, | ||
| 'hasTearDown': tearDowns.isNotEmpty, | ||
| 'commandPort': tearDowns.isNotEmpty ? commandPort.sendPort : null, | ||
| }); |
There was a problem hiding this comment.
Right now json.encode(result) validates encodability here, then the raw result is sent over sendPort.send across the Isolate.spawnUri isolate-group boundary, and GlobalSetupManager / standalone_global_setup.dart call json.encode(result) a second time to cache the string.
If result is (or contains) an object with a toJson() method, json.encode(result) succeeds here and then sendPort.send fails because arbitrary class instances can't be sent across isolate groups. Sending the already-encoded JSON String in 'result' (so the JSON round-trip straddles the isolate boundary instead of happening on one side) fixes toJson() values and avoids encoding twice.
There was a problem hiding this comment.
Good catch. internalBootstrapVmHook now sends the encoded JSON String in 'result', and GlobalSetupManager / standalone_global_setup.dart cache it directly without re-encoding. Added a test for objects with toJson().
| } catch (error, stack) { | ||
| commandPort.close(); | ||
| sendPort.send({ | ||
| 'success': false, | ||
| 'error': error.toString(), | ||
| 'stackTrace': stack.toString(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
If setUp() starts a resource, registers an addGlobalTearDown callback, and then throws later in setUp() (or returns a value that fails json.encode), we enter catch, report 'success': false, and the runner kills the isolate without running the already-registered tearDowns. Should we run tearDowns.reversed on a best-effort basis before reporting failure so partially initialized resources get cleaned up?
There was a problem hiding this comment.
Done — the catch block now runs tearDowns.reversed on a best-effort basis before closing commandPort and reporting failure. Added a test verifying cleanup on partial setup failure.
| Future<void> close() => _closeMemo.runOnce(() async { | ||
| for (var active in _activeSetups.reversed) { | ||
| final replyPort = RawReceivePort(); | ||
| final completer = Completer<Map<Object?, Object?>>(); | ||
| replyPort.handler = (Object? response) { | ||
| if (!completer.isCompleted) { | ||
| completer.complete(response as Map<Object?, Object?>); | ||
| } | ||
| }; | ||
| try { | ||
| active.commandPort.send([replyPort.sendPort, 'teardown']); | ||
| final response = await completer.future.timeout( | ||
| const Duration(seconds: 30), | ||
| onTimeout: () => {'success': false, 'error': 'Teardown timed out'}, | ||
| ); | ||
| if (response['success'] != true) { | ||
| stderr.writeln( | ||
| 'Global teardown for "${active.url}" failed: ${response["error"]}', | ||
| ); | ||
| exitCode = 1; | ||
| } | ||
| } catch (error) { | ||
| stderr.writeln('Global teardown for "${active.url}" failed: $error'); | ||
| exitCode = 1; | ||
| } finally { | ||
| replyPort.close(); | ||
| active.isolate.kill(); | ||
| } | ||
| } | ||
| _activeSetups.clear(); | ||
| }); |
There was a problem hiding this comment.
Two lifecycle edge cases around close():
- In-flight
_runSetupvsclose():_activeSetupsis only populated oncesetUp()finishes (line 122). IfRunner.close()runs while_runSetupis still executing (e.g. Ctrl-C,--stop-on-first-failure, or a test timeout),close()sees_activeSetupsempty and completes immediately; ifsetUp()finishes afterward, it appends to_activeSetupsafterclose()has already run, so its teardowns never run and its isolate is never killed (and whilesetUp()is running, the openRawReceivePorts keep the VM alive). Tracking/closing in-flight setups or tearing down late completions when_closeMemo.hasRunwould avoid leaking them. - Unexpected isolate exit before
close():errorPortis closed as soon assetUp()returns (line 153) and there is noonExitlistener (unlikespawn_hybrid.dart). If a background server in the setup isolate crashes with an unhandled async error or callsIsolate.exit(),close()sends'teardown'to a deadcommandPortand waits the full 30-second timeout before reportingTeardown timed out.
There was a problem hiding this comment.
Addressed both:
- Tracked in-flight setups so
close()aborts in-flight isolates/ports and tears down any late completions if_closeMemo.hasRunis true. - Added an
onExitport and kepterrorPort/exitPortopen whenhasTearDownis true so unexpected isolate exits before or during teardown fail immediately instead of hitting the 30s timeout.
| if (_runCalled) { | ||
| await _suiteController.close(); | ||
| } |
There was a problem hiding this comment.
Is this if (_runCalled) check left over from an earlier iteration of the PR? If nothing in the current design needs it, let's revert this change to engine.dart.
| d.file('test_test.dart', ''' | ||
| import 'package:test/scaffolding.dart'; | ||
| import 'package:test/test.dart'; | ||
|
|
||
| void main() { | ||
| test("concurrent call 1", () async { | ||
| final result = await globalSetup(Uri.parse('/test/slow_setup.dart')); | ||
| expect(result, equals(1)); | ||
| }); | ||
|
|
||
| test("concurrent call 2", () async { | ||
| final result = await globalSetup(Uri.parse('/test/slow_setup.dart')); | ||
| expect(result, equals(1)); | ||
| }); | ||
|
|
||
| test("concurrent call 3", () async { | ||
| final result = await globalSetup(Uri.parse('/test/slow_setup.dart')); | ||
| expect(result, equals(1)); | ||
| }); | ||
| } | ||
| '''), | ||
| ]).create(); | ||
|
|
||
| var test = await runTest(['test/test_test.dart'], concurrency: 3); |
There was a problem hiding this comment.
Tests within a single suite run sequentially even with concurrency: 3, so the second and third globalSetup calls here only run after the first has already completed. Put these in separate suite files (or use Future.wait inside a test) so concurrent in-flight globalSetup deduplication is exercised.
There was a problem hiding this comment.
Updated to run 3 separate suite files with concurrency: 3 (and Future.wait inside the first suite).
Revisions updated by `dart tools/rev_sdk_deps.dart`. core (https://github.com/dart-lang/core/compare/36f57a9..fa0c6b7): fa0c6b77 Fri Nov 6 12:56:58 2026 +0100 Michael Thomsen Release os_detect 2.0.4 (dart-lang/core#1038) dartdoc (https://github.com/dart-lang/dartdoc/compare/03ec6df..7e2508c): 7e2508c6 Wed Nov 11 14:52:22 2026 +0200 Sergey G. Grekhov Add constructor doc comment tests for enums and extension types (dart-lang/dartdoc#4316) 03dfb467 Mon Nov 9 11:00:32 2026 +0200 Sergey G. Grekhov `#3330`. Split test for parameters to separate tests (dart-lang/dartdoc#4314) 23302d53 Mon Nov 9 10:56:34 2026 +0200 Sergey G. Grekhov Add tests for primary constructors (dart-lang/dartdoc#4313) ecosystem (https://github.com/dart-lang/ecosystem/compare/54e1673..1d2b5ff): 1d2b5ff Tue Nov 10 09:04:24 2026 -0800 Kevin Moore feat(repo_manage): add --replace-pattern to report changelog (dart-lang/ecosystem#482) http (https://github.com/dart-lang/http/compare/4646fe6..2bac218): 2bac218 Fri Nov 6 17:49:23 2026 -0800 dart-ecosystem-bot[bot] Tidy: package:cronet_http cdf967c Fri Nov 6 15:40:49 2026 -0800 Brian Quinlan chore(groundkeeper): use stable for flutter (dart-lang/http#2066) 6582177 Thu Nov 5 13:04:35 2026 -0800 Brian Quinlan doc: use timeout as an abort example (dart-lang/http#2062) native (https://github.com/dart-lang/native/compare/6287745..b21ef2d): b21ef2da3 Wed Nov 11 16:07:34 2026 +0530 dhairya05 [json_syntax_generator] Pass the parameter name to ArgumentError.value (dart-lang/native#3787) 20a607e48 Wed Nov 11 07:28:03 2026 -0300 Daco Harkes [code_assets] [hooks] [hooks_runner] Adopt `.g.dart` for generated files (dart-lang/native#3790) cc974ddec Wed Nov 11 19:59:39 2026 +1100 Liam Appelbe [objective_c] Fix `isValidObject` bug on iOS simulator (dart-lang/native#3788) f7edbdee3 Sun Nov 8 13:18:25 2026 +1100 Liam Appelbe [jni] Fix possible receiver NPEs (dart-lang/native#3784) 0ca7d6745 Fri Nov 6 10:20:21 2026 -0300 Daco Harkes [code_assets] [hooks_runner] [native_toolchain_c] Prepare for release (dart-lang/native#3781) baad15ab4 Fri Nov 6 02:56:52 2026 +0100 Kravchenko Igor [objective_c] Link Foundation in the native support library (dart-lang/native#3782) shelf (https://github.com/dart-lang/shelf/compare/9946de5..b175e9f): b175e9f Tue Nov 10 09:14:31 2026 +0530 Muhammad Uvesh shelf_static: ignore invalid If-Modified-Since headers (dart-lang/shelf#557) 02b5f94 Sat Nov 7 23:09:28 2026 +0530 dhairya05 Fix typo in shelf_router docs (dart-lang/shelf#556) test (https://github.com/dart-lang/test/compare/2a9e017..3533ae1): 3533ae1e Tue Nov 10 15:06:08 2026 -0800 Nate Bosch Never retry tearDownAll (dart-lang/test#2815) 5680b0b1 Fri Nov 6 12:52:11 2026 +0100 Sigurd Meldgaard Add globalSetup and addGlobalTearDown APIs for cross-suite setup and teardown (dart-lang/test#2714) b8dc888a Thu Nov 5 18:11:37 2026 -0800 Nate Bosch [checks_codegen] Skip private fields and fields referencing private types (dart-lang/test#2782) 66e062c4 Thu Nov 5 16:05:36 2026 -0800 Nate Bosch [checks_codegen] Generate full field types with imports (dart-lang/test#2781) tools (https://github.com/dart-lang/tools/compare/727fd9c..589d844): 589d844c4 Wed Nov 11 20:27:32 2026 +0300 Hasan Armoush fix(markdown): combine adjacent Text nodes when the element is last in its list (dart-lang/tools#2598) Change-Id: I13acaa7378c86f2c33d88f3c4b163acf2b48ff08 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/576820 Commit-Queue: Nate Bosch <[email protected]> Reviewed-by: Nicholas Shahan <[email protected]>
Adds
globalSetup()andaddGlobalTearDown()APIs inpackage:test_api/package:testto execute shared global setup scripts once on the host test runner, memoizing results across all test suites and executing registered teardowns when the test runner closes.Addresses #2225
The main motivation is to support setup shared across multiple test suites (
_test.dartfiles). Examples include:Example
Key Features
globalSetup(Uri uri):uriin an isolate on the host runner and returns its JSON-encodable result.setUp()/setUpAll()callback.data:URI usingIsolate.spawnUri(sharing architecture withspawnHybridUri), avoiding filesystem compilation or runner-specific compiler hooks.addGlobalTearDown(FutureOr<void> Function() callback)(exported frompackage:test/global.dart):Uri(path: '/test/setup.dart')): Resolved relative to package root (pubspec.yaml).package:URIs (Uri.parse('package:my_pkg/test_helpers.dart')): Resolved synchronously via package config.Uri(path: 'setup.dart')orUri(path: '../setup.dart')): Resolved relative to the executing test suite's directory.file:URIs: Resolved as absolute filesystem paths.test_channel, allowing browser, Node, and VM tests alike to trigger host-side setup.dart <test_file>.dartexecute setup scripts using a standalone scaffolding fallback and run teardowns on process exit.flutter testwithout runner-level code changes.