Unverified Commit 52ae102f authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Adds tool warning log level and command line options to fail on warning/error output (#92031)

parent 02cfbc32
......@@ -170,6 +170,7 @@ Future<void> main(List<String> args) async {
try {
flutterTestArgs.addAll(args);
final Set<String> removeArgs = <String>{};
bool runSmokeTests = true;
for (final String arg in args) {
if (arg.startsWith('--local-engine=')) {
localEngineEnv['FLUTTER_LOCAL_ENGINE'] = arg.substring('--local-engine='.length);
......@@ -181,12 +182,18 @@ Future<void> main(List<String> args) async {
_shuffleSeed = arg.substring('--test-randomize-ordering-seed='.length);
removeArgs.add(arg);
}
if (arg == '--no-smoke-tests') {
runSmokeTests = false;
removeArgs.add(arg);
}
}
flutterTestArgs.removeWhere((String arg) => removeArgs.contains(arg));
if (Platform.environment.containsKey(CIRRUS_TASK_NAME))
print('Running task: ${Platform.environment[CIRRUS_TASK_NAME]}');
print('═' * 80);
if (runSmokeTests) {
await _runSmokeTests();
}
print('═' * 80);
await selectShard(<String, ShardRunner>{
'add_to_app_life_cycle_tests': _runAddToAppLifeCycleTests,
......@@ -474,7 +481,7 @@ Future<void> _runBuildTests() async {
Future<void> _runExampleProjectBuildTests(Directory exampleDirectory, [File? mainFile]) async {
// Only verify caching with flutter gallery.
final bool verifyCaching = exampleDirectory.path.contains('flutter_gallery');
final String examplePath = exampleDirectory.path;
final String examplePath = path.relative(exampleDirectory.path, from: Directory.current.path);
final bool hasNullSafety = File(path.join(examplePath, 'null_safety')).existsSync();
final List<String> additionalArgs = <String>[
if (hasNullSafety) '--no-sound-null-safety',
......@@ -786,7 +793,8 @@ Future<void> _runFrameworkTests() async {
await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'));
await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'), ensurePrecompiledTool: false); // See https://github.com/flutter/flutter/issues/86209
await _pubRunTest(path.join(flutterRoot, 'dev', 'conductor', 'core'), forceSingleCore: true);
await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'));
// TODO(gspencergoog): Remove the exception for fatalWarnings once https://github.com/flutter/flutter/pull/91127 has landed.
await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'), fatalWarnings: false);
await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'));
await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'));
await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'gen_keycodes'));
......@@ -1621,6 +1629,7 @@ Future<void> _runFlutterTest(String workingDirectory, {
Map<String, String>? environment,
List<String> tests = const <String>[],
bool shuffleTests = true,
bool fatalWarnings = true,
}) async {
assert(!printOutput || outputChecker == null, 'Output either can be printed or checked but not both');
......@@ -1634,6 +1643,7 @@ Future<void> _runFlutterTest(String workingDirectory, {
final List<String> args = <String>[
'test',
if (shuffleTests) '--test-randomize-ordering-seed=$shuffleSeed',
if (fatalWarnings) '--fatal-warnings',
...options,
...tags,
...flutterTestArgs,
......
......@@ -43,14 +43,12 @@ class CodesignCommand extends Command<void> {
}
argParser.addFlag(
kVerify,
help:
'Only verify expected binaries exist and are codesigned with entitlements.',
help: 'Only verify expected binaries exist and are codesigned with entitlements.',
);
argParser.addFlag(
kSignatures,
defaultsTo: true,
help:
'When off, this command will only verify the existence of binaries, and not their\n'
help: 'When off, this command will only verify the existence of binaries, and not their\n'
'signatures or entitlements. Must be used with --verify flag.',
);
argParser.addOption(
......@@ -93,23 +91,24 @@ class CodesignCommand extends Command<void> {
if (!platform.isMacOS) {
throw ConductorException(
'Error! Expected operating system "macos", actual operating system is: '
'"${platform.operatingSystem}"');
'"${platform.operatingSystem}"',
);
}
if (argResults!['verify'] as bool != true) {
throw ConductorException(
'Sorry, but codesigning is not implemented yet. Please pass the '
'--$kVerify flag to verify signatures.');
'--$kVerify flag to verify signatures.',
);
}
String revision;
if (argResults!.wasParsed(kRevision)) {
stdio.printError(
'Warning! When providing an arbitrary revision, the contents of the cache may not');
stdio.printError(
'match the expected binaries in the conductor tool. It is preferred to check out');
stdio.printError(
'the desired revision and run that version of the conductor.\n');
stdio.printWarning(
'Warning! When providing an arbitrary revision, the contents of the cache may not '
'match the expected binaries in the conductor tool. It is preferred to check out '
'the desired revision and run that version of the conductor.\n',
);
revision = argResults![kRevision] as String;
} else {
revision = ((await processManager.run(
......@@ -225,13 +224,11 @@ class CodesignCommand extends Command<void> {
)
.toList();
stdio.printError(
'Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n');
stdio.printError(
'If this commit is removing binaries from the cache, this test should be fixed by');
stdio.printError(
'removing the relevant entry from either the `binariesWithEntitlements` or');
stdio.printError(
'`binariesWithoutEntitlements` getters in dev/tools/lib/codesign.dart.');
'Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n\n'
'If this commit is removing binaries from the cache, this test should be fixed by\n'
'removing the relevant entry from either the "binariesWithEntitlements" or\n'
'"binariesWithoutEntitlements" getters in dev/tools/lib/codesign.dart.',
);
throw ConductorException('Did not find all expected binaries!');
}
......@@ -273,7 +270,8 @@ class CodesignCommand extends Command<void> {
stdio.printError(
'File "$binaryPath" does not appear to be codesigned.\n'
'The `codesign` command failed with exit code ${codeSignResult.exitCode}:\n'
'${codeSignResult.stderr}\n');
'${codeSignResult.stderr}\n',
);
continue;
}
if (verifyEntitlements) {
......@@ -291,42 +289,39 @@ class CodesignCommand extends Command<void> {
}
if (wrongEntitlementBinaries.isNotEmpty) {
stdio.printError(
'Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:');
stdio.printError('Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:');
wrongEntitlementBinaries.forEach(stdio.printError);
}
if (unexpectedBinaries.isNotEmpty) {
stdio.printError(
'Found ${unexpectedBinaries.length} unexpected binaries in the cache:');
stdio.printError('Found ${unexpectedBinaries.length} unexpected binaries in the cache:');
unexpectedBinaries.forEach(print);
}
// Finally, exit on any invalid state
if (unsignedBinaries.isNotEmpty) {
throw ConductorException(
'Test failed because unsigned binaries detected.');
throw ConductorException('Test failed because unsigned binaries detected.');
}
if (wrongEntitlementBinaries.isNotEmpty) {
throw ConductorException(
'Test failed because files found with the wrong entitlements:\n'
'${wrongEntitlementBinaries.join('\n')}');
'${wrongEntitlementBinaries.join('\n')}',
);
}
if (unexpectedBinaries.isNotEmpty) {
throw ConductorException(
'Test failed because unexpected binaries found in the cache.');
throw ConductorException('Test failed because unexpected binaries found in the cache.');
}
final String? desiredRevision = argResults![kRevision] as String?;
if (desiredRevision == null) {
stdio.printStatus(
'Verified that binaries are codesigned and have expected entitlements.');
stdio.printStatus('Verified that binaries are codesigned and have expected entitlements.');
} else {
stdio.printStatus(
'Verified that binaries for commit $desiredRevision are codesigned and have '
'expected entitlements.');
'expected entitlements.',
);
}
}
......@@ -388,7 +383,8 @@ class CodesignCommand extends Command<void> {
if (entitlementResult.exitCode != 0) {
stdio.printError(
'The `codesign --entitlements` command failed with exit code ${entitlementResult.exitCode}:\n'
'${entitlementResult.stderr}\n');
'${entitlementResult.stderr}\n',
);
return false;
}
......@@ -400,7 +396,8 @@ class CodesignCommand extends Command<void> {
if (output.contains(entitlement) != entitlementExpected) {
stdio.printError(
'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} '
'entitlement $entitlement.');
'entitlement $entitlement.',
);
passes = false;
}
}
......
......@@ -9,25 +9,44 @@ import 'package:meta/meta.dart';
abstract class Stdio {
final List<String> logs = <String>[];
/// Error/warning messages printed to STDERR.
/// Error messages printed to STDERR.
///
/// Display an error `message` to the user on stderr. Print errors if the code
/// fails in some way. Errors are typically followed shortly by exiting the
/// app with a non-zero exit status.
@mustCallSuper
void printError(String message) {
logs.add('[error] $message');
}
/// Warning messages printed to STDERR.
///
/// Display a warning `message` to the user on stderr. Print warnings if there
/// is important information to convey to the user that is not fatal.
@mustCallSuper
void printWarning(String message) {
logs.add('[warning] $message');
}
/// Ordinary STDOUT messages.
///
/// Displays normal output on stdout. This should be used for things like
/// progress messages, success messages, or just normal command output.
@mustCallSuper
void printStatus(String message) {
logs.add('[status] $message');
}
/// Debug messages that are only printed in verbose mode.
///
/// Use this for verbose tracing output. Users can turn this output on in order
/// to help diagnose issues.
@mustCallSuper
void printTrace(String message) {
logs.add('[trace] $message');
}
/// Write string to STDOUT without trailing newline.
/// Write the `message` string to STDOUT without a trailing newline.
@mustCallSuper
void write(String message) {
logs.add('[write] $message');
......
......@@ -49,6 +49,7 @@ class TestCommand extends Command<void> {
'task, will write test results to the file.');
argParser.addFlag(
'silent',
help: 'Suppresses standard output and only print standard error output.',
);
}
......
......@@ -6,6 +6,7 @@
import 'package:args/args.dart';
import 'package:flutter_tools/src/asset.dart' hide defaultManifestPath;
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart' as libfs;
import 'package:flutter_tools/src/base/io.dart';
......@@ -70,8 +71,7 @@ Future<void> run(List<String> args) async {
);
if (assets == null) {
print('Unable to find assets.');
exit(1);
throwToolExit('Unable to find assets.', exitCode: 1);
}
final List<Future<void>> calls = <Future<void>>[];
......
......@@ -39,7 +39,7 @@ Future<int> run(
// Remove the verbose option; for help and doctor, users don't need to see
// verbose logs.
args = List<String>.of(args);
args.removeWhere((String option) => option == '-v' || option == '--verbose');
args.removeWhere((String option) => option == '-vv' || option == '-v' || option == '--verbose');
}
return runInContext<int>(() async {
......
......@@ -751,7 +751,7 @@ HostPlatform getCurrentHostPlatform() {
return HostPlatform.windows_x64;
}
globals.printError('Unsupported host platform, defaulting to Linux');
globals.printWarning('Unsupported host platform, defaulting to Linux');
return HostPlatform.linux_x64;
}
......
......@@ -266,7 +266,7 @@ class Dart2JSTarget extends Target {
final File dart2jsDeps = environment.buildDir
.childFile('app.dill.deps');
if (!dart2jsDeps.existsSync()) {
globals.printError('Warning: dart2js did not produced expected deps list at '
globals.printWarning('Warning: dart2js did not produced expected deps list at '
'${dart2jsDeps.path}');
return;
}
......
......@@ -140,7 +140,7 @@ Future<void> writeBundle(
try {
bundleDir.deleteSync(recursive: true);
} on FileSystemException catch (err) {
loggerOverride.printError(
loggerOverride.printWarning(
'Failed to clean up asset directory ${bundleDir.path}: $err\n'
'To clean build artifacts, use the command "flutter clean".'
);
......
......@@ -164,6 +164,7 @@ class Cache {
late final ArtifactUpdater _artifactUpdater = _createUpdater();
@visibleForTesting
@protected
void registerArtifact(ArtifactSet artifactSet) {
_artifacts.add(artifactSet);
......@@ -320,13 +321,17 @@ class Cache {
} on FileSystemException {
if (!printed) {
_logger.printTrace('Waiting to be able to obtain lock of Flutter binary artifacts directory: ${_lock!.path}');
// This needs to go to stderr to avoid cluttering up stdout if a parent
// process is collecting stdout. It's not really an "error" though,
// so print it in grey.
_logger.printError(
// This needs to go to stderr to avoid cluttering up stdout if a
// parent process is collecting stdout (e.g. when calling "flutter
// version --machine"). It's not really a "warning" though, so print it
// in grey. Also, make sure that it isn't counted as a warning for
// Logger.warningsAreFatal.
final bool oldWarnings = _logger.hadWarningOutput;
_logger.printWarning(
'Waiting for another flutter command to release the startup lock...',
color: TerminalColor.grey,
);
_logger.hadWarningOutput = oldWarnings;
printed = true;
}
await Future<void>.delayed(const Duration(milliseconds: 50));
......@@ -509,7 +514,7 @@ class Cache {
ErrorHandlingFileSystem.deleteIfExists(file);
}
} on FileSystemException catch (err) {
_logger.printError('Failed to delete some stamp files: $err');
_logger.printWarning('Failed to delete some stamp files: $err');
}
}
......@@ -703,7 +708,7 @@ abstract class CachedArtifact extends ArtifactSet {
await updateInner(artifactUpdater, fileSystem, operatingSystemUtils);
try {
if (version == null) {
logger.printError(
logger.printWarning(
'No known version for the artifact name "$name". '
'Flutter can continue, but the artifact may be re-downloaded on '
'subsequent invocations until the problem is resolved.',
......@@ -712,7 +717,7 @@ abstract class CachedArtifact extends ArtifactSet {
cache.setStampFor(stampName, version!);
}
} on FileSystemException catch (err) {
logger.printError(
logger.printWarning(
'The new artifact "$name" was downloaded, but Flutter failed to update '
'its stamp file, receiving the error "$err". '
'Flutter can continue, but the artifact may be re-downloaded on '
......@@ -1103,7 +1108,7 @@ class ArtifactUpdater {
try {
file.deleteSync();
} on FileSystemException catch (e) {
_logger.printError('Failed to delete "${file.path}". Please delete manually. $e');
_logger.printWarning('Failed to delete "${file.path}". Please delete manually. $e');
continue;
}
for (Directory directory = file.parent; directory.absolute.path != _tempStorage.absolute.path; directory = directory.parent) {
......
......@@ -65,8 +65,9 @@ class BuildCommand extends FlutterCommand {
}
abstract class BuildSubCommand extends FlutterCommand {
BuildSubCommand() {
BuildSubCommand({@required bool verboseHelp}) {
requiresPubspecYaml();
usesFatalWarningsOption(verboseHelp: verboseHelp);
}
@override
......
......@@ -20,7 +20,7 @@ import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart';
class BuildAarCommand extends BuildSubCommand {
BuildAarCommand({ @required bool verboseHelp }) {
BuildAarCommand({ @required bool verboseHelp }) : super(verboseHelp: verboseHelp) {
argParser
..addFlag(
'debug',
......
......@@ -16,7 +16,7 @@ import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart';
class BuildApkCommand extends BuildSubCommand {
BuildApkCommand({bool verboseHelp = false}) {
BuildApkCommand({bool verboseHelp = false}) : super(verboseHelp: verboseHelp) {
addTreeShakeIconsFlag();
usesTargetOption();
addBuildModeFlags(verboseHelp: verboseHelp);
......
......@@ -19,7 +19,9 @@ import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart';
class BuildAppBundleCommand extends BuildSubCommand {
BuildAppBundleCommand({bool verboseHelp = false}) {
BuildAppBundleCommand({
bool verboseHelp = false,
}) : super(verboseHelp: verboseHelp) {
addTreeShakeIconsFlag();
usesTargetOption();
addBuildModeFlags(verboseHelp: verboseHelp);
......
......@@ -16,7 +16,10 @@ import '../runner/flutter_command.dart';
import 'build.dart';
class BuildBundleCommand extends BuildSubCommand {
BuildBundleCommand({bool verboseHelp = false, this.bundleBuilder}) {
BuildBundleCommand({
bool verboseHelp = false,
this.bundleBuilder,
}) : super(verboseHelp: verboseHelp) {
usesTargetOption();
usesFilesystemOptions(hide: !verboseHelp);
usesBuildNumberOption();
......
......@@ -19,7 +19,9 @@ import 'build.dart';
/// A command to build a Fuchsia target.
class BuildFuchsiaCommand extends BuildSubCommand {
BuildFuchsiaCommand({ @required bool verboseHelp }) {
BuildFuchsiaCommand({
@required bool verboseHelp,
}) : super(verboseHelp: verboseHelp) {
addTreeShakeIconsFlag();
usesTargetOption();
usesDartDefineOption();
......
......@@ -129,7 +129,7 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
// xcarchive failed or not at expected location.
if (xcarchiveResult.exitStatus != ExitStatus.success) {
globals.logger.printStatus('Skipping IPA');
globals.printStatus('Skipping IPA');
return xcarchiveResult;
}
......@@ -176,14 +176,16 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
throwToolExit('Encountered error while building IPA:\n$errorMessage');
}
globals.logger.printStatus('Built IPA to $outputPath.');
globals.printStatus('Built IPA to $outputPath.');
return FlutterCommandResult.success();
}
}
abstract class _BuildIOSSubCommand extends BuildSubCommand {
_BuildIOSSubCommand({ @required bool verboseHelp }) {
_BuildIOSSubCommand({
@required bool verboseHelp
}) : super(verboseHelp: verboseHelp) {
addTreeShakeIconsFlag();
addSplitDebugInfoOption();
addBuildModeFlags(verboseHelp: verboseHelp);
......
......@@ -39,7 +39,8 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
}) : _flutterVersion = flutterVersion,
_buildSystem = buildSystem,
_injectedCache = cache,
_injectedPlatform = platform {
_injectedPlatform = platform,
super(verboseHelp: verboseHelp) {
addTreeShakeIconsFlag();
usesTargetOption();
usesFlavorOption();
......
......@@ -23,7 +23,8 @@ class BuildLinuxCommand extends BuildSubCommand {
BuildLinuxCommand({
@required OperatingSystemUtils operatingSystemUtils,
bool verboseHelp = false,
}) : _operatingSystemUtils = operatingSystemUtils {
}) : _operatingSystemUtils = operatingSystemUtils,
super(verboseHelp: verboseHelp) {
addCommonDesktopBuildOptions(verboseHelp: verboseHelp);
final String defaultTargetPlatform =
(_operatingSystemUtils.hostPlatform == HostPlatform.linux_arm64) ?
......
......@@ -19,7 +19,9 @@ import 'build.dart';
/// A command to build a macOS desktop target through a build shell script.
class BuildMacosCommand extends BuildSubCommand {
BuildMacosCommand({ @required bool verboseHelp }) {
BuildMacosCommand({
@required bool verboseHelp,
}) : super(verboseHelp: verboseHelp) {
addCommonDesktopBuildOptions(verboseHelp: verboseHelp);
usesBuildNumberOption();
usesBuildNameOption();
......
......@@ -20,7 +20,7 @@ import 'build.dart';
class BuildWebCommand extends BuildSubCommand {
BuildWebCommand({
@required bool verboseHelp,
}) {
}) : super(verboseHelp: verboseHelp) {
addTreeShakeIconsFlag(enabledByDefault: false);
usesTargetOption();
usesPubOption();
......
......@@ -20,7 +20,9 @@ import 'build.dart';
/// A command to build a windows desktop target through a build shell script.
class BuildWindowsCommand extends BuildSubCommand {
BuildWindowsCommand({ bool verboseHelp = false }) {
BuildWindowsCommand({
bool verboseHelp = false,
}) : super(verboseHelp: verboseHelp) {
addCommonDesktopBuildOptions(verboseHelp: verboseHelp);
}
......
......@@ -19,7 +19,9 @@ import 'build.dart';
/// A command to build a Windows UWP desktop target.
class BuildWindowsUwpCommand extends BuildSubCommand {
BuildWindowsUwpCommand({ bool verboseHelp = false }) {
BuildWindowsUwpCommand({
bool verboseHelp = false,
}) : super(verboseHelp: verboseHelp) {
addCommonDesktopBuildOptions(verboseHelp: verboseHelp);
}
......
......@@ -228,7 +228,7 @@ class CreateCommand extends CreateBase {
validateProjectDir(overwrite: overwrite);
if (boolArg('with-driver-test')) {
globals.printError(
globals.printWarning(
'The "--with-driver-test" argument has been deprecated and will no longer add a flutter '
'driver template. Instead, learn how to use package:integration_test by '
'visiting https://pub.dev/packages/integration_test .'
......
......@@ -309,7 +309,7 @@ class DaemonDomain extends Domain {
// capture the print output for testing.
// ignore: avoid_print
print(message.message);
} else if (message.level == 'error') {
} else if (message.level == 'error' || message.level == 'warning') {
globals.stdio.stderrWrite('${message.message}\n');
if (message.stackTrace != null) {
globals.stdio.stderrWrite(
......@@ -1011,6 +1011,18 @@ class NotifyingLogger extends DelegatingLogger {
_sendMessage(LogMessage('error', message, stackTrace));
}
@override
void printWarning(
String message, {
bool emphasis = false,
TerminalColor color,
int indent,
int hangingIndent,
bool wrap,
}) {
_sendMessage(LogMessage('warning', message));
}
@override
void printStatus(
String message, {
......
......@@ -48,7 +48,7 @@ class DevicesCommand extends FlutterCommand {
@override
Future<void> validateCommand() {
if (argResults?['timeout'] != null) {
globals.printError('${globals.logger.terminal.warningMark} The "--timeout" argument is deprecated; use "--${FlutterOptions.kDeviceTimeout}" instead.');
globals.printWarning('${globals.logger.terminal.warningMark} The "--timeout" argument is deprecated; use "--${FlutterOptions.kDeviceTimeout}" instead.');
}
return super.validateCommand();
}
......
......@@ -98,7 +98,7 @@ Future<bool> installApp(
if (uninstall && await device.isAppInstalled(package, userIdentifier: userIdentifier)) {
globals.printStatus('Uninstalling old version...');
if (!await device.uninstallApp(package, userIdentifier: userIdentifier)) {
globals.printError('Warning: uninstalling old version failed');
globals.printWarning('Warning: uninstalling old version failed');
}
}
} on ProcessException catch (e) {
......
......@@ -153,6 +153,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
addDdsOptions(verboseHelp: verboseHelp);
addDevToolsOptions(verboseHelp: verboseHelp);
addAndroidSpecificBuildOptions(hide: !verboseHelp);
usesFatalWarningsOption(verboseHelp: verboseHelp);
}
bool get traceStartup => boolArg('trace-startup');
......
......@@ -217,6 +217,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
defaultsTo: '30s',
);
addDdsOptions(verboseHelp: verboseHelp);
usesFatalWarningsOption(verboseHelp: verboseHelp);
}
/// The interface for starting and configuring the tester.
......@@ -283,7 +284,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
// correct [requiredArtifacts] can be identified before [run] takes place.
_isIntegrationTest = _shouldRunAsIntegrationTests(globals.fs.currentDirectory.absolute.path, _testFiles);
globals.logger.printTrace(
globals.printTrace(
'Found ${_testFiles.length} files which will be executed as '
'${_isIntegrationTest ? 'Integration' : 'Widget'} Tests.',
);
......@@ -338,7 +339,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
}
if (_isIntegrationTest) {
if (argResults.wasParsed('concurrency')) {
globals.logger.printStatus(
globals.printStatus(
'-j/--concurrency was parsed but will be ignored, this option is not '
'supported when running Integration Tests.',
);
......
......@@ -190,7 +190,7 @@ class UpdatePackagesCommand extends FlutterCommand {
if (pubspec.checksum.value == null) {
// If the checksum is invalid or missing, we can just ask them run to run
// upgrade again to compute it.
globals.printError(
globals.printWarning(
'Warning: pubspec in ${directory.path} has out of date dependencies. '
'Please run "flutter update-packages --force-upgrade" to update them correctly.'
);
......@@ -207,7 +207,7 @@ class UpdatePackagesCommand extends FlutterCommand {
if (checksum != pubspec.checksum.value) {
// If the checksum doesn't match, they may have added or removed some dependencies.
// we need to run update-packages to recapture the transitive deps.
globals.printError(
globals.printWarning(
'Warning: pubspec in ${directory.path} has updated or new dependencies. '
'Please run "flutter update-packages --force-upgrade" to update them correctly '
'(checksum ${pubspec.checksum.value} != $checksum).'
......@@ -1501,7 +1501,7 @@ Directory createTemporaryFlutterSdk(
..createSync(recursive: true);
final PubspecYaml pubspecYaml = pubspecsByName[flutterPackage];
if (pubspecYaml == null) {
logger.printError(
logger.printWarning(
"Unexpected package '$flutterPackage' found in packages directory",
);
continue;
......
......@@ -339,11 +339,11 @@ class FlutterManifest {
final YamlList? fontFiles = fontFamily['fonts'] as YamlList?;
final String? familyName = fontFamily['family'] as String?;
if (familyName == null) {
_logger.printError('Warning: Missing family name for font.', emphasis: true);
_logger.printWarning('Warning: Missing family name for font.', emphasis: true);
continue;
}
if (fontFiles == null) {
_logger.printError('Warning: No fonts specified for font $familyName', emphasis: true);
_logger.printWarning('Warning: No fonts specified for font $familyName', emphasis: true);
continue;
}
......@@ -351,7 +351,7 @@ class FlutterManifest {
for (final Map<Object?, Object?> fontFile in fontFiles.cast<Map<Object?, Object?>>()) {
final String? asset = fontFile['asset'] as String?;
if (asset == null) {
_logger.printError('Warning: Missing asset in fonts for $familyName', emphasis: true);
_logger.printWarning('Warning: Missing asset in fonts for $familyName', emphasis: true);
continue;
}
......
......@@ -393,7 +393,7 @@ Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin>
}
}
if (pluginsUsingV1.length > 1) {
globals.printError(
globals.printWarning(
'The plugins `${pluginsUsingV1.join(', ')}` use a deprecated version of the Android embedding.\n'
'To avoid unexpected runtime failures, or future build failures, try to see if these plugins '
'support the Android V2 embedding. Otherwise, consider removing them since a future release '
......@@ -402,7 +402,7 @@ Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin>
'https://flutter.dev/go/android-plugin-migration.'
);
} else if (pluginsUsingV1.isNotEmpty) {
globals.printError(
globals.printWarning(
'The plugin `${pluginsUsingV1.first}` uses a deprecated version of the Android embedding.\n'
'To avoid unexpected runtime failures, or future build failures, try to see if this plugin '
'supports the Android V2 embedding. Otherwise, consider removing it since a future release '
......@@ -414,7 +414,7 @@ Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin>
templateContent = _androidPluginRegistryTemplateNewEmbedding;
break;
case AndroidEmbeddingVersion.v1:
globals.printError(
globals.printWarning(
'This app is using a deprecated version of the Android embedding.\n'
'To avoid unexpected runtime failures, or future build failures, try to migrate this '
'app to the V2 embedding.\n'
......@@ -1301,7 +1301,7 @@ Future<void> generateMainDartWithPluginRegistrant(
newMainDart.deleteSync();
}
} on FileSystemException catch (error) {
globals.printError(
globals.printWarning(
'Unable to remove ${newMainDart.path}, received error: $error.\n'
'You might need to run flutter clean.'
);
......
......@@ -124,6 +124,10 @@ Future<void> _buildAssets(
assetDirPath: assetDir,
);
if (assets == null) {
throwToolExit('Unable to find assets.', exitCode: 1);
}
final Map<String, DevFSContent> assetEntries =
Map<String, DevFSContent>.of(assets.entries);
await writeBundle(globals.fs.directory(assetDir), assetEntries);
......
......@@ -150,6 +150,30 @@ void printError(
);
}
/// Display a warning level message to the user. Commands should use this if they
/// have important warnings to convey that aren't fatal.
///
/// Set [emphasis] to true to make the output bold if it's supported.
/// Set [color] to a [TerminalColor] to color the output, if the logger
/// supports it. The [color] defaults to [TerminalColor.cyan].
void printWarning(
String message, {
bool? emphasis,
TerminalColor? color,
int? indent,
int? hangingIndent,
bool? wrap,
}) {
logger.printWarning(
message,
emphasis: emphasis ?? false,
color: color,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
);
}
/// Display normal output of the command. This should be used for things like
/// progress messages, success messages, or just normal command output.
///
......
......@@ -176,7 +176,7 @@ class CocoaPods {
final CocoaPodsStatus installation = await evaluateCocoaPodsInstallation;
switch (installation) {
case CocoaPodsStatus.notInstalled:
_logger.printError(
_logger.printWarning(
'Warning: CocoaPods not installed. Skipping pod install.\n'
'$noCocoaPodsConsequence\n'
'To install $cocoaPodsInstallInstructions\n',
......@@ -184,7 +184,7 @@ class CocoaPods {
);
return false;
case CocoaPodsStatus.brokenInstall:
_logger.printError(
_logger.printWarning(
'Warning: CocoaPods is installed but broken. Skipping pod install.\n'
'$brokenCocoaPodsConsequence\n'
'To re-install $cocoaPodsInstallInstructions\n',
......@@ -192,7 +192,7 @@ class CocoaPods {
);
return false;
case CocoaPodsStatus.unknownVersion:
_logger.printError(
_logger.printWarning(
'Warning: Unknown CocoaPods version installed.\n'
'$unknownCocoaPodsConsequence\n'
'To upgrade $cocoaPodsInstallInstructions\n',
......@@ -200,7 +200,7 @@ class CocoaPods {
);
break;
case CocoaPodsStatus.belowMinimumVersion:
_logger.printError(
_logger.printWarning(
'Warning: CocoaPods minimum required version $cocoaPodsMinimumVersion or greater not installed. Skipping pod install.\n'
'$noCocoaPodsConsequence\n'
'To upgrade $cocoaPodsInstallInstructions\n',
......@@ -208,7 +208,7 @@ class CocoaPods {
);
return false;
case CocoaPodsStatus.belowRecommendedVersion:
_logger.printError(
_logger.printWarning(
'Warning: CocoaPods recommended version $cocoaPodsRecommendedVersion or greater not installed.\n'
'Pods handling may fail on some projects involving plugins.\n'
'To upgrade $cocoaPodsInstallInstructions\n',
......@@ -406,15 +406,15 @@ class CocoaPods {
// plugin_pods = parse_KV_file('../.flutter-plugins')
if (xcodeProject.podfile.existsSync() &&
xcodeProject.podfile.readAsStringSync().contains(".flutter-plugins'")) {
const String error = 'Warning: Podfile is out of date\n'
const String warning = 'Warning: Podfile is out of date\n'
'$outOfDatePluginsPodfileConsequence\n'
'To regenerate the Podfile, run:\n';
if (isIos) {
throwToolExit('$error\n$podfileIosMigrationInstructions\n');
throwToolExit('$warning\n$podfileIosMigrationInstructions\n');
} else {
// The old macOS Podfile will work until `.flutter-plugins` is removed.
// Warn instead of exit.
_logger.printError('$error\n$podfileMacOSMigrationInstructions\n', emphasis: true);
_logger.printWarning('$warning\n$podfileMacOSMigrationInstructions\n', emphasis: true);
}
}
}
......
......@@ -415,7 +415,7 @@ class XCDevice {
} else {
cpuArchitecture = DarwinArch.arm64;
}
_logger.printError(
_logger.printWarning(
'Unknown architecture $architecture, defaulting to '
'${getNameForDarwinArch(cpuArchitecture)}',
);
......
......@@ -110,7 +110,7 @@ class MDnsObservatoryDiscovery {
return null;
}
if (srv.length > 1) {
_logger.printError('Unexpectedly found more than one observatory report for $domainName '
_logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
}
_logger.printTrace('Checking for authentication code for $domainName');
......
......@@ -479,23 +479,6 @@ class AndroidProject extends FlutterProjectPlatform {
}
Future<void> ensureReadyForPlatformSpecificTooling() async {
if (getEmbeddingVersion() == AndroidEmbeddingVersion.v1) {
globals.printStatus(
"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Warning
──────────────────────────────────────────────────────────────────────────────
Your Flutter application is created using an older version of the Android
embedding. It's being deprecated in favor of Android embedding v2. Follow the
steps at
https://flutter.dev/go/android-project-migration
to migrate your project.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
);
}
if (isModule && _shouldRegenerateFromTemplate()) {
await _regenerateLibrary();
// Add ephemeral host app, if an editable host app does not already exist.
......
......@@ -1178,7 +1178,7 @@ abstract class ResidentRunner extends ResidentHandlers {
);
if (!_lastBuild.success) {
for (final ExceptionMeasurement exceptionMeasurement in _lastBuild.exceptions.values) {
globals.logger.printError(
globals.printError(
exceptionMeasurement.exception.toString(),
stackTrace: globals.logger.isVerbose
? exceptionMeasurement.stackTrace
......@@ -1186,7 +1186,7 @@ abstract class ResidentRunner extends ResidentHandlers {
);
}
}
globals.logger.printTrace('complete');
globals.printTrace('complete');
}
@protected
......@@ -1241,7 +1241,7 @@ abstract class ResidentRunner extends ResidentHandlers {
if (_dillOutputPath != null) {
return;
}
globals.logger.printTrace('Caching compiled dill');
globals.printTrace('Caching compiled dill');
final File outputDill = globals.fs.file(dillOutputPath);
if (outputDill.existsSync()) {
final String copyPath = getDefaultCachedKernelPath(
......@@ -1561,7 +1561,7 @@ class TerminalHandler {
_logger.printTrace('Deleting pid file (${_actualPidFile.path}).');
_actualPidFile.deleteSync();
} on FileSystemException catch (error) {
_logger.printError('Failed to delete pid file (${_actualPidFile.path}): ${error.message}');
_logger.printWarning('Failed to delete pid file (${_actualPidFile.path}): ${error.message}');
}
_actualPidFile = null;
}
......
......@@ -77,8 +77,8 @@ class ColdRunner extends ResidentRunner {
return result;
}
}
} on Exception catch (err) {
globals.printError(err.toString());
} on Exception catch (err, stack) {
globals.printError('$err\n$stack');
appFailedToStart();
return 1;
}
......
......@@ -115,6 +115,7 @@ class FlutterOptions {
static const String kDeferredComponents = 'deferred-components';
static const String kAndroidProjectArgs = 'android-project-arg';
static const String kInitializeFromDill = 'initialize-from-dill';
static const String kFatalWarnings = 'fatal-warnings';
}
/// flutter command categories for usage.
......@@ -178,6 +179,8 @@ abstract class FlutterCommand extends Command<void> {
bool _usesIpv6Flag = false;
bool _usesFatalWarnings = false;
bool get shouldRunPub => _usesPubOption && boolArg('pub');
bool get shouldUpdateCache => true;
......@@ -271,6 +274,15 @@ abstract class FlutterCommand extends Command<void> {
_usesTargetOption = true;
}
void usesFatalWarningsOption({ required bool verboseHelp }) {
argParser.addFlag(FlutterOptions.kFatalWarnings,
hide: !verboseHelp,
help: 'Causes the command to fail if warnings are sent to the console '
'during its execution.'
);
_usesFatalWarnings = true;
}
String get targetFile {
if (argResults?.wasParsed('target') == true) {
return stringArg('target')!;
......@@ -413,10 +425,10 @@ abstract class FlutterCommand extends Command<void> {
// TODO(ianh): enable the following code once google3 is migrated away from --disable-dds (and add test to flutter_command_test.dart)
if (false) { // ignore: dead_code
if (ddsEnabled) {
globals.printError('${globals.logger.terminal
globals.printWarning('${globals.logger.terminal
.warningMark} The "--no-disable-dds" argument is deprecated and redundant, and should be omitted.');
} else {
globals.printError('${globals.logger.terminal
globals.printWarning('${globals.logger.terminal
.warningMark} The "--disable-dds" argument is deprecated. Use "--no-dds" instead.');
}
}
......@@ -1123,6 +1135,9 @@ abstract class FlutterCommand extends Command<void> {
name: 'command',
overrides: <Type, Generator>{FlutterCommand: () => this},
body: () async {
if (_usesFatalWarnings) {
globals.logger.fatalWarnings = boolArg(FlutterOptions.kFatalWarnings);
}
// Prints the welcome message if needed.
globals.flutterUsage.printWelcome();
_printDeprecationWarning();
......@@ -1139,6 +1154,9 @@ abstract class FlutterCommand extends Command<void> {
if (commandPath != null) {
_sendPostUsage(commandPath, commandResult, startTime, endTime);
}
if (_usesFatalWarnings) {
globals.logger.checkForFatalLogs();
}
}
},
);
......@@ -1146,13 +1164,12 @@ abstract class FlutterCommand extends Command<void> {
void _printDeprecationWarning() {
if (deprecated) {
globals.printError(
globals.printWarning(
'${globals.logger.terminal.warningMark} The "$name" command is deprecated and '
'will be removed in a future version of Flutter. '
'See https://flutter.dev/docs/development/tools/sdk/releases '
'for previous releases of Flutter.',
'for previous releases of Flutter.\n',
);
globals.printError('');
}
}
......
......@@ -277,10 +277,10 @@ class FlutterVersion {
);
} on VersionCheckError catch (error) {
if (globals.platform.environment.containsKey('FLUTTER_GIT_URL')) {
globals.logger.printError('Warning: the Flutter git upstream was overridden '
globals.printWarning('Warning: the Flutter git upstream was overridden '
'by the environment variable FLUTTER_GIT_URL = ${globals.flutterGit}');
}
globals.logger.printError(error.toString());
globals.printError(error.toString());
rethrow;
} finally {
await _removeVersionCheckRemoteIfExists();
......
......@@ -568,6 +568,20 @@ class StreamLogger extends Logger {
int hangingIndent,
bool wrap,
}) {
hadErrorOutput = true;
_log('[stderr] $message');
}
@override
void printWarning(
String message, {
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
bool wrap,
}) {
hadWarningOutput = true;
_log('[stderr] $message');
}
......
......@@ -227,6 +227,8 @@ void main() {
const <String>['build', 'linux', '--debug', '--no-pub']
);
expect(testLogger.statusText, isNot(contains('STDOUT STUFF')));
expect(testLogger.warningText, isNot(contains('STDOUT STUFF')));
expect(testLogger.errorText, isNot(contains('STDOUT STUFF')));
expect(testLogger.traceText, contains('STDOUT STUFF'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
......@@ -308,6 +310,8 @@ ERROR: No file or variants found for asset: images/a_dot_burr.jpeg
);
expect(testLogger.statusText, contains('STDOUT STUFF'));
expect(testLogger.traceText, isNot(contains('STDOUT STUFF')));
expect(testLogger.warningText, isNot(contains('STDOUT STUFF')));
expect(testLogger.errorText, isNot(contains('STDOUT STUFF')));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
......
......@@ -3,9 +3,13 @@
// found in the LICENSE file.
// @dart = 2.8
import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:meta/meta.dart';
import '../../src/common.dart';
import '../../src/context.dart';
......@@ -13,27 +17,97 @@ import '../../src/test_flutter_command_runner.dart';
void main() {
testUsingContext('obfuscate requires split-debug-info', () {
final FakeBuildCommand command = FakeBuildCommand();
final FakeBuildInfoCommand command = FakeBuildInfoCommand();
final CommandRunner<void> commandRunner = createTestCommandRunner(command);
expect(() => commandRunner.run(<String>[
'build',
'fake',
'--obfuscate',
]), throwsToolExit());
]), throwsToolExit(message: '"--${FlutterOptions.kDartObfuscationOption}" can only be used in '
'combination with "--${FlutterOptions.kSplitDebugInfoOption}"'));
});
group('Fatal Logs', () {
FakeBuildCommand command;
MemoryFileSystem fs;
setUp(() {
fs = MemoryFileSystem.test();
fs.file('/package/pubspec.yaml').createSync(recursive: true);
fs.currentDirectory = '/package';
Cache.disableLocking();
});
testUsingContext("doesn't fail if --fatal-warnings specified and no warnings occur", () async {
command = FakeBuildCommand();
try {
await createTestCommandRunner(command).run(<String>[
'build',
'test',
'--${FlutterOptions.kFatalWarnings}',
]);
} on Exception {
fail('Unexpected exception thrown');
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("doesn't fail if --fatal-warnings not specified", () async {
command = FakeBuildCommand();
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
try {
await createTestCommandRunner(command).run(<String>[
'build',
'test',
]);
} on Exception {
fail('Unexpected exception thrown');
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails if --fatal-warnings specified and warnings emitted', () async {
command = FakeBuildCommand();
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
await expectLater(createTestCommandRunner(command).run(<String>[
'build',
'test',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received warning output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails if --fatal-warnings specified and errors emitted', () async {
command = FakeBuildCommand();
testLogger.printError('Error: Danger Will Robinson!');
await expectLater(createTestCommandRunner(command).run(<String>[
'build',
'test',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received error output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
});
}
class FakeBuildCommand extends FlutterCommand {
FakeBuildCommand() {
class FakeBuildInfoCommand extends FlutterCommand {
FakeBuildInfoCommand() : super() {
addSplitDebugInfoOption();
addDartObfuscationOption();
}
@override
String get description => throw UnimplementedError();
String get description => '';
@override
String get name => 'build';
String get name => 'fake';
@override
Future<FlutterCommandResult> runCommand() async {
......@@ -41,3 +115,35 @@ class FakeBuildCommand extends FlutterCommand {
return FlutterCommandResult.success();
}
}
class FakeBuildCommand extends BuildCommand {
FakeBuildCommand({bool verboseHelp = false}) : super(verboseHelp: verboseHelp) {
addSubcommand(FakeBuildSubcommand(verboseHelp: verboseHelp));
}
@override
String get description => '';
@override
String get name => 'build';
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
class FakeBuildSubcommand extends BuildSubCommand {
FakeBuildSubcommand({@required bool verboseHelp}) : super(verboseHelp: verboseHelp);
@override
String get description => '';
@override
String get name => 'test';
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
......@@ -130,6 +130,29 @@ void main() {
Logger: () => notifyingLogger,
});
testUsingContext('printWarning should send daemon.logMessage event', () async {
final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
daemon = Daemon(
commands.stream,
responses.add,
notifyingLogger: notifyingLogger,
);
globals.printWarning('daemon.logMessage test');
final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) {
return map['event'] == 'daemon.logMessage' && (map['params'] as Map<String, dynamic>)['level'] == 'warning';
});
expect(response['id'], isNull);
expect(response['event'], 'daemon.logMessage');
final Map<String, String> logMessage = castStringKeyedMap(response['params']).cast<String, String>();
expect(logMessage['level'], 'warning');
expect(logMessage['message'], 'daemon.logMessage test');
await responses.close();
await commands.close();
}, overrides: <Type, Generator>{
Logger: () => notifyingLogger,
});
testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
final StringBuffer buffer = await capturedConsolePrint(() {
final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
......
......@@ -294,6 +294,75 @@ void main() {
});
});
group('Fatal Logs', () {
TestRunCommandWithFakeResidentRunner command;
MemoryFileSystem fs;
setUp(() {
command = TestRunCommandWithFakeResidentRunner()
..fakeResidentRunner = FakeResidentRunner();
fs = MemoryFileSystem.test();
});
testUsingContext("doesn't fail if --fatal-warnings specified and no warnings occur", () async {
try {
await createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--${FlutterOptions.kFatalWarnings}',
]);
} on Exception {
fail('Unexpected exception thrown');
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("doesn't fail if --fatal-warnings not specified", () async {
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
try {
await createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
]);
} on Exception {
fail('Unexpected exception thrown');
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails if --fatal-warnings specified and warnings emitted', () async {
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received warning output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails if --fatal-warnings specified and errors emitted', () async {
testLogger.printError('Error: Danger Will Robinson!');
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received error output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
});
testUsingContext('should only request artifacts corresponding to connected devices', () async {
mockDeviceManager.devices = <Device>[FakeDevice(targetPlatform: TargetPlatform.android_arm)];
......@@ -496,7 +565,7 @@ class FakeDevice extends Fake implements Device {
@override
String get id => 'fake_device';
void _throwToolExit(int code) => throwToolExit(null, exitCode: code);
void _throwToolExit(int code) => throwToolExit('FakeDevice tool exit', exitCode: code);
@override
Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator);
......@@ -504,6 +573,9 @@ class FakeDevice extends Fake implements Device {
@override
bool supportsRuntimeMode(BuildMode mode) => true;
@override
Future<bool> get supportsHardwareRendering async => true;
@override
bool supportsHotReload = false;
......@@ -542,7 +614,7 @@ class FakeDevice extends Fake implements Device {
@override
final PlatformType platformType = PlatformType.ios;
bool startAppSuccess = true;
bool startAppSuccess;
@override
DevFSWriter createDevFSWriter(
......@@ -564,9 +636,12 @@ class FakeDevice extends Fake implements Device {
bool ipv6 = false,
String userIdentifier,
}) async {
if (!startAppSuccess) {
if (startAppSuccess == false) {
return LaunchResult.failed();
}
if (startAppSuccess == true) {
return LaunchResult.succeeded();
}
final String dartFlags = debuggingOptions.dartFlags;
// In release mode, --dart-flags should be set to the empty string and
// provided flags should be dropped. In debug and profile modes,
......@@ -587,18 +662,20 @@ class FakeDevice extends Fake implements Device {
}
class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFactory {
ApplicationPackage package;
FakeApplicationPackageFactory(this.applicationPackage);
ApplicationPackage applicationPackage;
@override
Future<ApplicationPackage> getPackageForPlatform(
TargetPlatform platform, {
BuildInfo buildInfo,
File applicationBinary,
}) async {
return package;
}
}) async => applicationPackage;
}
class FakeApplicationPackage extends Fake implements ApplicationPackage { }
class TestRunCommandWithFakeResidentRunner extends RunCommand {
FakeResidentRunner fakeResidentRunner;
......
......@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/test.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/test/runner.dart';
import 'package:flutter_tools/src/test/test_wrapper.dart';
import 'package:flutter_tools/src/test/watcher.dart';
......@@ -644,6 +645,60 @@ dev_dependencies:
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => _FakeDeviceManager(<Device>[]),
});
group('Fatal Logs', () {
testUsingContext("doesn't fail when --fatal-warnings is set and no warning output", () async {
final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0);
final TestCommand testCommand = TestCommand(testRunner: testRunner);
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
try {
await commandRunner.run(const <String>[
'test',
'--no-pub',
'--${FlutterOptions.kFatalWarnings}',
]);
} on Exception {
fail('Unexpected exception thrown');
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails if --fatal-warnings specified and warnings emitted', () async {
final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0);
final TestCommand testCommand = TestCommand(testRunner: testRunner);
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
testLogger.printWarning('Warning: Mild annoyance, Will Robinson!');
expect(commandRunner.run(const <String>[
'test',
'--no-pub',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received warning output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails when --fatal-warnings is set and only errors emitted', () async {
final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0);
final TestCommand testCommand = TestCommand(testRunner: testRunner);
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
testLogger.printError('Error: Danger Will Robinson!');
expect(commandRunner.run(const <String>[
'test',
'--no-pub',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received error output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
});
}
class FakeFlutterTestRunner implements FlutterTestRunner {
......
......@@ -311,7 +311,7 @@ flutter:
await writeBundle(directory, <String, DevFSContent>{}, loggerOverride: testLogger);
expect(testLogger.errorText, contains('Expected Error Text'));
expect(testLogger.warningText, contains('Expected Error Text'));
});
testUsingContext('does not unnecessarily recreate asset manifest, font manifest, license', () async {
......
......@@ -137,7 +137,7 @@ void main() {
final FakeSimpleArtifact artifact = FakeSimpleArtifact(cache);
await artifact.update(FakeArtifactUpdater(), logger, fileSystem, FakeOperatingSystemUtils());
expect(logger.errorText, contains('stamp write failed'));
expect(logger.warningText, contains('stamp write failed'));
});
testWithoutContext('Continues on missing version file', () async {
......@@ -153,7 +153,7 @@ void main() {
final FakeSimpleArtifact artifact = FakeSimpleArtifact(cache);
await artifact.update(FakeArtifactUpdater(), logger, fileSystem, FakeOperatingSystemUtils());
expect(logger.errorText, contains('No known version for the artifact name "fake"'));
expect(logger.warningText, contains('No known version for the artifact name "fake"'));
});
testWithoutContext('Gradle wrapper should not be up to date, if some cached artifact is not available', () {
......@@ -725,7 +725,7 @@ void main() {
cache.clearStampFiles();
expect(logger.errorText, contains('Failed to delete some stamp files'));
expect(logger.warningText, contains('Failed to delete some stamp files'));
});
testWithoutContext('FlutterWebSdk fetches web artifacts and deletes previous directory contents', () async {
......
......@@ -83,6 +83,8 @@ void main() {
}
class FakeBuildSubCommand extends BuildSubCommand {
FakeBuildSubCommand() : super(verboseHelp: false);
@override
String get description => throw UnimplementedError();
......
......@@ -432,8 +432,8 @@ void main() {
buildMode: BuildMode.debug,
);
expect(logger.errorText, contains('Warning: Podfile is out of date'));
expect(logger.errorText, contains('rm macos/Podfile'));
expect(logger.warningText, contains('Warning: Podfile is out of date'));
expect(logger.warningText, contains('rm macos/Podfile'));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
......
......@@ -747,7 +747,7 @@ dependencies:
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.readAsStringSync(),
contains('plugin3.UseOldEmbedding.registerWith(shimPluginRegistry.registrarFor("plugin3.UseOldEmbedding"));'));
expect(testLogger.errorText, equals(
expect(testLogger.warningText, equals(
'The plugin `plugin3` uses a deprecated version of the Android embedding.\n'
'To avoid unexpected runtime failures, or future build failures, try to see if this plugin supports the Android V2 embedding. '
'Otherwise, consider removing it since a future release of Flutter will remove these deprecated APIs.\n'
......@@ -827,7 +827,7 @@ dependencies:
await injectPlugins(flutterProject, androidPlatform: true);
expect(testLogger.errorText, equals(
expect(testLogger.warningText, equals(
'This app is using a deprecated version of the Android embedding.\n'
'To avoid unexpected runtime failures, or future build failures, try to migrate this app to the V2 embedding.\n'
'Take a look at the docs for migrating an app: https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects\n'
......@@ -854,7 +854,7 @@ dependencies:
contains('plugin3.UseOldEmbedding.registerWith(shimPluginRegistry.registrarFor("plugin3.UseOldEmbedding"));'));
expect(registrant.readAsStringSync(),
contains('plugin4.UseOldEmbedding.registerWith(shimPluginRegistry.registrarFor("plugin4.UseOldEmbedding"));'));
expect(testLogger.errorText, equals(
expect(testLogger.warningText, equals(
'The plugins `plugin3, plugin4` use a deprecated version of the Android embedding.\n'
'To avoid unexpected runtime failures, or future build failures, try to see if these plugins support the Android V2 embedding. '
'Otherwise, consider removing them since a future release of Flutter will remove these deprecated APIs.\n'
......@@ -882,7 +882,7 @@ dependencies:
contains('plugin3.UseOldEmbedding.registerWith(shimPluginRegistry.registrarFor("plugin3.UseOldEmbedding"));'));
expect(registrant.readAsStringSync(),
contains('plugin4.UseOldEmbedding.registerWith(shimPluginRegistry.registrarFor("plugin4.UseOldEmbedding"));'));
expect(testLogger.errorText, equals(
expect(testLogger.warningText, equals(
'The plugins `plugin3, plugin4` use a deprecated version of the Android embedding.\n'
'To avoid unexpected runtime failures, or future build failures, try to see if these plugins support the Android V2 embedding. '
'Otherwise, consider removing them since a future release of Flutter will remove these deprecated APIs.\n'
......
......@@ -183,7 +183,7 @@ void main() {
// android:name="flutterEmbedding" android:value="2" />.
await project.regeneratePlatformSpecificTooling();
expect(testLogger.statusText, contains('https://flutter.dev/go/android-project-migration'));
expect(testLogger.warningText, contains('https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects'));
});
_testInMemory('Android plugin without example app does not show a warning', () async {
final FlutterProject project = await aPluginProject();
......
......@@ -87,7 +87,7 @@ void main() {
final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
await runner.run(<String>['deprecated']);
expect(testLogger.errorText,
expect(testLogger.warningText,
contains('The "deprecated" command is deprecated and will be removed in '
'a future version of Flutter.'));
expect(flutterCommand.usage,
......
......@@ -129,7 +129,7 @@ void main() {
// We get a warning about the unexpected package.
expect(
bufferLogger.errorText,
bufferLogger.warningText,
contains("Unexpected package 'extra' found in packages directory"),
);
......
......@@ -10,12 +10,15 @@ import 'package:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:process/process.dart';
import 'package:test/fake.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
import 'test_utils.dart';
final String dart = fileSystem.path
......@@ -24,7 +27,9 @@ final String dart = fileSystem.path
void main() {
group('Cache.lock', () {
// Windows locking is too flaky for this to work reliably.
if (!platform.isWindows) {
if (platform.isWindows) {
return;
}
testWithoutContext(
'should log a message to stderr when lock is not acquired', () async {
final String oldRoot = Cache.flutterRoot;
......@@ -33,6 +38,7 @@ void main() {
terminal: Terminal.test(supportsColor: false, supportsEmoji: false),
outputPreferences: OutputPreferences(),
);
logger.fatalWarnings = true;
try {
Cache.flutterRoot = tempDir.absolute.path;
final Cache cache = Cache.test(
......@@ -50,11 +56,11 @@ import 'dart:async';
import 'dart:io';
Future<void> main(List<String> args) async {
File file = File(args[0]);
RandomAccessFile lock = file.openSync(mode: FileMode.write);
lock.lockSync();
await Future<void>.delayed(const Duration(milliseconds: 1000));
exit(0);
File file = File(args[0]);
RandomAccessFile lock = file.openSync(mode: FileMode.write);
lock.lockSync();
await Future<void>.delayed(const Duration(milliseconds: 1000));
exit(0);
}
''');
final Process process = await const LocalProcessManager().start(
......@@ -68,9 +74,75 @@ Future<void> main(List<String> args) async {
Cache.flutterRoot = oldRoot;
}
expect(logger.statusText, isEmpty);
expect(logger.errorText,
expect(logger.errorText, isEmpty);
expect(logger.warningText,
equals('Waiting for another flutter command to release the startup lock...\n'));
expect(logger.hadErrorOutput, isFalse);
// Should still be false, since the particular "Waiting..." message above aims to
// avoid triggering failure as a fatal warning.
expect(logger.hadWarningOutput, isFalse);
});
testWithoutContext(
'should log a warning message for unknown version ', () async {
final String oldRoot = Cache.flutterRoot;
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('cache_test.');
final BufferLogger logger = BufferLogger(
terminal: Terminal.test(supportsColor: false, supportsEmoji: false),
outputPreferences: OutputPreferences(),
);
logger.fatalWarnings = true;
try {
Cache.flutterRoot = tempDir.absolute.path;
final Cache cache = Cache.test(
fileSystem: fileSystem,
processManager: FakeProcessManager.any(),
logger: logger,
);
final FakeVersionlessArtifact artifact = FakeVersionlessArtifact(cache);
cache.registerArtifact(artifact);
await artifact.update(FakeArtifactUpdater(), logger, fileSystem, FakeOperatingSystemUtils());
} finally {
tryToDelete(tempDir);
Cache.flutterRoot = oldRoot;
}
expect(logger.statusText, isEmpty);
expect(logger.warningText, equals('No known version for the artifact name "fake". '
'Flutter can continue, but the artifact may be re-downloaded on '
'subsequent invocations until the problem is resolved.\n'));
expect(logger.hadErrorOutput, isFalse);
expect(logger.hadWarningOutput, isTrue);
});
});
}
class FakeArtifactUpdater extends Fake implements ArtifactUpdater {
void Function(String, Uri, Directory) onDownloadZipArchive;
void Function(String, Uri, Directory) onDownloadZipTarball;
@override
Future<void> downloadZippedTarball(String message, Uri url, Directory location) async {
onDownloadZipTarball?.call(message, url, location);
}
@override
Future<void> downloadZipArchive(String message, Uri url, Directory location) async {
onDownloadZipArchive?.call(message, url, location);
}
@override
void removeDownloadedFiles() { }
}
class FakeVersionlessArtifact extends CachedArtifact {
FakeVersionlessArtifact(Cache cache) : super(
'fake',
cache,
DevelopmentArtifact.universal,
);
@override
String get version => null;
@override
Future<void> updateInner(ArtifactUpdater artifactUpdater, FileSystem fileSystem, OperatingSystemUtils operatingSystemUtils) async { }
}
......@@ -115,84 +115,66 @@ void main() {
testWithoutContext('flutter test should run a test when its name matches a regexp', () async {
final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--name', 'inc.*de']);
if (!(result.stdout as String).contains('+1: All tests passed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should run a test when its name contains a string', () async {
final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--plain-name', 'include']);
if (!(result.stdout as String).contains('+1: All tests passed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should run a test with a given tag', () async {
final ProcessResult result = await _runFlutterTest('filtering_tag', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--tags', 'include-tag']);
if (!(result.stdout as String).contains('+1: All tests passed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should not run a test with excluded tag', () async {
final ProcessResult result = await _runFlutterTest('filtering_tag', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--exclude-tags', 'exclude-tag']);
if (!(result.stdout as String).contains('+1: All tests passed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should run all tests when tags are unspecified', () async {
final ProcessResult result = await _runFlutterTest('filtering_tag', automatedTestsDirectory, flutterTestDirectory);
if (!(result.stdout as String).contains('+1 -1: Some tests failed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+ -1: Some tests failed\.')));
expect(result.exitCode, 1);
});
testWithoutContext('flutter test should run a widgetTest with a given tag', () async {
final ProcessResult result = await _runFlutterTest('filtering_tag_widget', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--tags', 'include-tag']);
if (!(result.stdout as String).contains('+1: All tests passed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should not run a widgetTest with excluded tag', () async {
final ProcessResult result = await _runFlutterTest('filtering_tag_widget', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--exclude-tags', 'exclude-tag']);
if (!(result.stdout as String).contains('+1: All tests passed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+: All tests passed!')));
expect(result.exitCode, 0);
});
testWithoutContext('flutter test should run all widgetTest when tags are unspecified', () async {
final ProcessResult result = await _runFlutterTest('filtering_tag_widget', automatedTestsDirectory, flutterTestDirectory);
if (!(result.stdout as String).contains('+1 -1: Some tests failed')) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
expect(result.stdout, contains(RegExp(r'\+\d+ -1: Some tests failed\.')));
expect(result.exitCode, 1);
});
testWithoutContext('flutter test should test runs to completion', () async {
final ProcessResult result = await _runFlutterTest('trivial', automatedTestsDirectory, flutterTestDirectory,
extraArguments: const <String>['--verbose']);
final String stdout = result.stdout as String;
if ((!stdout.contains('+1: All tests passed')) ||
(!stdout.contains('test 0: Starting flutter_tester process with command')) ||
(!stdout.contains('test 0: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
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');
}
......@@ -202,14 +184,12 @@ void main() {
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 <String>['--verbose']);
final String stdout = result.stdout as String;
if ((!stdout.contains('+2: All tests passed')) ||
(!stdout.contains('test 0: Starting flutter_tester process with command')) ||
(!stdout.contains('test 0: deleting temporary directory')) ||
(!stdout.contains('test 0: finished')) ||
(!stdout.contains('test package returned with exit code 0'))) {
fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
}
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');
}
......
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