mirror of
https://github.com/flutter/flutter
synced 2024-10-05 15:59:49 +00:00
Support using lightweight Flutter Engines to run tests (#141726)
This PR implements the functionality described above and hides it behind the `--experimental-faster-testing` flag of `flutter test`. ### The following are some performance measurements from test runs conducted on GitHub Actions run 1 logs: https://github.com/derekxu16/flutter_test_ci/actions/runs/8008029772/attempts/1 run 2 logs: https://github.com/derekxu16/flutter_test_ci/actions/runs/8008029772/attempts/2 run 3 logs: https://github.com/derekxu16/flutter_test_ci/actions/runs/8008029772/attempts/3 **length of `flutter test --reporter=expanded test/animation test/foundation` step** run 1: 54s run 2: 52s run 3: 56s average: 54s **length of `flutter test --experimental-faster-testing --reporter=expanded test/animation test/foundation` step** run 1: 27s run 2: 27s run 3: 29s average: 27.67s (~48.77% shorter than 54s) **length of `flutter test --reporter=expanded test/animation test/foundation test/gestures test/painting test/physics test/rendering test/scheduler test/semantics test/services` step** run 1: 260s run 2: 270s run 3: 305s average: 278.33s **length of `flutter test --experimental-faster-testing --reporter=expanded test/animation test/foundation test/gestures test/painting test/physics test/rendering test/scheduler test/semantics test/services` step** from a clean build (right after deleting the build folder): run 1: 215s run 2: 227s run 3: 245s average: 229s (~17.72% shorter than 278.33s) Note that in reality, `test/material` was not passed to `flutter test` in the trials below. All of the test files under `test/material` except for `test/material/icons_test.dart` were listed out individually **length of `flutter test --reporter=expanded test/material` step** run 1: 408s run 2: 421s run 3: 451s average: 426.67s **length of `flutter test --experimental-faster-testing --reporter=expanded test/material` step** run 1: 382s run 2: 373s run 3: 400s average: 385s (~9.77% shorter than 426.67s) --------- Co-authored-by: Dan Field <dnfield@google.com>
This commit is contained in:
parent
44bcc9ce1b
commit
dfb5888e8f
|
@ -195,6 +195,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||
TestWidgetsFlutterBinding() : platformDispatcher = TestPlatformDispatcher(
|
||||
platformDispatcher: PlatformDispatcher.instance,
|
||||
) {
|
||||
platformDispatcher.defaultRouteNameTestValue = '/';
|
||||
debugPrint = debugPrintOverride;
|
||||
debugDisableShadows = disableShadows;
|
||||
}
|
||||
|
@ -246,6 +247,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||
void reset() {
|
||||
_restorationManager?.dispose();
|
||||
_restorationManager = null;
|
||||
platformDispatcher.defaultRouteNameTestValue = '/';
|
||||
resetGestureBinding();
|
||||
testTextInput.reset();
|
||||
if (registerTestTextInput) {
|
||||
|
|
|
@ -80,6 +80,11 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
|
|||
addEnableImpellerFlag(verboseHelp: verboseHelp);
|
||||
|
||||
argParser
|
||||
..addFlag('experimental-faster-testing',
|
||||
negatable: false,
|
||||
hide: !verboseHelp,
|
||||
help: 'Run each test in a separate lightweight Flutter Engine to speed up testing.'
|
||||
)
|
||||
..addMultiOption('name',
|
||||
help: 'A regular expression matching substrings of the names of tests to run.',
|
||||
valueHelp: 'regexp',
|
||||
|
@ -350,6 +355,23 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
|
|||
);
|
||||
}
|
||||
|
||||
bool experimentalFasterTesting = boolArg('experimental-faster-testing');
|
||||
if (experimentalFasterTesting) {
|
||||
if (_isIntegrationTest || isWeb) {
|
||||
experimentalFasterTesting = false;
|
||||
globals.printStatus(
|
||||
'--experimental-faster-testing was parsed but will be ignored. This '
|
||||
'option is not supported when running integration tests or web tests.',
|
||||
);
|
||||
} else if (_testFileUris.length == 1) {
|
||||
experimentalFasterTesting = false;
|
||||
globals.printStatus(
|
||||
'--experimental-faster-testing was parsed but will be ignored. This '
|
||||
'option should not be used when running a single test file.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final bool startPaused = boolArg('start-paused');
|
||||
if (startPaused && _testFileUris.length != 1) {
|
||||
throwToolExit(
|
||||
|
@ -402,6 +424,13 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
|
|||
// Running with concurrency will result in deploying multiple test apps
|
||||
// on the connected device concurrently, which is not supported.
|
||||
jobs = 1;
|
||||
} else if (experimentalFasterTesting) {
|
||||
if (argResults!.wasParsed('concurrency')) {
|
||||
globals.printStatus(
|
||||
'-j/--concurrency was parsed but will be ignored. This option is not '
|
||||
'compatible with --experimental-faster-testing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final int? shardIndex = int.tryParse(stringArg('shard-index') ?? '');
|
||||
|
@ -425,6 +454,25 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
|
|||
'If you set --shard-index you need to also set --total-shards.');
|
||||
}
|
||||
|
||||
final bool enableVmService = boolArg('enable-vmservice');
|
||||
if (experimentalFasterTesting && enableVmService) {
|
||||
globals.printStatus(
|
||||
'--enable-vmservice was parsed but will be ignored. This option is not '
|
||||
'compatible with --experimental-faster-testing.',
|
||||
);
|
||||
}
|
||||
|
||||
final bool ipv6 = boolArg('ipv6');
|
||||
if (experimentalFasterTesting && ipv6) {
|
||||
// [ipv6] is set when the user desires for the test harness server to use
|
||||
// IPv6, but a test harness server will not be started at all when
|
||||
// [experimentalFasterTesting] is set.
|
||||
globals.printStatus(
|
||||
'--ipv6 was parsed but will be ignored. This option is not compatible '
|
||||
'with --experimental-faster-testing.',
|
||||
);
|
||||
}
|
||||
|
||||
final bool machine = boolArg('machine');
|
||||
CoverageCollector? collector;
|
||||
if (boolArg('coverage') || boolArg('merge-coverage') ||
|
||||
|
@ -487,35 +535,61 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
|
|||
}
|
||||
|
||||
final Stopwatch? testRunnerTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.TestRunner);
|
||||
final int result = await testRunner.runTests(
|
||||
testWrapper,
|
||||
_testFileUris.toList(),
|
||||
debuggingOptions: debuggingOptions,
|
||||
names: names,
|
||||
plainNames: plainNames,
|
||||
tags: tags,
|
||||
excludeTags: excludeTags,
|
||||
watcher: watcher,
|
||||
enableVmService: collector != null || startPaused || boolArg('enable-vmservice'),
|
||||
ipv6: boolArg('ipv6'),
|
||||
machine: machine,
|
||||
updateGoldens: boolArg('update-goldens'),
|
||||
concurrency: jobs,
|
||||
testAssetDirectory: testAssetDirectory,
|
||||
flutterProject: flutterProject,
|
||||
web: isWeb,
|
||||
randomSeed: stringArg('test-randomize-ordering-seed'),
|
||||
reporter: stringArg('reporter'),
|
||||
fileReporter: stringArg('file-reporter'),
|
||||
timeout: stringArg('timeout'),
|
||||
runSkipped: boolArg('run-skipped'),
|
||||
shardIndex: shardIndex,
|
||||
totalShards: totalShards,
|
||||
integrationTestDevice: integrationTestDevice,
|
||||
integrationTestUserIdentifier: stringArg(FlutterOptions.kDeviceUser),
|
||||
testTimeRecorder: testTimeRecorder,
|
||||
nativeAssetsBuilder: nativeAssetsBuilder,
|
||||
);
|
||||
final int result;
|
||||
if (experimentalFasterTesting) {
|
||||
assert(!isWeb && !_isIntegrationTest && _testFileUris.length > 1);
|
||||
result = await testRunner.runTestsBySpawningLightweightEngines(
|
||||
_testFileUris.toList(),
|
||||
debuggingOptions: debuggingOptions,
|
||||
names: names,
|
||||
plainNames: plainNames,
|
||||
tags: tags,
|
||||
excludeTags: excludeTags,
|
||||
machine: machine,
|
||||
updateGoldens: boolArg('update-goldens'),
|
||||
concurrency: jobs,
|
||||
testAssetDirectory: testAssetDirectory,
|
||||
flutterProject: flutterProject,
|
||||
randomSeed: stringArg('test-randomize-ordering-seed'),
|
||||
reporter: stringArg('reporter'),
|
||||
fileReporter: stringArg('file-reporter'),
|
||||
timeout: stringArg('timeout'),
|
||||
runSkipped: boolArg('run-skipped'),
|
||||
shardIndex: shardIndex,
|
||||
totalShards: totalShards,
|
||||
testTimeRecorder: testTimeRecorder,
|
||||
);
|
||||
} else {
|
||||
result = await testRunner.runTests(
|
||||
testWrapper,
|
||||
_testFileUris.toList(),
|
||||
debuggingOptions: debuggingOptions,
|
||||
names: names,
|
||||
plainNames: plainNames,
|
||||
tags: tags,
|
||||
excludeTags: excludeTags,
|
||||
watcher: watcher,
|
||||
enableVmService: collector != null || startPaused || enableVmService,
|
||||
ipv6: ipv6,
|
||||
machine: machine,
|
||||
updateGoldens: boolArg('update-goldens'),
|
||||
concurrency: jobs,
|
||||
testAssetDirectory: testAssetDirectory,
|
||||
flutterProject: flutterProject,
|
||||
web: isWeb,
|
||||
randomSeed: stringArg('test-randomize-ordering-seed'),
|
||||
reporter: stringArg('reporter'),
|
||||
fileReporter: stringArg('file-reporter'),
|
||||
timeout: stringArg('timeout'),
|
||||
runSkipped: boolArg('run-skipped'),
|
||||
shardIndex: shardIndex,
|
||||
totalShards: totalShards,
|
||||
integrationTestDevice: integrationTestDevice,
|
||||
integrationTestUserIdentifier: stringArg(FlutterOptions.kDeviceUser),
|
||||
testTimeRecorder: testTimeRecorder,
|
||||
nativeAssetsBuilder: nativeAssetsBuilder,
|
||||
);
|
||||
}
|
||||
testTimeRecorder?.stop(TestTimePhases.TestRunner, testRunnerTimeRecorderStopwatch!);
|
||||
|
||||
if (collector != null) {
|
||||
|
|
|
@ -2,9 +2,15 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:package_config/package_config.dart';
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../build_info.dart';
|
||||
import '../cache.dart';
|
||||
import '../compile.dart';
|
||||
import '../convert.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart' as globals;
|
||||
import '../native_assets.dart';
|
||||
|
@ -13,6 +19,8 @@ import '../web/chrome.dart';
|
|||
import '../web/memory_fs.dart';
|
||||
import 'flutter_platform.dart' as loader;
|
||||
import 'flutter_web_platform.dart';
|
||||
import 'font_config_manager.dart';
|
||||
import 'test_config.dart';
|
||||
import 'test_time_recorder.dart';
|
||||
import 'test_wrapper.dart';
|
||||
import 'watcher.dart';
|
||||
|
@ -56,6 +64,32 @@ abstract class FlutterTestRunner {
|
|||
TestTimeRecorder? testTimeRecorder,
|
||||
TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
|
||||
});
|
||||
|
||||
/// Runs tests using the experimental strategy of spawning each test in a
|
||||
/// separate lightweight Engine.
|
||||
Future<int> runTestsBySpawningLightweightEngines(
|
||||
List<Uri> testFiles, {
|
||||
required DebuggingOptions debuggingOptions,
|
||||
List<String> names = const <String>[],
|
||||
List<String> plainNames = const <String>[],
|
||||
String? tags,
|
||||
String? excludeTags,
|
||||
bool machine = false,
|
||||
bool updateGoldens = false,
|
||||
required int? concurrency,
|
||||
String? testAssetDirectory,
|
||||
FlutterProject? flutterProject,
|
||||
String? icudtlPath,
|
||||
String? randomSeed,
|
||||
String? reporter,
|
||||
String? fileReporter,
|
||||
String? timeout,
|
||||
bool runSkipped = false,
|
||||
int? shardIndex,
|
||||
int? totalShards,
|
||||
TestTimeRecorder? testTimeRecorder,
|
||||
TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
|
||||
});
|
||||
}
|
||||
|
||||
class _FlutterTestRunnerImpl implements FlutterTestRunner {
|
||||
|
@ -230,4 +264,575 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
|
|||
await platform.close();
|
||||
}
|
||||
}
|
||||
|
||||
// To compile root_test_isolate_spawner.dart and
|
||||
// child_test_isolate_spawner.dart successfully, we will need to pass a
|
||||
// package_config.json to the frontend server that contains the
|
||||
// union of package:test_core, package:ffi, and all the dependencies of the
|
||||
// project under test. This function generates such a package_config.json.
|
||||
static Future<void> _generateIsolateSpawningTesterPackageConfig({
|
||||
required FlutterProject flutterProject,
|
||||
required File isolateSpawningTesterPackageConfigFile,
|
||||
}) async {
|
||||
final File projectPackageConfigFile = globals.fs.directory(
|
||||
flutterProject.directory.path,
|
||||
).childDirectory('.dart_tool').childFile('package_config.json');
|
||||
final PackageConfig projectPackageConfig = PackageConfig.parseBytes(
|
||||
projectPackageConfigFile.readAsBytesSync(),
|
||||
projectPackageConfigFile.uri,
|
||||
);
|
||||
|
||||
// The flutter_tools package_config.json is guaranteed to include
|
||||
// package:ffi and package:test_core.
|
||||
final File flutterToolsPackageConfigFile = globals.fs.directory(
|
||||
globals.fs.path.join(
|
||||
Cache.flutterRoot!,
|
||||
'packages',
|
||||
'flutter_tools',
|
||||
),
|
||||
).childDirectory('.dart_tool').childFile('package_config.json');
|
||||
final PackageConfig flutterToolsPackageConfig = PackageConfig.parseBytes(
|
||||
flutterToolsPackageConfigFile.readAsBytesSync(),
|
||||
flutterToolsPackageConfigFile.uri,
|
||||
);
|
||||
|
||||
final List<Package> mergedPackages = <Package>[
|
||||
...projectPackageConfig.packages,
|
||||
];
|
||||
final Set<String> projectPackageNames = Set<String>.from(
|
||||
mergedPackages.map((Package p) => p.name),
|
||||
);
|
||||
for (final Package p in flutterToolsPackageConfig.packages) {
|
||||
if (!projectPackageNames.contains(p.name)) {
|
||||
mergedPackages.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
final PackageConfig mergedPackageConfig = PackageConfig(mergedPackages);
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
PackageConfig.writeString(mergedPackageConfig, buffer);
|
||||
isolateSpawningTesterPackageConfigFile.writeAsStringSync(buffer.toString());
|
||||
}
|
||||
|
||||
static void _generateChildTestIsolateSpawnerSourceFile(
|
||||
List<Uri> paths, {
|
||||
required List<String> packageTestArgs,
|
||||
required bool autoUpdateGoldenFiles,
|
||||
required File childTestIsolateSpawnerSourceFile,
|
||||
required File childTestIsolateSpawnerDillFile,
|
||||
}) {
|
||||
final Map<String, String> testConfigPaths = <String, String>{};
|
||||
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
buffer.writeln('''
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:stream_channel/isolate_channel.dart';
|
||||
import 'package:test_api/backend.dart'; // flutter_ignore: test_api_import
|
||||
''');
|
||||
|
||||
String pathToImport(String path) {
|
||||
assert(path.endsWith('.dart'));
|
||||
return path
|
||||
.replaceAll('.', '_')
|
||||
.replaceAll(':', '_')
|
||||
.replaceAll('/', '_')
|
||||
.replaceAll(r'\', '_')
|
||||
.replaceRange(path.length - '.dart'.length, null, '');
|
||||
}
|
||||
|
||||
final Map<String, String> testImports = <String, String>{};
|
||||
final Set<String> seenTestConfigPaths = <String>{};
|
||||
for (final Uri path in paths) {
|
||||
final String sanitizedPath = !path.path.endsWith('?')
|
||||
? path.path
|
||||
: path.path.substring(0, path.path.length - 1);
|
||||
final String sanitizedImport = pathToImport(sanitizedPath);
|
||||
buffer.writeln("import '$sanitizedPath' as $sanitizedImport;");
|
||||
testImports[sanitizedPath] = sanitizedImport;
|
||||
final File? testConfigFile = findTestConfigFile(
|
||||
globals.fs.file(
|
||||
globals.platform.isWindows
|
||||
? sanitizedPath.replaceAll('/', r'\').replaceFirst(r'\', '')
|
||||
: sanitizedPath,
|
||||
),
|
||||
globals.logger,
|
||||
);
|
||||
if (testConfigFile != null) {
|
||||
final String sanitizedTestConfigImport = pathToImport(testConfigFile.path);
|
||||
testConfigPaths[sanitizedImport] = sanitizedTestConfigImport;
|
||||
if (seenTestConfigPaths.add(testConfigFile.path)) {
|
||||
buffer.writeln("import '${Uri.file(testConfigFile.path, windows: true)}' as $sanitizedTestConfigImport;");
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('const List<String> packageTestArgs = <String>[');
|
||||
for (final String arg in packageTestArgs) {
|
||||
buffer.writeln(" '$arg',");
|
||||
}
|
||||
buffer.writeln('];');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln('const List<String> testPaths = <String>[');
|
||||
for (final Uri path in paths) {
|
||||
buffer.writeln(" '$path',");
|
||||
}
|
||||
buffer.writeln('];');
|
||||
buffer.writeln();
|
||||
|
||||
buffer.writeln(r'''
|
||||
@Native<Void Function(Pointer<Utf8>, Pointer<Utf8>)>(symbol: 'Spawn')
|
||||
external void _spawn(Pointer<Utf8> entrypoint, Pointer<Utf8> route);
|
||||
|
||||
void spawn({required SendPort port, String entrypoint = 'main', String route = '/'}) {
|
||||
assert(
|
||||
entrypoint != 'main' || route != '/',
|
||||
'Spawn should not be used to spawn main with the default route name',
|
||||
);
|
||||
IsolateNameServer.registerPortWithName(port, route);
|
||||
_spawn(entrypoint.toNativeUtf8(), route.toNativeUtf8());
|
||||
}
|
||||
''');
|
||||
|
||||
buffer.write('''
|
||||
/// Runs on a spawned isolate.
|
||||
void createChannelAndConnect(String path, String name, Function testMain) {
|
||||
goldenFileComparator = LocalFileComparator(Uri.parse(path));
|
||||
autoUpdateGoldenFiles = $autoUpdateGoldenFiles;
|
||||
final IsolateChannel<dynamic> channel = IsolateChannel<dynamic>.connectSend(
|
||||
IsolateNameServer.lookupPortByName(name)!,
|
||||
);
|
||||
channel.pipe(RemoteListener.start(() => testMain));
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
final String route = PlatformDispatcher.instance.defaultRouteName;
|
||||
switch (route) {
|
||||
''');
|
||||
|
||||
for (final MapEntry<String, String> kvp in testImports.entries) {
|
||||
final String importName = kvp.value;
|
||||
final String path = kvp.key;
|
||||
final String? testConfigImport = testConfigPaths[importName];
|
||||
if (testConfigImport != null) {
|
||||
buffer.writeln(" case '$importName':");
|
||||
buffer.writeln(" createChannelAndConnect('$path', route, () => $testConfigImport.testExecutable($importName.main));");
|
||||
} else {
|
||||
buffer.writeln(" case '$importName':");
|
||||
buffer.writeln(" createChannelAndConnect('$path', route, $importName.main);");
|
||||
}
|
||||
}
|
||||
|
||||
buffer.write(r'''
|
||||
}
|
||||
}
|
||||
|
||||
void main([dynamic sendPort]) {
|
||||
if (sendPort is SendPort) {
|
||||
final ReceivePort receivePort = ReceivePort();
|
||||
receivePort.listen((dynamic msg) {
|
||||
switch (msg as List<dynamic>) {
|
||||
case ['spawn', final SendPort port, final String entrypoint, final String route]:
|
||||
spawn(port: port, entrypoint: entrypoint, route: route);
|
||||
case ['close']:
|
||||
receivePort.close();
|
||||
}
|
||||
});
|
||||
|
||||
sendPort.send(<Object>[receivePort.sendPort, packageTestArgs, testPaths]);
|
||||
}
|
||||
}
|
||||
''');
|
||||
|
||||
childTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString());
|
||||
}
|
||||
|
||||
static void _generateRootTestIsolateSpawnerSourceFile({
|
||||
required File childTestIsolateSpawnerSourceFile,
|
||||
required File childTestIsolateSpawnerDillFile,
|
||||
required File rootTestIsolateSpawnerSourceFile,
|
||||
}) {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
buffer.writeln('''
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io' show exit, exitCode; // flutter_ignore: dart_io_import
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:stream_channel/isolate_channel.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports
|
||||
import 'package:test_core/src/platform.dart'; // ignore: implementation_imports
|
||||
|
||||
@Native<Handle Function(Pointer<Utf8>)>(symbol: 'LoadLibraryFromKernel')
|
||||
external Object _loadLibraryFromKernel(Pointer<Utf8> path);
|
||||
|
||||
@Native<Handle Function(Pointer<Utf8>, Pointer<Utf8>)>(symbol: 'LookupEntryPoint')
|
||||
external Object _lookupEntryPoint(Pointer<Utf8> library, Pointer<Utf8> name);
|
||||
|
||||
late final List<String> packageTestArgs;
|
||||
late final List<String> testPaths;
|
||||
|
||||
/// Runs on the main isolate.
|
||||
Future<void> registerPluginAndRun() {
|
||||
final SpawnPlugin platform = SpawnPlugin();
|
||||
registerPlatformPlugin(
|
||||
<Runtime>[Runtime.vm],
|
||||
() {
|
||||
return platform;
|
||||
},
|
||||
);
|
||||
return test.main(<String>[...packageTestArgs, '--', ...testPaths]);
|
||||
}
|
||||
|
||||
late final Isolate rootTestIsolate;
|
||||
late final SendPort commandPort;
|
||||
bool readyToRun = false;
|
||||
final Completer<void> readyToRunSignal = Completer<void>();
|
||||
|
||||
Future<void> spawn({
|
||||
required SendPort port,
|
||||
String entrypoint = 'main',
|
||||
String route = '/',
|
||||
}) async {
|
||||
if (!readyToRun) {
|
||||
await readyToRunSignal.future;
|
||||
}
|
||||
|
||||
commandPort.send(<Object>['spawn', port, entrypoint, route]);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
final String route = PlatformDispatcher.instance.defaultRouteName;
|
||||
|
||||
if (route == '/') {
|
||||
final ReceivePort port = ReceivePort();
|
||||
|
||||
port.listen((dynamic message) {
|
||||
final [SendPort sendPort, List<String> args, List<String> paths] = message as List<dynamic>;
|
||||
|
||||
commandPort = sendPort;
|
||||
packageTestArgs = args;
|
||||
testPaths = paths;
|
||||
readyToRun = true;
|
||||
readyToRunSignal.complete();
|
||||
});
|
||||
|
||||
rootTestIsolate = await Isolate.spawn(
|
||||
_loadLibraryFromKernel(
|
||||
r'${childTestIsolateSpawnerDillFile.absolute.path}'
|
||||
.toNativeUtf8()) as void Function(SendPort),
|
||||
port.sendPort,
|
||||
);
|
||||
|
||||
await readyToRunSignal.future;
|
||||
port.close(); // Not expecting anything else.
|
||||
await registerPluginAndRun();
|
||||
// The [test.main] call in [registerPluginAndRun] sets dart:io's [exitCode]
|
||||
// global.
|
||||
exit(exitCode);
|
||||
} else {
|
||||
(_lookupEntryPoint(
|
||||
r'file://${childTestIsolateSpawnerSourceFile.absolute.uri.toFilePath(windows: false)}'
|
||||
.toNativeUtf8(),
|
||||
'testMain'.toNativeUtf8()) as void Function())();
|
||||
}
|
||||
}
|
||||
''');
|
||||
|
||||
buffer.write(r'''
|
||||
String pathToImport(String path) {
|
||||
assert(path.endsWith('.dart'));
|
||||
return path
|
||||
.replaceRange(path.length - '.dart'.length, null, '')
|
||||
.replaceAll('.', '_')
|
||||
.replaceAll(':', '_')
|
||||
.replaceAll('/', '_')
|
||||
.replaceAll(r'\', '_');
|
||||
}
|
||||
|
||||
class SpawnPlugin extends PlatformPlugin {
|
||||
SpawnPlugin();
|
||||
|
||||
final Map<String, IsolateChannel<dynamic>> _channels = <String, IsolateChannel<dynamic>>{};
|
||||
|
||||
Future<void> launchIsolate(String path) async {
|
||||
final String name = pathToImport(path);
|
||||
final ReceivePort port = ReceivePort();
|
||||
_channels[name] = IsolateChannel<dynamic>.connectReceive(port);
|
||||
await spawn(port: port.sendPort, route: name);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
commandPort.send(<String>['close']);
|
||||
}
|
||||
''');
|
||||
|
||||
buffer.write('''
|
||||
@override
|
||||
Future<RunnerSuite> load(
|
||||
String path,
|
||||
SuitePlatform platform,
|
||||
SuiteConfiguration suiteConfig,
|
||||
Object message,
|
||||
) async {
|
||||
final String correctedPath = ${globals.platform.isWindows ? r'"/$path"' : 'path'};
|
||||
await launchIsolate(correctedPath);
|
||||
|
||||
final StreamChannel<dynamic> channel = _channels[pathToImport(correctedPath)]!;
|
||||
final RunnerSuiteController controller = deserializeSuite(correctedPath, platform,
|
||||
suiteConfig, const PluginEnvironment(), channel, message);
|
||||
return controller.suite;
|
||||
}
|
||||
}
|
||||
''');
|
||||
|
||||
rootTestIsolateSpawnerSourceFile.writeAsStringSync(buffer.toString());
|
||||
}
|
||||
|
||||
static Future<void> _compileFile({
|
||||
required DebuggingOptions debuggingOptions,
|
||||
required File packageConfigFile,
|
||||
required PackageConfig packageConfig,
|
||||
required File sourceFile,
|
||||
required File outputDillFile,
|
||||
required TestTimeRecorder? testTimeRecorder,
|
||||
Uri? nativeAssetsYaml,
|
||||
}) async {
|
||||
globals.printTrace('Compiling ${sourceFile.absolute.uri}');
|
||||
final Stopwatch compilerTime = Stopwatch()..start();
|
||||
final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Compile);
|
||||
|
||||
final ResidentCompiler residentCompiler = ResidentCompiler(
|
||||
globals.artifacts!.getArtifactPath(Artifact.flutterPatchedSdkPath),
|
||||
artifacts: globals.artifacts!,
|
||||
logger: globals.logger,
|
||||
processManager: globals.processManager,
|
||||
buildMode: debuggingOptions.buildInfo.mode,
|
||||
trackWidgetCreation: debuggingOptions. buildInfo.trackWidgetCreation,
|
||||
dartDefines: debuggingOptions.buildInfo.dartDefines,
|
||||
packagesPath: packageConfigFile.path,
|
||||
frontendServerStarterPath: debuggingOptions.buildInfo.frontendServerStarterPath,
|
||||
extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions,
|
||||
platform: globals.platform,
|
||||
testCompilation: true,
|
||||
fileSystem: globals.fs,
|
||||
fileSystemRoots: debuggingOptions.buildInfo.fileSystemRoots,
|
||||
fileSystemScheme: debuggingOptions.buildInfo.fileSystemScheme,
|
||||
);
|
||||
|
||||
await residentCompiler.recompile(
|
||||
sourceFile.absolute.uri,
|
||||
null,
|
||||
outputPath: outputDillFile.absolute.path,
|
||||
packageConfig: packageConfig,
|
||||
fs: globals.fs,
|
||||
nativeAssetsYaml: nativeAssetsYaml,
|
||||
);
|
||||
residentCompiler.accept();
|
||||
|
||||
globals.printTrace('Compiling ${sourceFile.absolute.uri} took ${compilerTime.elapsedMilliseconds}ms');
|
||||
testTimeRecorder?.stop(TestTimePhases.Compile, testTimeRecorderStopwatch!);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runTestsBySpawningLightweightEngines(
|
||||
List<Uri> testFiles, {
|
||||
required DebuggingOptions debuggingOptions,
|
||||
List<String> names = const <String>[],
|
||||
List<String> plainNames = const <String>[],
|
||||
String? tags,
|
||||
String? excludeTags,
|
||||
bool machine = false,
|
||||
bool updateGoldens = false,
|
||||
required int? concurrency,
|
||||
String? testAssetDirectory,
|
||||
FlutterProject? flutterProject,
|
||||
String? icudtlPath,
|
||||
String? randomSeed,
|
||||
String? reporter,
|
||||
String? fileReporter,
|
||||
String? timeout,
|
||||
bool runSkipped = false,
|
||||
int? shardIndex,
|
||||
int? totalShards,
|
||||
TestTimeRecorder? testTimeRecorder,
|
||||
TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
|
||||
}) async {
|
||||
assert(testFiles.length > 1);
|
||||
|
||||
final Directory buildDirectory = globals.fs.directory(globals.fs.path.join(
|
||||
flutterProject!.directory.path,
|
||||
getBuildDirectory(),
|
||||
));
|
||||
final Directory isolateSpawningTesterDirectory = buildDirectory.childDirectory(
|
||||
'isolate_spawning_tester',
|
||||
);
|
||||
isolateSpawningTesterDirectory.createSync();
|
||||
|
||||
final File isolateSpawningTesterPackageConfigFile = isolateSpawningTesterDirectory
|
||||
.childDirectory('.dart_tool')
|
||||
.childFile(
|
||||
'package_config.json',
|
||||
);
|
||||
isolateSpawningTesterPackageConfigFile.createSync(recursive: true);
|
||||
await _generateIsolateSpawningTesterPackageConfig(
|
||||
flutterProject: flutterProject,
|
||||
isolateSpawningTesterPackageConfigFile: isolateSpawningTesterPackageConfigFile,
|
||||
);
|
||||
final PackageConfig isolateSpawningTesterPackageConfig = PackageConfig.parseBytes(
|
||||
isolateSpawningTesterPackageConfigFile.readAsBytesSync(),
|
||||
isolateSpawningTesterPackageConfigFile.uri,
|
||||
);
|
||||
|
||||
final File childTestIsolateSpawnerSourceFile = isolateSpawningTesterDirectory.childFile(
|
||||
'child_test_isolate_spawner.dart',
|
||||
);
|
||||
final File rootTestIsolateSpawnerSourceFile = isolateSpawningTesterDirectory.childFile(
|
||||
'root_test_isolate_spawner.dart',
|
||||
);
|
||||
final File childTestIsolateSpawnerDillFile = isolateSpawningTesterDirectory.childFile(
|
||||
'child_test_isolate_spawner.dill',
|
||||
);
|
||||
final File rootTestIsolateSpawnerDillFile = isolateSpawningTesterDirectory.childFile(
|
||||
'root_test_isolate_spawner.dill',
|
||||
);
|
||||
|
||||
// Compute the command-line arguments for package:test.
|
||||
final List<String> packageTestArgs = <String>[
|
||||
if (!globals.terminal.supportsColor)
|
||||
'--no-color',
|
||||
if (machine)
|
||||
...<String>['-r', 'json']
|
||||
else if (reporter != null)
|
||||
...<String>['-r', reporter],
|
||||
if (fileReporter != null)
|
||||
'--file-reporter=$fileReporter',
|
||||
if (timeout != null)
|
||||
...<String>['--timeout', timeout],
|
||||
if (concurrency != null)
|
||||
'--concurrency=$concurrency',
|
||||
for (final String name in names)
|
||||
...<String>['--name', name],
|
||||
for (final String plainName in plainNames)
|
||||
...<String>['--plain-name', plainName],
|
||||
if (randomSeed != null)
|
||||
'--test-randomize-ordering-seed=$randomSeed',
|
||||
if (tags != null)
|
||||
...<String>['--tags', tags],
|
||||
if (excludeTags != null)
|
||||
...<String>['--exclude-tags', excludeTags],
|
||||
if (runSkipped)
|
||||
'--run-skipped',
|
||||
if (totalShards != null)
|
||||
'--total-shards=$totalShards',
|
||||
if (shardIndex != null)
|
||||
'--shard-index=$shardIndex',
|
||||
'--chain-stack-traces',
|
||||
];
|
||||
|
||||
_generateChildTestIsolateSpawnerSourceFile(
|
||||
testFiles,
|
||||
packageTestArgs: packageTestArgs,
|
||||
autoUpdateGoldenFiles: updateGoldens,
|
||||
childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile,
|
||||
childTestIsolateSpawnerDillFile: childTestIsolateSpawnerDillFile,
|
||||
);
|
||||
|
||||
_generateRootTestIsolateSpawnerSourceFile(
|
||||
childTestIsolateSpawnerSourceFile: childTestIsolateSpawnerSourceFile,
|
||||
childTestIsolateSpawnerDillFile: childTestIsolateSpawnerDillFile,
|
||||
rootTestIsolateSpawnerSourceFile: rootTestIsolateSpawnerSourceFile,
|
||||
);
|
||||
|
||||
final Uri? nativeAssetsYaml = await nativeAssetsBuilder?.build(
|
||||
debuggingOptions.buildInfo,
|
||||
);
|
||||
|
||||
await _compileFile(
|
||||
debuggingOptions: debuggingOptions,
|
||||
packageConfigFile: isolateSpawningTesterPackageConfigFile,
|
||||
packageConfig: isolateSpawningTesterPackageConfig,
|
||||
sourceFile: childTestIsolateSpawnerSourceFile,
|
||||
outputDillFile: childTestIsolateSpawnerDillFile,
|
||||
testTimeRecorder: testTimeRecorder,
|
||||
nativeAssetsYaml: nativeAssetsYaml,
|
||||
);
|
||||
|
||||
await _compileFile(
|
||||
debuggingOptions: debuggingOptions,
|
||||
packageConfigFile: isolateSpawningTesterPackageConfigFile,
|
||||
packageConfig: isolateSpawningTesterPackageConfig,
|
||||
sourceFile: rootTestIsolateSpawnerSourceFile,
|
||||
outputDillFile: rootTestIsolateSpawnerDillFile,
|
||||
testTimeRecorder: testTimeRecorder,
|
||||
);
|
||||
|
||||
final List<String> command = <String>[
|
||||
globals.artifacts!.getArtifactPath(Artifact.flutterTester),
|
||||
'--disable-vm-service',
|
||||
if (icudtlPath != null) '--icu-data-file-path=$icudtlPath',
|
||||
'--enable-checked-mode',
|
||||
'--verify-entry-points',
|
||||
'--enable-software-rendering',
|
||||
'--skia-deterministic-rendering',
|
||||
if (debuggingOptions.enableDartProfiling)
|
||||
'--enable-dart-profiling',
|
||||
'--non-interactive',
|
||||
'--use-test-fonts',
|
||||
'--disable-asset-fonts',
|
||||
'--packages=${debuggingOptions.buildInfo.packagesPath}',
|
||||
if (testAssetDirectory != null)
|
||||
'--flutter-assets-dir=$testAssetDirectory',
|
||||
if (debuggingOptions.nullAssertions)
|
||||
'--dart-flags=--null_assertions',
|
||||
...debuggingOptions.dartEntrypointArgs,
|
||||
rootTestIsolateSpawnerDillFile.absolute.path
|
||||
];
|
||||
|
||||
// If the FLUTTER_TEST environment variable has been set, then pass it on
|
||||
// for package:flutter_test to handle the value.
|
||||
//
|
||||
// If FLUTTER_TEST has not been set, assume from this context that this
|
||||
// call was invoked by the command 'flutter test'.
|
||||
final String flutterTest = globals.platform.environment.containsKey('FLUTTER_TEST')
|
||||
? globals.platform.environment['FLUTTER_TEST']!
|
||||
: 'true';
|
||||
final Map<String, String> environment = <String, String>{
|
||||
'FLUTTER_TEST': flutterTest,
|
||||
'FONTCONFIG_FILE': FontConfigManager().fontConfigFile.path,
|
||||
'APP_NAME': flutterProject.manifest.appName,
|
||||
if (testAssetDirectory != null)
|
||||
'UNIT_TEST_ASSETS': testAssetDirectory,
|
||||
};
|
||||
|
||||
globals.logger.printTrace('Starting flutter_tester process with command=$command, environment=$environment');
|
||||
final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Run);
|
||||
final Process process = await globals.processManager.start(command, environment: environment);
|
||||
globals.logger.printTrace('Started flutter_tester process at pid ${process.pid}');
|
||||
|
||||
for (final Stream<List<int>> stream in <Stream<List<int>>>[
|
||||
process.stderr,
|
||||
process.stdout,
|
||||
]) {
|
||||
stream
|
||||
.transform<String>(utf8.decoder)
|
||||
.listen(globals.stdio.stdoutWrite);
|
||||
}
|
||||
|
||||
return process.exitCode.then((int exitCode) {
|
||||
testTimeRecorder?.stop(TestTimePhases.Run, testTimeRecorderStopwatch!);
|
||||
globals.logger.printTrace('flutter_tester process at pid ${process.pid} exited with code=$exitCode');
|
||||
return exitCode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ dev_dependencies:
|
|||
pubspec_parse: 1.2.3
|
||||
|
||||
checked_yaml: 2.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
ffi: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
json_annotation: 4.8.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
test: 1.25.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
|
@ -118,4 +119,4 @@ dartdoc:
|
|||
# Exclude this package from the hosted API docs.
|
||||
nodoc: true
|
||||
|
||||
# PUBSPEC CHECKSUM: a00d
|
||||
# PUBSPEC CHECKSUM: 588f
|
||||
|
|
|
@ -7,9 +7,11 @@ import 'dart:convert';
|
|||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/base/async_guard.dart';
|
||||
import 'package:flutter_tools/src/base/common.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/terminal.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/commands/test.dart';
|
||||
import 'package:flutter_tools/src/device.dart';
|
||||
|
@ -25,6 +27,7 @@ import 'package:flutter_tools/src/test/test_wrapper.dart';
|
|||
import 'package:flutter_tools/src/test/watcher.dart';
|
||||
import 'package:flutter_tools/src/web/compile.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:test/fake.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
|
@ -34,6 +37,38 @@ import '../../src/fake_vm_services.dart';
|
|||
import '../../src/logging_logger.dart';
|
||||
import '../../src/test_flutter_command_runner.dart';
|
||||
|
||||
final String _flutterToolsPackageConfigContents = json.encode(<String, Object>{
|
||||
'configVersion': 2,
|
||||
'packages': <Map<String, Object>>[
|
||||
<String, String>{
|
||||
'name': 'ffi',
|
||||
'rootUri': 'file:///path/to/pubcache/.pub-cache/hosted/pub.dev/ffi-2.1.2',
|
||||
'packageUri': 'lib/',
|
||||
'languageVersion': '3.3',
|
||||
},
|
||||
<String, String>{
|
||||
'name': 'test',
|
||||
'rootUri': 'file:///path/to/pubcache/.pub-cache/hosted/pub.dev/test-1.24.9',
|
||||
'packageUri': 'lib/',
|
||||
'languageVersion': '3.0'
|
||||
},
|
||||
<String, String>{
|
||||
'name': 'test_api',
|
||||
'rootUri': 'file:///path/to/pubcache/.pub-cache/hosted/pub.dev/test_api-0.6.1',
|
||||
'packageUri': 'lib/',
|
||||
'languageVersion': '3.0'
|
||||
},
|
||||
<String, String>{
|
||||
'name': 'test_core',
|
||||
'rootUri': 'file:///path/to/pubcache/.pub-cache/hosted/pub.dev/test_core-0.5.9',
|
||||
'packageUri': 'lib/',
|
||||
'languageVersion': '3.0'
|
||||
},
|
||||
],
|
||||
'generated': '2021-02-24T07:55:20.084834Z',
|
||||
'generator': 'pub',
|
||||
'generatorVersion': '2.13.0-68.0.dev',
|
||||
});
|
||||
const String _pubspecContents = '''
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -68,6 +103,7 @@ void main() {
|
|||
|
||||
setUp(() {
|
||||
fs = MemoryFileSystem.test(style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix);
|
||||
|
||||
final Directory package = fs.directory('package');
|
||||
package.childFile('pubspec.yaml').createSync(recursive: true);
|
||||
package.childFile('pubspec.yaml').writeAsStringSync(_pubspecContents);
|
||||
|
@ -78,6 +114,18 @@ void main() {
|
|||
package.childDirectory('test').childFile('some_test.dart').createSync(recursive: true);
|
||||
package.childDirectory('integration_test').childFile('some_integration_test.dart').createSync(recursive: true);
|
||||
|
||||
final File flutterToolsPackageConfigFile = fs.directory(
|
||||
fs.path.join(
|
||||
getFlutterRoot(),
|
||||
'packages',
|
||||
'flutter_tools'
|
||||
),
|
||||
).childDirectory('.dart_tool').childFile('package_config.json');
|
||||
flutterToolsPackageConfigFile.createSync(recursive: true);
|
||||
flutterToolsPackageConfigFile.writeAsStringSync(
|
||||
_flutterToolsPackageConfigContents,
|
||||
);
|
||||
|
||||
fs.currentDirectory = package.path;
|
||||
|
||||
logger = LoggingLogger();
|
||||
|
@ -485,6 +533,194 @@ dev_dependencies:
|
|||
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
|
||||
});
|
||||
|
||||
testUsingContext('Generates a satisfactory test runner package_config.json when --experimental-faster-testing is set',
|
||||
() async {
|
||||
final TestCommand testCommand = TestCommand();
|
||||
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
|
||||
|
||||
bool caughtToolExit = false;
|
||||
await asyncGuard<void>(
|
||||
() => commandRunner.run(const <String>[
|
||||
'test',
|
||||
'--no-pub',
|
||||
'--experimental-faster-testing',
|
||||
'--',
|
||||
'test/fake_test.dart',
|
||||
'test/fake_test_2.dart',
|
||||
]),
|
||||
onError: (Object error) async {
|
||||
expect(error, isA<ToolExit>());
|
||||
// We expect this message because we are using a fake ProcessManager.
|
||||
expect(
|
||||
(error as ToolExit).message,
|
||||
contains('the Dart compiler exited unexpectedly.'),
|
||||
);
|
||||
caughtToolExit = true;
|
||||
|
||||
final File isolateSpawningTesterPackageConfigFile = fs.directory(
|
||||
fs.path.join(
|
||||
'build',
|
||||
'isolate_spawning_tester',
|
||||
),
|
||||
).childDirectory('.dart_tool').childFile('package_config.json');
|
||||
expect(isolateSpawningTesterPackageConfigFile.existsSync(), true);
|
||||
// We expect [isolateSpawningTesterPackageConfigFile] to contain the
|
||||
// union of the packages in [_packageConfigContents] and
|
||||
// [_flutterToolsPackageConfigContents].
|
||||
expect(
|
||||
isolateSpawningTesterPackageConfigFile.readAsStringSync().contains('"name": "integration_test"'),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isolateSpawningTesterPackageConfigFile.readAsStringSync().contains('"name": "ffi"'),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isolateSpawningTesterPackageConfigFile.readAsStringSync().contains('"name": "test"'),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isolateSpawningTesterPackageConfigFile.readAsStringSync().contains('"name": "test_api"'),
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isolateSpawningTesterPackageConfigFile.readAsStringSync().contains('"name": "test_core"'),
|
||||
true,
|
||||
);
|
||||
}
|
||||
);
|
||||
expect(caughtToolExit, true);
|
||||
}, overrides: <Type, Generator>{
|
||||
AnsiTerminal: () => _FakeTerminal(),
|
||||
FileSystem: () => fs,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
DeviceManager: () => _FakeDeviceManager(<Device>[]),
|
||||
});
|
||||
|
||||
testUsingContext('Pipes specified arguments to package:test when --experimental-faster-testing is set',
|
||||
() async {
|
||||
final TestCommand testCommand = TestCommand();
|
||||
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
|
||||
|
||||
bool caughtToolExit = false;
|
||||
await asyncGuard<void>(
|
||||
() => commandRunner.run(const <String>[
|
||||
'test',
|
||||
'--no-pub',
|
||||
'--experimental-faster-testing',
|
||||
'--reporter=compact',
|
||||
'--file-reporter=json:reports/tests.json',
|
||||
'--timeout=100',
|
||||
'--concurrency=3',
|
||||
'--name=name1',
|
||||
'--plain-name=name2',
|
||||
'--test-randomize-ordering-seed=random',
|
||||
'--tags=tag1',
|
||||
'--exclude-tags=tag2',
|
||||
'--run-skipped',
|
||||
'--total-shards=1',
|
||||
'--shard-index=1',
|
||||
'--',
|
||||
'test/fake_test.dart',
|
||||
'test/fake_test_2.dart',
|
||||
]),
|
||||
onError: (Object error) async {
|
||||
expect(error, isA<ToolExit>());
|
||||
// We expect this message because we are using a fake ProcessManager.
|
||||
expect(
|
||||
(error as ToolExit).message,
|
||||
contains('the Dart compiler exited unexpectedly.'),
|
||||
);
|
||||
caughtToolExit = true;
|
||||
|
||||
final File childTestIsolateSpawnerSourceFile = fs.directory(
|
||||
fs.path.join(
|
||||
'build',
|
||||
'isolate_spawning_tester',
|
||||
),
|
||||
).childFile('child_test_isolate_spawner.dart');
|
||||
expect(childTestIsolateSpawnerSourceFile.existsSync(), true);
|
||||
expect(childTestIsolateSpawnerSourceFile.readAsStringSync().contains('''
|
||||
const List<String> packageTestArgs = <String>[
|
||||
'--no-color',
|
||||
'-r',
|
||||
'compact',
|
||||
'--file-reporter=json:reports/tests.json',
|
||||
'--timeout',
|
||||
'100',
|
||||
'--concurrency=3',
|
||||
'--name',
|
||||
'name1',
|
||||
'--plain-name',
|
||||
'name2',
|
||||
'--test-randomize-ordering-seed=random',
|
||||
'--tags',
|
||||
'tag1',
|
||||
'--exclude-tags',
|
||||
'tag2',
|
||||
'--run-skipped',
|
||||
'--total-shards=1',
|
||||
'--shard-index=1',
|
||||
'--chain-stack-traces',
|
||||
];
|
||||
'''), true);
|
||||
}
|
||||
);
|
||||
expect(caughtToolExit, true);
|
||||
}, overrides: <Type, Generator>{
|
||||
AnsiTerminal: () => _FakeTerminal(),
|
||||
FileSystem: () => fs,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
DeviceManager: () => _FakeDeviceManager(<Device>[]),
|
||||
});
|
||||
|
||||
testUsingContext('Only passes --no-color and --chain-stack-traces to package:test by default when --experimental-faster-testing is set',
|
||||
() async {
|
||||
final TestCommand testCommand = TestCommand();
|
||||
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
|
||||
|
||||
bool caughtToolExit = false;
|
||||
await asyncGuard<void>(
|
||||
() => commandRunner.run(const <String>[
|
||||
'test',
|
||||
'--no-pub',
|
||||
'--experimental-faster-testing',
|
||||
'--',
|
||||
'test/fake_test.dart',
|
||||
'test/fake_test_2.dart',
|
||||
]),
|
||||
onError: (Object error) async {
|
||||
expect(error, isA<ToolExit>());
|
||||
// We expect this message because we are using a fake ProcessManager.
|
||||
expect(
|
||||
(error as ToolExit).message,
|
||||
contains('the Dart compiler exited unexpectedly.'),
|
||||
);
|
||||
caughtToolExit = true;
|
||||
|
||||
final File childTestIsolateSpawnerSourceFile = fs.directory(
|
||||
fs.path.join(
|
||||
'build',
|
||||
'isolate_spawning_tester',
|
||||
),
|
||||
).childFile('child_test_isolate_spawner.dart');
|
||||
expect(childTestIsolateSpawnerSourceFile.existsSync(), true);
|
||||
expect(childTestIsolateSpawnerSourceFile.readAsStringSync().contains('''
|
||||
const List<String> packageTestArgs = <String>[
|
||||
'--no-color',
|
||||
'--chain-stack-traces',
|
||||
];
|
||||
'''), true);
|
||||
}
|
||||
);
|
||||
expect(caughtToolExit, true);
|
||||
}, overrides: <Type, Generator>{
|
||||
AnsiTerminal: () => _FakeTerminal(),
|
||||
FileSystem: () => fs,
|
||||
ProcessManager: () => FakeProcessManager.any(),
|
||||
DeviceManager: () => _FakeDeviceManager(<Device>[]),
|
||||
});
|
||||
|
||||
testUsingContext('Verbose prints phase timings', () async {
|
||||
final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0, const Duration(milliseconds: 1));
|
||||
|
||||
|
@ -1182,6 +1418,33 @@ class FakeFlutterTestRunner implements FlutterTestRunner {
|
|||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
@override
|
||||
Never runTestsBySpawningLightweightEngines(
|
||||
List<Uri> testFiles, {
|
||||
required DebuggingOptions debuggingOptions,
|
||||
List<String> names = const <String>[],
|
||||
List<String> plainNames = const <String>[],
|
||||
String? tags,
|
||||
String? excludeTags,
|
||||
bool machine = false,
|
||||
bool updateGoldens = false,
|
||||
required int? concurrency,
|
||||
String? testAssetDirectory,
|
||||
FlutterProject? flutterProject,
|
||||
String? icudtlPath,
|
||||
String? randomSeed,
|
||||
String? reporter,
|
||||
String? fileReporter,
|
||||
String? timeout,
|
||||
bool runSkipped = false,
|
||||
int? shardIndex,
|
||||
int? totalShards,
|
||||
TestTimeRecorder? testTimeRecorder,
|
||||
TestCompilerNativeAssetsBuilder? nativeAssetsBuilder,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class TestTestDevice extends TestDevice {
|
||||
|
@ -1215,6 +1478,14 @@ class FakePackageTest implements TestWrapper {
|
|||
) {}
|
||||
}
|
||||
|
||||
class _FakeTerminal extends Fake implements AnsiTerminal {
|
||||
@override
|
||||
final bool supportsColor = false;
|
||||
|
||||
@override
|
||||
bool get isCliAnimationEnabled => supportsColor;
|
||||
}
|
||||
|
||||
class _FakeDeviceManager extends DeviceManager {
|
||||
_FakeDeviceManager(this._connectedDevices) : super(logger: testLogger);
|
||||
|
||||
|
|
|
@ -130,6 +130,23 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run a test when its name matches a regexp when --experimental-faster-testing is set',
|
||||
() async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
null,
|
||||
automatedTestsDirectory,
|
||||
flutterTestDirectory,
|
||||
extraArguments: const <String>[
|
||||
'--experimental-faster-testing',
|
||||
'--name=inc.*de',
|
||||
],
|
||||
);
|
||||
expect(
|
||||
result,
|
||||
ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!')),
|
||||
);
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run a test when its name contains a string', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
'filtering',
|
||||
|
@ -143,6 +160,22 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run a test when its name contains a string when --experimental-faster-testing is set', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
null,
|
||||
automatedTestsDirectory,
|
||||
flutterTestDirectory,
|
||||
extraArguments: const <String>[
|
||||
'--experimental-faster-testing',
|
||||
'--plain-name=include',
|
||||
],
|
||||
);
|
||||
expect(
|
||||
result,
|
||||
ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!')),
|
||||
);
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run a test with a given tag', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
'filtering_tag',
|
||||
|
@ -156,6 +189,23 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run a test with a given tag when --experimental-faster-testing is set', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
null,
|
||||
automatedTestsDirectory,
|
||||
flutterTestDirectory,
|
||||
extraArguments: const <String>[
|
||||
'--experimental-faster-testing',
|
||||
'--tags=include-tag',
|
||||
],
|
||||
);
|
||||
expect(
|
||||
result,
|
||||
ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!')),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
testWithoutContext('flutter test should not run a test with excluded tag', () async {
|
||||
final ProcessResult result = await _runFlutterTest('filtering_tag', automatedTestsDirectory, flutterTestDirectory,
|
||||
extraArguments: const <String>['--exclude-tags', 'exclude-tag']);
|
||||
|
@ -176,6 +226,22 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run all tests when tags are unspecified when --experimental-faster-testing is set', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
null,
|
||||
automatedTestsDirectory,
|
||||
flutterTestDirectory,
|
||||
extraArguments: const <String>['--experimental-faster-testing'],
|
||||
);
|
||||
expect(
|
||||
result,
|
||||
ProcessResultMatcher(
|
||||
exitCode: 1,
|
||||
stdoutPattern: RegExp(r'\+\d+ -\d+: Some tests failed\.'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run a widgetTest with a given tag', () async {
|
||||
final ProcessResult result = await _runFlutterTest('filtering_tag_widget', automatedTestsDirectory, flutterTestDirectory,
|
||||
extraArguments: const <String>['--tags', 'include-tag']);
|
||||
|
@ -238,6 +304,75 @@ void main() {
|
|||
expect(result, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should test runs to completion when --experimental-faster-testing is set', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
null,
|
||||
automatedTestsDirectory,
|
||||
'$flutterTestDirectory/child_directory',
|
||||
extraArguments: const <String>[
|
||||
'--experimental-faster-testing',
|
||||
'--verbose',
|
||||
],
|
||||
);
|
||||
final String stdout = (result.stdout as String).replaceAll('\r', '\n');
|
||||
expect(stdout, contains(RegExp(r'\+\d+: All tests passed\!')));
|
||||
expect(stdout, contains('Starting flutter_tester process with command'));
|
||||
if ((result.stderr as String).isNotEmpty) {
|
||||
fail('unexpected error output from test:\n\n${result.stderr}\n-- end stderr --\n\n');
|
||||
}
|
||||
expect(result, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should ignore --experimental-faster-testing when only a single test file is specified', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
'trivial',
|
||||
automatedTestsDirectory,
|
||||
flutterTestDirectory,
|
||||
extraArguments: const <String>[
|
||||
'--experimental-faster-testing',
|
||||
'--verbose'
|
||||
],
|
||||
);
|
||||
final String stdout = (result.stdout as String).replaceAll('\r', '\n');
|
||||
expect(
|
||||
stdout,
|
||||
contains('--experimental-faster-testing was parsed but will be ignored. '
|
||||
'This option should not be used when running a single test file.'),
|
||||
);
|
||||
expect(stdout, contains(RegExp(r'\+\d+: All tests passed\!')));
|
||||
expect(stdout, contains('test 0: Starting flutter_tester process with command'));
|
||||
expect(stdout, contains('test 0: deleting temporary directory'));
|
||||
expect(stdout, contains('test 0: finished'));
|
||||
expect(stdout, contains('test package returned with exit code 0'));
|
||||
if ((result.stderr as String).isNotEmpty) {
|
||||
fail('unexpected error output from test:\n\n${result.stderr}\n-- end stderr --\n\n');
|
||||
}
|
||||
expect(result, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should ignore --experimental-faster-testing when running integration tests', () async {
|
||||
final ProcessResult result = await _runFlutterTest(
|
||||
'trivial_widget',
|
||||
automatedTestsDirectory,
|
||||
integrationTestDirectory,
|
||||
extraArguments: <String>[
|
||||
...integrationTestExtraArgs,
|
||||
'--experimental-faster-testing',
|
||||
],
|
||||
);
|
||||
final String stdout = (result.stdout as String).replaceAll('\r', '\n');
|
||||
expect(
|
||||
stdout,
|
||||
contains('--experimental-faster-testing was parsed but will be ignored. '
|
||||
'This option is not supported when running integration tests or web '
|
||||
'tests.'),
|
||||
);
|
||||
if ((result.stderr as String).isNotEmpty) {
|
||||
fail('unexpected error output from test:\n\n${result.stderr}\n-- end stderr --\n\n');
|
||||
}
|
||||
expect(result, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
testWithoutContext('flutter test should run all tests inside of a directory with no trailing slash', () async {
|
||||
final ProcessResult result = await _runFlutterTest(null, automatedTestsDirectory, '$flutterTestDirectory/child_directory',
|
||||
extraArguments: const <String>['--verbose']);
|
||||
|
|
Loading…
Reference in a new issue