// 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 'dart:async'; import 'dart:convert'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import '../src/common.dart'; import 'test_utils.dart'; // This test depends on some files in ///dev/automated_tests/flutter_test/* final String automatedTestsDirectory = fileSystem.path.join('..', '..', 'dev', 'automated_tests'); final String missingDependencyDirectory = fileSystem.path.join( '..', '..', 'dev', 'missing_dependency_tests', ); final String flutterTestDirectory = fileSystem.path.join(automatedTestsDirectory, 'flutter_test'); final String integrationTestDirectory = fileSystem.path.join( automatedTestsDirectory, 'integration_test', ); // Running Integration Tests in the Flutter Tester will still exercise the same // flows specific to Integration Tests. final List integrationTestExtraArgs = ['-d', 'flutter-tester']; void main() { setUpAll(() async { expect( await processManager.run([ flutterBin, 'pub', 'get', ], workingDirectory: flutterTestDirectory), const ProcessResultMatcher(), ); expect( await processManager.run([ flutterBin, 'pub', 'get', ], workingDirectory: missingDependencyDirectory), const ProcessResultMatcher(), ); }); testWithoutContext('flutter test should not have extraneous error messages', () async { return _testFile( 'trivial_widget', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero, ); }); testWithoutContext('integration test should not have extraneous error messages', () async { return _testFile( 'trivial_widget', automatedTestsDirectory, integrationTestDirectory, exitCode: isZero, extraArguments: integrationTestExtraArgs, ); }); testWithoutContext('flutter test set the working directory correctly', () async { return _testFile( 'working_directory', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero, ); }); testWithoutContext( 'flutter test should report nice errors for exceptions thrown within testWidgets()', () async { return _testFile('exception_handling', automatedTestsDirectory, flutterTestDirectory); }, ); testWithoutContext( 'integration test should report nice errors for exceptions thrown within testWidgets()', () async { return _testFile( 'exception_handling', automatedTestsDirectory, integrationTestDirectory, extraArguments: integrationTestExtraArgs, ); }, ); testWithoutContext( 'flutter test should report a nice error when a guarded function was called without await', () async { return _testFile('test_async_utils_guarded', automatedTestsDirectory, flutterTestDirectory); }, ); testWithoutContext( 'flutter test should report a nice error when an async function was called without await', () async { return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory); }, ); testWithoutContext( 'flutter test should report a nice error when a Ticker is left running', () async { return _testFile('ticker', automatedTestsDirectory, flutterTestDirectory); }, ); testWithoutContext( 'flutter test should report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async { final String missingDependencyTests = fileSystem.path.join( '..', '..', 'dev', 'missing_dependency_tests', ); return _testFile('trivial', missingDependencyTests, missingDependencyTests); }, ); testWithoutContext( 'flutter test should report which user-created widget caused the error', () async { return _testFile( 'print_user_created_ancestor', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--track-widget-creation'], ); }, ); testWithoutContext( 'flutter test should report which user-created widget caused the error - no flag', () async { return _testFile( 'print_user_created_ancestor_no_flag', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--no-track-widget-creation'], ); }, ); testWithoutContext( 'flutter test should report the correct user-created widget that caused the error', () async { return _testFile( 'print_correct_local_widget', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--track-widget-creation'], ); }, ); testWithoutContext('flutter test should can load assets within its own package', () async { return _testFile( 'package_assets', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero, ); }); testWithoutContext('flutter test should support dart defines', () async { return _testFile( 'dart_defines', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero, extraArguments: ['--dart-define=flutter.test.foo=bar'], ); }); testWithoutContext('flutter test should run a test when its name matches a regexp', () async { final ProcessResult result = await _runFlutterTest( 'filtering', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--name', 'inc.*de'], ); expect(result, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); 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 ['--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', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--plain-name', 'include'], ); expect(result, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); 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 ['--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', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--tags', 'include-tag'], ); expect(result, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); 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 ['--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 ['--exclude-tags', 'exclude-tag'], ); expect(result, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); testWithoutContext('flutter test should run all tests when tags are unspecified', () async { final ProcessResult result = await _runFlutterTest( 'filtering_tag', automatedTestsDirectory, flutterTestDirectory, ); expect( result, ProcessResultMatcher(exitCode: 1, stdoutPattern: RegExp(r'\+\d+ -1: Some tests failed\.')), ); }); 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 ['--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 ['--tags', 'include-tag'], ); expect(result, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); testWithoutContext('flutter test should not run a widgetTest with excluded tag', () async { final ProcessResult result = await _runFlutterTest( 'filtering_tag_widget', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--exclude-tags', 'exclude-tag'], ); expect(result, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); testWithoutContext('flutter test should run all widgetTest when tags are unspecified', () async { final ProcessResult result = await _runFlutterTest( 'filtering_tag_widget', automatedTestsDirectory, flutterTestDirectory, ); expect( result, ProcessResultMatcher(exitCode: 1, stdoutPattern: RegExp(r'\+\d+ -1: Some tests failed\.')), ); }); 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, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); 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, ProcessResultMatcher(stdoutPattern: RegExp(r'\+\d+: All tests passed!'))); }); testWithoutContext('flutter test should test runs to completion', () async { final ProcessResult result = await _runFlutterTest( 'trivial', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--verbose'], ); final String stdout = (result.stdout as String).replaceAll('\r', '\n'); 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 test runs to completion when --experimental-faster-testing is set', () async { final ProcessResult result = await _runFlutterTest( null, automatedTestsDirectory, '$flutterTestDirectory/child_directory', extraArguments: const ['--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 ['--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: [...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 ['--verbose'], ); final String stdout = (result.stdout as String).replaceAll('\r', '\n'); expect(result.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 gold skips tests where the expectations are missing', () async { return _testFile( 'flutter_gold', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero, ); }); testWithoutContext('flutter test should respect --serve-observatory', () async { Process? process; StreamSubscription? sub; try { process = await _runFlutterTestConcurrent( 'trivial', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--start-paused', '--serve-observatory'], ); final Completer completer = Completer(); final RegExp vmServiceUriRegExp = RegExp(r'((http)?:\/\/)[^\s]+'); sub = process.stdout.transform(utf8.decoder).listen((String e) { if (!completer.isCompleted && vmServiceUriRegExp.hasMatch(e)) { completer.complete(Uri.parse(vmServiceUriRegExp.firstMatch(e)!.group(0)!)); } }); final Uri vmServiceUri = await completer.future; final HttpClient client = HttpClient(); final HttpClientRequest request = await client.getUrl(vmServiceUri); final HttpClientResponse response = await request.close(); final String content = await response.transform(utf8.decoder).join(); expect(content, contains('Dart VM Observatory')); } finally { await sub?.cancel(); process?.kill(); } }); testWithoutContext('flutter test should serve DevTools', () async { Process? process; StreamSubscription? sub; try { process = await _runFlutterTestConcurrent( 'trivial', automatedTestsDirectory, flutterTestDirectory, extraArguments: const ['--start-paused'], ); final Completer completer = Completer(); final RegExp devToolsUriRegExp = RegExp( r'The Flutter DevTools debugger and profiler is available at: (http://[^\s]+)', ); sub = process.stdout.transform(utf8.decoder).listen((String e) { if (!completer.isCompleted && devToolsUriRegExp.hasMatch(e)) { completer.complete(Uri.parse(devToolsUriRegExp.firstMatch(e)!.group(1)!)); } }); final Uri devToolsUri = await completer.future; final HttpClient client = HttpClient(); final HttpClientRequest request = await client.getUrl(devToolsUri); final HttpClientResponse response = await request.close(); final String content = await response.transform(utf8.decoder).join(); expect(content, contains('DevTools')); } finally { await sub?.cancel(); process?.kill(); } }); } Future _testFile( String testName, String workingDirectory, String testDirectory, { Matcher? exitCode, List extraArguments = const [], }) async { exitCode ??= isNonZero; final String fullTestExpectation = fileSystem.path.join( testDirectory, '${testName}_expectation.txt', ); final File expectationFile = fileSystem.file(fullTestExpectation); if (!expectationFile.existsSync()) { fail('missing expectation file: $expectationFile'); } final ProcessResult exec = await _runFlutterTest( testName, workingDirectory, testDirectory, extraArguments: extraArguments, ); expect( exec.exitCode, exitCode, reason: '"$testName" returned code ${exec.exitCode}\n\nstdout:\n' '${exec.stdout}\nstderr:\n${exec.stderr}', ); List output = (exec.stdout as String).split('\n'); output = _removeMacFontServerWarning(output); if (output.first.startsWith( 'Waiting for another flutter command to release the startup lock...', )) { output.removeAt(0); } if (output.first.startsWith('Running "flutter pub get" in')) { output.removeAt(0); } // Whether cached artifacts need to be downloaded is dependent on what // previous tests have run. Disregard these messages. output.removeWhere(RegExp(r'Downloading .*\.\.\.').hasMatch); output.add('<>'); output.addAll((exec.stderr as String).split('\n')); final List expectations = fileSystem.file(fullTestExpectation).readAsLinesSync(); bool allowSkip = false; int expectationLineNumber = 0; int outputLineNumber = 0; bool haveSeenStdErrMarker = false; while (expectationLineNumber < expectations.length) { expect( output, hasLength(greaterThan(outputLineNumber)), reason: 'Failure in $testName to compare to $fullTestExpectation', ); final String expectationLine = expectations[expectationLineNumber]; String outputLine = output[outputLineNumber]; if (expectationLine == '<>') { allowSkip = true; expectationLineNumber += 1; continue; } if (allowSkip) { if (!RegExp(expectationLine).hasMatch(outputLine)) { outputLineNumber += 1; continue; } allowSkip = false; } if (expectationLine == '<>') { expect(haveSeenStdErrMarker, isFalse); haveSeenStdErrMarker = true; } if (!RegExp(expectationLine).hasMatch(outputLine) && outputLineNumber + 1 < output.length) { // Check if the RegExp can match the next two lines in the output so // that it is possible to write expectations that still hold even if a // line is wrapped slightly differently due to for example a file name // being longer on one platform than another. final String mergedLines = '$outputLine\n${output[outputLineNumber + 1]}'; if (RegExp(expectationLine).hasMatch(mergedLines)) { outputLineNumber += 1; outputLine = mergedLines; } } expect( outputLine, matches(expectationLine), reason: 'Full output:\n- - - -----8<----- - - -\n${output.join("\n")}\n- - - -----8<----- - - -', ); expectationLineNumber += 1; outputLineNumber += 1; } expect(allowSkip, isFalse); if (!haveSeenStdErrMarker) { expect(exec.stderr, ''); } } final RegExp _fontServerProtocolPattern = RegExp( r'flutter_tester.*Font server protocol version mismatch', ); final RegExp _unableToConnectToFontDaemonPattern = RegExp( r'flutter_tester.*XType: unable to make a connection to the font daemon!', ); final RegExp _xtFontStaticRegistryPattern = RegExp( r'flutter_tester.*XType: XTFontStaticRegistry is enabled as fontd is not available', ); // https://github.com/flutter/flutter/issues/132990 List _removeMacFontServerWarning(List output) { return output.where((String line) { if (_fontServerProtocolPattern.hasMatch(line)) { return false; } if (_unableToConnectToFontDaemonPattern.hasMatch(line)) { return false; } if (_xtFontStaticRegistryPattern.hasMatch(line)) { return false; } return true; }).toList(); } Future _runFlutterTest( String? testName, String workingDirectory, String testDirectory, { List extraArguments = const [], String? query, }) async { String testPath; if (testName == null) { // Test everything in the directory. testPath = testDirectory; final Directory directoryToTest = fileSystem.directory(testPath); if (!directoryToTest.existsSync()) { fail('missing test directory: $directoryToTest'); } } else { // Test just a specific test file. testPath = fileSystem.path.join(testDirectory, '${testName}_test.dart'); final File testFile = fileSystem.file(testPath); if (!testFile.existsSync()) { fail('missing test file: $testFile'); } } final List args = [ 'test', '--no-color', '--no-version-check', '--no-pub', '--reporter', 'compact', ...extraArguments, if (query != null) Uri.file(testPath).replace(query: query).toString() else testPath, ]; return Process.run( flutterBin, // Uses the precompiled flutter tool for faster tests, args, workingDirectory: workingDirectory, stdoutEncoding: utf8, stderrEncoding: utf8, ); } Future _runFlutterTestConcurrent( String? testName, String workingDirectory, String testDirectory, { List extraArguments = const [], }) async { String testPath; if (testName == null) { // Test everything in the directory. testPath = testDirectory; final Directory directoryToTest = fileSystem.directory(testPath); if (!directoryToTest.existsSync()) { fail('missing test directory: $directoryToTest'); } } else { // Test just a specific test file. testPath = fileSystem.path.join(testDirectory, '${testName}_test.dart'); final File testFile = fileSystem.file(testPath); if (!testFile.existsSync()) { fail('missing test file: $testFile'); } } final List args = [ 'test', '--no-color', '--no-version-check', '--no-pub', '--reporter', 'compact', ...extraArguments, testPath, ]; return Process.start( flutterBin, // Uses the precompiled flutter tool for faster tests, args, workingDirectory: workingDirectory, ); }