[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
This commit is contained in:
parent
9a4e897965
commit
06952ba254
17
dev/automated_tests/flutter_test/uri_format_test.dart
Normal file
17
dev/automated_tests/flutter_test/uri_format_test.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// 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 {
|
|||||||
// 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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
@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 {
|
|||||||
'--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 {
|
|||||||
).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 {
|
|||||||
|
|
||||||
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';
|
|||||||
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() {
|
|||||||
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:
|
|||||||
'--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:
|
|||||||
'--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 {
|
|||||||
@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() {
|
|||||||
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(
|
|||||||
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(
|
|||||||
'--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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user