Unverified Commit 06952ba2 authored by Danny Tuppeny's avatar Danny Tuppeny Committed by GitHub

[flutter_tools] Add support for URI formats like ?line=x for "flutter test" (#119740)

* [flutter_tools] Add support for URI formats like ?line=x for "flutter test"

* Remove unnecessary function

* Handle parsing absolute paths on Windows

* Use Windows-style paths when running on Windows

* Fix paths in isFile

* Remove unnecessary clear
parent 9a4e8979
// 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:test/test.dart' hide TypeMatcher, isInstanceOf;
void main() {
// This test must start on Line 11 or the test
// "flutter test should run a test by line number in URI format"
// in test/integration.shard/test_test.dart updated.
test('exactTestName', () {
expect(2 + 2, 4);
});
test('not exactTestName', () {
throw 'this test should have been filtered out';
});
}
...@@ -138,7 +138,7 @@ Future<void> run(List<String> args) async { ...@@ -138,7 +138,7 @@ Future<void> run(List<String> args) async {
// TODO(dnfield): This should be injected. // TODO(dnfield): This should be injected.
exitCode = await const FlutterTestRunner().runTests( exitCode = await const FlutterTestRunner().runTests(
const TestWrapper(), const TestWrapper(),
tests.keys.toList(), tests.keys.map(Uri.file).toList(),
debuggingOptions: DebuggingOptions.enabled( debuggingOptions: DebuggingOptions.enabled(
BuildInfo( BuildInfo(
BuildMode.debug, BuildMode.debug,
......
...@@ -236,7 +236,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -236,7 +236,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
bool get isIntegrationTest => _isIntegrationTest; bool get isIntegrationTest => _isIntegrationTest;
bool _isIntegrationTest = false; bool _isIntegrationTest = false;
List<String> _testFiles = <String>[]; final Set<Uri> _testFileUris = <Uri>{};
@override @override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async { Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
...@@ -261,37 +261,43 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -261,37 +261,43 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
@override @override
Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) { Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) {
_testFiles = argResults!.rest.map<String>(globals.fs.path.absolute).toList(); final List<Uri> testUris = argResults!.rest.map(_parseTestArgument).toList();
if (_testFiles.isEmpty) { if (testUris.isEmpty) {
// We don't scan the entire package, only the test/ subdirectory, so that // We don't scan the entire package, only the test/ subdirectory, so that
// files with names like "hit_test.dart" don't get run. // files with names like "hit_test.dart" don't get run.
final Directory testDir = globals.fs.directory('test'); final Directory testDir = globals.fs.directory('test');
if (!testDir.existsSync()) { if (!testDir.existsSync()) {
throwToolExit('Test directory "${testDir.path}" not found.'); throwToolExit('Test directory "${testDir.path}" not found.');
} }
_testFiles = _findTests(testDir).toList(); _testFileUris.addAll(_findTests(testDir).map(Uri.file));
if (_testFiles.isEmpty) { if (_testFileUris.isEmpty) {
throwToolExit( throwToolExit(
'Test directory "${testDir.path}" does not appear to contain any test files.\n' '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".' 'Test files must be in that directory and end with the pattern "_test.dart".'
); );
} }
} else { } else {
_testFiles = <String>[ for (final Uri uri in testUris) {
for (String path in _testFiles) // Test files may have query strings to support name/line/col:
if (globals.fs.isDirectorySync(path)) // flutter test test/foo.dart?name=a&line=1
..._findTests(globals.fs.directory(path)) String testPath = uri.replace(query: '').toFilePath();
else testPath = globals.fs.path.absolute(testPath);
globals.fs.path.normalize(globals.fs.path.absolute(path)), 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 // This needs to be set before [super.verifyThenRunCommand] so that the
// correct [requiredArtifacts] can be identified before [run] takes place. // correct [requiredArtifacts] can be identified before [run] takes place.
_isIntegrationTest = _shouldRunAsIntegrationTests(globals.fs.currentDirectory.absolute.path, _testFiles); final List<String> testFilePaths = _testFileUris.map((Uri uri) => uri.replace(query: '').toFilePath()).toList();
_isIntegrationTest = _shouldRunAsIntegrationTests(globals.fs.currentDirectory.absolute.path, testFilePaths);
globals.printTrace( globals.printTrace(
'Found ${_testFiles.length} files which will be executed as ' 'Found ${_testFileUris.length} files which will be executed as '
'${_isIntegrationTest ? 'Integration' : 'Widget'} Tests.', '${_isIntegrationTest ? 'Integration' : 'Widget'} Tests.',
); );
return super.verifyThenRunCommand(commandPath); return super.verifyThenRunCommand(commandPath);
...@@ -338,7 +344,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -338,7 +344,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
} }
final bool startPaused = boolArgDeprecated('start-paused'); final bool startPaused = boolArgDeprecated('start-paused');
if (startPaused && _testFiles.length != 1) { if (startPaused && _testFileUris.length != 1) {
throwToolExit( throwToolExit(
'When using --start-paused, you must specify a single test file to run.', 'When using --start-paused, you must specify a single test file to run.',
exitCode: 1, exitCode: 1,
...@@ -451,7 +457,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -451,7 +457,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
final Stopwatch? testRunnerTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.TestRunner); final Stopwatch? testRunnerTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.TestRunner);
final int result = await testRunner.runTests( final int result = await testRunner.runTests(
testWrapper, testWrapper,
_testFiles, _testFileUris.toList(),
debuggingOptions: debuggingOptions, debuggingOptions: debuggingOptions,
names: names, names: names,
plainNames: plainNames, plainNames: plainNames,
...@@ -500,6 +506,22 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { ...@@ -500,6 +506,22 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
return FlutterCommandResult.success(); return FlutterCommandResult.success();
} }
/// 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() async { Future<void> _buildTestAsset() async {
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle(); final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final int build = await assetBundle.build(packagesPath: '.packages'); final int build = await assetBundle.build(packagesPath: '.packages');
......
...@@ -24,7 +24,7 @@ abstract class FlutterTestRunner { ...@@ -24,7 +24,7 @@ abstract class FlutterTestRunner {
/// Runs tests using package:test and the Flutter engine. /// Runs tests using package:test and the Flutter engine.
Future<int> runTests( Future<int> runTests(
TestWrapper testWrapper, TestWrapper testWrapper,
List<String> testFiles, { List<Uri> testFiles, {
required DebuggingOptions debuggingOptions, required DebuggingOptions debuggingOptions,
List<String> names = const <String>[], List<String> names = const <String>[],
List<String> plainNames = const <String>[], List<String> plainNames = const <String>[],
...@@ -62,7 +62,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -62,7 +62,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
@override @override
Future<int> runTests( Future<int> runTests(
TestWrapper testWrapper, TestWrapper testWrapper,
List<String> testFiles, { List<Uri> testFiles, {
required DebuggingOptions debuggingOptions, required DebuggingOptions debuggingOptions,
List<String> names = const <String>[], List<String> names = const <String>[],
List<String> plainNames = const <String>[], List<String> plainNames = const <String>[],
...@@ -128,6 +128,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -128,6 +128,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
'--shard-index=$shardIndex', '--shard-index=$shardIndex',
'--chain-stack-traces', '--chain-stack-traces',
]; ];
if (web) { if (web) {
final String tempBuildDir = globals.fs.systemTempDirectory final String tempBuildDir = globals.fs.systemTempDirectory
.createTempSync('flutter_test.') .createTempSync('flutter_test.')
...@@ -144,13 +145,13 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -144,13 +145,13 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
).initialize( ).initialize(
projectDirectory: flutterProject!.directory, projectDirectory: flutterProject!.directory,
testOutputDir: tempBuildDir, testOutputDir: tempBuildDir,
testFiles: testFiles, testFiles: testFiles.map((Uri uri) => uri.toFilePath()).toList(),
buildInfo: debuggingOptions.buildInfo, buildInfo: debuggingOptions.buildInfo,
); );
testArgs testArgs
..add('--platform=chrome') ..add('--platform=chrome')
..add('--') ..add('--')
..addAll(testFiles); ..addAll(testFiles.map((Uri uri) => uri.toString()));
testWrapper.registerPlatformPlugin( testWrapper.registerPlatformPlugin(
<Runtime>[Runtime.chrome], <Runtime>[Runtime.chrome],
() { () {
...@@ -186,7 +187,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner { ...@@ -186,7 +187,7 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
testArgs testArgs
..add('--') ..add('--')
..addAll(testFiles); ..addAll(testFiles.map((Uri uri) => uri.toString()));
final InternetAddressType serverType = final InternetAddressType serverType =
ipv6 ? InternetAddressType.IPv6 : InternetAddressType.IPv4; ipv6 ? InternetAddressType.IPv6 : InternetAddressType.IPv4;
......
...@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/logger.dart'; ...@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/test.dart'; import 'package:flutter_tools/src/commands/test.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/test/runner.dart'; import 'package:flutter_tools/src/test/runner.dart';
...@@ -59,17 +60,18 @@ void main() { ...@@ -59,17 +60,18 @@ void main() {
late LoggingLogger logger; late LoggingLogger logger;
setUp(() { setUp(() {
fs = MemoryFileSystem.test(); fs = MemoryFileSystem.test(style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix);
fs.file('/package/pubspec.yaml').createSync(recursive: true); final Directory package = fs.directory('package');
fs.file('/package/pubspec.yaml').writeAsStringSync(_pubspecContents); package.childFile('pubspec.yaml').createSync(recursive: true);
(fs.directory('/package/.dart_tool') package.childFile('pubspec.yaml').writeAsStringSync(_pubspecContents);
(package.childDirectory('.dart_tool')
.childFile('package_config.json') .childFile('package_config.json')
..createSync(recursive: true)) ..createSync(recursive: true))
.writeAsString(_packageConfigContents); .writeAsString(_packageConfigContents);
fs.directory('/package/test').childFile('some_test.dart').createSync(recursive: true); package.childDirectory('test').childFile('some_test.dart').createSync(recursive: true);
fs.directory('/package/integration_test').childFile('some_integration_test.dart').createSync(recursive: true); package.childDirectory('integration_test').childFile('some_integration_test.dart').createSync(recursive: true);
fs.currentDirectory = '/package'; fs.currentDirectory = package.path;
logger = LoggingLogger(); logger = LoggingLogger();
}); });
...@@ -721,7 +723,7 @@ dev_dependencies: ...@@ -721,7 +723,7 @@ dev_dependencies:
'--no-pub', '--no-pub',
]); ]);
final bool fileExists = await fs.isFile('build/unit_test_assets/AssetManifest.json'); final bool fileExists = await fs.isFile(globals.fs.path.join('build', 'unit_test_assets', 'AssetManifest.json'));
expect(fileExists, true); expect(fileExists, true);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -742,7 +744,7 @@ dev_dependencies: ...@@ -742,7 +744,7 @@ dev_dependencies:
'--no-test-assets', '--no-test-assets',
]); ]);
final bool fileExists = await fs.isFile('build/unit_test_assets/AssetManifest.json'); final bool fileExists = await fs.isFile(globals.fs.path.join('build', 'unit_test_assets', 'AssetManifest.json'));
expect(fileExists, false); expect(fileExists, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -854,7 +856,7 @@ class FakeFlutterTestRunner implements FlutterTestRunner { ...@@ -854,7 +856,7 @@ class FakeFlutterTestRunner implements FlutterTestRunner {
@override @override
Future<int> runTests( Future<int> runTests(
TestWrapper testWrapper, TestWrapper testWrapper,
List<String> testFiles, { List<Uri> testFiles, {
required DebuggingOptions debuggingOptions, required DebuggingOptions debuggingOptions,
List<String> names = const <String>[], List<String> names = const <String>[],
List<String> plainNames = const <String>[], List<String> plainNames = const <String>[],
......
...@@ -165,6 +165,20 @@ void main() { ...@@ -165,6 +165,20 @@ void main() {
expect(result.exitCode, 1); expect(result.exitCode, 1);
}); });
testWithoutContext('flutter test should run a test with an exact name in URI format', () async {
final ProcessResult result = await _runFlutterTest('uri_format', automatedTestsDirectory, flutterTestDirectory,
query: 'full-name=exactTestName');
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should run a test by line number in URI format', () async {
final ProcessResult result = await _runFlutterTest('uri_format', automatedTestsDirectory, flutterTestDirectory,
query: 'line=11');
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should test runs to completion', () async { testWithoutContext('flutter test should test runs to completion', () async {
final ProcessResult result = await _runFlutterTest('trivial', automatedTestsDirectory, flutterTestDirectory, final ProcessResult result = await _runFlutterTest('trivial', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--verbose']); extraArguments: const <String>['--verbose']);
...@@ -315,6 +329,7 @@ Future<ProcessResult> _runFlutterTest( ...@@ -315,6 +329,7 @@ Future<ProcessResult> _runFlutterTest(
String workingDirectory, String workingDirectory,
String testDirectory, { String testDirectory, {
List<String> extraArguments = const <String>[], List<String> extraArguments = const <String>[],
String? query,
}) async { }) async {
String testPath; String testPath;
...@@ -342,7 +357,8 @@ Future<ProcessResult> _runFlutterTest( ...@@ -342,7 +357,8 @@ Future<ProcessResult> _runFlutterTest(
'--reporter', '--reporter',
'compact', 'compact',
...extraArguments, ...extraArguments,
testPath, if (query != null) Uri.file(testPath).replace(query: query).toString()
else testPath,
]; ];
return Process.run( return Process.run(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment