mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

initialize the flutter root as early as possible. This allows all interfaces to be normally injected into commands, even if those objects might read from the flutter root.
606 lines
18 KiB
Dart
606 lines
18 KiB
Dart
// 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:flutter_tools/src/base/error_handling_io.dart';
|
|
import 'package:flutter_tools/src/base/user_messages.dart';
|
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
|
import 'package:flutter_tools/src/artifacts.dart';
|
|
import 'package:flutter_tools/src/base/common.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/platform.dart';
|
|
import 'package:flutter_tools/src/base/terminal.dart';
|
|
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:process/process.dart';
|
|
|
|
import '../../src/common.dart';
|
|
import '../../src/context.dart';
|
|
|
|
final Platform _kNoColorTerminalPlatform = FakePlatform(stdoutSupportsAnsi: false);
|
|
|
|
void main() {
|
|
String analyzerSeparator;
|
|
FileSystem fileSystem;
|
|
Platform platform;
|
|
BufferLogger logger;
|
|
AnsiTerminal terminal;
|
|
ProcessManager processManager;
|
|
Directory tempDir;
|
|
String projectPath;
|
|
File libMain;
|
|
Artifacts artifacts;
|
|
|
|
Future<void> runCommand({
|
|
FlutterCommand command,
|
|
List<String> arguments,
|
|
List<String> statusTextContains,
|
|
List<String> errorTextContains,
|
|
bool toolExit = false,
|
|
String exitMessageContains,
|
|
int exitCode = 0,
|
|
}) async {
|
|
try {
|
|
await createTestCommandRunner(command).run(arguments);
|
|
expect(toolExit, isFalse, reason: 'Expected ToolExit exception');
|
|
} on ToolExit catch (e) {
|
|
if (!toolExit) {
|
|
testLogger.clear();
|
|
rethrow;
|
|
}
|
|
if (exitMessageContains != null) {
|
|
expect(e.message, contains(exitMessageContains));
|
|
// May not analyzer exception the `exitCode` is `null`.
|
|
expect(e.exitCode ?? 0, exitCode);
|
|
}
|
|
}
|
|
assertContains(logger.statusText, statusTextContains);
|
|
assertContains(logger.errorText, errorTextContains);
|
|
|
|
logger.clear();
|
|
}
|
|
|
|
void _createDotPackages(String projectPath, [bool nullSafe = false]) {
|
|
final StringBuffer flutterRootUri = StringBuffer('file://');
|
|
final String canonicalizedFlutterRootPath = fileSystem.path.canonicalize(Cache.flutterRoot);
|
|
if (platform.isWindows) {
|
|
flutterRootUri
|
|
..write('/')
|
|
..write(canonicalizedFlutterRootPath.replaceAll(r'\', '/'));
|
|
} else {
|
|
flutterRootUri.write(canonicalizedFlutterRootPath);
|
|
}
|
|
final String dotPackagesSrc = '''
|
|
{
|
|
"configVersion": 2,
|
|
"packages": [
|
|
{
|
|
"name": "flutter",
|
|
"rootUri": "$flutterRootUri/packages/flutter",
|
|
"packageUri": "lib/",
|
|
"languageVersion": "2.10"
|
|
},
|
|
{
|
|
"name": "sky_engine",
|
|
"rootUri": "$flutterRootUri/bin/cache/pkg/sky_engine",
|
|
"packageUri": "lib/",
|
|
"languageVersion": "2.10"
|
|
},
|
|
{
|
|
"name": "flutter_project",
|
|
"rootUri": "../",
|
|
"packageUri": "lib/",
|
|
"languageVersion": "${nullSafe ? "2.10" : "2.7"}"
|
|
}
|
|
]
|
|
}
|
|
''';
|
|
|
|
fileSystem.file(fileSystem.path.join(projectPath, '.dart_tool', 'package_config.json'))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(dotPackagesSrc);
|
|
}
|
|
|
|
setUpAll(() {
|
|
Cache.disableLocking();
|
|
processManager = const LocalProcessManager();
|
|
platform = const LocalPlatform();
|
|
terminal = AnsiTerminal(platform: platform, stdio: Stdio());
|
|
fileSystem = LocalFileSystem.instance;
|
|
logger = BufferLogger.test();
|
|
analyzerSeparator = platform.isWindows ? '-' : '•';
|
|
artifacts = CachedArtifacts(
|
|
cache: globals.cache,
|
|
fileSystem: fileSystem,
|
|
platform: platform,
|
|
);
|
|
Cache.flutterRoot = Cache.defaultFlutterRoot(
|
|
fileSystem: fileSystem,
|
|
platform: platform,
|
|
userMessages: UserMessages(),
|
|
);
|
|
});
|
|
|
|
setUp(() {
|
|
tempDir = fileSystem.systemTempDirectory.createTempSync(
|
|
'flutter_analyze_once_test_1.',
|
|
).absolute;
|
|
projectPath = fileSystem.path.join(tempDir.path, 'flutter_project');
|
|
fileSystem.file(fileSystem.path.join(projectPath, 'pubspec.yaml'))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(pubspecYamlSrc);
|
|
_createDotPackages(projectPath);
|
|
libMain = fileSystem.file(fileSystem.path.join(projectPath, 'lib', 'main.dart'))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(mainDartSrc);
|
|
});
|
|
|
|
tearDown(() {
|
|
tryToDelete(tempDir);
|
|
});
|
|
|
|
// Analyze in the current directory - no arguments
|
|
testUsingContext('working directory', () async {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(projectPath),
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
platform: platform,
|
|
processManager: processManager,
|
|
terminal: terminal,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub'],
|
|
statusTextContains: <String>['No issues found!'],
|
|
);
|
|
});
|
|
|
|
// Analyze a specific file outside the current directory
|
|
testUsingContext('passing one file throws', () async {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
platform: platform,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
processManager: processManager,
|
|
terminal: terminal,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub', libMain.path],
|
|
toolExit: true,
|
|
exitMessageContains: 'is not a directory',
|
|
);
|
|
});
|
|
|
|
// Analyze in the current directory - no arguments
|
|
testUsingContext('working directory with errors', () async {
|
|
// Break the code to produce the "Avoid empty else" hint
|
|
// that is upgraded to a warning in package:flutter/analysis_options_user.yaml
|
|
// to assert that we are using the default Flutter analysis options.
|
|
// Also insert a statement that should not trigger a lint here
|
|
// but will trigger a lint later on when an analysis_options.yaml is added.
|
|
String source = await libMain.readAsString();
|
|
source = source.replaceFirst(
|
|
'return MaterialApp(',
|
|
'if (debugPrintRebuildDirtyWidgets) {} else ; return MaterialApp(',
|
|
);
|
|
source = source.replaceFirst(
|
|
'onPressed: _incrementCounter,',
|
|
'// onPressed: _incrementCounter,',
|
|
);
|
|
source = source.replaceFirst(
|
|
'_counter++;',
|
|
'_counter++; throw "an error message";',
|
|
);
|
|
libMain.writeAsStringSync(source);
|
|
|
|
// Analyze in the current directory - no arguments
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(projectPath),
|
|
platform: platform,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
processManager: processManager,
|
|
terminal: terminal,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub'],
|
|
statusTextContains: <String>[
|
|
'Analyzing',
|
|
'info $analyzerSeparator Avoid empty else statements',
|
|
'info $analyzerSeparator Avoid empty statements',
|
|
'info $analyzerSeparator The declaration \'_incrementCounter\' isn\'t',
|
|
'warning $analyzerSeparator The parameter \'onPressed\' is required',
|
|
],
|
|
exitMessageContains: '4 issues found.',
|
|
toolExit: true,
|
|
exitCode: 1,
|
|
);
|
|
});
|
|
|
|
// Analyze in the current directory - no arguments
|
|
testUsingContext('working directory with local options', () async {
|
|
// Insert an analysis_options.yaml file in the project
|
|
// which will trigger a lint for broken code that was inserted earlier
|
|
final File optionsFile = fileSystem.file(fileSystem.path.join(projectPath, 'analysis_options.yaml'));
|
|
try {
|
|
optionsFile.writeAsStringSync('''
|
|
include: package:flutter/analysis_options_user.yaml
|
|
linter:
|
|
rules:
|
|
- only_throw_errors
|
|
''');
|
|
String source = libMain.readAsStringSync();
|
|
source = source.replaceFirst(
|
|
'onPressed: _incrementCounter,',
|
|
'// onPressed: _incrementCounter,',
|
|
);
|
|
source = source.replaceFirst(
|
|
'_counter++;',
|
|
'_counter++; throw "an error message";',
|
|
);
|
|
libMain.writeAsStringSync(source);
|
|
|
|
// Analyze in the current directory - no arguments
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(projectPath),
|
|
platform: platform,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
processManager: processManager,
|
|
terminal: terminal,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub'],
|
|
statusTextContains: <String>[
|
|
'Analyzing',
|
|
'info $analyzerSeparator The declaration \'_incrementCounter\' isn\'t',
|
|
'info $analyzerSeparator Only throw instances of classes extending either Exception or Error',
|
|
'warning $analyzerSeparator The parameter \'onPressed\' is required',
|
|
],
|
|
exitMessageContains: '3 issues found.',
|
|
toolExit: true,
|
|
exitCode: 1,
|
|
);
|
|
} finally {
|
|
ErrorHandlingFileSystem.deleteIfExists(optionsFile);
|
|
}
|
|
});
|
|
|
|
testUsingContext('analyze once no duplicate issues', () async {
|
|
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_analyze_once_test_2.').absolute;
|
|
_createDotPackages(tempDir.path);
|
|
|
|
try {
|
|
final File foo = fileSystem.file(fileSystem.path.join(tempDir.path, 'foo.dart'));
|
|
foo.writeAsStringSync('''
|
|
import 'bar.dart';
|
|
|
|
void foo() => bar();
|
|
''');
|
|
|
|
final File bar = fileSystem.file(fileSystem.path.join(tempDir.path, 'bar.dart'));
|
|
bar.writeAsStringSync('''
|
|
import 'dart:async'; // unused
|
|
|
|
void bar() {
|
|
}
|
|
''');
|
|
|
|
// Analyze in the current directory - no arguments
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: tempDir,
|
|
platform: platform,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
processManager: processManager,
|
|
terminal: terminal,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub'],
|
|
statusTextContains: <String>[
|
|
'Analyzing',
|
|
],
|
|
exitMessageContains: '1 issue found.',
|
|
toolExit: true,
|
|
exitCode: 1
|
|
);
|
|
} finally {
|
|
tryToDelete(tempDir);
|
|
}
|
|
});
|
|
|
|
testUsingContext('analyze once returns no issues when source is error-free', () async {
|
|
const String contents = '''
|
|
StringBuffer bar = StringBuffer('baz');
|
|
''';
|
|
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_analyze_once_test_3.');
|
|
_createDotPackages(tempDir.path);
|
|
|
|
tempDir.childFile('main.dart').writeAsStringSync(contents);
|
|
try {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(tempDir),
|
|
platform: _kNoColorTerminalPlatform,
|
|
fileSystem: fileSystem,
|
|
logger: logger,
|
|
processManager: processManager,
|
|
terminal: terminal,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub'],
|
|
statusTextContains: <String>['No issues found!'],
|
|
);
|
|
} finally {
|
|
tryToDelete(tempDir);
|
|
}
|
|
});
|
|
|
|
testUsingContext('analyze once returns no issues for todo comments', () async {
|
|
const String contents = '''
|
|
// TODO(foobar):
|
|
StringBuffer bar = StringBuffer('baz');
|
|
''';
|
|
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_analyze_once_test_4.');
|
|
_createDotPackages(tempDir.path);
|
|
|
|
tempDir.childFile('main.dart').writeAsStringSync(contents);
|
|
try {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(tempDir),
|
|
platform: _kNoColorTerminalPlatform,
|
|
terminal: terminal,
|
|
processManager: processManager,
|
|
logger: logger,
|
|
fileSystem: fileSystem,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub'],
|
|
statusTextContains: <String>['No issues found!'],
|
|
);
|
|
} finally {
|
|
tryToDelete(tempDir);
|
|
}
|
|
});
|
|
|
|
testUsingContext('analyze once with default options has info issue finally exit code 1.', () async {
|
|
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync(
|
|
'flutter_analyze_once_default_options_info_issue_exit_code_1.');
|
|
_createDotPackages(tempDir.path);
|
|
|
|
const String infoSourceCode = '''
|
|
int analyze() {}
|
|
''';
|
|
|
|
tempDir.childFile('main.dart').writeAsStringSync(infoSourceCode);
|
|
try {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(tempDir),
|
|
platform: _kNoColorTerminalPlatform,
|
|
terminal: terminal,
|
|
processManager: processManager,
|
|
logger: logger,
|
|
fileSystem: fileSystem,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub'],
|
|
statusTextContains: <String>[
|
|
'info',
|
|
'missing_return',
|
|
],
|
|
exitMessageContains: '1 issue found.',
|
|
toolExit: true,
|
|
exitCode: 1,
|
|
);
|
|
} finally {
|
|
tryToDelete(tempDir);
|
|
}
|
|
});
|
|
|
|
testUsingContext('analyze once with no-fatal-infos has info issue finally exit code 0.', () async {
|
|
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync(
|
|
'flutter_analyze_once_no_fatal_infos_info_issue_exit_code_0.');
|
|
_createDotPackages(tempDir.path);
|
|
|
|
const String infoSourceCode = '''
|
|
int analyze() {}
|
|
''';
|
|
|
|
tempDir.childFile('main.dart').writeAsStringSync(infoSourceCode);
|
|
try {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(tempDir),
|
|
platform: _kNoColorTerminalPlatform,
|
|
terminal: terminal,
|
|
processManager: processManager,
|
|
logger: logger,
|
|
fileSystem: fileSystem,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub', '--no-fatal-infos'],
|
|
statusTextContains: <String>[
|
|
'info',
|
|
'missing_return',
|
|
],
|
|
exitMessageContains: '1 issue found.',
|
|
toolExit: true,
|
|
exitCode: 0,
|
|
);
|
|
} finally {
|
|
tryToDelete(tempDir);
|
|
}
|
|
});
|
|
|
|
testUsingContext('analyze once only fatal-warnings has info issue finally exit code 0.', () async {
|
|
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync(
|
|
'flutter_analyze_once_only_fatal_warnings_info_issue_exit_code_0.');
|
|
_createDotPackages(tempDir.path);
|
|
|
|
const String infoSourceCode = '''
|
|
int analyze() {}
|
|
''';
|
|
|
|
tempDir.childFile('main.dart').writeAsStringSync(infoSourceCode);
|
|
try {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(tempDir),
|
|
platform: _kNoColorTerminalPlatform,
|
|
terminal: terminal,
|
|
processManager: processManager,
|
|
logger: logger,
|
|
fileSystem: fileSystem,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze', '--no-pub', '--fatal-warnings', '--no-fatal-infos'],
|
|
statusTextContains: <String>[
|
|
'info',
|
|
'missing_return',
|
|
],
|
|
exitMessageContains: '1 issue found.',
|
|
toolExit: true,
|
|
exitCode: 0,
|
|
);
|
|
} finally {
|
|
tryToDelete(tempDir);
|
|
}
|
|
});
|
|
|
|
testUsingContext('analyze once only fatal-infos has warning issue finally exit code 1.', () async {
|
|
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync(
|
|
'flutter_analyze_once_only_fatal_infos_warning_issue_exit_code_1.');
|
|
_createDotPackages(tempDir.path);
|
|
|
|
const String warningSourceCode = '''
|
|
int analyze() {}
|
|
''';
|
|
|
|
final File optionsFile = fileSystem.file(fileSystem.path.join(tempDir.path, 'analysis_options.yaml'));
|
|
optionsFile.writeAsStringSync('''
|
|
analyzer:
|
|
errors:
|
|
missing_return: warning
|
|
''');
|
|
|
|
tempDir.childFile('main.dart').writeAsStringSync(warningSourceCode);
|
|
try {
|
|
await runCommand(
|
|
command: AnalyzeCommand(
|
|
workingDirectory: fileSystem.directory(tempDir),
|
|
platform: _kNoColorTerminalPlatform,
|
|
terminal: terminal,
|
|
processManager: processManager,
|
|
logger: logger,
|
|
fileSystem: fileSystem,
|
|
artifacts: artifacts,
|
|
),
|
|
arguments: <String>['analyze','--no-pub', '--fatal-infos', '--no-fatal-warnings'],
|
|
statusTextContains: <String>[
|
|
'warning',
|
|
'missing_return',
|
|
],
|
|
exitMessageContains: '1 issue found.',
|
|
toolExit: true,
|
|
exitCode: 1,
|
|
);
|
|
} finally {
|
|
tryToDelete(tempDir);
|
|
}
|
|
});
|
|
}
|
|
|
|
void assertContains(String text, List<String> patterns) {
|
|
if (patterns != null) {
|
|
for (final String pattern in patterns) {
|
|
expect(text, contains(pattern));
|
|
}
|
|
}
|
|
}
|
|
|
|
const String mainDartSrc = r'''
|
|
import 'package:flutter/material.dart';
|
|
|
|
void main() => runApp(MyApp());
|
|
|
|
class MyApp extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Flutter Demo',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.blue,
|
|
),
|
|
home: MyHomePage(title: 'Flutter Demo Home Page'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MyHomePage extends StatefulWidget {
|
|
MyHomePage({Key key, this.title}) : super(key: key);
|
|
|
|
final String title;
|
|
|
|
@override
|
|
_MyHomePageState createState() => _MyHomePageState();
|
|
}
|
|
|
|
class _MyHomePageState extends State<MyHomePage> {
|
|
int _counter = 0;
|
|
|
|
void _incrementCounter() {
|
|
setState(() {
|
|
_counter++;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.title),
|
|
),
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
Text(
|
|
'You have pushed the button this many times:',
|
|
),
|
|
Text(
|
|
'$_counter',
|
|
style: Theme.of(context).textTheme.headline4,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: _incrementCounter,
|
|
tooltip: 'Increment',
|
|
child: Icon(Icons.add),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
''';
|
|
|
|
const String pubspecYamlSrc = r'''
|
|
name: flutter_project
|
|
environment:
|
|
sdk: ">=2.1.0 <3.0.0"
|
|
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
''';
|