auto-submit[bot] 7cdc23b3e1
Reverts "Launch DDS from Dart SDK and prepare to serve DevTools from DDS (#146593)" (#151781)
Reverts: flutter/flutter#146593
Initiated by: zanderso
Reason for reverting: Consistently failing `Windows_android native_assets_android` as in https://ci.chromium.org/ui/p/flutter/builders/prod/Windows_android%20native_assets_android/2533/overview 
Original PR Author: bkonyi

Reviewed By: {christopherfujino, kenzieschmoll}

This change reverts the following previous change:
This change is a major step towards moving away from shipping DDS via Pub.

The first component of this PR is the move away from importing package:dds to launch DDS. Instead, DDS is launched out of process using the `dart development-service` command shipped with the Dart SDK. This makes Flutter's handling of DDS consistent with the standalone Dart VM.

The second component of this PR is the initial work to prepare for the removal of instances of DevTools being served manually by the flutter_tool, instead relying on DDS to serve DevTools. This will be consistent with how the standalone Dart VM serves DevTools, tying the DevTools lifecycle to a live DDS instance. This will allow for the removal of much of the logic needed to properly manage the lifecycle of the DevTools server in a future PR. Also, by serving DevTools from DDS, users will no longer need to forward a secondary port in remote workflows as DevTools will be available on the DDS port.

There's two remaining circumstances that will prevent us from removing DevtoolsRunner completely:

 - The daemon's `devtools.serve` endpoint
 - `flutter drive`'s `--profile-memory` flag used for recording memory profiles

This PR also includes some refactoring around `DebuggingOptions` to reduce the number of debugging related arguments being passed as parameters adjacent to a `DebuggingOptions` instance.
2024-07-15 19:55:18 +00:00

800 lines
31 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:package_config/package_config_types.dart';
import '../asset.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../build_info.dart';
import '../bundle_builder.dart';
import '../devfs.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../native_assets.dart';
import '../project.dart';
import '../runner/flutter_command.dart';
import '../test/coverage_collector.dart';
import '../test/event_printer.dart';
import '../test/runner.dart';
import '../test/test_time_recorder.dart';
import '../test/test_wrapper.dart';
import '../test/watcher.dart';
import '../web/compile.dart';
import '../web/web_constants.dart';
/// The name of the directory where Integration Tests are placed.
///
/// When there are test files specified for the test command that are part of
/// this directory, *relative to the package root*, the files will be executed
/// as Integration Tests.
const String _kIntegrationTestDirectory = 'integration_test';
/// A command to run tests.
///
/// This command has two modes of execution:
///
/// ## Unit / Widget Tests
///
/// These tests run in the Flutter Tester, which is a desktop-based Flutter
/// embedder. In this mode, tests are quick to compile and run.
///
/// By default, if no flags are passed to the `flutter test` command, the Tool
/// will recursively find all files within the `test/` directory that end with
/// the `*_test.dart` suffix, and run them in a single invocation.
///
/// See:
/// - https://flutter.dev/to/unit-testing
/// - https://flutter.dev/to/widget-testing
///
/// ## Integration Tests
///
/// These tests run in a connected Flutter Device, similar to `flutter run`. As
/// a result, iteration is slower because device-based artifacts have to be
/// built.
///
/// Integration tests should be placed in the `integration_test/` directory of
/// your package. To run these tests, use `flutter test integration_test`.
///
/// See:
/// - https://flutter.dev/to/integration-testing
class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
TestCommand({
bool verboseHelp = false,
this.testWrapper = const TestWrapper(),
this.testRunner = const FlutterTestRunner(),
this.verbose = false,
this.nativeAssetsBuilder,
}) {
requiresPubspecYaml();
usesPubOption();
addNullSafetyModeOptions(hide: !verboseHelp);
usesFrontendServerStarterPathOption(verboseHelp: verboseHelp);
usesTrackWidgetCreation(verboseHelp: verboseHelp);
addEnableExperimentation(hide: !verboseHelp);
usesDartDefineOption();
usesWebRendererOption();
usesDeviceUserOption();
usesFlavorOption();
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',
splitCommas: false,
)
..addMultiOption('plain-name',
help: 'A plain-text substring of the names of tests to run.',
valueHelp: 'substring',
splitCommas: false,
)
..addOption('tags',
abbr: 't',
help: 'Run only tests associated with the specified tags. See: https://pub.dev/packages/test#tagging-tests',
)
..addOption('exclude-tags',
abbr: 'x',
help: 'Run only tests that do not have the specified tags. See: https://pub.dev/packages/test#tagging-tests',
)
..addFlag('start-paused',
negatable: false,
help: 'Start in a paused mode and wait for a debugger to connect.\n'
'You must specify a single test file to run, explicitly.\n'
'Instructions for connecting with a debugger are printed to the '
'console once the test has started.',
)
..addFlag('fail-fast',
help: 'Stop running tests after the first failure.',
)
..addFlag('run-skipped',
help: 'Run skipped tests instead of skipping them.',
)
..addFlag('disable-service-auth-codes',
negatable: false,
hide: !verboseHelp,
help: '(deprecated) Allow connections to the VM service without using authentication codes. '
'(Not recommended! This can open your device to remote code execution attacks!)'
)
..addFlag('coverage',
negatable: false,
help: 'Whether to collect coverage information.',
)
..addFlag('merge-coverage',
negatable: false,
help: 'Whether to merge coverage data with "coverage/lcov.base.info".\n'
'Implies collecting coverage data. (Requires lcov.)',
)
..addFlag('branch-coverage',
negatable: false,
help: 'Whether to collect branch coverage information. '
'Implies collecting coverage data.',
)
..addFlag('ipv6',
negatable: false,
hide: !verboseHelp,
help: 'Whether to use IPv6 for the test harness server socket.',
)
..addOption('coverage-path',
defaultsTo: 'coverage/lcov.info',
help: 'Where to store coverage information (if coverage is enabled).',
)
..addMultiOption('coverage-package',
help: 'A regular expression matching packages names '
'to include in the coverage report (if coverage is enabled). '
'If unset, matches the current package name.',
valueHelp: 'package-name-regexp',
splitCommas: false,
)
..addFlag('machine',
hide: !verboseHelp,
negatable: false,
help: 'Handle machine structured JSON command input '
'and provide output and progress in machine friendly format.',
)
..addFlag('update-goldens',
negatable: false,
help: 'Whether "matchesGoldenFile()" calls within your test methods should ' // flutter_ignore: golden_tag (see analyze.dart)
'update the golden files rather than test for an existing match.',
)
..addOption('concurrency',
abbr: 'j',
help: 'The number of concurrent test processes to run. This will be ignored '
'when running integration tests.',
valueHelp: 'jobs',
)
..addFlag('test-assets',
defaultsTo: true,
help: 'Whether to build the assets bundle for testing. '
'This takes additional time before running the tests. '
'Consider using "--no-test-assets" if assets are not required.',
)
// --platform is not supported to be used by Flutter developers. It only
// exists to test the Flutter framework itself and may be removed entirely
// in the future. Developers should either use plain `flutter test`, or
// `package:integration_test` instead.
..addOption('platform',
allowed: const <String>['tester', 'chrome'],
hide: !verboseHelp,
defaultsTo: 'tester',
help: 'Selects the test backend.',
allowedHelp: <String, String>{
'tester': 'Run tests using the VM-based test environment.',
'chrome': '(deprecated) Run tests using the Google Chrome web browser. '
'This value is intended for testing the Flutter framework '
'itself and may be removed at any time.',
},
)
..addOption('test-randomize-ordering-seed',
help: 'The seed to randomize the execution order of test cases within test files. '
'Must be a 32bit unsigned integer or the string "random", '
'which indicates that a seed should be selected randomly. '
'By default, tests run in the order they are declared.',
)
..addOption('total-shards',
help: 'Tests can be sharded with the "--total-shards" and "--shard-index" '
'arguments, allowing you to split up your test suites and run '
'them separately.'
)
..addOption('shard-index',
help: 'Tests can be sharded with the "--total-shards" and "--shard-index" '
'arguments, allowing you to split up your test suites and run '
'them separately.'
)
..addFlag('enable-vmservice',
hide: !verboseHelp,
help: 'Enables the VM service without "--start-paused". This flag is '
'intended for use with tests that will use "dart:developer" to '
'interact with the VM service at runtime.\n'
'This flag is ignored if "--start-paused" or coverage are requested, as '
'the VM service will be enabled in those cases regardless.'
)
..addOption('reporter',
abbr: 'r',
help: 'Set how to print test results. If unset, value will default to either compact or expanded.',
allowed: <String>['compact', 'expanded', 'failures-only', 'github', 'json', 'silent'],
allowedHelp: <String, String>{
'compact': 'A single line, updated continuously (the default).',
'expanded': 'A separate line for each update. May be preferred when logging to a file or in continuous integration.',
'failures-only': 'A separate line for failing tests, with no output for passing tests.',
'github': 'A custom reporter for GitHub Actions (the default reporter when running on GitHub Actions).',
'json': 'A machine-readable format. See: https://dart.dev/go/test-docs/json_reporter.md',
'silent': 'A reporter with no output. May be useful when only the exit code is meaningful.'
},
)
..addOption('file-reporter',
help: 'Enable an additional reporter writing test results to a file.\n'
'Should be in the form <reporter>:<filepath>, '
'Example: "json:reports/tests.json".'
)
..addOption('timeout',
help: 'The default test timeout, specified either '
'in seconds (e.g. "60s"), '
'as a multiplier of the default timeout (e.g. "2x"), '
'or as the string "none" to disable the timeout entirely.',
)
..addFlag(
FlutterOptions.kWebWasmFlag,
help: 'Compile to WebAssembly rather than JavaScript.\n$kWasmMoreInfo',
negatable: false,
);
addDdsOptions(verboseHelp: verboseHelp);
addServeObservatoryOptions(verboseHelp: verboseHelp);
usesFatalWarningsOption(verboseHelp: verboseHelp);
}
/// The interface for starting and configuring the tester.
final TestWrapper testWrapper;
/// Interface for running the tester process.
final FlutterTestRunner testRunner;
final TestCompilerNativeAssetsBuilder? nativeAssetsBuilder;
final bool verbose;
@visibleForTesting
bool get isIntegrationTest => _isIntegrationTest;
bool _isIntegrationTest = false;
final Set<Uri> _testFileUris = <Uri>{};
bool get isWeb => stringArg('platform') == 'chrome';
bool get useWasm => boolArg(FlutterOptions.kWebWasmFlag);
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
final Set<DevelopmentArtifact> results = _isIntegrationTest
// Use [DeviceBasedDevelopmentArtifacts].
? await super.requiredArtifacts
: <DevelopmentArtifact>{};
if (isWeb) {
results.add(DevelopmentArtifact.web);
}
return results;
}
@override
String get name => 'test';
@override
String get description => 'Run Flutter unit tests for the current project.';
@override
String get category => FlutterCommandCategory.project;
@override
Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) {
final List<Uri> testUris = argResults!.rest.map(_parseTestArgument).toList();
if (testUris.isEmpty) {
// We don't scan the entire package, only the test/ subdirectory, so that
// files with names like "hit_test.dart" don't get run.
final Directory testDir = globals.fs.directory('test');
if (!testDir.existsSync()) {
throwToolExit('Test directory "${testDir.path}" not found.');
}
_testFileUris.addAll(_findTests(testDir).map(Uri.file));
if (_testFileUris.isEmpty) {
throwToolExit(
'Test directory "${testDir.path}" does not appear to contain any test files.\n'
'Test files must be in that directory and end with the pattern "_test.dart".'
);
}
} else {
for (final Uri uri in testUris) {
// Test files may have query strings to support name/line/col:
// flutter test test/foo.dart?name=a&line=1
String testPath = uri.replace(query: '').toFilePath();
testPath = globals.fs.path.absolute(testPath);
testPath = globals.fs.path.normalize(testPath);
if (globals.fs.isDirectorySync(testPath)) {
_testFileUris.addAll(_findTests(globals.fs.directory(testPath)).map(Uri.file));
} else {
_testFileUris.add(Uri.file(testPath).replace(query: uri.query));
}
}
}
// This needs to be set before [super.verifyThenRunCommand] so that the
// correct [requiredArtifacts] can be identified before [run] takes place.
final List<String> testFilePaths = _testFileUris.map((Uri uri) => uri.replace(query: '').toFilePath()).toList();
_isIntegrationTest = _shouldRunAsIntegrationTests(globals.fs.currentDirectory.absolute.path, testFilePaths);
globals.printTrace(
'Found ${_testFileUris.length} files which will be executed as '
'${_isIntegrationTest ? 'Integration' : 'Widget'} Tests.',
);
return super.verifyThenRunCommand(commandPath);
}
WebRendererMode get webRenderer => WebRendererMode.fromCliOption(
stringArg(FlutterOptions.kWebRendererFlag),
useWasm: useWasm
);
@override
Future<FlutterCommandResult> runCommand() async {
if (!globals.fs.isFileSync('pubspec.yaml')) {
throwToolExit(
'Error: No pubspec.yaml file found in the current working directory.\n'
'Run this command from the root of your project. Test files must be '
"called *_test.dart and must reside in the package's 'test' "
'directory (or one of its subdirectories).');
}
final FlutterProject flutterProject = FlutterProject.current();
final bool buildTestAssets = boolArg('test-assets');
final List<String> names = stringsArg('name');
final List<String> plainNames = stringsArg('plain-name');
final String? tags = stringArg('tags');
final String? excludeTags = stringArg('exclude-tags');
final BuildInfo buildInfo = await getBuildInfo(forcedBuildMode: BuildMode.debug);
TestTimeRecorder? testTimeRecorder;
if (verbose) {
testTimeRecorder = TestTimeRecorder(globals.logger);
}
if (buildInfo.packageConfig['test_api'] == null) {
throwToolExit(
'Error: cannot run without a dependency on either "package:flutter_test" or "package:test". '
'Ensure the following lines are present in your pubspec.yaml:'
'\n\n'
'dev_dependencies:\n'
' flutter_test:\n'
' sdk: flutter\n',
);
}
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(
'When using --start-paused, you must specify a single test file to run.',
exitCode: 1,
);
}
final DebuggingOptions debuggingOptions = DebuggingOptions.enabled(
buildInfo,
startPaused: startPaused,
disableServiceAuthCodes: boolArg('disable-service-auth-codes'),
serveObservatory: boolArg('serve-observatory'),
// On iOS >=14, keeping this enabled will leave a prompt on the screen.
disablePortPublication: true,
enableDds: enableDds,
nullAssertions: boolArg(FlutterOptions.kNullAssertions),
usingCISystem: usingCISystem,
enableImpeller: ImpellerStatus.fromBool(argResults!['enable-impeller'] as bool?),
debugLogsDirectoryPath: debugLogsDirectoryPath,
webRenderer: webRenderer,
webUseWasm: useWasm,
webUseLocalCanvaskit: true,
);
String? testAssetDirectory;
if (buildTestAssets) {
await _buildTestAsset(
flavor: buildInfo.flavor,
impellerStatus: debuggingOptions.enableImpeller,
buildMode: debuggingOptions.buildInfo.mode,
);
testAssetDirectory = globals.fs.path.
join(flutterProject.directory.path, 'build', 'unit_test_assets');
}
final String? concurrencyString = stringArg('concurrency');
int? jobs = concurrencyString == null ? null : int.tryParse(concurrencyString);
if (jobs != null && (jobs <= 0 || !jobs.isFinite)) {
throwToolExit(
'Could not parse -j/--concurrency argument. It must be an integer greater than zero.'
);
}
if (_isIntegrationTest || isWeb) {
if (argResults!.wasParsed('concurrency')) {
globals.printStatus(
'-j/--concurrency was parsed but will be ignored, this option is not '
'supported when running Integration Tests or web tests.',
);
}
// 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') ?? '');
if (shardIndex != null && (shardIndex < 0 || !shardIndex.isFinite)) {
throwToolExit(
'Could not parse --shard-index=$shardIndex argument. It must be an integer greater than -1.');
}
final int? totalShards = int.tryParse(stringArg('total-shards') ?? '');
if (totalShards != null && (totalShards <= 0 || !totalShards.isFinite)) {
throwToolExit(
'Could not parse --total-shards=$totalShards argument. It must be an integer greater than zero.');
}
if (totalShards != null && shardIndex == null) {
throwToolExit(
'If you set --total-shards you need to also set --shard-index.');
}
if (shardIndex != null && totalShards == null) {
throwToolExit(
'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') ||
boolArg('branch-coverage')) {
final Set<String> packagesToInclude = _getCoveragePackages(
stringsArg('coverage-package'),
flutterProject,
buildInfo.packageConfig,
);
collector = CoverageCollector(
verbose: !machine,
libraryNames: packagesToInclude,
packagesPath: buildInfo.packageConfigPath,
resolver: await CoverageCollector.getResolver(buildInfo.packageConfigPath),
testTimeRecorder: testTimeRecorder,
branchCoverage: boolArg('branch-coverage'),
);
}
TestWatcher? watcher;
if (machine) {
watcher = EventPrinter(parent: collector, out: globals.stdio.stdout);
} else if (collector != null) {
watcher = collector;
}
if (!isWeb && useWasm) {
throwToolExit('--wasm is only supported on the web platform');
}
if (webRenderer == WebRendererMode.skwasm && !useWasm) {
throwToolExit('Skwasm renderer requires --wasm');
}
Device? integrationTestDevice;
if (_isIntegrationTest) {
integrationTestDevice = await findTargetDevice();
// Disable reporting of test results to native test frameworks. This isn't
// needed as the Flutter Tool will be responsible for reporting results.
buildInfo.dartDefines.add('INTEGRATION_TEST_SHOULD_REPORT_RESULTS_TO_NATIVE=false');
if (integrationTestDevice == null) {
throwToolExit(
'No devices are connected. '
'Ensure that `flutter doctor` shows at least one connected device',
);
}
if (integrationTestDevice.platformType == PlatformType.web) {
// TODO(jiahaog): Support web. https://github.com/flutter/flutter/issues/66264
throwToolExit('Web devices are not supported for integration tests yet.');
}
if (buildInfo.packageConfig['integration_test'] == null) {
throwToolExit(
'Error: cannot run without a dependency on "package:integration_test". '
'Ensure the following lines are present in your pubspec.yaml:'
'\n\n'
'dev_dependencies:\n'
' integration_test:\n'
' sdk: flutter\n',
);
}
if (stringArg('flavor') != null && !integrationTestDevice.supportsFlavors) {
throwToolExit('--flavor is only supported for Android, macOS, and iOS devices.');
}
}
final Stopwatch? testRunnerTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.TestRunner);
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'),
failFast: boolArg('fail-fast'),
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'),
failFast: boolArg('fail-fast'),
runSkipped: boolArg('run-skipped'),
shardIndex: shardIndex,
totalShards: totalShards,
integrationTestDevice: integrationTestDevice,
integrationTestUserIdentifier: stringArg(FlutterOptions.kDeviceUser),
testTimeRecorder: testTimeRecorder,
nativeAssetsBuilder: nativeAssetsBuilder,
buildInfo: buildInfo,
);
}
testTimeRecorder?.stop(TestTimePhases.TestRunner, testRunnerTimeRecorderStopwatch!);
if (collector != null) {
final Stopwatch? collectTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.CoverageDataCollect);
final bool collectionResult = await collector.collectCoverageData(
stringArg('coverage-path'),
mergeCoverageData: boolArg('merge-coverage'),
);
testTimeRecorder?.stop(TestTimePhases.CoverageDataCollect, collectTimeRecorderStopwatch!);
if (!collectionResult) {
testTimeRecorder?.print();
throwToolExit(null);
}
}
testTimeRecorder?.print();
if (result != 0) {
throwToolExit(null);
}
return FlutterCommandResult.success();
}
Set<String> _getCoveragePackages(
List<String> packagesRegExps,
FlutterProject flutterProject,
PackageConfig packageConfig,
) {
final String projectName = flutterProject.manifest.appName;
final Set<String> packagesToInclude = <String>{
if (packagesRegExps.isEmpty) projectName,
};
try {
for (final String regExpStr in packagesRegExps) {
final RegExp regExp = RegExp(regExpStr);
packagesToInclude.addAll(
packageConfig.packages
.map((Package e) => e.name)
.where((String e) => regExp.hasMatch(e)),
);
}
} on FormatException catch (e) {
throwToolExit('Regular expression syntax is invalid. $e');
}
return packagesToInclude;
}
/// Parses a test file/directory target passed as an argument and returns it
/// as an absolute file:/// [URI] with optional querystring for name/line/col.
Uri _parseTestArgument(String arg) {
// We can't parse Windows paths as URIs if they have query strings, so
// parse the file and query parts separately.
final int queryStart = arg.indexOf('?');
String filePart = queryStart == -1 ? arg : arg.substring(0, queryStart);
final String queryPart = queryStart == -1 ? '' : arg.substring(queryStart + 1);
filePart = globals.fs.path.absolute(filePart);
filePart = globals.fs.path.normalize(filePart);
return Uri.file(filePart)
.replace(query: queryPart.isEmpty ? null : queryPart);
}
Future<void> _buildTestAsset({
required String? flavor,
required ImpellerStatus impellerStatus,
required BuildMode buildMode,
}) async {
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final int build = await assetBundle.build(
packagesPath: '.packages',
flavor: flavor,
);
if (build != 0) {
throwToolExit('Error: Failed to build asset bundle');
}
if (_needsRebuild(assetBundle.entries, flavor)) {
await writeBundle(
globals.fs.directory(globals.fs.path.join('build', 'unit_test_assets')),
assetBundle.entries,
targetPlatform: TargetPlatform.tester,
impellerStatus: impellerStatus,
processManager: globals.processManager,
fileSystem: globals.fs,
artifacts: globals.artifacts!,
logger: globals.logger,
projectDir: globals.fs.currentDirectory,
buildMode: buildMode,
);
final File cachedFlavorFile = globals.fs.file(
globals.fs.path.join('build', 'test_cache', 'flavor.txt'),
);
if (cachedFlavorFile.existsSync()) {
await cachedFlavorFile.delete();
}
if (flavor != null) {
cachedFlavorFile.createSync(recursive: true);
cachedFlavorFile.writeAsStringSync(flavor);
}
}
}
bool _needsRebuild(Map<String, AssetBundleEntry> entries, String? flavor) {
// TODO(andrewkolos): This logic might fail in the future if we change the
// schema of the contents of the asset manifest file and the user does not
// perform a `flutter clean` after upgrading.
// See https://github.com/flutter/flutter/issues/128563.
final File manifest = globals.fs.file(globals.fs.path.join('build', 'unit_test_assets', 'AssetManifest.bin'));
if (!manifest.existsSync()) {
return true;
}
final DateTime lastModified = manifest.lastModifiedSync();
final File pub = globals.fs.file('pubspec.yaml');
if (pub.lastModifiedSync().isAfter(lastModified)) {
return true;
}
final Iterable<DevFSFileContent> files = entries.values
.map((AssetBundleEntry asset) => asset.content)
.whereType<DevFSFileContent>();
for (final DevFSFileContent entry in files) {
// Calling isModified to access file stats first in order for isModifiedAfter
// to work.
if (entry.isModified && entry.isModifiedAfter(lastModified)) {
return true;
}
}
final File cachedFlavorFile = globals.fs.file(
globals.fs.path.join('build', 'test_cache', 'flavor.txt'),
);
final String? cachedFlavor = cachedFlavorFile.existsSync()
? cachedFlavorFile.readAsStringSync()
: null;
if (cachedFlavor != flavor) {
return true;
}
return false;
}
}
/// Searches [directory] and returns files that end with `_test.dart` as
/// absolute paths.
Iterable<String> _findTests(Directory directory) {
return directory.listSync(recursive: true, followLinks: false)
.where((FileSystemEntity entity) => entity.path.endsWith('_test.dart') &&
globals.fs.isFileSync(entity.path))
.map((FileSystemEntity entity) => globals.fs.path.absolute(entity.path));
}
/// Returns true if there are files that are Integration Tests.
///
/// The [currentDirectory] and [testFiles] parameters here must be provided as
/// absolute paths.
///
/// Throws an exception if there are both Integration Tests and Widget Tests
/// found in [testFiles].
bool _shouldRunAsIntegrationTests(String currentDirectory, List<String> testFiles) {
final String integrationTestDirectory = globals.fs.path.join(currentDirectory, _kIntegrationTestDirectory);
if (testFiles.every((String absolutePath) => !absolutePath.startsWith(integrationTestDirectory))) {
return false;
}
if (testFiles.every((String absolutePath) => absolutePath.startsWith(integrationTestDirectory))) {
return true;
}
throwToolExit(
'Integration tests and unit tests cannot be run in a single invocation.'
' Use separate invocations of `flutter test` to run integration tests'
' and unit tests.'
);
}