From 74669e4bf1352a5134ad68398a6bf7fac0a6473b Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 4 Dec 2024 16:51:08 -0500 Subject: [PATCH] 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 --- packages/flutter_tools/lib/executable.dart | 2 + .../lib/src/build_system/build_system.dart | 2 +- .../lib/src/commands/create.dart | 75 ++++- .../lib/src/commands/create_base.dart | 118 +++----- .../lib/src/commands/widget_preview.dart | 149 ++++++++++ packages/flutter_tools/lib/src/project.dart | 10 + .../templates/template_manifest.json | 4 + .../widget_preview_scaffold/README.md.tmpl | 4 + .../lib/main.dart.tmpl | 7 + .../widget_preview_scaffold/pubspec.yaml.tmpl | 16 + .../commands.shard/permeable/create_test.dart | 159 +--------- .../utils/project_testing_utils.dart | 159 ++++++++++ .../permeable/widget_preview_test.dart | 276 ++++++++++++++++++ .../web_plugin_registrant_test.dart | 64 +--- 14 files changed, 741 insertions(+), 304 deletions(-) create mode 100644 packages/flutter_tools/lib/src/commands/widget_preview.dart create mode 100644 packages/flutter_tools/templates/widget_preview_scaffold/README.md.tmpl create mode 100644 packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl create mode 100644 packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl create mode 100644 packages/flutter_tools/test/commands.shard/permeable/utils/project_testing_utils.dart create mode 100644 packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 1d67a73e4e..ce0a5a66a1 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -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 generateCommands({ verbose: verbose, nativeAssetsBuilder: globals.nativeAssetsBuilder, ), + WidgetPreviewCommand(), UpgradeCommand(verboseHelp: verboseHelp), SymbolizeCommand( stdio: globals.stdio, diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart index 0fd777ab22..83631b2afe 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -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. diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 761202e629..e93cb96fd8 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -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: ['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: ['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 diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index 76ff09a32b..c69b868e36 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -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: ['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: ['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, diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart new file mode 100644 index 0000000000..c7e776688a --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -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 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 [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 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( + ['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 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(); + } +} diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index d222e273a2..1b392042a9 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -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; diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index 857c6dcc3e..62bd63c3f8 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -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", diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/README.md.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/README.md.tmpl new file mode 100644 index 0000000000..3648fd7744 --- /dev/null +++ b/packages/flutter_tools/templates/widget_preview_scaffold/README.md.tmpl @@ -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. diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl new file mode 100644 index 0000000000..3cba337835 --- /dev/null +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl @@ -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 main() async { + // TODO(bkonyi): implement. +} diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl new file mode 100644 index 0000000000..fb82124b13 --- /dev/null +++ b/packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl @@ -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 diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index a59303fadc..98f1aadbd8 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -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(['create', '--no-pub', '--template=plugin', projectDir.path]); await _getPackages(projectDir); - await _analyzeProject(projectDir.path); + await analyzeProject(projectDir.path); await _runFlutterTest(projectDir); }, overrides: { 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: { 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 _createAndAnalyzeProject( unexpectedPaths: unexpectedPaths, expectedGitignoreLines: expectedGitignoreLines, ); - await _analyzeProject(dir.path); -} - -Future _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 snapshotArgs = [ - '--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 _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 _analyzeProject(String workingDir, { List expectedFailures = const [] }) async { - final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join( - '..', - '..', - 'bin', - 'cache', - 'flutter_tools.snapshot', - )); - - final List args = [ - 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 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 errors = []; - 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 _getPackages(Directory workingDir) async { @@ -4285,35 +4170,7 @@ Future _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> commands = >[]; - @override - Future start( - List command, { - String? workingDirectory, - Map? 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 plist = plistFile.readAsLinesSync().map((String line) => line.trim()).toList(); diff --git a/packages/flutter_tools/test/commands.shard/permeable/utils/project_testing_utils.dart b/packages/flutter_tools/test/commands.shard/permeable/utils/project_testing_utils.dart new file mode 100644 index 0000000000..d95e44c824 --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/permeable/utils/project_testing_utils.dart @@ -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> commands = >[]; + + @override + Future start( + List command, { + String? workingDirectory, + Map? 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 analyzeProject(String workingDir, { List expectedFailures = const [] }) async { + final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join( + '..', + '..', + 'bin', + 'cache', + 'flutter_tools.snapshot', + )); + + final List args = [ + 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 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 errors = []; + 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 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 snapshotArgs = [ + '--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 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); +} diff --git a/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart new file mode 100644 index 0000000000..da46ae707e --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart @@ -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 createRootProject() async { + return createProject( + tempDir, + arguments: ['--pub'], + ); + } + + Future runWidgetPreviewCommand(List arguments) async { + final CommandRunner runner = createTestCommandRunner( + WidgetPreviewCommand(), + ); + await runner.run(['widget-preview', ...arguments]); + } + + Future startWidgetPreview({ + required String? rootProjectPath, + List? arguments, + }) async { + await runWidgetPreviewCommand( + [ + 'start', + ...?arguments, + if (rootProjectPath != null) rootProjectPath, + ], + ); + expect( + globals.fs + .directory(rootProjectPath ?? globals.fs.currentDirectory.path) + .childDirectory('.dart_tool') + .childDirectory('widget_preview_scaffold'), + exists, + ); + } + + Future cleanWidgetPreview({ + required String rootProjectPath, + }) async { + await runWidgetPreviewCommand(['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( + [ + '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( + [ + '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: { + 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>( + () async { + // Try to execute using the CWD. + await startWidgetPreview(rootProjectPath: null); + }, + getCurrentDirectory: () => globals.fs.directory(rootProjectPath), + ); + }, + overrides: { + 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: { + 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 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: ['--pub', '--offline'], + ); + + expect( + loggingProcessManager.commands, + contains( + predicate( + (List c) => + dartCommand.hasMatch(c[0]) && + c[1].contains('pub') && + c.contains('--offline'), + ), + ), + ); + }, + overrides: { + 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, + ), + }, + ); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart b/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart index f3907b530f..b7bce71929 100644 --- a/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart +++ b/packages/flutter_tools/test/integration.shard/web_plugin_registrant_test.dart @@ -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 _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 snapshotArgs = [ - '--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 _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 _createProject(Directory dir, List createArgs) async { Cache.flutterRoot = '../..'; final CreateCommand command = CreateCommand();