Add flutter widget-preview {start, clean} commands (#159510)

This is the initial tooling work for Flutter Widget Previews, adding two
commands: `flutter widget-preview start` and `flutter widget-preview
clean`.

The `start` command currently only checks to see if
`.dart_tool/widget_preview_scaffold/` exists and creates a new Flutter
project using the widget_preview_scaffold template if one isn't found.
The widget_preview_scaffold template currently only contains some
placeholder files and will be updated to include additional code
required by the scaffold.

The `clean` command simply deletes `.dart_tool/widget_preview_scaffold/`
if it's found.

This change also includes some refactoring of the `create` command in
order to share some project creation logic without requiring `flutter
widget-preview start` to spawn a new process simply to run `flutter
create -t widget_preview .dart_tool/widget_preview_scaffold`.

Related issue: https://github.com/flutter/flutter/issues/115704

---------

Co-authored-by: Andrew Kolos <andrewrkolos@gmail.com>
This commit is contained in:
Ben Konyi 2024-12-04 16:51:08 -05:00 committed by GitHub
parent 3de19db7d4
commit 74669e4bf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 741 additions and 304 deletions

View File

@ -43,6 +43,7 @@ import 'src/commands/symbolize.dart';
import 'src/commands/test.dart';
import 'src/commands/update_packages.dart';
import 'src/commands/upgrade.dart';
import 'src/commands/widget_preview.dart';
import 'src/devtools_launcher.dart';
import 'src/features.dart';
import 'src/globals.dart' as globals;
@ -250,6 +251,7 @@ List<FlutterCommand> generateCommands({
verbose: verbose,
nativeAssetsBuilder: globals.nativeAssetsBuilder,
),
WidgetPreviewCommand(),
UpgradeCommand(verboseHelp: verboseHelp),
SymbolizeCommand(
stdio: globals.stdio,

View File

@ -488,7 +488,7 @@ class Environment {
/// The path to the package configuration file to use for compilation.
///
/// This is used by package:package_config to locate the actual package_config.json
/// file. If not provided, defaults to `.dart_tool/package_config.json`.
/// file. If not provided in tests, defaults to `.dart_tool/package_config.json`.
final String packageConfigPath;
/// The `BUILD_DIR` environment variable.

View File

@ -35,10 +35,73 @@ const String kPlatformHelp =
'When adding platforms to a plugin project, the pubspec.yaml will be updated with the requested platform. '
'Adding desktop platforms requires the corresponding desktop config setting to be enabled.';
class CreateCommand extends CreateBase {
CreateCommand({
super.verboseHelp = false,
}) {
class CreateCommand extends FlutterCommand with CreateBase {
CreateCommand({bool verboseHelp = false}) {
addPubOptions();
argParser.addFlag(
'with-driver-test',
help: '(deprecated) Historically, this added a flutter_driver dependency and generated a '
'sample "flutter drive" test. Now it does nothing. Consider using the '
'"integration_test" package: https://pub.dev/packages/integration_test',
hide: !verboseHelp,
);
argParser.addFlag(
'overwrite',
help: 'When performing operations, overwrite existing files.',
);
argParser.addOption(
'description',
defaultsTo: 'A new Flutter project.',
help:
'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.',
);
argParser.addOption(
'org',
defaultsTo: 'com.example',
help:
'The organization responsible for your new Flutter project, in reverse domain name notation. '
'This string is used in Java package names and as prefix in the iOS bundle identifier.',
);
argParser.addOption(
'project-name',
help:
'The project name for this new Flutter project. This must be a valid dart package name.',
);
argParser.addOption(
'ios-language',
abbr: 'i',
defaultsTo: 'swift',
allowed: <String>['objc', 'swift'],
help: '(deprecated) The language to use for iOS-specific code, either Swift (recommended) or Objective-C (legacy).',
hide: !verboseHelp,
);
argParser.addOption(
'android-language',
abbr: 'a',
defaultsTo: 'kotlin',
allowed: <String>['java', 'kotlin'],
help: 'The language to use for Android-specific code, either Kotlin (recommended) or Java (legacy).',
);
argParser.addFlag(
'skip-name-checks',
help:
'Allow the creation of applications and plugins with invalid names. '
'This is only intended to enable testing of the tool itself.',
hide: !verboseHelp,
);
argParser.addFlag(
'implementation-tests',
help:
'Include implementation tests that verify the template functions correctly. '
'This is only intended to enable testing of the tool itself.',
hide: !verboseHelp,
);
argParser.addOption(
'initial-create-revision',
help: 'The Flutter SDK git commit hash to store in .migrate_config. This parameter is used by the tool '
'internally and should generally not be used manually.',
hide: !verboseHelp,
);
addPlatformsOptions(customHelp: kPlatformHelp);
argParser.addOption(
'template',
@ -437,12 +500,12 @@ class CreateCommand extends CreateBase {
pubContext = PubContext.createPackage;
}
if (boolArg('pub')) {
if (shouldCallPubGet) {
final FlutterProject project = FlutterProject.fromDirectory(relativeDir);
await pub.get(
context: pubContext,
project: project,
offline: boolArg('offline'),
offline: offline,
outputMode: PubOutputMode.summaryOnly,
);
// Setting `includeIos` etc to false as with FlutterProjectType.package

View File

@ -48,96 +48,15 @@ const String _kDefaultPlatformArgumentHelp =
'Platform folders (e.g. android/) will be generated in the target project. '
'Adding desktop platforms requires the corresponding desktop config setting to be enabled.';
/// Common behavior for `flutter create` commands.
abstract class CreateBase extends FlutterCommand {
CreateBase({
required bool verboseHelp,
}) {
argParser.addFlag(
'pub',
defaultsTo: true,
help:
'Whether to run "flutter pub get" after the project has been created.',
);
argParser.addFlag(
'offline',
help:
'When "flutter pub get" is run by the create command, this indicates '
'whether to run it in offline mode or not. In offline mode, it will need to '
'have all dependencies already available in the pub cache to succeed.',
);
argParser.addFlag(
'with-driver-test',
help: '(deprecated) Historically, this added a flutter_driver dependency and generated a '
'sample "flutter drive" test. Now it does nothing. Consider using the '
'"integration_test" package: https://pub.dev/packages/integration_test',
hide: !verboseHelp,
);
argParser.addFlag(
'overwrite',
help: 'When performing operations, overwrite existing files.',
);
argParser.addOption(
'description',
defaultsTo: 'A new Flutter project.',
help:
'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.',
);
argParser.addOption(
'org',
defaultsTo: 'com.example',
help:
'The organization responsible for your new Flutter project, in reverse domain name notation. '
'This string is used in Java package names and as prefix in the iOS bundle identifier.',
);
argParser.addOption(
'project-name',
help:
'The project name for this new Flutter project. This must be a valid dart package name.',
);
argParser.addOption(
'ios-language',
abbr: 'i',
defaultsTo: 'swift',
allowed: <String>['objc', 'swift'],
help: '(deprecated) The language to use for iOS-specific code, either Swift (recommended) or Objective-C (legacy).',
hide: !verboseHelp,
);
argParser.addOption(
'android-language',
abbr: 'a',
defaultsTo: 'kotlin',
allowed: <String>['java', 'kotlin'],
help: 'The language to use for Android-specific code, either Kotlin (recommended) or Java (legacy).',
);
argParser.addFlag(
'skip-name-checks',
help:
'Allow the creation of applications and plugins with invalid names. '
'This is only intended to enable testing of the tool itself.',
hide: !verboseHelp,
);
argParser.addFlag(
'implementation-tests',
help:
'Include implementation tests that verify the template functions correctly. '
'This is only intended to enable testing of the tool itself.',
hide: !verboseHelp,
);
argParser.addOption(
'initial-create-revision',
help: 'The Flutter SDK git commit hash to store in .migrate_config. This parameter is used by the tool '
'internally and should generally not be used manually.',
hide: !verboseHelp,
);
}
/// Common behavior for `flutter create` and `flutter widget-preview start` commands.
mixin CreateBase on FlutterCommand {
/// Pattern for a Windows file system drive (e.g. "D:").
///
/// `dart:io` does not recognize strings matching this pattern as absolute
/// paths, as they have no top level back-slash; however, users often specify
/// this
@visibleForTesting
@protected
static final RegExp kWindowsDrivePattern = RegExp(r'^[a-zA-Z]:$');
/// The output directory of the command.
@ -162,6 +81,35 @@ abstract class CreateBase extends FlutterCommand {
return globals.fs.path.normalize(projectDir.absolute.path);
}
@protected
bool get shouldCallPubGet {
return boolArg('pub');
}
@protected
bool get offline {
return boolArg('offline');
}
/// Adds `--pub` and `--offline` options.
@protected
void addPubOptions() {
argParser
..addFlag(
'pub',
defaultsTo: true,
help:
'Whether to run "flutter pub get" after the project has been created.',
)
..addFlag(
'offline',
help:
'When "flutter pub get" is run by the create command, this indicates '
'whether to run it in offline mode or not. In offline mode, it will need to '
'have all dependencies already available in the pub cache to succeed.',
);
}
/// Adds a `--platforms` argument.
///
/// The help message of the argument is replaced with `customHelp` if `customHelp` is not null.
@ -558,7 +506,7 @@ abstract class CreateBase extends FlutterCommand {
final bool windowsPlatform = templateContext['windows'] as bool? ?? false;
final bool webPlatform = templateContext['web'] as bool? ?? false;
if (boolArg('pub')) {
if (shouldCallPubGet) {
final Environment environment = Environment(
artifacts: globals.artifacts!,
logger: globals.logger,

View File

@ -0,0 +1,149 @@
// 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 '../base/common.dart';
import '../base/file_system.dart';
import '../base/platform.dart';
import '../cache.dart';
import '../dart/pub.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../runner/flutter_command.dart';
import 'create_base.dart';
class WidgetPreviewCommand extends FlutterCommand {
WidgetPreviewCommand() {
addSubcommand(WidgetPreviewStartCommand());
addSubcommand(WidgetPreviewCleanCommand());
}
@override
String get description => 'Manage the widget preview environment.';
@override
String get name => 'widget-preview';
@override
String get category => FlutterCommandCategory.tools;
// TODO(bkonyi): show when --verbose is not provided when this feature is
// ready to ship.
@override
bool get hidden => true;
@override
Future<FlutterCommandResult> runCommand() async =>
FlutterCommandResult.fail();
}
/// Common utilities for the 'start' and 'clean' commands.
mixin WidgetPreviewSubCommandMixin on FlutterCommand {
FlutterProject getRootProject() {
final ArgResults results = argResults!;
final Directory projectDir;
if (results.rest case <String>[final String directory]) {
projectDir = globals.fs.directory(directory);
if (!projectDir.existsSync()) {
throwToolExit('Could not find ${projectDir.path}.');
}
} else if (results.rest.length > 1) {
throwToolExit('Only one directory should be provided.');
} else {
projectDir = globals.fs.currentDirectory;
}
return validateFlutterProjectForPreview(projectDir);
}
FlutterProject validateFlutterProjectForPreview(Directory directory) {
globals.logger
.printTrace('Verifying that ${directory.path} is a Flutter project.');
final FlutterProject flutterProject =
globals.projectFactory.fromDirectory(directory);
if (!flutterProject.dartTool.existsSync()) {
throwToolExit(
'${flutterProject.directory.path} is not a valid Flutter project.',
);
}
return flutterProject;
}
}
class WidgetPreviewStartCommand extends FlutterCommand
with CreateBase, WidgetPreviewSubCommandMixin {
WidgetPreviewStartCommand() {
addPubOptions();
}
@override
String get description => 'Starts the widget preview environment.';
@override
String get name => 'start';
@override
Future<FlutterCommandResult> runCommand() async {
final FlutterProject rootProject = getRootProject();
// Check to see if a preview scaffold has already been generated. If not,
// generate one.
if (!rootProject.widgetPreviewScaffold.existsSync()) {
globals.logger.printStatus(
'Creating widget preview scaffolding at: ${rootProject.widgetPreviewScaffold.path}',
);
await generateApp(
<String>['widget_preview_scaffold'],
rootProject.widgetPreviewScaffold,
createTemplateContext(
organization: 'flutter',
projectName: 'widget_preview_scaffold',
titleCaseProjectName: 'Widget Preview Scaffold',
flutterRoot: Cache.flutterRoot!,
dartSdkVersionBounds: '^${globals.cache.dartSdkBuild}',
linux: const LocalPlatform().isLinux,
macos: const LocalPlatform().isMacOS,
windows: const LocalPlatform().isWindows,
),
overwrite: true,
generateMetadata: false,
);
if (shouldCallPubGet) {
await pub.get(
context: PubContext.create,
project: rootProject.widgetPreviewScaffoldProject,
offline: offline,
outputMode: PubOutputMode.summaryOnly,
);
}
}
return FlutterCommandResult.success();
}
}
class WidgetPreviewCleanCommand extends FlutterCommand
with WidgetPreviewSubCommandMixin {
@override
String get description => 'Cleans up widget preview state.';
@override
String get name => 'clean';
@override
Future<FlutterCommandResult> runCommand() async {
final Directory widgetPreviewScaffold =
getRootProject().widgetPreviewScaffold;
if (widgetPreviewScaffold.existsSync()) {
final String scaffoldPath = widgetPreviewScaffold.path;
globals.logger.printStatus(
'Deleting widget preview scaffold at $scaffoldPath.',
);
widgetPreviewScaffold.deleteSync(recursive: true);
} else {
globals.logger.printStatus('Nothing to clean up.');
}
return FlutterCommandResult.success();
}
}

View File

@ -229,6 +229,10 @@ class FlutterProject {
/// The `.dart-tool` directory of this project.
Directory get dartTool => directory.childDirectory('.dart_tool');
/// The location of the generated scaffolding project for hosting widget
/// previews from this project.
Directory get widgetPreviewScaffold => dartTool.childDirectory('widget_preview_scaffold');
/// The directory containing the generated code for this project.
Directory get generated => directory
.absolute
@ -249,6 +253,12 @@ class FlutterProject {
FlutterManifest.empty(logger: globals.logger),
);
/// The generated scaffolding project for hosting widget previews from this
/// project.
FlutterProject get widgetPreviewScaffoldProject => FlutterProject.fromDirectory(
widgetPreviewScaffold,
);
/// True if this project is a Flutter module project.
bool get isModule => manifest.isModule;

View File

@ -372,6 +372,10 @@
"templates/skeleton/test/unit_test.dart.tmpl",
"templates/skeleton/test/widget_test.dart.tmpl",
"templates/widget_preview_scaffold/lib/main.dart.tmpl",
"templates/widget_preview_scaffold/pubspec.yaml.tmpl",
"templates/widget_preview_scaffold/README.md.tmpl",
"templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/contents.xcworkspacedata",
"templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/IDEWorkspaceChecks.plist",
"templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/WorkspaceSettings.xcsettings",

View File

@ -0,0 +1,4 @@
# {{titleCaseProjectName}}
This project is generated by `flutter widget-preview` and is used to host Widgets
to be previewed in the widget previewer.

View File

@ -0,0 +1,7 @@
// 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.
Future<void> main() async {
// TODO(bkonyi): implement.
}

View File

@ -0,0 +1,16 @@
name: {{projectName}}
description: Scaffolding for Flutter Widget Previews
publish_to: 'none'
version: 0.0.1
environment:
sdk: {{dartSdkVersionBounds}}
dependencies:
flutter:
sdk: flutter
flutter_test:
sdk: flutter
flutter:
uses-material-design: true

View File

@ -29,7 +29,6 @@ import 'package:flutter_tools/src/flutter_project_metadata.dart' show FlutterPro
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:process/process.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:unified_analytics/unified_analytics.dart';
@ -42,6 +41,7 @@ import '../../src/fake_http_client.dart';
import '../../src/fakes.dart';
import '../../src/pubspec_schema.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils/project_testing_utils.dart';
const String _kNoPlatformsMessage = "You've created a plugin project that doesn't yet support any platforms.\n";
const String frameworkRevision = '12345678';
@ -84,7 +84,7 @@ void main() {
setUpAll(() async {
Cache.disableLocking();
await _ensureFlutterToolsSnapshot();
await ensureFlutterToolsSnapshot();
});
setUp(() {
@ -109,7 +109,7 @@ void main() {
});
tearDownAll(() async {
await _restoreFlutterToolsSnapshot();
await restoreFlutterToolsSnapshot();
});
test('createAndroidIdentifier emits a valid identifier', () {
@ -2472,7 +2472,7 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
await _getPackages(projectDir);
await _analyzeProject(projectDir.path);
await analyzeProject(projectDir.path);
await _runFlutterTest(projectDir);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(),
@ -2489,7 +2489,7 @@ void main() {
final Directory exampleDir = projectDir.childDirectory('example');
await _getPackages(exampleDir);
await _analyzeProject(exampleDir.path);
await analyzeProject(exampleDir.path);
await _runFlutterTest(exampleDir);
});
@ -2554,7 +2554,7 @@ void main() {
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
await _getPackages(projectDir);
await _analyzeProject(projectDir.path);
await analyzeProject(projectDir.path);
await _runFlutterTest(projectDir);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
@ -3603,7 +3603,7 @@ void main() {
'$relativePath:31:26: use_full_hex_values_for_flutter_colors',
];
expect(expectedFailures.length, '// LINT:'.allMatches(toAnalyze.readAsStringSync()).length);
await _analyzeProject(
await analyzeProject(
projectDir.path,
expectedFailures: expectedFailures,
);
@ -4116,122 +4116,7 @@ Future<void> _createAndAnalyzeProject(
unexpectedPaths: unexpectedPaths,
expectedGitignoreLines: expectedGitignoreLines,
);
await _analyzeProject(dir.path);
}
Future<void> _ensureFlutterToolsSnapshot() async {
final String flutterToolsPath = globals.fs.path.absolute(globals.fs.path.join(
'bin',
'flutter_tools.dart',
));
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
));
final String packageConfig = globals.fs.path.absolute(globals.fs.path.join(
'.dart_tool',
'package_config.json'
));
final File snapshotFile = globals.fs.file(flutterToolsSnapshotPath);
if (snapshotFile.existsSync()) {
snapshotFile.renameSync('$flutterToolsSnapshotPath.bak');
}
final List<String> snapshotArgs = <String>[
'--snapshot=$flutterToolsSnapshotPath',
'--packages=$packageConfig',
flutterToolsPath,
];
final ProcessResult snapshotResult = await Process.run(
'../../bin/cache/dart-sdk/bin/dart',
snapshotArgs,
);
printOnFailure('Results of generating snapshot:');
printOnFailure(snapshotResult.stdout.toString());
printOnFailure(snapshotResult.stderr.toString());
expect(snapshotResult.exitCode, 0);
}
Future<void> _restoreFlutterToolsSnapshot() async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
));
final File snapshotBackup = globals.fs.file('$flutterToolsSnapshotPath.bak');
if (!snapshotBackup.existsSync()) {
// No backup to restore.
return;
}
snapshotBackup.renameSync(flutterToolsSnapshotPath);
}
Future<void> _analyzeProject(String workingDir, { List<String> expectedFailures = const <String>[] }) async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
));
final List<String> args = <String>[
flutterToolsSnapshotPath,
'analyze',
];
final ProcessResult exec = await Process.run(
globals.artifacts!.getArtifactPath(Artifact.engineDartBinary),
args,
workingDirectory: workingDir,
);
if (expectedFailures.isEmpty) {
printOnFailure('Results of running analyzer:');
printOnFailure(exec.stdout.toString());
printOnFailure(exec.stderr.toString());
expect(exec.exitCode, 0);
return;
}
expect(exec.exitCode, isNot(0));
String lineParser(String line) {
try {
final String analyzerSeparator = globals.platform.isWindows ? ' - ' : '';
final List<String> lineComponents = line.trim().split(analyzerSeparator);
final String lintName = lineComponents.removeLast();
final String location = lineComponents.removeLast();
return '$location: $lintName';
} on RangeError catch (err) {
throw RangeError('Received "$err" while trying to parse: "$line".');
}
}
final String stdout = exec.stdout.toString();
final List<String> errors = <String>[];
try {
bool analyzeLineFound = false;
const LineSplitter().convert(stdout).forEach((String line) {
// Conditional to filter out any stdout from `pub get`
if (!analyzeLineFound && line.startsWith('Analyzing')) {
analyzeLineFound = true;
return;
}
if (analyzeLineFound && line.trim().isNotEmpty) {
errors.add(lineParser(line.trim()));
}
});
} on Exception catch (err) {
fail('$err\n\nComplete STDOUT was:\n\n$stdout');
}
expect(errors, unorderedEquals(expectedFailures),
reason: 'Failed with stdout:\n\n$stdout');
await analyzeProject(dir.path);
}
Future<void> _getPackages(Directory workingDir) async {
@ -4285,35 +4170,7 @@ Future<void> _runFlutterTest(Directory workingDir, { String? target }) async {
expect(exec.exitCode, 0);
}
/// A ProcessManager that invokes a real process manager, but keeps
/// track of all commands sent to it.
class LoggingProcessManager extends LocalProcessManager {
List<List<String>> commands = <List<String>>[];
@override
Future<Process> start(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) {
commands.add(command.map((Object arg) => arg.toString()).toList());
return super.start(
command,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell,
mode: mode,
);
}
void clear() {
commands.clear();
}
}
String _getStringValueFromPlist({required File plistFile, String? key}) {
final List<String> plist = plistFile.readAsLinesSync().map((String line) => line.trim()).toList();

View File

@ -0,0 +1,159 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:process/process.dart';
import '../../../src/test_flutter_command_runner.dart';
/// A ProcessManager that invokes a real process manager, but keeps
/// track of all commands sent to it.
class LoggingProcessManager extends LocalProcessManager {
List<List<String>> commands = <List<String>>[];
@override
Future<Process> start(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) {
commands.add(command.map((Object arg) => arg.toString()).toList());
return super.start(
command,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell,
mode: mode,
);
}
void clear() {
commands.clear();
}
}
Future<void> analyzeProject(String workingDir, { List<String> expectedFailures = const <String>[] }) async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
));
final List<String> args = <String>[
flutterToolsSnapshotPath,
'analyze',
];
final ProcessResult exec = await Process.run(
globals.artifacts!.getArtifactPath(Artifact.engineDartBinary),
args,
workingDirectory: workingDir,
);
if (expectedFailures.isEmpty) {
printOnFailure('Results of running analyzer:');
printOnFailure(exec.stdout.toString());
printOnFailure(exec.stderr.toString());
expect(exec.exitCode, 0);
return;
}
expect(exec.exitCode, isNot(0));
String lineParser(String line) {
try {
final String analyzerSeparator = globals.platform.isWindows ? ' - ' : '';
final List<String> lineComponents = line.trim().split(analyzerSeparator);
final String lintName = lineComponents.removeLast();
final String location = lineComponents.removeLast();
return '$location: $lintName';
} on RangeError catch (err) {
throw RangeError('Received "$err" while trying to parse: "$line".');
}
}
final String stdout = exec.stdout.toString();
final List<String> errors = <String>[];
try {
bool analyzeLineFound = false;
const LineSplitter().convert(stdout).forEach((String line) {
// Conditional to filter out any stdout from `pub get`
if (!analyzeLineFound && line.startsWith('Analyzing')) {
analyzeLineFound = true;
return;
}
if (analyzeLineFound && line.trim().isNotEmpty) {
errors.add(lineParser(line.trim()));
}
});
} on Exception catch (err) {
fail('$err\n\nComplete STDOUT was:\n\n$stdout');
}
expect(errors, unorderedEquals(expectedFailures),
reason: 'Failed with stdout:\n\n$stdout');
}
Future<void> ensureFlutterToolsSnapshot() async {
final String flutterToolsPath = globals.fs.path.absolute(globals.fs.path.join(
'bin',
'flutter_tools.dart',
));
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
));
final String packageConfig = globals.fs.path.absolute(globals.fs.path.join(
'.dart_tool',
'package_config.json'
));
final File snapshotFile = globals.fs.file(flutterToolsSnapshotPath);
if (snapshotFile.existsSync()) {
snapshotFile.renameSync('$flutterToolsSnapshotPath.bak');
}
final List<String> snapshotArgs = <String>[
'--snapshot=$flutterToolsSnapshotPath',
'--packages=$packageConfig',
flutterToolsPath,
];
final ProcessResult snapshotResult = await Process.run(
'../../bin/cache/dart-sdk/bin/dart',
snapshotArgs,
);
printOnFailure('Results of generating snapshot:');
printOnFailure(snapshotResult.stdout.toString());
printOnFailure(snapshotResult.stderr.toString());
expect(snapshotResult.exitCode, 0);
}
Future<void> restoreFlutterToolsSnapshot() async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
));
final File snapshotBackup = globals.fs.file('$flutterToolsSnapshotPath.bak');
if (!snapshotBackup.existsSync()) {
// No backup to restore.
return;
}
snapshotBackup.renameSync(flutterToolsSnapshotPath);
}

View File

@ -0,0 +1,276 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' as io show IOOverrides;
import 'package:args/command_runner.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils/project_testing_utils.dart';
void main() {
late Directory tempDir;
late LoggingProcessManager loggingProcessManager;
late FakeStdio mockStdio;
setUp(() {
loggingProcessManager = LoggingProcessManager();
tempDir = globals.fs.systemTempDirectory
.createTempSync('flutter_tools_create_test.');
mockStdio = FakeStdio();
});
tearDown(() {
tryToDelete(tempDir);
});
Future<String> createRootProject() async {
return createProject(
tempDir,
arguments: <String>['--pub'],
);
}
Future<void> runWidgetPreviewCommand(List<String> arguments) async {
final CommandRunner<void> runner = createTestCommandRunner(
WidgetPreviewCommand(),
);
await runner.run(<String>['widget-preview', ...arguments]);
}
Future<void> startWidgetPreview({
required String? rootProjectPath,
List<String>? arguments,
}) async {
await runWidgetPreviewCommand(
<String>[
'start',
...?arguments,
if (rootProjectPath != null) rootProjectPath,
],
);
expect(
globals.fs
.directory(rootProjectPath ?? globals.fs.currentDirectory.path)
.childDirectory('.dart_tool')
.childDirectory('widget_preview_scaffold'),
exists,
);
}
Future<void> cleanWidgetPreview({
required String rootProjectPath,
}) async {
await runWidgetPreviewCommand(<String>['clean', rootProjectPath]);
expect(
globals.fs
.directory(rootProjectPath)
.childDirectory('.dart_tool')
.childDirectory('widget_preview_scaffold'),
isNot(exists),
);
}
group('flutter widget-preview', () {
group('start exits if', () {
testUsingContext(
'given an invalid directory',
() async {
try {
await runWidgetPreviewCommand(
<String>[
'start',
'foo',
],
);
fail(
'Successfully executed with multiple project paths',
);
} on ToolExit catch (e) {
expect(
e.message,
contains(
'Could not find foo',
),
);
}
},
);
testUsingContext(
'more than one project directory is provided',
() async {
try {
await runWidgetPreviewCommand(
<String>[
'start',
tempDir.path,
tempDir.path,
],
);
fail(
'Successfully executed with multiple project paths',
);
} on ToolExit catch (e) {
expect(
e.message,
contains(
'Only one directory should be provided.',
),
);
}
},
);
testUsingContext(
'run outside of a Flutter project directory',
() async {
try {
await startWidgetPreview(rootProjectPath: tempDir.path);
fail(
'Successfully executed outside of a Flutter project directory',
);
} on ToolExit catch (e) {
expect(
e.message,
contains(
'${tempDir.path} is not a valid Flutter project.',
),
);
}
},
);
});
testUsingContext(
'start creates .dart_tool/widget_preview_scaffold',
() async {
final String rootProjectPath = await createRootProject();
await startWidgetPreview(rootProjectPath: rootProjectPath);
},
overrides: <Type, Generator>{
Pub: () => Pub.test(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: mockStdio,
),
},
);
testUsingContext(
'start creates .dart_tool/widget_preview_scaffold in the CWD',
() async {
final String rootProjectPath = await createRootProject();
await io.IOOverrides.runZoned<Future<void>>(
() async {
// Try to execute using the CWD.
await startWidgetPreview(rootProjectPath: null);
},
getCurrentDirectory: () => globals.fs.directory(rootProjectPath),
);
},
overrides: <Type, Generator>{
Pub: () => Pub.test(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: mockStdio,
),
},
);
testUsingContext(
'clean deletes .dart_tool/widget_preview_scaffold',
() async {
final String rootProjectPath = await createRootProject();
await startWidgetPreview(rootProjectPath: rootProjectPath);
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
},
overrides: <Type, Generator>{
Pub: () => Pub.test(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: mockStdio,
),
},
);
testUsingContext(
'invokes pub in online and offline modes',
() async {
// Run pub online first in order to populate the pub cache.
final String rootProjectPath = await createRootProject();
loggingProcessManager.clear();
final RegExp dartCommand = RegExp(r'dart-sdk[\\/]bin[\\/]dart');
await startWidgetPreview(rootProjectPath: rootProjectPath);
expect(
loggingProcessManager.commands,
contains(
predicate(
(List<String> c) =>
dartCommand.hasMatch(c[0]) &&
c[1].contains('pub') &&
!c.contains('--offline'),
),
),
);
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
// Run pub offline.
loggingProcessManager.clear();
await startWidgetPreview(
rootProjectPath: rootProjectPath,
arguments: <String>['--pub', '--offline'],
);
expect(
loggingProcessManager.commands,
contains(
predicate(
(List<String> c) =>
dartCommand.hasMatch(c[0]) &&
c[1].contains('pub') &&
c.contains('--offline'),
),
),
);
},
overrides: <Type, Generator>{
ProcessManager: () => loggingProcessManager,
Pub: () => Pub.test(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: mockStdio,
),
},
);
});
}

View File

@ -15,6 +15,7 @@ import 'package:flutter_tools/src/commands/create.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../commands.shard/permeable/utils/project_testing_utils.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/test_flutter_command_runner.dart';
@ -26,7 +27,7 @@ void main() {
setUpAll(() async {
Cache.disableLocking();
await _ensureFlutterToolsSnapshot();
await ensureFlutterToolsSnapshot();
});
setUp(() {
@ -40,7 +41,7 @@ void main() {
});
tearDownAll(() async {
await _restoreFlutterToolsSnapshot();
await restoreFlutterToolsSnapshot();
});
testUsingContext('generated plugin registrant passes analysis', () async {
@ -241,65 +242,6 @@ void main() {
});
}
Future<void> _ensureFlutterToolsSnapshot() async {
final String flutterToolsPath = globals.fs.path.absolute(globals.fs.path.join(
'bin',
'flutter_tools.dart',
));
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
),
);
final String dotPackages = globals.fs.path.absolute(globals.fs.path.join(
'.dart_tool/package_config.json',
));
final File snapshotFile = globals.fs.file(flutterToolsSnapshotPath);
if (snapshotFile.existsSync()) {
snapshotFile.renameSync('$flutterToolsSnapshotPath.bak');
}
final List<String> snapshotArgs = <String>[
'--snapshot=$flutterToolsSnapshotPath',
'--packages=$dotPackages',
flutterToolsPath,
];
final ProcessResult snapshotResult = await Process.run(
'../../bin/cache/dart-sdk/bin/dart',
snapshotArgs,
);
printOnFailure('Output of dart ${snapshotArgs.join(" ")}:');
printOnFailure(snapshotResult.stdout.toString());
printOnFailure(snapshotResult.stderr.toString());
expect(snapshotResult, const ProcessResultMatcher());
}
Future<void> _restoreFlutterToolsSnapshot() async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(
globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
),
);
final File snapshotBackup =
globals.fs.file('$flutterToolsSnapshotPath.bak');
if (!snapshotBackup.existsSync()) {
// No backup to restore.
return;
}
snapshotBackup.renameSync(flutterToolsSnapshotPath);
}
Future<void> _createProject(Directory dir, List<String> createArgs) async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();