Skip to content

Commit

Permalink
Custom artifact path, --version flag, config command, refactors (#47
Browse files Browse the repository at this point in the history
)

* split paths.dart into artifacts.dart, improve logging

* handle file system exception

* add version flag

* add `maestro config` command

* don't overwrite maestro.toml

* improve bootstrap_command.dart

* add basic readme

* use Exceptions instead of Errors

* fix readme

* move adb.dart and flutter_driver.dart to external directory

* replace all "maestro.toml" with a const

* test_driver/integration_test.dart: replace with consts

* stop suffixing $FILE to "done something with $FILE"

* remove TestDriverDirectory - just use const

* prepare for adding integration_test/app_test.dart file

* maestro bootstrap: add adding integration_test/app_test.dart

* make downloading artifacts simultaneous
  • Loading branch information
bartekpacia authored Jun 6, 2022
1 parent dc710c4 commit b0133ee
Show file tree
Hide file tree
Showing 15 changed files with 399 additions and 152 deletions.
5 changes: 4 additions & 1 deletion packages/maestro/example/integration_test/app_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'package:example/main.dart' as app;
// TODO: This is an example file. Use it as a base to create your own
// Maestro-powered test.

import 'package:example/main.dart' as app; // TODO: replace with your app.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
Expand Down
51 changes: 51 additions & 0 deletions packages/maestro_cli/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# maestro_cli

## Installation

### From pub.dev

> Not available yet!
```
$ dart pub global activate maestro_cli
```

### From git

1. Make sure that you have Dart >= 2.17 installed (it comes with Flutter 3).
2. Clone the repo.
3. Go to `packages/maestro_cli`.
4. Run `dart pub global activate --source path .`

Now you can should be able to run `maestro` in your terminal. If you can't and
the error is something along the lines of "command not found", make sure that
you've added appropriate directories to PATH:

- on Unix-like systems, add `$HOME/.pub-cache/bin`
- on Windows, add `%USERPROFILE%\AppData\Local\Pub\Cache\bin`

## Usage

### First run

On first run, `maestro` will download artifacts it needs to the _artifact path_.
By default it is `$HOME/.maestro`, but you can change it by setting
`MAESTRO_ARTIFACT_PATH` environment variable.

To learn about commands, run:

```
$ maestro --help
```

### Bootstrap

To use Maestro in your Flutter project, you need 4 things:

1. have `maestro.toml` file in the root of the project (i.e next to
`pubspec.yaml`)
2. have `maestro` added as a `dev_dependency` in `pubspec.yaml`
3. have `test_driver/integration_test.dart`
4. have `integration_test/app_test.dart`

Run `maestro bootstrap` to automatically do 1, 2, 3, and most of 4.
89 changes: 62 additions & 27 deletions packages/maestro_cli/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:maestro_cli/src/commands/bootstrap_command.dart';
import 'package:maestro_cli/src/commands/clean_command.dart';
import 'package:maestro_cli/src/commands/config_command.dart';
import 'package:maestro_cli/src/commands/drive_command.dart';
import 'package:maestro_cli/src/common/logging.dart';
import 'package:maestro_cli/src/common/paths.dart';
import 'package:maestro_cli/src/common/common.dart';

Future<int> maestroCommandRunner(List<String> args) async {
final runner = MaestroCommandRunner();

var exitCode = 0;
try {
exitCode = await runner.run(args) ?? 0;
final exitCode = await runner.run(args) ?? 0;
return exitCode;
} on UsageException catch (err) {
log.severe('Error: ${err.message}');
exitCode = 1;
log.severe(err.message);
return 1;
} on FormatException catch (err) {
log.severe(err.message);
return 1;
} on FileSystemException catch (err, st) {
log.severe('${err.message}: ${err.path}', err, st);
return 1;
} catch (err, st) {
log.severe(null, err, st);
return 1;
}

return exitCode;
}

