Skip to content

Commit

Permalink
feat(mason_logger): add ProgressOptions API (#478)
Browse files Browse the repository at this point in the history
  • Loading branch information
LukeMoody01 committed Oct 13, 2022
1 parent 4aa7e9b commit 43eae9f
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 21 deletions.
3 changes: 2 additions & 1 deletion packages/mason_logger/lib/mason_logger.dart
Expand Up @@ -3,4 +3,5 @@ library mason_logger;
export 'src/io.dart';
export 'src/level.dart';
export 'src/link.dart';
export 'src/mason_logger.dart' show Logger, Progress;
export 'src/mason_logger.dart'
show Logger, Progress, ProgressAnimation, ProgressOptions;
19 changes: 17 additions & 2 deletions packages/mason_logger/lib/src/mason_logger.dart
Expand Up @@ -11,11 +11,17 @@ part 'progress.dart';
/// {@endtemplate}
class Logger {
/// {@macro logger}
Logger({this.level = Level.info});
Logger({
this.level = Level.info,
this.progressOptions = const ProgressOptions(),
});

/// The current log level for this logger.
Level level;

/// The progress options for the logger instance.
ProgressOptions progressOptions;

final _queue = <String?>[];
final io.IOOverrides? _overrides = io.IOOverrides.current;

Expand Down Expand Up @@ -53,7 +59,16 @@ class Logger {
void delayed(String? message) => _queue.add(message);

/// Writes progress message to stdout.
Progress progress(String message) => Progress._(message, _stdout, level);
/// Optionally provide [options] to override the current
/// [ProgressOptions] for the generated [Progress].
Progress progress(String message, {ProgressOptions? options}) {
return Progress._(
message,
_stdout,
level,
options: options ?? progressOptions,
);
}

/// Writes error message to stderr.
void err(String? message) {
Expand Down
65 changes: 47 additions & 18 deletions packages/mason_logger/lib/src/progress.dart
@@ -1,5 +1,41 @@
part of 'mason_logger.dart';

/// {@template progress_options}
/// An object containing configuration for a [Progress] instance.
/// {@endtemplate}
class ProgressOptions {
/// {@macro progress_options}
const ProgressOptions({this.animation = const ProgressAnimation()});

/// The progress animation configuration.
final ProgressAnimation animation;
}

/// {@template progress_animation}
/// An object which contains configuration for the animation
/// of a [Progress] instance.
/// {@endtemplate}
class ProgressAnimation {
/// {@macro progress_animation}
const ProgressAnimation({this.frames = _defaultFrames});

static const _defaultFrames = [
'⠋',
'⠙',
'⠹',
'⠸',
'⠼',
'⠴',
'⠦',
'⠧',
'⠇',
'⠏'
];

/// The list of animation frames.
final List<String> frames;
}

/// {@template progress}
/// A class that can be used to display progress information to the user.
/// {@endtemplate}
Expand All @@ -8,26 +44,17 @@ class Progress {
Progress._(
this._message,
this._stdout,
this._level,
) : _stopwatch = Stopwatch() {
this._level, {
ProgressOptions options = const ProgressOptions(),
}) : _stopwatch = Stopwatch(),
_options = options {
_stopwatch
..reset()
..start();
_timer = Timer.periodic(const Duration(milliseconds: 80), _onTick);
}

static const List<String> _progressAnimation = [
'⠋',
'⠙',
'⠹',
'⠸',
'⠼',
'⠴',
'⠦',
'⠧',
'⠇',
'⠏'
];
final ProgressOptions _options;

final io.Stdout _stdout;

Expand Down Expand Up @@ -73,10 +100,12 @@ class Progress {

void _onTick(Timer _) {
_index++;
final char = _progressAnimation[_index % _progressAnimation.length];
_write(
'''${lightGreen.wrap('$_clearMessageLength$char')} $_message... $_time''',
);
final frames = _options.animation.frames;
final char = frames.isEmpty ? '' : frames[_index % frames.length];
final prefix = char.isEmpty
? _clearMessageLength
: '${lightGreen.wrap('$_clearMessageLength$char')} ';
_write('$prefix$_message... $_time');
}

void _write(Object? object) {
Expand Down
26 changes: 26 additions & 0 deletions packages/mason_logger/test/src/mason_logger_test.dart
Expand Up @@ -31,6 +31,32 @@ void main() {
});
});

group('progressOptions', () {
test('are set by default', () {
expect(Logger().progressOptions, equals(const ProgressOptions()));
});

test('can be injected via constructor', () {
const customProgressOptions = ProgressOptions(
animation: ProgressAnimation(frames: []),
);
expect(
Logger(progressOptions: customProgressOptions).progressOptions,
equals(customProgressOptions),
);
});

test('are mutable', () {
final logger = Logger();
const customProgressOptions = ProgressOptions(
animation: ProgressAnimation(frames: []),
);
expect(logger.progressOptions, equals(const ProgressOptions()));
logger.progressOptions = customProgressOptions;
expect(logger.progressOptions, equals(customProgressOptions));
});
});

group('.write', () {
test('writes to stdout', () {
IOOverrides.runZoned(
Expand Down
79 changes: 79 additions & 0 deletions packages/mason_logger/test/src/progress_test.dart
Expand Up @@ -15,6 +15,7 @@ void main() {

setUp(() {
stdout = MockStdout();
when(() => stdout.supportsAnsiEscapes).thenReturn(true);
});

test('writes ms when elapsed time is less than 0.1s', () async {
Expand All @@ -37,6 +38,84 @@ void main() {
);
});

test('writes custom progress animation to stdout', () async {
await IOOverrides.runZoned(
() async {
const time = '(0.Xs)';
const message = 'test message';
const progressOptions = ProgressOptions(
animation: ProgressAnimation(frames: ['+', 'x', '*']),
);
final done = Logger().progress(message, options: progressOptions);
await Future<void>.delayed(const Duration(milliseconds: 400));
done.complete();
verifyInOrder([
() {
stdout.write(
'''${lightGreen.wrap('\b${'\b' * (message.length + 4 + time.length)}+')} $message... ${darkGray.wrap('(0.1s)')}''',
);
},
() {
stdout.write(
'''${lightGreen.wrap('\b${'\b' * (message.length + 4 + time.length)}x')} $message... ${darkGray.wrap('(0.2s)')}''',
);
},
() {
stdout.write(
'''${lightGreen.wrap('\b${'\b' * (message.length + 4 + time.length)}*')} $message... ${darkGray.wrap('(0.3s)')}''',
);
},
() {
stdout.write(
'''\b${'\b' * (message.length + 4 + time.length)}\u001b[2K${lightGreen.wrap('✓')} $message ${darkGray.wrap('(0.4s)')}\n''',
);
},
]);
},
stdout: () => stdout,
stdin: () => stdin,
);
});

test('supports empty list of animation frames', () async {
await IOOverrides.runZoned(
() async {
const time = '(0.Xs)';
const message = 'test message';
const progressOptions = ProgressOptions(
animation: ProgressAnimation(frames: []),
);
final done = Logger().progress(message, options: progressOptions);
await Future<void>.delayed(const Duration(milliseconds: 400));
done.complete();
verifyInOrder([
() {
stdout.write(
'''${lightGreen.wrap('\b${'\b' * (message.length + 4 + time.length)}')}$message... ${darkGray.wrap('(0.1s)')}''',
);
},
() {
stdout.write(
'''${lightGreen.wrap('\b${'\b' * (message.length + 4 + time.length)}')}$message... ${darkGray.wrap('(0.2s)')}''',
);
},
() {
stdout.write(
'''${lightGreen.wrap('\b${'\b' * (message.length + 4 + time.length)}')}$message... ${darkGray.wrap('(0.3s)')}''',
);
},
() {
stdout.write(
'''\b${'\b' * (message.length + 4 + time.length)}\u001b[2K${lightGreen.wrap('✓')} $message ${darkGray.wrap('(0.4s)')}\n''',
);
},
]);
},
stdout: () => stdout,
stdin: () => stdin,
);
});

group('.complete', () {
test('writes lines to stdout', () async {
await runZoned(
Expand Down

0 comments on commit 43eae9f

Please sign in to comment.