Skip to content

Commit

Permalink
Merge pull request #2 from hoc081098/enhance_debug
Browse files Browse the repository at this point in the history
Enhance debug
  • Loading branch information
hoc081098 authored Jan 31, 2021
2 parents 3dff866 + d14724d commit e827ac8
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 41 deletions.
89 changes: 82 additions & 7 deletions lib/src/debug.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import 'dart:async';
import 'dart:math' as math;

import 'package:path/path.dart' as path;
import 'package:rxdart/rxdart.dart'
show DoStreamTransformer, Kind, Notification;
import 'package:stack_trace/stack_trace.dart';

extension _NotificationDescriptionExt<T> on Notification<T> {
@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
String get description {
switch (kind) {
case Kind.OnData:
Expand All @@ -17,20 +22,89 @@ extension _NotificationDescriptionExt<T> on Notification<T> {
}
}

extension on Frame {
@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
String get formatted {
final trimmedFile = path.basename(uri.toString());
return '$trimmedFile:$line ($member)';
}
}

extension on String {
@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
String take(int n) {
if (n < 0) {
throw ArgumentError.value(
n,
'n',
'Requested character count is less than zero.',
);
}
return substring(0, math.min(n, length));
}

@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
String takeLast(int n) {
if (n < 0) {
throw ArgumentError.value(
n,
'n',
'Requested character count is less than zero.',
);
}
return substring(length - math.min(n, length));
}
}

/// RxDart debug operator - Port from [RxSwift Debug Operator](https://github.com/ReactiveX/RxSwift/blob/main/RxSwift/Observables/Debug.swift)
///
/// Prints received events for all listeners on standard output.
extension DebugStreamExtension<T> on Stream<T> {
/// RxDart debug operator - Port from [RxSwift Debug Operator](https://github.com/ReactiveX/RxSwift/blob/main/RxSwift/Observables/Debug.swift)
///
/// Prints received events for all listeners on standard output.
///
/// The [identifier] is printed together with event description to standard output.
/// If [identifier] is null, it will be current stacktrace, including location, line and member.
///
/// If [log] is null, this [Stream] is returned without any transformations.
/// This is useful for disabling logging in release mode of an application.
///
/// If [trimOutput] is true, event text will be trimmed to max 40 characters.
Stream<T> debug({
String identifier = 'Debug',
void Function(String) log = print,
String? identifier,
void Function(String)? log = print,
bool trimOutput = false,
}) {
if (log == null) {
// logging is disabled.
return this;
}

identifier ??= Trace.current(1).frames.first.formatted;

@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline')
void logEvent(String content) =>
log('${DateTime.now()}: $identifier -> $content');

return transform<T>(
DoStreamTransformer<T>(
onEach: (notification) => logEvent('Event ${notification.description}'),
onEach: (notification) {
const maxEventTextLength = 40;

final description = notification.description;
final descriptionNormalized = description.length >
maxEventTextLength &&
trimOutput
? '${description.take(maxEventTextLength ~/ 2)}...${description.takeLast(maxEventTextLength ~/ 2)}'
: description;

logEvent('Event $descriptionNormalized');
},
onListen: () => logEvent('Listened'),
onCancel: () => logEvent('Cancelled'),
onPause: () => logEvent('Paused'),
Expand All @@ -43,15 +117,16 @@ extension DebugStreamExtension<T> on Stream<T> {
/// Listen without any handler.
extension ListenNullStreamExtension<T> on Stream<T> {
/// Listen without any handler.
StreamSubscription<T> collect() => CollectStreamSubscription<T>(listen(null));
StreamSubscription<T> collect() =>
_CollectStreamSubscription<T>(listen(null));
}

/// A [StreamSubscription] cannot replace any handler.
class CollectStreamSubscription<T> implements StreamSubscription<T> {
class _CollectStreamSubscription<T> implements StreamSubscription<T> {
final StreamSubscription<T> _delegate;

/// Construct a [CollectStreamSubscription] that delegates all implementation to other [StreamSubscription].
CollectStreamSubscription(this._delegate);
/// Construct a [_CollectStreamSubscription] that delegates all implementation to other [StreamSubscription].
_CollectStreamSubscription(this._delegate);

@override
Future<E> asFuture<E>([E? futureValue]) => _delegate.asFuture(futureValue);
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ environment:
dependencies:
rxdart: ^0.26.0-nullsafety.1
meta: ^1.3.0-nullsafety.6
stack_trace: ^1.10.0-nullsafety.6
path: ^1.8.0-nullsafety.3

dev_dependencies:
pedantic: ^1.10.0-nullsafety.3
Expand Down
161 changes: 127 additions & 34 deletions test/debug_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ void main() {
test('log all events', () {
// single
{
const identifier = 'debug_test.dart:26 (main.<fn>.<fn>)';

const logs = [
': Debug -> Listened',
': Debug -> Event data(1)',
': Debug -> Event done',
': Debug -> Cancelled',
': $identifier -> Listened',
': $identifier -> Event data(1)',
': $identifier -> Event done',
': $identifier -> Cancelled',
];

var i = 0;
Expand All @@ -33,13 +35,15 @@ void main() {

// many
{
const identifier = 'debug_test.dart:51 (main.<fn>.<fn>)';

const logs = [
': Debug -> Listened',
': Debug -> Event data(1)',
': Debug -> Event data(2)',
': Debug -> Event data(3)',
': Debug -> Event done',
': Debug -> Cancelled',
': $identifier -> Listened',
': $identifier -> Event data(1)',
': $identifier -> Event data(2)',
': $identifier -> Event data(3)',
': $identifier -> Event done',
': $identifier -> Cancelled',
];
var i = 0;

Expand All @@ -56,14 +60,16 @@ void main() {

// many with error
{
const identifier = 'debug_test.dart:81 (main.<fn>.<fn>)';

const logs = [
': Debug -> Listened',
': Debug -> Event data(1)',
': Debug -> Event data(2)',
': Debug -> Event error(Exception, )',
': Debug -> Event data(3)',
': Debug -> Event done',
': Debug -> Cancelled',
': $identifier -> Listened',
': $identifier -> Event data(1)',
': $identifier -> Event data(2)',
': $identifier -> Event error(Exception, )',
': $identifier -> Event data(3)',
': $identifier -> Event done',
': $identifier -> Cancelled',
];
var i = 0;

Expand Down Expand Up @@ -170,24 +176,111 @@ void main() {
);
}
});

test('trimOutput', () {
const logs = [
': DEBUG -> Listened',
': DEBUG -> Event data(This is a long ...ed for test purpose)',
': DEBUG -> Event data(2)',
': DEBUG -> Event error(Exception, )',
': DEBUG -> Event data(3)',
': DEBUG -> Event done',
': DEBUG -> Cancelled',
];
var i = 0;

expect(
Rx.concat<String>([
Stream.fromIterable(
['This is a long string value, used for test purpose', '2']),
Stream.error(Exception(), StackTrace.empty),
Stream.value('3'),
]).debug(
log: (v) {
expect(v.endsWith(logs[i++]), isTrue);
expect(v.startsWith(dateTimeToStringRegex), isTrue);
},
identifier: 'DEBUG',
trimOutput: true,
),
emitsInOrder(<dynamic>[
'This is a long string value, used for test purpose',
'2',
emitsError(isException),
'3',
emitsDone,
]),
);
});

test('pause resume', () async {
const logs = [
': DEBUG -> Listened',
': DEBUG -> Paused',
': DEBUG -> Resumed',
': DEBUG -> Event data(1)',
': DEBUG -> Event done',
': DEBUG -> Cancelled',
];

var i = 0;
final subscription = Stream.value(1)
.debug(
log: (v) {
expect(v.endsWith(logs[i++]), isTrue);
expect(v.startsWith(dateTimeToStringRegex), isTrue);
},
identifier: 'DEBUG',
)
.listen(null);
subscription.onData((data) => expect(data, 1));

subscription.pause();
await Future<void>.delayed(const Duration(milliseconds: 500));
subscription.resume();
});
});

test('collect', () {
final streamSubscription = Stream.value(1).collect();
expect(streamSubscription, isA<StreamSubscription<void>>());
expect(streamSubscription, isA<StreamSubscription<int>>());

expect(
() => streamSubscription.onData((data) {}),
throwsStateError,
);
expect(
() => streamSubscription.onError((Object e, StackTrace s) {}),
throwsStateError,
);
expect(
() => streamSubscription.onDone(() {}),
throwsStateError,
);
group('collect', () {
test(
'returns a StreamSubscription, any handlers of this subscription cannot be replaced',
() {
final streamSubscription = Stream.value(1).collect();
expect(streamSubscription, isA<StreamSubscription<void>>());
expect(streamSubscription, isA<StreamSubscription<int>>());

expect(
() => streamSubscription.onData((data) {}),
throwsStateError,
);
expect(
() => streamSubscription.onError((Object e, StackTrace s) {}),
throwsStateError,
);
expect(
() => streamSubscription.onDone(() {}),
throwsStateError,
);
});

test('pause resume isPaused', () {
final subscription = Stream.value(1).collect();

subscription.pause();
expect(subscription.isPaused, isTrue);

subscription.resume();
expect(subscription.isPaused, isFalse);
});

test('asFuture', () {
final subscription = Stream.value(1).collect();
expect(subscription.asFuture<void>(), completes);
});

test('cancel', () {
Stream.value(1).doOnData((v) => expect(true, isFalse)).collect()
..cancel();
});
});
}

0 comments on commit e827ac8

Please sign in to comment.