class MaestroCommandRunner extends CommandRunner<int> {
Expand All @@ -27,38 +36,64 @@ class MaestroCommandRunner extends CommandRunner<int> {
) {
addCommand(BootstrapCommand());
addCommand(DriveCommand());
addCommand(ConfigCommand());
addCommand(CleanCommand());

argParser.addFlag('verbose', abbr: 'v', help: 'Increase logging.');
argParser
..addFlag(
'verbose',
abbr: 'v',
help: 'Increase logging.',
negatable: false,
)
..addFlag('version', help: 'Show version.', negatable: false);
}

@override
Future<int?> run(Iterable<String> args) async {
await setUpLogger(); // argParser.parse() can fail, so we setup logger early
final results = argParser.parse(args);
final verbose = results['verbose'] as bool;
final help = results['help'] as bool;
setUpLogger(verbose: verbose);
final verboseFlag = results['verbose'] as bool;
final helpFlag = results['help'] as bool;
final versionFlag = results['version'] as bool;
await setUpLogger(verbose: verboseFlag);

if (versionFlag) {
log.info('maestro v$version');
return 0;
}

if (helpFlag) {
return super.run(args);
}

if (!results.arguments.contains('clean')) {
try {
await _ensureArtifactsArePresent();
} catch (err, st) {
log.severe(null, err, st);
return 1;
}

if (!results.arguments.contains('clean') && !help) {
await _ensureArtifactsArePresent();
return super.run(args);
}

return super.run(args);
}
}

Future<void> _ensureArtifactsArePresent() async {
if (areArtifactsPresent()) {
return;
}
Future<void> _ensureArtifactsArePresent() async {
if (areArtifactsPresent()) {
return;
}

final progress = log.progress('Downloading artifacts');
try {
await downloadArtifacts();
} catch (err, st) {
progress.fail('Failed to download artifacts');
log.severe(null, err, st);
}
final progress = log.progress('Downloading artifacts');
try {
await downloadArtifacts();
} catch (_) {
progress.fail('Failed to download artifacts');
rethrow;
}

progress.complete('Downloaded artifacts');
progress.complete('Downloaded artifacts');
}
}
77 changes: 51 additions & 26 deletions packages/maestro_cli/lib/src/commands/bootstrap_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:args/command_runner.dart';
import 'package:maestro_cli/src/common/constants.dart';
import 'package:maestro_cli/src/common/logging.dart';
import 'package:maestro_cli/src/maestro_config.dart';
import 'package:path/path.dart' as path;

class BootstrapCommand extends Command<int> {
@override
Expand All @@ -15,71 +16,95 @@ class BootstrapCommand extends Command<int> {

@override
Future<int> run() async {
if (!File('pubspec.yaml').existsSync()) {
if (!_hasPubspec()) {
log.severe(
'No pubspec.yaml found. Maestro must be run from Flutter project root.',
);
return 1;
}

await _createConfigFile();

_createDefaultIntegrationTestFile();

await _addMaestroToPubspec();
await _createDefaultTestDriverFile();
await _createDefaultIntegrationTestFile();

return 0;
}
}

bool _hasPubspec() => File('pubspec.yaml').existsSync();

Future<void> _createConfigFile() async {
final progress = log.progress('Creating default maestro.toml config file');
final file = File(configFileName);
if (file.existsSync()) {
throw const FileSystemException('Already exists', configFileName);
}

final progress = log.progress('Creating default $configFileName');

try {
final contents = MaestroConfig.defaultConfig().toToml();
await File('maestro.toml').writeAsString(contents);
await File(configFileName).writeAsString(contents);
} catch (err, st) {
progress.fail('Failed to create default maestro.toml config file');
progress.fail('Failed to create default $configFileName');
log.severe(null, err, st);
return;
}

progress.complete('Created default maestro.toml config file');
progress.complete('Created default $configFileName');
}

void _createDefaultIntegrationTestFile() {
final progress = log.progress(
'Creating default test_driver/integration_test.dart file',
);
Future<void> _createDefaultTestDriverFile() async {
final relativeFilePath = path.join(driverDirName, driverFileName);

final progress = log.progress('Creating default $relativeFilePath');

try {
final testDriverDir = Directory('test_driver');
if (!testDriverDir.existsSync()) {
testDriverDir.createSync();
final dir = Directory(driverDirName);
if (!dir.existsSync()) {
await dir.create();
}

final testDriverFile = File('test_driver/integration_test.dart');
if (!testDriverFile.existsSync()) {
testDriverFile.writeAsStringSync(
TestDriverDirectory.defaultTestFileContents,
);
final file = File(relativeFilePath);
if (!file.existsSync()) {
await file.writeAsString(driverFileContent);
}
} catch (err, st) {
progress.fail('Failed to create default $relativeFilePath');
log.severe(null, err, st);
return;
}

progress.complete('Created default $relativeFilePath');
}

Future<void> _createDefaultIntegrationTestFile() async {
final relativeFilePath = path.join(testDirName, testFileName);

final progress = log.progress('Creating default $relativeFilePath');

try {
final dir = Directory(testDirName);
if (!dir.existsSync()) {
await dir.create();
}

final file = File(relativeFilePath);
if (!file.existsSync()) {
await file.writeAsString(testFileContent);
}
} catch (err, st) {
progress.fail(
'Failed to create default test_driver/integration_test.dart file,',
);
progress.fail('Failed to create default $relativeFilePath');
log.severe(null, err, st);
return;
}

progress.complete('Created default test_driver/integration_test.dart file');
progress.complete('Created default $relativeFilePath');
}

Future<void> _addMaestroToPubspec() async {
final progress = log.progress('Adding $maestroPackage to dev_dependencies');

await Future<void>.delayed(const Duration(seconds: 1));

final result = await Process.run(
'flutter',
['pub', 'add', maestroPackage, '--dev'],
Expand Down
21 changes: 18 additions & 3 deletions packages/maestro_cli/lib/src/commands/clean_command.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:maestro_cli/src/common/paths.dart';
import 'package:maestro_cli/src/common/common.dart';

class CleanCommand extends Command<int> {
@override
String get name => 'clean';

@override
String get description => 'Remove all downloaded artifacts';
String get description => 'Delete all downloaded artifacts.';

@override
Future<int> run() async {
Directory(artifactsPath).deleteSync(recursive: true);
final progress = log.progress('Deleting $artifactPath');

try {
final dir = Directory(artifactPath);
if (!dir.existsSync()) {
progress.complete("Nothing to clean, $artifactPath doesn't exist");
return 1;
}

await dir.delete(recursive: true);
} catch (err) {
progress.fail('Failed to delete $artifactPath');
rethrow;
}

progress.complete('Deleted $artifactPath');
return 0;
}
}
21 changes: 21 additions & 0 deletions packages/maestro_cli/lib/src/commands/config_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:args/command_runner.dart';

import '../common/common.dart';

class ConfigCommand extends Command<int> {
@override
String get name => 'config';

@override
String get description => 'Show configuration.';

@override
Future<int> run() async {
final extra = artifactPathSetFromEnv
? '(set from $maestroArtifactPathEnv)'
: '(default)';

log.info('artifact path: $artifactPath $extra');
return 0;
}
}
Loading

0 comments on commit b0133ee

Please sign in to comment.