Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mason_logger): add ProgressOptions API #478

Merged
merged 13 commits into from Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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