Skip to content

Commit

Permalink
refactor(dart_cli): stub process.run in unit tests (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel committed Jun 24, 2022
1 parent f1eae7e commit 4725542
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 3 deletions.
59 changes: 58 additions & 1 deletion lib/src/cli/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:glob/glob.dart';
import 'package:lcov_parser/lcov_parser.dart';
import 'package:mason/mason.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:stack_trace/stack_trace.dart';
Expand All @@ -14,6 +15,61 @@ part 'dart_cli.dart';
part 'flutter_cli.dart';
part 'git_cli.dart';

const _asyncRunZoned = runZoned;

/// Type definition for [Process.run].
typedef RunProcess = Future<ProcessResult> Function(
String executable,
List<String> arguments, {
String? workingDirectory,
bool runInShell,
});

/// This class facilitates overriding [Process.run].
/// It should be extended by another class in client code with overrides
/// that construct a custom implementation.
@visibleForTesting
abstract class ProcessOverrides {
static final _token = Object();

/// Returns the current [ProcessOverrides] instance.
///
/// This will return `null` if the current [Zone] does not contain
/// any [ProcessOverrides].
///
/// See also:
/// * [ProcessOverrides.runZoned] to provide [ProcessOverrides]
/// in a fresh [Zone].
///
static ProcessOverrides? get current {
return Zone.current[_token] as ProcessOverrides?;
}

/// Runs [body] in a fresh [Zone] using the provided overrides.
static R runZoned<R>(
R Function() body, {
RunProcess? runProcess,
}) {
final overrides = _ProcessOverridesScope(runProcess);
return _asyncRunZoned(body, zoneValues: {_token: overrides});
}

/// The method used to run a [Process].
RunProcess get runProcess => Process.run;
}

class _ProcessOverridesScope extends ProcessOverrides {
_ProcessOverridesScope(this._runProcess);

final ProcessOverrides? _previous = ProcessOverrides.current;
final RunProcess? _runProcess;

@override
RunProcess get runProcess {
return _runProcess ?? _previous?.runProcess ?? super.runProcess;
}
}

/// Abstraction for running commands via command-line.
class _Cmd {
/// Runs the specified [cmd] with the provided [args].
Expand All @@ -23,7 +79,8 @@ class _Cmd {
bool throwOnError = true,
String? workingDirectory,
}) async {
final result = await Process.run(
final runProcess = ProcessOverrides.current?.runProcess ?? Process.run;
final result = await runProcess(
cmd,
args,
workingDirectory: workingDirectory,
Expand Down
53 changes: 51 additions & 2 deletions test/src/cli/dart_cli_test.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,66 @@
import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
import 'package:universal_io/io.dart';
import 'package:very_good_cli/src/cli/cli.dart';

class _TestProcess {
Future<ProcessResult> run(
String command,
List<String> args, {
bool runInShell = false,
String? workingDirectory,
}) {
throw UnimplementedError();
}
}

class _MockProcess extends Mock implements _TestProcess {}

class _MockProcessResult extends Mock implements ProcessResult {}

void main() {
group('Dart', () {
late ProcessResult processResult;
late _TestProcess process;

setUp(() {
processResult = _MockProcessResult();
process = _MockProcess();
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
when(
() => process.run(
any(),
any(),
runInShell: any(named: 'runInShell'),
workingDirectory: any(named: 'workingDirectory'),
),
).thenAnswer((_) async => processResult);
});

group('.installed', () {
test('returns true when dart is installed', () {
expectLater(Dart.installed(), completion(isTrue));
ProcessOverrides.runZoned(
() => expectLater(Dart.installed(), completion(isTrue)),
runProcess: process.run,
);
});

test('returns false when dart is not installed', () {
when(() => processResult.exitCode).thenReturn(ExitCode.software.code);
ProcessOverrides.runZoned(
() => expectLater(Dart.installed(), completion(isFalse)),
runProcess: process.run,
);
});
});

group('.applyFixes', () {
test('completes normally', () {
expectLater(Dart.applyFixes(), completes);
ProcessOverrides.runZoned(
() => expectLater(Dart.applyFixes(), completes),
runProcess: process.run,
);
});
});
});
Expand Down
74 changes: 74 additions & 0 deletions test/src/cli/process_overrides_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'package:test/test.dart';
import 'package:universal_io/io.dart';
import 'package:very_good_cli/src/cli/cli.dart';

class _FakeProcess {
Future<ProcessResult> run(
String command,
List<String> args, {
bool runInShell = false,
String? workingDirectory,
}) {
throw UnimplementedError();
}
}

void main() {
group('ProcessOverrides', () {
group('runZoned', () {
test('uses default Process.run when not specified', () {
ProcessOverrides.runZoned(() {
final overrides = ProcessOverrides.current;
expect(overrides!.runProcess, isA<Function>());
});
});

test('uses custom Process.run when specified', () {
final process = _FakeProcess();
ProcessOverrides.runZoned(
() {
final overrides = ProcessOverrides.current;
expect(overrides!.runProcess, equals(process.run));
},
runProcess: process.run,
);
});

test(
'uses current Process.run when not specified '
'and zone already contains a Process.run', () {
final process = _FakeProcess();
ProcessOverrides.runZoned(
() {
ProcessOverrides.runZoned(() {
final overrides = ProcessOverrides.current;
expect(overrides!.runProcess, equals(process.run));
});
},
runProcess: process.run,
);
});

test(
'uses nested Process.run when specified '
'and zone already contains a Process.run', () {
final rootProcess = _FakeProcess();
ProcessOverrides.runZoned(
() {
final nestedProcess = _FakeProcess();
final overrides = ProcessOverrides.current;
expect(overrides!.runProcess, equals(rootProcess.run));
ProcessOverrides.runZoned(
() {
final overrides = ProcessOverrides.current;
expect(overrides!.runProcess, equals(nestedProcess.run));
},
runProcess: nestedProcess.run,
);
},
runProcess: rootProcess.run,
);
});
});
});
}

0 comments on commit 4725542

Please sign in to comment.