diff --git a/packages/flutter_tools/lib/src/commands/validate_project.dart b/packages/flutter_tools/lib/src/commands/validate_project.dart new file mode 100644 index 0000000000..0f7569ff63 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/validate_project.dart @@ -0,0 +1,101 @@ +// 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 '../base/file_system.dart'; +import '../base/logger.dart'; +import '../project.dart'; +import '../project_validator.dart'; +import '../project_validator_result.dart'; +import '../runner/flutter_command.dart'; + +class ValidateProjectCommand extends FlutterCommand { + ValidateProjectCommand({ + required this.fileSystem, + required this.logger, + required this.allProjectValidators, + this.verbose = false + }); + + final FileSystem fileSystem; + final Logger logger; + final bool verbose; + final List allProjectValidators; + + @override + final String name = 'validate-project'; + + @override + final String description = 'Show information about the current project.'; + + @override + final String category = FlutterCommandCategory.project; + + @override + Future runCommand() async { + final String userPath = getUserPath(); + final Directory workingDirectory = userPath.isEmpty ? fileSystem.currentDirectory : fileSystem.directory(userPath); + + final FlutterProject project = FlutterProject.fromDirectory(workingDirectory); + final Map>> results = >>{}; + + bool hasCrash = false; + for (final ProjectValidator validator in allProjectValidators) { + if (!results.containsKey(validator) && validator.supportsProject(project)) { + results[validator] = validator.start(project).catchError((Object exception, StackTrace trace) { + hasCrash = true; + return [ProjectValidatorResult.crash(exception, trace)]; + }); + } + } + + final StringBuffer buffer = StringBuffer(); + final List resultsString = []; + for (final ProjectValidator validator in results.keys) { + if (results[validator] != null) { + resultsString.add(validator.title); + addResultString(validator.title, await results[validator], resultsString); + } + } + buffer.writeAll(resultsString, '\n'); + logger.printBox(buffer.toString()); + + if (hasCrash) { + return const FlutterCommandResult(ExitStatus.fail); + } + return const FlutterCommandResult(ExitStatus.success); + } + + + void addResultString(final String title, final List? results, final List resultsString) { + if (results != null) { + for (final ProjectValidatorResult result in results) { + resultsString.add(getStringResult(result)); + } + } + } + + String getStringResult(ProjectValidatorResult result) { + final String icon; + switch(result.status) { + case StatusProjectValidator.error: + icon = '[✗]'; + break; + case StatusProjectValidator.success: + icon = '[✓]'; + break; + case StatusProjectValidator.warning: + icon = '[!]'; + break; + case StatusProjectValidator.crash: + icon = '[☠]'; + break; + } + + return '$icon $result'; + } + + String getUserPath(){ + return (argResults == null || argResults!.rest.isEmpty) ? '' : argResults!.rest[0]; + } +} diff --git a/packages/flutter_tools/lib/src/project_validator.dart b/packages/flutter_tools/lib/src/project_validator.dart new file mode 100644 index 0000000000..3c72188e93 --- /dev/null +++ b/packages/flutter_tools/lib/src/project_validator.dart @@ -0,0 +1,17 @@ +// 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 'project.dart'; +import 'project_validator_result.dart'; + +abstract class ProjectValidator { + String get title; + bool supportsProject(FlutterProject project); + /// Can return more than one result in case a file/command have a lot of info to share to the user + Future> start(FlutterProject project); + /// new ProjectValidators should be added here for the ValidateProjectCommand to run + static const List allProjectValidators = [ + // TODO(jasguerrero): add validators + ]; +} diff --git a/packages/flutter_tools/lib/src/project_validator_result.dart b/packages/flutter_tools/lib/src/project_validator_result.dart new file mode 100644 index 0000000000..08b78a955f --- /dev/null +++ b/packages/flutter_tools/lib/src/project_validator_result.dart @@ -0,0 +1,41 @@ +// 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. + +enum StatusProjectValidator { + error, + warning, + success, + crash, +} + +class ProjectValidatorResult { + + const ProjectValidatorResult({ + required this.name, + required this.value, + required this.status, + this.warning, + }); + + final String name; + final String value; + final String? warning; + final StatusProjectValidator status; + + @override + String toString() { + if (warning != null) { + return '$name: $value (warning: $warning)'; + } + return '$name: $value'; + } + + static ProjectValidatorResult crash(Object exception, StackTrace trace) { + return ProjectValidatorResult( + name: exception.toString(), + value: trace.toString(), + status: StatusProjectValidator.crash + ); + } +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/project_validator_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/project_validator_test.dart new file mode 100644 index 0000000000..977b1a1daa --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/hermetic/project_validator_test.dart @@ -0,0 +1,124 @@ +// 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. + +// @dart = 2.8 + +import 'package:args/command_runner.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/commands/validate_project.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/project_validator.dart'; +import 'package:flutter_tools/src/project_validator_result.dart'; + +import '../../src/context.dart'; +import '../../src/test_flutter_command_runner.dart'; + +class ProjectValidatorDummy extends ProjectValidator { + @override + Future> start(FlutterProject project) async{ + return [ + const ProjectValidatorResult(name: 'pass', value: 'value', status: StatusProjectValidator.success), + const ProjectValidatorResult(name: 'fail', value: 'my error', status: StatusProjectValidator.error), + const ProjectValidatorResult(name: 'pass two', value: 'pass', warning: 'my warning', status: StatusProjectValidator.warning), + ]; + } + + @override + bool supportsProject(FlutterProject project) { + return true; + } + + @override + String get title => 'First Dummy'; +} + +class ProjectValidatorSecondDummy extends ProjectValidator { + @override + Future> start(FlutterProject project) async{ + return [ + const ProjectValidatorResult(name: 'second', value: 'pass', status: StatusProjectValidator.success), + const ProjectValidatorResult(name: 'other fail', value: 'second fail', status: StatusProjectValidator.error), + ]; + } + + @override + bool supportsProject(FlutterProject project) { + return true; + } + + @override + String get title => 'Second Dummy'; +} + +class ProjectValidatorCrash extends ProjectValidator { + @override + Future> start(FlutterProject project) async{ + throw Exception('my exception'); + } + + @override + bool supportsProject(FlutterProject project) { + return true; + } + + @override + String get title => 'Crash'; +} + +void main() { + FileSystem fileSystem; + + group('analyze project command', () { + + setUp(() { + fileSystem = MemoryFileSystem.test(); + }); + + testUsingContext('success, error and warning', () async { + final BufferLogger loggerTest = BufferLogger.test(); + final ValidateProjectCommand command = ValidateProjectCommand( + fileSystem: fileSystem, + logger: loggerTest, + allProjectValidators: [ + ProjectValidatorDummy(), + ProjectValidatorSecondDummy() + ] + ); + final CommandRunner runner = createTestCommandRunner(command); + + await runner.run(['validate-project']); + + const String expected = '\n' + '┌──────────────────────────────────────────┐\n' + '│ First Dummy │\n' + '│ [✓] pass: value │\n' + '│ [✗] fail: my error │\n' + '│ [!] pass two: pass (warning: my warning) │\n' + '│ Second Dummy │\n' + '│ [✓] second: pass │\n' + '│ [✗] other fail: second fail │\n' + '└──────────────────────────────────────────┘\n'; + + expect(loggerTest.statusText, contains(expected)); + }); + + testUsingContext('crash', () async { + final BufferLogger loggerTest = BufferLogger.test(); + final ValidateProjectCommand command = ValidateProjectCommand( + fileSystem: fileSystem, + logger: loggerTest, + allProjectValidators: [ProjectValidatorCrash()] + ); + final CommandRunner runner = createTestCommandRunner(command); + + await runner.run(['validate-project']); + + const String expected = '[☠] Exception: my exception: #0 ProjectValidatorCrash.start'; + + expect(loggerTest.statusText, contains(expected)); + }); + }); +} diff --git a/packages/flutter_tools/test/general.shard/project_validator_result_test.dart b/packages/flutter_tools/test/general.shard/project_validator_result_test.dart new file mode 100644 index 0000000000..9920e3d7bd --- /dev/null +++ b/packages/flutter_tools/test/general.shard/project_validator_result_test.dart @@ -0,0 +1,99 @@ +// 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:file/memory.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/project_validator.dart'; +import 'package:flutter_tools/src/project_validator_result.dart'; + +import '../src/common.dart'; + +class ProjectValidatorTaskImpl extends ProjectValidator { + + @override + Future> start(FlutterProject project) async { + const ProjectValidatorResult error = ProjectValidatorResult( + name: 'result_1', + value: 'this is an error', + status: StatusProjectValidator.error, + ); + + const ProjectValidatorResult success = ProjectValidatorResult( + name: 'result_2', + value: 'correct', + status: StatusProjectValidator.success, + ); + + const ProjectValidatorResult warning = ProjectValidatorResult( + name: 'result_3', + value: 'this passed', + status: StatusProjectValidator.success, + warning: 'with a warning' + ); + + return [error, success, warning]; + } + + @override + bool supportsProject(FlutterProject project) { + return true; + } + + @override + String get title => 'Impl'; +} + +void main() { + group('ProjectValidatorResult', () { + + testWithoutContext('success status', () { + const ProjectValidatorResult result = ProjectValidatorResult( + name: 'name', + value: 'value', + status: StatusProjectValidator.success, + ); + expect(result.toString(), 'name: value'); + expect(result.status, StatusProjectValidator.success); + }); + + testWithoutContext('success status with warning', () { + const ProjectValidatorResult result = ProjectValidatorResult( + name: 'name', + value: 'value', + status: StatusProjectValidator.success, + warning: 'my warning' + ); + expect(result.toString(), 'name: value (warning: my warning)'); + expect(result.status, StatusProjectValidator.success); + }); + + testWithoutContext('error status', () { + const ProjectValidatorResult result = ProjectValidatorResult( + name: 'name', + value: 'my error', + status: StatusProjectValidator.error, + ); + expect(result.toString(), 'name: my error'); + expect(result.status, StatusProjectValidator.error); + }); + }); + + group('ProjectValidatorTask', () { + late ProjectValidatorTaskImpl task; + + setUp(() { + task = ProjectValidatorTaskImpl(); + }); + + testWithoutContext('error status', () async { + final MemoryFileSystem fs = MemoryFileSystem.test(); + final FlutterProject project = FlutterProject.fromDirectoryTest(fs.currentDirectory); + final List results = await task.start(project); + expect(results.length, 3); + expect(results[0].toString(), 'result_1: this is an error'); + expect(results[1].toString(), 'result_2: correct'); + expect(results[2].toString(), 'result_3: this passed (warning: with a warning)'); + }); + }); +}