// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:flutter_tools/executable.dart' as executable; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/analyze.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/testbed.dart'; import 'runner/utils.dart'; void main() { setUpAll(() { Cache.disableLocking(); }); tearDownAll(() { Cache.enableLocking(); }); test('Help for command line arguments is consistently styled and complete', () => Testbed().run(() { final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); executable.generateCommands( verboseHelp: true, verbose: true, ).forEach(runner.addCommand); verifyCommandRunner(runner); for (final Command command in runner.commands.values) { if (command.name == 'analyze') { final AnalyzeCommand analyze = command as AnalyzeCommand; expect(analyze.allProjectValidators().length, 2); } } })); testUsingContext('Global arg results are available in FlutterCommands', () async { final DummyFlutterCommand command = DummyFlutterCommand( commandFunction: () async { return const FlutterCommandResult(ExitStatus.success); }, ); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); runner.addCommand(command); await runner.run(['dummy', '--${FlutterGlobalOptions.kContinuousIntegrationFlag}']); expect(command.globalResults, isNotNull); expect(command.boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true), true); }); testUsingContext('Global arg results are available in FlutterCommands sub commands', () async { final DummyFlutterCommand command = DummyFlutterCommand( commandFunction: () async { return const FlutterCommandResult(ExitStatus.success); }, ); final DummyFlutterCommand subcommand = DummyFlutterCommand( name: 'sub', commandFunction: () async { return const FlutterCommandResult(ExitStatus.success); }, ); command.addSubcommand(subcommand); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); runner.addCommand(command); runner.addCommand(subcommand); await runner.run(['dummy', 'sub', '--${FlutterGlobalOptions.kContinuousIntegrationFlag}']); expect(subcommand.globalResults, isNotNull); expect(subcommand.boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true), true); }); testUsingContext('bool? safe argResults', () async { final DummyFlutterCommand command = DummyFlutterCommand( commandFunction: () async { return const FlutterCommandResult(ExitStatus.success); }, ); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); command.argParser.addFlag('key'); command.argParser.addFlag('key-false'); // argResults will be null at this point, if attempt to read them is made, // exception `Null check operator used on a null value` would be thrown. expect(() => command.boolArg('key'), throwsA(const TypeMatcher())); runner.addCommand(command); await runner.run(['dummy', '--key']); expect(command.boolArg('key'), true); expect(() => command.boolArg('non-existent'), throwsArgumentError); expect(command.boolArg('key'), true); expect(() => command.boolArg('non-existent'), throwsA(const TypeMatcher())); expect(command.boolArg('key-false'), false); expect(command.boolArg('key-false'), false); }); testUsingContext('String? safe argResults', () async { final DummyFlutterCommand command = DummyFlutterCommand( commandFunction: () async { return const FlutterCommandResult(ExitStatus.success); }, ); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); command.argParser.addOption('key'); // argResults will be null at this point, if attempt to read them is made, // exception `Null check operator used on a null value` would be thrown expect(() => command.stringArg('key'), throwsA(const TypeMatcher())); runner.addCommand(command); await runner.run(['dummy', '--key=value']); expect(command.stringArg('key'), 'value'); expect(() => command.stringArg('non-existent'), throwsArgumentError); expect(command.stringArg('key'), 'value'); expect(() => command.stringArg('non-existent'), throwsA(const TypeMatcher())); }); testUsingContext('List safe argResults', () async { final DummyFlutterCommand command = DummyFlutterCommand( commandFunction: () async { return const FlutterCommandResult(ExitStatus.success); }, ); final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: true); command.argParser.addMultiOption( 'key', allowed: ['a', 'b', 'c'], ); // argResults will be null at this point, if attempt to read them is made, // exception `Null check operator used on a null value` would be thrown. expect(() => command.stringsArg('key'), throwsA(const TypeMatcher())); runner.addCommand(command); await runner.run(['dummy', '--key', 'a']); // throws error when trying to parse non-existent key. expect(() => command.stringsArg('non-existent'), throwsA(const TypeMatcher())); expect(command.stringsArg('key'), ['a']); await runner.run(['dummy', '--key', 'a', '--key', 'b']); expect(command.stringsArg('key'), ['a', 'b']); await runner.run(['dummy']); expect(command.stringsArg('key'), []); }); } void verifyCommandRunner(CommandRunner runner) { expect(runner.argParser, isNotNull, reason: '${runner.runtimeType} has no argParser'); expect(runner.argParser.allowsAnything, isFalse, reason: '${runner.runtimeType} allows anything'); expect(runner.argParser.allowTrailingOptions, isFalse, reason: '${runner.runtimeType} allows trailing options'); verifyOptions(null, runner.argParser.options.values); runner.commands.values.forEach(verifyCommand); } void verifyCommand(Command runner) { expect(runner.argParser, isNotNull, reason: 'command ${runner.name} has no argParser'); verifyOptions(runner.name, runner.argParser.options.values); final String firstDescriptionLine = runner.description.split('\n').first; expect(firstDescriptionLine, matches(_allowedTrailingPatterns), reason: "command ${runner.name}'s description does not end with the expected single period that a full sentence should end with"); if (!runner.hidden && runner.parent == null) { expect( runner.category, anyOf( FlutterCommandCategory.sdk, FlutterCommandCategory.project, FlutterCommandCategory.tools, ), reason: "top-level command ${runner.name} doesn't have a valid category", ); } runner.subcommands.values.forEach(verifyCommand); } // Patterns for arguments names. final RegExp _allowedArgumentNamePattern = RegExp(r'^([-a-z0-9]+)$'); final RegExp _allowedArgumentNamePatternForPrecache = RegExp(r'^([-a-z0-9_]+)$'); final RegExp _bannedArgumentNamePattern = RegExp(r'-uri$'); // Patterns for help messages. final RegExp _bannedLeadingPatterns = RegExp(r'^[-a-z]', multiLine: true); final RegExp _allowedTrailingPatterns = RegExp(r'([^ ]([^.^!^:][.!:])\)?|: https?://[^ ]+[^.]|^)$'); final RegExp _bannedQuotePatterns = RegExp(r" '|' |'\.|\('|'\)|`"); final RegExp _bannedArgumentReferencePatterns = RegExp(r'[^"=]--[^ ]'); final RegExp _questionablePatterns = RegExp(r'[a-z]\.[A-Z]'); final RegExp _bannedUri = RegExp(r'\b[Uu][Rr][Ii]\b'); final RegExp _nonSecureFlutterDartUrl = RegExp(r'http://([a-z0-9-]+\.)*(flutter|dart)\.dev', caseSensitive: false); const String _needHelp = "Every option must have help explaining what it does, even if it's " 'for testing purposes, because this is the bare minimum of ' 'documentation we can add just for ourselves. If it is not intended ' 'for developers, then use "hide: !verboseHelp" to only show the ' 'help when people run with "--help --verbose".'; const String _header = ' Comment: '; void verifyOptions(String? command, Iterable