Unverified Commit 03aa059c authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Do not abort at first error when tests fail. (#108936)

parent 274a9349
...@@ -45,10 +45,10 @@ Future<void> main(List<String> arguments) async { ...@@ -45,10 +45,10 @@ Future<void> main(List<String> arguments) async {
dart = path.join(dartSdk, 'bin', Platform.isWindows ? 'dart.exe' : 'dart'); dart = path.join(dartSdk, 'bin', Platform.isWindows ? 'dart.exe' : 'dart');
pub = path.join(dartSdk, 'bin', Platform.isWindows ? 'pub.bat' : 'pub'); pub = path.join(dartSdk, 'bin', Platform.isWindows ? 'pub.bat' : 'pub');
print('$clock STARTING ANALYSIS'); print('$clock STARTING ANALYSIS');
try {
await run(arguments); await run(arguments);
} on ExitException catch (error) { if (hasError) {
error.apply(); print('$clock ${bold}Test failed.$reset');
reportErrorsAndExit();
} }
print('$clock ${bold}Analysis successful.$reset'); print('$clock ${bold}Analysis successful.$reset');
} }
...@@ -60,17 +60,20 @@ String? _getDartSdkFromArguments(List<String> arguments) { ...@@ -60,17 +60,20 @@ String? _getDartSdkFromArguments(List<String> arguments) {
for (int i = 0; i < arguments.length; i += 1) { for (int i = 0; i < arguments.length; i += 1) {
if (arguments[i] == '--dart-sdk') { if (arguments[i] == '--dart-sdk') {
if (result != null) { if (result != null) {
exitWithError(<String>['The --dart-sdk argument must not be used more than once.']); foundError(<String>['The --dart-sdk argument must not be used more than once.']);
return null;
} }
if (i + 1 < arguments.length) { if (i + 1 < arguments.length) {
result = arguments[i + 1]; result = arguments[i + 1];
} else { } else {
exitWithError(<String>['--dart-sdk must be followed by a path.']); foundError(<String>['--dart-sdk must be followed by a path.']);
return null;
} }
} }
if (arguments[i].startsWith('--dart-sdk=')) { if (arguments[i].startsWith('--dart-sdk=')) {
if (result != null) { if (result != null) {
exitWithError(<String>['The --dart-sdk argument must not be used more than once.']); foundError(<String>['The --dart-sdk argument must not be used more than once.']);
return null;
} }
result = arguments[i].substring('--dart-sdk='.length); result = arguments[i].substring('--dart-sdk='.length);
} }
...@@ -82,7 +85,7 @@ Future<void> run(List<String> arguments) async { ...@@ -82,7 +85,7 @@ Future<void> run(List<String> arguments) async {
bool assertsEnabled = false; bool assertsEnabled = false;
assert(() { assertsEnabled = true; return true; }()); assert(() { assertsEnabled = true; return true; }());
if (!assertsEnabled) { if (!assertsEnabled) {
exitWithError(<String>['The analyze.dart script must be run with --enable-asserts.']); foundError(<String>['The analyze.dart script must be run with --enable-asserts.']);
} }
print('$clock No Double.clamp'); print('$clock No Double.clamp');
...@@ -268,7 +271,7 @@ Future<void> verifyNoDoubleClamp(String workingDirectory) async { ...@@ -268,7 +271,7 @@ Future<void> verifyNoDoubleClamp(String workingDirectory) async {
} }
} }
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
...errors, ...errors,
'\n${bold}See: https://github.com/flutter/flutter/pull/103559', '\n${bold}See: https://github.com/flutter/flutter/pull/103559',
]); ]);
...@@ -315,7 +318,7 @@ Future<void> verifyToolTestsEndInTestDart(String workingDirectory) async { ...@@ -315,7 +318,7 @@ Future<void> verifyToolTestsEndInTestDart(String workingDirectory) async {
violations.add(file.path); violations.add(file.path);
} }
if (violations.isNotEmpty) { if (violations.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
'${bold}Found flutter_tools tests that do not end in `_test.dart`; these will not be run by the test runner$reset', '${bold}Found flutter_tools tests that do not end in `_test.dart`; these will not be run by the test runner$reset',
...violations, ...violations,
]); ]);
...@@ -354,7 +357,7 @@ Future<void> verifyNoSyncAsyncStar(String workingDirectory, {int minimumMatches ...@@ -354,7 +357,7 @@ Future<void> verifyNoSyncAsyncStar(String workingDirectory, {int minimumMatches
} }
} }
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
'${bold}Do not use sync*/async* methods. See https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#avoid-syncasync for details.$reset', '${bold}Do not use sync*/async* methods. See https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#avoid-syncasync for details.$reset',
...errors, ...errors,
]); ]);
...@@ -420,7 +423,7 @@ Future<void> verifyGoldenTags(String workingDirectory, { int minimumMatches = 20 ...@@ -420,7 +423,7 @@ Future<void> verifyGoldenTags(String workingDirectory, { int minimumMatches = 20
} }
} }
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
...errors, ...errors,
'${bold}See: https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter$reset', '${bold}See: https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter$reset',
]); ]);
...@@ -529,7 +532,7 @@ Future<void> verifyDeprecations(String workingDirectory, { int minimumMatches = ...@@ -529,7 +532,7 @@ Future<void> verifyDeprecations(String workingDirectory, { int minimumMatches =
} }
// Fail if any errors // Fail if any errors
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
...errors, ...errors,
'${bold}See: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes$reset', '${bold}See: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes$reset',
]); ]);
...@@ -561,7 +564,7 @@ Future<void> verifyNoMissingLicense(String workingDirectory, { bool checkMinimum ...@@ -561,7 +564,7 @@ Future<void> verifyNoMissingLicense(String workingDirectory, { bool checkMinimum
failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'xml', overrideMinimumMatches ?? 1, '<!-- ${_generateLicense('')} -->', header: r'(<\?xml version="1.0" encoding="utf-8"\?>\n)?'); failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'xml', overrideMinimumMatches ?? 1, '<!-- ${_generateLicense('')} -->', header: r'(<\?xml version="1.0" encoding="utf-8"\?>\n)?');
failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'frag', overrideMinimumMatches ?? 1, _generateLicense('// '), header: r'#version 320 es(\n)+'); failed += await _verifyNoMissingLicenseForExtension(workingDirectory, 'frag', overrideMinimumMatches ?? 1, _generateLicense('// '), header: r'#version 320 es(\n)+');
if (failed > 0) { if (failed > 0) {
exitWithError(<String>['License check failed.']); foundError(<String>['License check failed.']);
} }
} }
...@@ -670,7 +673,7 @@ Future<void> verifySkipTestComments(String workingDirectory) async { ...@@ -670,7 +673,7 @@ Future<void> verifySkipTestComments(String workingDirectory) async {
// Fail if any errors // Fail if any errors
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
...errors, ...errors,
'\n${bold}See: https://github.com/flutter/flutter/wiki/Tree-hygiene#skipped-tests$reset', '\n${bold}See: https://github.com/flutter/flutter/wiki/Tree-hygiene#skipped-tests$reset',
]); ]);
...@@ -700,7 +703,7 @@ Future<void> verifyNoTestImports(String workingDirectory) async { ...@@ -700,7 +703,7 @@ Future<void> verifyNoTestImports(String workingDirectory) async {
// Fail if any errors // Fail if any errors
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
final String s = errors.length == 1 ? '' : 's'; final String s = errors.length == 1 ? '' : 's';
exitWithError(<String>[ foundError(<String>[
'${bold}The following file$s import a test directly. Test utilities should be in their own file.$reset', '${bold}The following file$s import a test directly. Test utilities should be in their own file.$reset',
...errors, ...errors,
]); ]);
...@@ -769,7 +772,7 @@ Future<void> verifyNoBadImportsInFlutter(String workingDirectory) async { ...@@ -769,7 +772,7 @@ Future<void> verifyNoBadImportsInFlutter(String workingDirectory) async {
} }
// Fail if any errors // Fail if any errors
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
if (errors.length == 1) if (errors.length == 1)
'${bold}An error was detected when looking at import dependencies within the Flutter package:$reset' '${bold}An error was detected when looking at import dependencies within the Flutter package:$reset'
else else
...@@ -789,7 +792,7 @@ Future<void> verifyNoBadImportsInFlutterTools(String workingDirectory) async { ...@@ -789,7 +792,7 @@ Future<void> verifyNoBadImportsInFlutterTools(String workingDirectory) async {
} }
// Fail if any errors // Fail if any errors
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
if (errors.length == 1) if (errors.length == 1)
'${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset' '${bold}An error was detected when looking at import dependencies within the flutter_tools package:$reset'
else else
...@@ -814,7 +817,7 @@ Future<void> verifyIntegrationTestTimeouts(String workingDirectory) async { ...@@ -814,7 +817,7 @@ Future<void> verifyIntegrationTestTimeouts(String workingDirectory) async {
} }
} }
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
if (errors.length == 1) if (errors.length == 1)
'${bold}An error was detected when looking at integration test timeouts:$reset' '${bold}An error was detected when looking at integration test timeouts:$reset'
else else
...@@ -850,7 +853,7 @@ Future<void> verifyInternationalizations(String workingDirectory, String dartExe ...@@ -850,7 +853,7 @@ Future<void> verifyInternationalizations(String workingDirectory, String dartExe
final String expectedCupertinoResult = await File(cupertinoLocalizationsFile).readAsString(); final String expectedCupertinoResult = await File(cupertinoLocalizationsFile).readAsString();
if (materialGenResult.stdout.trim() != expectedMaterialResult.trim()) { if (materialGenResult.stdout.trim() != expectedMaterialResult.trim()) {
exitWithError(<String>[ foundError(<String>[
'<<<<<<< $materialLocalizationsFile', '<<<<<<< $materialLocalizationsFile',
expectedMaterialResult.trim(), expectedMaterialResult.trim(),
'=======', '=======',
...@@ -862,7 +865,7 @@ Future<void> verifyInternationalizations(String workingDirectory, String dartExe ...@@ -862,7 +865,7 @@ Future<void> verifyInternationalizations(String workingDirectory, String dartExe
]); ]);
} }
if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) { if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) {
exitWithError(<String>[ foundError(<String>[
'<<<<<<< $cupertinoLocalizationsFile', '<<<<<<< $cupertinoLocalizationsFile',
expectedCupertinoResult.trim(), expectedCupertinoResult.trim(),
'=======', '=======',
...@@ -893,7 +896,7 @@ Future<void> verifyNoCheckedMode(String workingDirectory) async { ...@@ -893,7 +896,7 @@ Future<void> verifyNoCheckedMode(String workingDirectory) async {
} }
} }
if (problems.isNotEmpty) { if (problems.isNotEmpty) {
exitWithError(problems); foundError(problems);
} }
} }
...@@ -947,7 +950,7 @@ Future<void> verifyNoRuntimeTypeInToString(String workingDirectory) async { ...@@ -947,7 +950,7 @@ Future<void> verifyNoRuntimeTypeInToString(String workingDirectory) async {
} }
} }
if (problems.isNotEmpty) { if (problems.isNotEmpty) {
exitWithError(problems); foundError(problems);
} }
} }
...@@ -977,7 +980,7 @@ Future<void> verifyNoTrailingSpaces(String workingDirectory, { int minimumMatche ...@@ -977,7 +980,7 @@ Future<void> verifyNoTrailingSpaces(String workingDirectory, { int minimumMatche
} }
} }
if (problems.isNotEmpty) { if (problems.isNotEmpty) {
exitWithError(problems); foundError(problems);
} }
} }
...@@ -1050,7 +1053,7 @@ Future<void> verifyIssueLinks(String workingDirectory) async { ...@@ -1050,7 +1053,7 @@ Future<void> verifyIssueLinks(String workingDirectory) async {
} }
assert(problems.isEmpty == suggestions.isEmpty); assert(problems.isEmpty == suggestions.isEmpty);
if (problems.isNotEmpty) { if (problems.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
...problems, ...problems,
...suggestions, ...suggestions,
]); ]);
...@@ -1507,7 +1510,7 @@ Future<void> verifyNoBinaries(String workingDirectory, { Set<Hash256>? legacyBin ...@@ -1507,7 +1510,7 @@ Future<void> verifyNoBinaries(String workingDirectory, { Set<Hash256>? legacyBin
} }
} }
if (problems.isNotEmpty) { if (problems.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
...problems, ...problems,
'All files in this repository must be UTF-8. In particular, images and other binaries', 'All files in this repository must be UTF-8. In particular, images and other binaries',
'must not be checked into this repository. This is because we are very sensitive to the', 'must not be checked into this repository. This is because we are very sensitive to the',
...@@ -1545,7 +1548,7 @@ Future<List<File>> _gitFiles(String workingDirectory, {bool runSilently = true}) ...@@ -1545,7 +1548,7 @@ Future<List<File>> _gitFiles(String workingDirectory, {bool runSilently = true})
runSilently: runSilently, runSilently: runSilently,
); );
if (evalResult.exitCode != 0) { if (evalResult.exitCode != 0) {
exitWithError(<String>[ foundError(<String>[
'git ls-files failed with exit code ${evalResult.exitCode}', 'git ls-files failed with exit code ${evalResult.exitCode}',
'${bold}stdout:$reset', '${bold}stdout:$reset',
evalResult.stdout, evalResult.stdout,
...@@ -1671,7 +1674,7 @@ Future<EvalResult> _evalCommand(String executable, List<String> arguments, { ...@@ -1671,7 +1674,7 @@ Future<EvalResult> _evalCommand(String executable, List<String> arguments, {
if (exitCode != 0 && !allowNonZeroExit) { if (exitCode != 0 && !allowNonZeroExit) {
stderr.write(result.stderr); stderr.write(result.stderr);
exitWithError(<String>[ foundError(<String>[
'${bold}ERROR:$red Last command exited with $exitCode.$reset', '${bold}ERROR:$red Last command exited with $exitCode.$reset',
'${bold}Command:$red $commandDescription$reset', '${bold}Command:$red $commandDescription$reset',
'${bold}Relative working directory:$red $relativeWorkingDir$reset', '${bold}Relative working directory:$red $relativeWorkingDir$reset',
...@@ -1702,9 +1705,11 @@ Future<void> _checkConsumerDependencies() async { ...@@ -1702,9 +1705,11 @@ Future<void> _checkConsumerDependencies() async {
'--directory=${path.join(flutterRoot, 'packages', package)}', '--directory=${path.join(flutterRoot, 'packages', package)}',
]); ]);
if (result.exitCode != 0) { if (result.exitCode != 0) {
print(result.stdout as Object); foundError(<String>[
print(result.stderr as Object); result.stdout.toString(),
exit(result.exitCode); result.stderr.toString(),
]);
return;
} }
final Map<String, Object?> rawJson = json.decode(result.stdout as String) as Map<String, Object?>; final Map<String, Object?> rawJson = json.decode(result.stdout as String) as Map<String, Object?>;
final Map<String, Map<String, Object?>> dependencyTree = <String, Map<String, Object?>>{ final Map<String, Map<String, Object?>> dependencyTree = <String, Map<String, Object?>>{
...@@ -1734,7 +1739,7 @@ Future<void> _checkConsumerDependencies() async { ...@@ -1734,7 +1739,7 @@ Future<void> _checkConsumerDependencies() async {
String plural(int n, String s, String p) => n == 1 ? s : p; String plural(int n, String s, String p) => n == 1 ? s : p;
if (added.isNotEmpty) { if (added.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
'The transitive closure of package dependencies contains ${plural(added.length, "a non-allowlisted package", "non-allowlisted packages")}:', 'The transitive closure of package dependencies contains ${plural(added.length, "a non-allowlisted package", "non-allowlisted packages")}:',
' ${added.join(', ')}', ' ${added.join(', ')}',
'We strongly desire to keep the number of dependencies to a minimum and', 'We strongly desire to keep the number of dependencies to a minimum and',
...@@ -1745,7 +1750,7 @@ Future<void> _checkConsumerDependencies() async { ...@@ -1745,7 +1750,7 @@ Future<void> _checkConsumerDependencies() async {
} }
if (removed.isNotEmpty) { if (removed.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
'Excellent news! ${plural(removed.length, "A package dependency has been removed!", "Multiple package dependencies have been removed!")}', 'Excellent news! ${plural(removed.length, "A package dependency has been removed!", "Multiple package dependencies have been removed!")}',
' ${removed.join(', ')}', ' ${removed.join(', ')}',
'To make sure we do not accidentally add ${plural(removed.length, "this dependency", "these dependencies")} back in the future,', 'To make sure we do not accidentally add ${plural(removed.length, "this dependency", "these dependencies")} back in the future,',
...@@ -1778,7 +1783,7 @@ Future<void> verifyNullInitializedDebugExpensiveFields(String workingDirectory, ...@@ -1778,7 +1783,7 @@ Future<void> verifyNullInitializedDebugExpensiveFields(String workingDirectory,
} }
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
exitWithError(<String>[ foundError(<String>[
'${bold}ERROR: ${red}fields annotated with @_debugOnly must null initialize.$reset', '${bold}ERROR: ${red}fields annotated with @_debugOnly must null initialize.$reset',
'to ensure both the field and initializer are removed from profile/release mode.', 'to ensure both the field and initializer are removed from profile/release mode.',
'These fields should be written as:\n', 'These fields should be written as:\n',
......
...@@ -149,8 +149,8 @@ Future<Command> startCommand(String executable, List<String> arguments, { ...@@ -149,8 +149,8 @@ Future<Command> startCommand(String executable, List<String> arguments, {
/// Runs the `executable` and waits until the process exits. /// Runs the `executable` and waits until the process exits.
/// ///
/// If the process exits with a non-zero exit code, exits this process with /// If the process exits with a non-zero exit code and `expectNonZeroExit` is
/// exit code 1, unless `expectNonZeroExit` is set to true. /// false, calls foundError (which does not terminate execution!).
/// ///
/// `outputListener` is called for every line of standard output from the /// `outputListener` is called for every line of standard output from the
/// process, and is given the [Process] object. This can be used to interrupt /// process, and is given the [Process] object. This can be used to interrupt
...@@ -195,7 +195,7 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, { ...@@ -195,7 +195,7 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, {
io.stdout.writeln(result.flattenedStderr); io.stdout.writeln(result.flattenedStderr);
break; break;
} }
exitWithError(<String>[ foundError(<String>[
if (failureMessage != null) if (failureMessage != null)
failureMessage failureMessage
else else
...@@ -203,8 +203,9 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, { ...@@ -203,8 +203,9 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, {
'${bold}Command: $green$commandDescription$reset', '${bold}Command: $green$commandDescription$reset',
'${bold}Relative working directory: $cyan$relativeWorkingDir$reset', '${bold}Relative working directory: $cyan$relativeWorkingDir$reset',
]); ]);
} } else {
print('$clock ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset'); print('$clock ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
}
return result; return result;
} }
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:core' hide print;
import 'dart:io'; import 'dart:io' hide exit;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
...@@ -43,6 +43,10 @@ Future<void> main() async { ...@@ -43,6 +43,10 @@ Future<void> main() async {
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort); await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent); await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false); await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false);
if (hasError) {
print('One or more tests failed.');
reportErrorsAndExit();
}
} }
Future<void> _setAppVersion(int version) async { Future<void> _setAppVersion(int version) async {
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:core' hide print;
import 'dart:io' hide exit;
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:typed_data'; import 'dart:typed_data';
...@@ -170,7 +171,6 @@ String get shuffleSeed { ...@@ -170,7 +171,6 @@ String get shuffleSeed {
/// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt /// bin/cache/dart-sdk/bin/dart dev/bots/test.dart --local-engine=host_debug_unopt
Future<void> main(List<String> args) async { Future<void> main(List<String> args) async {
print('$clock STARTING ANALYSIS'); print('$clock STARTING ANALYSIS');
try {
flutterTestArgs.addAll(args); flutterTestArgs.addAll(args);
final Set<String> removeArgs = <String>{}; final Set<String> removeArgs = <String>{};
for (final String arg in args) { for (final String arg in args) {
...@@ -214,8 +214,9 @@ Future<void> main(List<String> args) async { ...@@ -214,8 +214,9 @@ Future<void> main(List<String> args) async {
'skp_generator': _runSkpGeneratorTests, 'skp_generator': _runSkpGeneratorTests,
kTestHarnessShardName: _runTestHarnessTests, // Used for testing this script. kTestHarnessShardName: _runTestHarnessTests, // Used for testing this script.
}); });
} on ExitException catch (error) { if (hasError) {
error.apply(); print('$clock ${bold}Test failed.$reset');
reportErrorsAndExit();
} }
print('$clock ${bold}Test successful.$reset'); print('$clock ${bold}Test successful.$reset');
} }
...@@ -241,9 +242,7 @@ Future<void> _validateEngineHash() async { ...@@ -241,9 +242,7 @@ Future<void> _validateEngineHash() async {
return line.startsWith('Flutter Engine Version:'); return line.startsWith('Flutter Engine Version:');
}); });
if (!actualVersion.contains(expectedVersion)) { if (!actualVersion.contains(expectedVersion)) {
print('${red}Expected "Flutter Engine Version: $expectedVersion", ' foundError(<String>['${red}Expected "Flutter Engine Version: $expectedVersion", but found "$actualVersion".']);
'but found "$actualVersion".');
exit(1);
} }
} }
...@@ -332,7 +331,7 @@ Future<void> _runTestHarnessTests() async { ...@@ -332,7 +331,7 @@ Future<void> _runTestHarnessTests() async {
// Verify that we correctly generated the version file. // Verify that we correctly generated the version file.
final String? versionError = await verifyVersion(File(path.join(flutterRoot, 'version'))); final String? versionError = await verifyVersion(File(path.join(flutterRoot, 'version')));
if (versionError != null) { if (versionError != null) {
exitWithError(<String>[versionError]); foundError(<String>[versionError]);
} }
} }
...@@ -652,9 +651,10 @@ Future<void> _flutterBuild( ...@@ -652,9 +651,10 @@ Future<void> _flutterBuild(
); );
final File file = File(path.join(flutterRoot, relativePathToApplication, 'perf.json')); final File file = File(path.join(flutterRoot, relativePathToApplication, 'perf.json'));
if (!_allTargetsCached(file)) { if (!_allTargetsCached(file)) {
print('${red}Not all build targets cached after second run.$reset'); foundError(<String>[
print('The target performance data was: ${file.readAsStringSync().replaceAll('},', '},\n')}'); '${red}Not all build targets cached after second run.$reset',
exit(1); 'The target performance data was: ${file.readAsStringSync().replaceAll('},', '},\n')}',
]);
} }
} }
} }
...@@ -840,8 +840,7 @@ Future<void> _runFrameworkTests() async { ...@@ -840,8 +840,7 @@ Future<void> _runFrameworkTests() async {
}, },
)); ));
if (results.isNotEmpty) { if (results.isNotEmpty) {
print(results.join('\n')); foundError(results);
exit(1);
} }
} }
...@@ -941,20 +940,24 @@ Future<void> _runFrameworkTests() async { ...@@ -941,20 +940,24 @@ Future<void> _runFrameworkTests() async {
Future<void> _runFrameworkCoverage() async { Future<void> _runFrameworkCoverage() async {
final File coverageFile = File(path.join(flutterRoot, 'packages', 'flutter', 'coverage', 'lcov.info')); final File coverageFile = File(path.join(flutterRoot, 'packages', 'flutter', 'coverage', 'lcov.info'));
if (!coverageFile.existsSync()) { if (!coverageFile.existsSync()) {
print('${red}Coverage file not found.$reset'); foundError(<String>[
print('Expected to find: $cyan${coverageFile.absolute}$reset'); '${red}Coverage file not found.$reset',
print('This file is normally obtained by running `${green}flutter update-packages$reset`.'); 'Expected to find: $cyan${coverageFile.absolute}$reset',
exit(1); 'This file is normally obtained by running `${green}flutter update-packages$reset`.',
]);
return;
} }
coverageFile.deleteSync(); coverageFile.deleteSync();
await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter'), await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter'),
options: const <String>['--coverage'], options: const <String>['--coverage'],
); );
if (!coverageFile.existsSync()) { if (!coverageFile.existsSync()) {
print('${red}Coverage file not found.$reset'); foundError(<String>[
print('Expected to find: $cyan${coverageFile.absolute}$reset'); '${red}Coverage file not found.$reset',
print('This file should have been generated by the `${green}flutter test --coverage$reset` script, but was not.'); 'Expected to find: $cyan${coverageFile.absolute}$reset',
exit(1); 'This file should have been generated by the `${green}flutter test --coverage$reset` script, but was not.',
]);
return;
} }
} }
...@@ -1496,12 +1499,11 @@ Future<void> _runWebStackTraceTest(String buildMode, String entrypoint) async { ...@@ -1496,12 +1499,11 @@ Future<void> _runWebStackTraceTest(String buildMode, String entrypoint) async {
browserDebugPort: browserDebugPort, browserDebugPort: browserDebugPort,
); );
if (result.contains('--- TEST SUCCEEDED ---')) { if (!result.contains('--- TEST SUCCEEDED ---')) {
print('${green}Web stack trace integration test passed.$reset'); foundError(<String>[
} else { result,
print(result); '${red}Web stack trace integration test failed.$reset',
print('${red}Web stack trace integration test failed.$reset'); ]);
exit(1);
} }
} }
...@@ -1545,12 +1547,11 @@ Future<void> _runWebReleaseTest(String target, { ...@@ -1545,12 +1547,11 @@ Future<void> _runWebReleaseTest(String target, {
browserDebugPort: browserDebugPort, browserDebugPort: browserDebugPort,
); );
if (result.contains('--- TEST SUCCEEDED ---')) { if (!result.contains('--- TEST SUCCEEDED ---')) {
print('${green}Web release mode test passed.$reset'); foundError(<String>[
} else { result,
print(result); '${red}Web release mode test failed.$reset',
print('${red}Web release mode test failed.$reset'); ]);
exit(1);
} }
} }
...@@ -1599,13 +1600,12 @@ Future<void> _runWebDebugTest(String target, { ...@@ -1599,13 +1600,12 @@ Future<void> _runWebDebugTest(String target, {
environment: environment, environment: environment,
); );
if (success) { if (!success) {
print('${green}Web stack trace integration test passed.$reset'); foundError(<String>[
} else { result.flattenedStdout!,
print(result.flattenedStdout!); result.flattenedStderr!,
print(result.flattenedStderr!); '${red}Web stack trace integration test failed.$reset',
print('${red}Web stack trace integration test failed.$reset'); ]);
exit(1);
} }
} }
...@@ -1652,9 +1652,11 @@ Future<void> _dartRunTest(String workingDirectory, { ...@@ -1652,9 +1652,11 @@ Future<void> _dartRunTest(String workingDirectory, {
if (cpuVariable != null) { if (cpuVariable != null) {
cpus = int.tryParse(cpuVariable, radix: 10); cpus = int.tryParse(cpuVariable, radix: 10);
if (cpus == null) { if (cpus == null) {
print('${red}The CPU environment variable, if set, must be set to the integer number of available cores.$reset'); foundError(<String>[
print('Actual value: "$cpuVariable"'); '${red}The CPU environment variable, if set, must be set to the integer number of available cores.$reset',
exit(1); 'Actual value: "$cpuVariable"',
]);
return;
} }
} else { } else {
cpus = 2; // Don't default to 1, otherwise we won't catch race conditions. cpus = 2; // Don't default to 1, otherwise we won't catch race conditions.
...@@ -1762,13 +1764,14 @@ Future<void> _runFlutterTest(String workingDirectory, { ...@@ -1762,13 +1764,14 @@ Future<void> _runFlutterTest(String workingDirectory, {
if (script != null) { if (script != null) {
final String fullScriptPath = path.join(workingDirectory, script); final String fullScriptPath = path.join(workingDirectory, script);
if (!FileSystemEntity.isFileSync(fullScriptPath)) { if (!FileSystemEntity.isFileSync(fullScriptPath)) {
print('${red}Could not find test$reset: $green$fullScriptPath$reset'); foundError(<String>[
print('Working directory: $cyan$workingDirectory$reset'); '${red}Could not find test$reset: $green$fullScriptPath$reset',
print('Script: $green$script$reset'); 'Working directory: $cyan$workingDirectory$reset',
if (!printOutput) { 'Script: $green$script$reset',
print('This is one of the tests that does not normally print output.'); if (!printOutput)
} 'This is one of the tests that does not normally print output.',
exit(1); ]);
return;
} }
args.add(script); args.add(script);
} }
...@@ -1792,7 +1795,7 @@ Future<void> _runFlutterTest(String workingDirectory, { ...@@ -1792,7 +1795,7 @@ Future<void> _runFlutterTest(String workingDirectory, {
if (outputChecker != null) { if (outputChecker != null) {
final String? message = outputChecker(result); final String? message = outputChecker(result);
if (message != null) { if (message != null) {
exitWithError(<String>[message]); foundError(<String>[message]);
} }
} }
return; return;
...@@ -1906,15 +1909,19 @@ List<T> _selectIndexOfTotalSubshard<T>(List<T> tests, {String subshardKey = kSub ...@@ -1906,15 +1909,19 @@ List<T> _selectIndexOfTotalSubshard<T>(List<T> tests, {String subshardKey = kSub
final RegExp pattern = RegExp(r'^(\d+)_(\d+)$'); final RegExp pattern = RegExp(r'^(\d+)_(\d+)$');
final Match? match = pattern.firstMatch(subshardName); final Match? match = pattern.firstMatch(subshardName);
if (match == null || match.groupCount != 2) { if (match == null || match.groupCount != 2) {
print('${red}Invalid subshard name "$subshardName". Expected format "[int]_[int]" ex. "1_3"'); foundError(<String>[
exit(1); '${red}Invalid subshard name "$subshardName". Expected format "[int]_[int]" ex. "1_3"',
]);
return <T>[];
} }
// One-indexed. // One-indexed.
final int index = int.parse(match!.group(1)!); final int index = int.parse(match.group(1)!);
final int total = int.parse(match.group(2)!); final int total = int.parse(match.group(2)!);
if (index > total) { if (index > total) {
print('${red}Invalid subshard name "$subshardName". Index number must be greater or equal to total.'); foundError(<String>[
exit(1); '${red}Invalid subshard name "$subshardName". Index number must be greater or equal to total.',
]);
return <T>[];
} }
final int testsPerShard = (tests.length / total).ceil(); final int testsPerShard = (tests.length / total).ceil();
...@@ -1965,9 +1972,11 @@ Future<void> _runFromList(Map<String, ShardRunner> items, String key, String nam ...@@ -1965,9 +1972,11 @@ Future<void> _runFromList(Map<String, ShardRunner> items, String key, String nam
} }
} else { } else {
if (!items.containsKey(item)) { if (!items.containsKey(item)) {
print('${red}Invalid $name: $item$reset'); foundError(<String>[
print('The available ${name}s are: ${items.keys.join(", ")}'); '${red}Invalid $name: $item$reset',
exit(1); 'The available ${name}s are: ${items.keys.join(", ")}',
]);
return;
} }
print('$bold$key=$item$reset'); print('$bold$key=$item$reset');
await items[item]!(); await items[item]!();
......
...@@ -12,21 +12,22 @@ import 'common.dart'; ...@@ -12,21 +12,22 @@ import 'common.dart';
typedef AsyncVoidCallback = Future<void> Function(); typedef AsyncVoidCallback = Future<void> Function();
Future<String> capture(AsyncVoidCallback callback, { int exitCode = 0 }) async { Future<String> capture(AsyncVoidCallback callback, { bool shouldHaveErrors = false }) async {
final StringBuffer buffer = StringBuffer(); final StringBuffer buffer = StringBuffer();
final PrintCallback oldPrint = print; final PrintCallback oldPrint = print;
try { try {
print = (Object line) { print = (Object line) {
buffer.writeln(line); buffer.writeln(line);
}; };
try {
await callback(); await callback();
expect(exitCode, 0); expect(
} on ExitException catch (error) { hasError,
expect(error.exitCode, exitCode); shouldHaveErrors,
} reason: buffer.isEmpty ? '(No output to report.)' : hasError ? 'Unexpected errors:\n$buffer' : 'Unexpected success:\n$buffer',
);
} finally { } finally {
print = oldPrint; print = oldPrint;
resetErrorStatus();
} }
if (stdout.supportsAnsiEscapes) { if (stdout.supportsAnsiEscapes) {
// Remove ANSI escapes when this test is running on a terminal. // Remove ANSI escapes when this test is running on a terminal.
...@@ -42,7 +43,7 @@ void main() { ...@@ -42,7 +43,7 @@ void main() {
final String dartPath = path.canonicalize(path.join('..', '..', 'bin', 'cache', 'dart-sdk', 'bin', dartName)); final String dartPath = path.canonicalize(path.join('..', '..', 'bin', 'cache', 'dart-sdk', 'bin', dartName));
test('analyze.dart - verifyDeprecations', () async { test('analyze.dart - verifyDeprecations', () async {
final String result = await capture(() => verifyDeprecations(testRootPath, minimumMatches: 2), exitCode: 1); final String result = await capture(() => verifyDeprecations(testRootPath, minimumMatches: 2), shouldHaveErrors: true);
final String lines = <String>[ final String lines = <String>[
'test/analyze-test-input/root/packages/foo/deprecation.dart:12: Deprecation notice does not match required pattern.', 'test/analyze-test-input/root/packages/foo/deprecation.dart:12: Deprecation notice does not match required pattern.',
'test/analyze-test-input/root/packages/foo/deprecation.dart:18: Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: STYLE_GUIDE_URL', 'test/analyze-test-input/root/packages/foo/deprecation.dart:18: Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: STYLE_GUIDE_URL',
...@@ -73,7 +74,7 @@ void main() { ...@@ -73,7 +74,7 @@ void main() {
}); });
test('analyze.dart - verifyGoldenTags', () async { test('analyze.dart - verifyGoldenTags', () async {
final String result = await capture(() => verifyGoldenTags(testRootPath, minimumMatches: 6), exitCode: 1); final String result = await capture(() => verifyGoldenTags(testRootPath, minimumMatches: 6), shouldHaveErrors: true);
const String noTag = "Files containing golden tests must be tagged using @Tags(<String>['reduced-test-set']) " const String noTag = "Files containing golden tests must be tagged using @Tags(<String>['reduced-test-set']) "
'at the top of the file before import statements.'; 'at the top of the file before import statements.';
const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'."; const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
...@@ -117,7 +118,7 @@ void main() { ...@@ -117,7 +118,7 @@ void main() {
}); });
test('analyze.dart - verifyNoMissingLicense', () async { test('analyze.dart - verifyNoMissingLicense', () async {
final String result = await capture(() => verifyNoMissingLicense(testRootPath, checkMinimums: false), exitCode: 1); final String result = await capture(() => verifyNoMissingLicense(testRootPath, checkMinimums: false), shouldHaveErrors: true);
final String file = 'test/analyze-test-input/root/packages/foo/foo.dart' final String file = 'test/analyze-test-input/root/packages/foo/foo.dart'
.replaceAll('/', Platform.isWindows ? r'\' : '/'); .replaceAll('/', Platform.isWindows ? r'\' : '/');
expect(result, expect(result,
...@@ -137,7 +138,7 @@ void main() { ...@@ -137,7 +138,7 @@ void main() {
}); });
test('analyze.dart - verifyNoTrailingSpaces', () async { test('analyze.dart - verifyNoTrailingSpaces', () async {
final String result = await capture(() => verifyNoTrailingSpaces(testRootPath, minimumMatches: 2), exitCode: 1); final String result = await capture(() => verifyNoTrailingSpaces(testRootPath, minimumMatches: 2), shouldHaveErrors: true);
final String lines = <String>[ final String lines = <String>[
'test/analyze-test-input/root/packages/foo/spaces.txt:5: trailing U+0020 space character', 'test/analyze-test-input/root/packages/foo/spaces.txt:5: trailing U+0020 space character',
'test/analyze-test-input/root/packages/foo/spaces.txt:9: trailing blank line', 'test/analyze-test-input/root/packages/foo/spaces.txt:9: trailing blank line',
...@@ -155,7 +156,7 @@ void main() { ...@@ -155,7 +156,7 @@ void main() {
final String result = await capture(() => verifyNoBinaries( final String result = await capture(() => verifyNoBinaries(
testRootPath, testRootPath,
legacyBinaries: <Hash256>{const Hash256(0x39A050CD69434936, 0, 0, 0)}, legacyBinaries: <Hash256>{const Hash256(0x39A050CD69434936, 0, 0, 0)},
), exitCode: Platform.isWindows ? 0 : 1); ), shouldHaveErrors: !Platform.isWindows);
if (!Platform.isWindows) { if (!Platform.isWindows) {
expect(result, expect(result,
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'
...@@ -173,7 +174,7 @@ void main() { ...@@ -173,7 +174,7 @@ void main() {
}); });
test('analyze.dart - verifyInternationalizations - comparison fails', () async { test('analyze.dart - verifyInternationalizations - comparison fails', () async {
final String result = await capture(() => verifyInternationalizations(testRootPath, dartPath), exitCode: 1); final String result = await capture(() => verifyInternationalizations(testRootPath, dartPath), shouldHaveErrors: true);
final String genLocalizationsScript = path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'); final String genLocalizationsScript = path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart');
expect(result, expect(result,
contains('$dartName $genLocalizationsScript --cupertino')); contains('$dartName $genLocalizationsScript --cupertino'));
...@@ -201,7 +202,7 @@ void main() { ...@@ -201,7 +202,7 @@ void main() {
final String result = await capture(() => verifyNullInitializedDebugExpensiveFields( final String result = await capture(() => verifyNullInitializedDebugExpensiveFields(
testRootPath, testRootPath,
minimumMatches: 1, minimumMatches: 1,
), exitCode: 1); ), shouldHaveErrors: true);
expect(result, contains('L15')); expect(result, contains('L15'));
expect(result, isNot(contains('L12'))); expect(result, isNot(contains('L12')));
......
...@@ -4,9 +4,11 @@ ...@@ -4,9 +4,11 @@
import 'dart:core' as core_internals show print; import 'dart:core' as core_internals show print;
import 'dart:core' hide print; import 'dart:core' hide print;
import 'dart:io' as io_internals show exit; import 'dart:io' as system show exit;
import 'dart:io' hide exit; import 'dart:io' hide exit;
import 'package:meta/meta.dart';
final bool hasColor = stdout.supportsAnsiEscapes; final bool hasColor = stdout.supportsAnsiEscapes;
final String bold = hasColor ? '\x1B[1m' : ''; // used for shard titles final String bold = hasColor ? '\x1B[1m' : ''; // used for shard titles
...@@ -16,36 +18,41 @@ final String yellow = hasColor ? '\x1B[33m' : ''; // used for skips ...@@ -16,36 +18,41 @@ final String yellow = hasColor ? '\x1B[33m' : ''; // used for skips
final String cyan = hasColor ? '\x1B[36m' : ''; // used for paths final String cyan = hasColor ? '\x1B[36m' : ''; // used for paths
final String reverse = hasColor ? '\x1B[7m' : ''; // used for clocks final String reverse = hasColor ? '\x1B[7m' : ''; // used for clocks
final String reset = hasColor ? '\x1B[0m' : ''; final String reset = hasColor ? '\x1B[0m' : '';
final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset';
class ExitException implements Exception { typedef PrintCallback = void Function(Object line);
ExitException(this.exitCode);
final int exitCode; // Allow print() to be overridden, for tests.
PrintCallback print = core_internals.print;
void apply() { bool get hasError => _hasError;
io_internals.exit(exitCode); bool _hasError = false;
}
}
// We actually reimplement exit() so that it uses exceptions rather Iterable<String> get errorMessages => _errorMessages;
// than truly immediately terminating the application, so that we can List<String> _errorMessages = <String>[];
// test the exit code in unit tests (see test/analyze_test.dart).
void exit(int exitCode) {
throw ExitException(exitCode);
}
void exitWithError(List<String> messages) { void foundError(List<String> messages) {
final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset'; assert(messages.isNotEmpty);
print(redLine); print(redLine);
messages.forEach(print); messages.forEach(print);
print(redLine); print(redLine);
exit(1); _errorMessages.addAll(messages);
_hasError = true;
} }
typedef PrintCallback = void Function(Object line); @visibleForTesting
void resetErrorStatus() {
_hasError = false;
_errorMessages.clear();
}
// Allow print() to be overridden, for tests. Never reportErrorsAndExit() {
PrintCallback print = core_internals.print; print(redLine);
print('For your convenience, the error messages reported above are repeated here:');
_errorMessages.forEach(print);
print(redLine);
system.exit(1);
}
String get clock { String get clock {
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
......
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