Skip to content

Commit 91902ff

Browse files
committed
test: Pin Log.progress scoping and no-relabel contracts
- progress_nesting_test: a log emitted inside a progress runner must be attributed to the progress scope via the Zone, and a nested progress must child under the outer scope. - progress_relabel_test: progress must never surface the runner's result as a scope label (uses a generic recording writer, no terminal rendering).
1 parent 446682d commit 91902ff

2 files changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'package:serverpod_logging/serverpod_logging.dart';
2+
import 'package:test/test.dart';
3+
4+
/// Regression guard for the Zone-based scoping contract of [Log.progress].
5+
///
6+
/// The whole point of [Log.progress] is documented as:
7+
/// "Log calls inside the runner are automatically scoped via the Zone."
8+
///
9+
/// A `log.info(...)` emitted from within the runner must be attributed
10+
/// to the progress scope, not to the enclosing/root scope. This is a pure
11+
/// framework property and has nothing to do with how (or whether) the scope
12+
/// is rendered.
13+
void main() {
14+
group('Given Log.progress with a runner that logs', () {
15+
late TestLogWriter writer;
16+
late Log log;
17+
18+
setUp(() {
19+
writer = TestLogWriter();
20+
log = Log(writer);
21+
});
22+
23+
test(
24+
'when the runner emits a log entry, '
25+
'then that entry is scoped to the progress scope', () async {
26+
await log.progress('outer', () async {
27+
log.info('inside');
28+
});
29+
await log.flush();
30+
31+
final opened = writer.openedScopes.single;
32+
expect(opened.label, 'outer');
33+
34+
final inside = writer.entries.singleWhere((e) => e.message == 'inside');
35+
expect(
36+
inside.scope.id,
37+
opened.id,
38+
reason: 'a log emitted inside the runner must reference the '
39+
'progress scope, via the Zone',
40+
);
41+
expect(inside.scope.label, 'outer');
42+
});
43+
44+
test(
45+
'when the runner opens a nested progress scope, '
46+
'then the inner scope is a child of the outer scope', () async {
47+
await log.progress('outer', () async {
48+
await log.progress('inner', () async => true);
49+
});
50+
await log.flush();
51+
52+
final outer = writer.openedScopes.firstWhere((s) => s.label == 'outer');
53+
final inner = writer.openedScopes.firstWhere((s) => s.label == 'inner');
54+
expect(
55+
inner.parent?.id,
56+
outer.id,
57+
reason: 'the inner progress scope must nest under the outer one, '
58+
'which only works if the runner executes inside the outer Zone',
59+
);
60+
});
61+
});
62+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import 'package:serverpod_logging/serverpod_logging.dart';
2+
import 'package:test/test.dart';
3+
4+
/// A writer that records every scope label it is ever asked to render,
5+
/// regardless of which lifecycle hook delivered it.
6+
///
7+
/// This is deliberately a *generic* sink (think: a JSON line writer, a DB
8+
/// writer, an OTEL exporter) - it does not animate spinners or care about
9+
/// terminals. It exists to pin a framework-level contract:
10+
///
11+
/// `Log.progress(label, runner)` describes ONE operation called [label].
12+
/// The runner's *return value* is data, not a new name for the operation.
13+
///
14+
class _LabelRecordingWriter extends LogWriter {
15+
final List<String> observedLabels = [];
16+
17+
@override
18+
Future<void> log(LogEntry entry) async {}
19+
20+
@override
21+
Future<void> openScope(LogScope scope) async =>
22+
observedLabels.add(scope.label);
23+
24+
@override
25+
Future<void> closeScope(
26+
LogScope scope, {
27+
required bool success,
28+
required Duration duration,
29+
Object? error,
30+
StackTrace? stackTrace,
31+
}) async =>
32+
observedLabels.add(scope.label);
33+
}
34+
35+
void main() {
36+
group('Given Log.progress with a plain (non-stream) runner', () {
37+
late _LabelRecordingWriter writer;
38+
late Log log;
39+
40+
setUp(() {
41+
writer = _LabelRecordingWriter();
42+
log = Log(writer);
43+
});
44+
45+
test(
46+
'when the runner returns a value, '
47+
'then no scope event is labelled with that value', () async {
48+
await log.progress<String>('op', () async => 'sentinel-result');
49+
await log.flush();
50+
51+
expect(
52+
writer.observedLabels,
53+
everyElement('op'),
54+
reason: 'progress must not relabel the scope with the runner result; '
55+
'the operation is called "op" from start to finish',
56+
);
57+
expect(
58+
writer.observedLabels,
59+
isNot(contains('sentinel-result')),
60+
reason: 'the runner return value must never surface as a scope label',
61+
);
62+
});
63+
64+
test(
65+
'when the runner returns true, '
66+
'then the scope label stays "op" (not "true")', () async {
67+
await log.progress<bool>('op', () async => true);
68+
await log.flush();
69+
70+
expect(writer.observedLabels, everyElement('op'));
71+
expect(writer.observedLabels, isNot(contains('true')));
72+
});
73+
});
74+
}

0 commit comments

Comments
 (0)