Skip to content

Commit f7d39b6

Browse files
feat: add tests for policy and io for file handler
1 parent 2418e03 commit f7d39b6

File tree

2 files changed

+398
-0
lines changed

2 files changed

+398
-0
lines changed

test/file_handler_policy_test.dart

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import 'package:strlog/src/handlers/file_handler/policy.dart';
2+
import 'package:test/test.dart';
3+
4+
class TestLogFileStat implements LogFileStat {
5+
TestLogFileStat({
6+
required this.firstChanged,
7+
required this.size,
8+
});
9+
10+
@override
11+
final DateTime firstChanged;
12+
13+
@override
14+
final int size;
15+
}
16+
17+
void main() {
18+
group('LogFileRotationPolicy', () {
19+
group('never', () {
20+
test('should never rotate', () {
21+
final policy = LogFileRotationPolicy.never();
22+
final stat = TestLogFileStat(
23+
firstChanged: DateTime.now(),
24+
size: 1000000,
25+
);
26+
27+
expect(policy.shouldRotate(stat), isFalse);
28+
});
29+
});
30+
31+
group('periodic', () {
32+
test('should not rotate if period has not elapsed', () {
33+
final now = DateTime.now();
34+
final policy = LogFileRotationPolicy.periodic(Duration(hours: 1));
35+
final stat = TestLogFileStat(
36+
firstChanged: now,
37+
size: 100,
38+
);
39+
40+
expect(policy.shouldRotate(stat), isFalse);
41+
});
42+
43+
test('should rotate if period has elapsed', () {
44+
final oneHourAgo = DateTime.now().subtract(Duration(hours: 2));
45+
final policy = LogFileRotationPolicy.periodic(Duration(hours: 1));
46+
final stat = TestLogFileStat(
47+
firstChanged: oneHourAgo,
48+
size: 100,
49+
);
50+
51+
expect(policy.shouldRotate(stat), isTrue);
52+
});
53+
});
54+
55+
group('sized', () {
56+
test('should not rotate if size is below limit', () {
57+
final policy = LogFileRotationPolicy.sized(1000);
58+
final stat = TestLogFileStat(
59+
firstChanged: DateTime.now(),
60+
size: 500,
61+
);
62+
63+
expect(policy.shouldRotate(stat), isFalse);
64+
});
65+
66+
test('should rotate if size equals limit', () {
67+
final policy = LogFileRotationPolicy.sized(1000);
68+
final stat = TestLogFileStat(
69+
firstChanged: DateTime.now(),
70+
size: 1000,
71+
);
72+
73+
expect(policy.shouldRotate(stat), isTrue);
74+
});
75+
76+
test('should rotate if size exceeds limit', () {
77+
final policy = LogFileRotationPolicy.sized(1000);
78+
final stat = TestLogFileStat(
79+
firstChanged: DateTime.now(),
80+
size: 1500,
81+
);
82+
83+
expect(policy.shouldRotate(stat), isTrue);
84+
});
85+
});
86+
87+
group('union', () {
88+
test('should not rotate if no policy triggers', () {
89+
final policy = LogFileRotationPolicy.union([
90+
LogFileRotationPolicy.sized(1000),
91+
LogFileRotationPolicy.periodic(Duration(hours: 1)),
92+
]);
93+
final stat = TestLogFileStat(
94+
firstChanged: DateTime.now(),
95+
size: 500,
96+
);
97+
98+
expect(policy.shouldRotate(stat), isFalse);
99+
});
100+
101+
test('should rotate if any policy triggers - size', () {
102+
final policy = LogFileRotationPolicy.union([
103+
LogFileRotationPolicy.sized(1000),
104+
LogFileRotationPolicy.periodic(Duration(hours: 1)),
105+
]);
106+
final stat = TestLogFileStat(
107+
firstChanged: DateTime.now(),
108+
size: 1500,
109+
);
110+
111+
expect(policy.shouldRotate(stat), isTrue);
112+
});
113+
114+
test('should rotate if any policy triggers - time', () {
115+
final policy = LogFileRotationPolicy.union([
116+
LogFileRotationPolicy.sized(1000),
117+
LogFileRotationPolicy.periodic(Duration(hours: 1)),
118+
]);
119+
final stat = TestLogFileStat(
120+
firstChanged: DateTime.now().subtract(Duration(hours: 2)),
121+
size: 500,
122+
);
123+
124+
expect(policy.shouldRotate(stat), isTrue);
125+
});
126+
127+
test('empty union should never rotate', () {
128+
final policy = LogFileRotationPolicy.union([]);
129+
final stat = TestLogFileStat(
130+
firstChanged: DateTime.now(),
131+
size: 1500,
132+
);
133+
134+
expect(policy.shouldRotate(stat), isFalse);
135+
});
136+
});
137+
});
138+
}

test/file_handler_test.dart

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import 'dart:async';
2+
3+
import 'package:file/memory.dart';
4+
import 'package:path/path.dart' as path;
5+
import 'package:strlog/formatters.dart';
6+
import 'package:strlog/src/handlers/file_handler/io.dart';
7+
import 'package:strlog/src/record_impl.dart';
8+
import 'package:strlog/src/handlers/file_handler/policy.dart';
9+
import 'package:strlog/strlog.dart';
10+
import 'package:test/test.dart';
11+
12+
void main() {
13+
group('FileHandler', () {
14+
late MemoryFileSystem fs;
15+
late String logPath;
16+
late Formatter formatter;
17+
18+
setUp(() {
19+
fs = MemoryFileSystem();
20+
logPath = path.join('logs', 'test.log');
21+
formatter = (record) => '${record.message}\n'.codeUnits;
22+
});
23+
24+
test('creates log file and directory if they do not exist', () {
25+
final handler = FileHandler(
26+
const LogFileRotationPolicy.never(),
27+
formatter: formatter,
28+
path: logPath,
29+
fs: fs,
30+
);
31+
32+
expect(fs.file(logPath).existsSync(), isTrue);
33+
handler.close();
34+
});
35+
36+
test('writes records to file', () async {
37+
final handler = FileHandler(
38+
const LogFileRotationPolicy.never(),
39+
formatter: formatter,
40+
path: logPath,
41+
fs: fs,
42+
);
43+
44+
final record = RecordImpl(
45+
level: Level.info,
46+
message: 'test message',
47+
timestamp: DateTime.now(),
48+
);
49+
50+
handler.handle(record);
51+
await handler.close();
52+
53+
final content = fs.file(logPath).readAsStringSync();
54+
expect(content, contains('test message'));
55+
});
56+
57+
test('respects filter', () async {
58+
final handler = FileHandler(
59+
const LogFileRotationPolicy.never(),
60+
formatter: formatter,
61+
path: logPath,
62+
fs: fs,
63+
);
64+
65+
handler.filter = (record) => record.level >= Level.error;
66+
67+
final infoRecord = RecordImpl(
68+
level: Level.info,
69+
message: 'info message',
70+
timestamp: DateTime.now(),
71+
);
72+
73+
final errorRecord = RecordImpl(
74+
level: Level.error,
75+
message: 'error message',
76+
timestamp: DateTime.now(),
77+
);
78+
79+
handler.handle(infoRecord);
80+
handler.handle(errorRecord);
81+
await handler.close();
82+
83+
final content = fs.file(logPath).readAsStringSync();
84+
expect(content, isNot(contains('info message')));
85+
expect(content, contains('error message'));
86+
});
87+
88+
test('rotates file based on size policy', () async {
89+
final maxSize = 20; // Small size to trigger rotation easily
90+
final handler = FileHandler(
91+
LogFileRotationPolicy.sized(maxSize),
92+
formatter: formatter,
93+
path: logPath,
94+
fs: fs,
95+
);
96+
97+
final longMessage = 'a' * maxSize;
98+
final record = RecordImpl(
99+
level: Level.info,
100+
message: longMessage,
101+
timestamp: DateTime.now(),
102+
);
103+
104+
// Write enough to trigger rotation
105+
handler.handle(record);
106+
handler.handle(record);
107+
await handler.close();
108+
109+
// Check that we have more than one log file
110+
final directory = fs.directory(path.dirname(logPath));
111+
final files = directory.listSync().where((f) => f.path.endsWith('.log'));
112+
expect(files.length, greaterThan(1));
113+
});
114+
115+
test('rotates file based on time policy', () async {
116+
final handler = FileHandler(
117+
LogFileRotationPolicy.periodic(Duration(milliseconds: 100)),
118+
formatter: formatter,
119+
path: logPath,
120+
fs: fs,
121+
);
122+
123+
final record = RecordImpl(
124+
level: Level.info,
125+
message: 'test message',
126+
timestamp: DateTime.now(),
127+
);
128+
129+
handler.handle(record);
130+
await Future<void>.delayed(Duration(milliseconds: 150));
131+
handler.handle(record);
132+
await handler.close();
133+
134+
final directory = fs.directory(path.dirname(logPath));
135+
final files = directory.listSync().where((f) => f.path.endsWith('.log'));
136+
expect(files.length, greaterThan(1));
137+
});
138+
139+
test('respects max backups count', () async {
140+
final maxBackups = 2;
141+
final handler = FileHandler(
142+
LogFileRotationPolicy.sized(10),
143+
formatter: formatter,
144+
path: logPath,
145+
fs: fs,
146+
maxBackupsCount: maxBackups,
147+
);
148+
149+
final record = RecordImpl(
150+
level: Level.info,
151+
message: 'a' * 20, // Ensure rotation
152+
timestamp: DateTime.now(),
153+
);
154+
155+
// Create multiple rotations
156+
for (var i = 0; i < maxBackups + 2; i++) {
157+
handler.handle(record);
158+
// give time to elapse for a unique file name
159+
await Future<void>.delayed(Duration(seconds: 1));
160+
}
161+
162+
await handler.close();
163+
164+
final directory = fs.directory(path.dirname(logPath));
165+
final files = directory.listSync().where((f) => f.path.endsWith('.log'));
166+
// Current file + max backups
167+
expect(files.length, equals(maxBackups + 1));
168+
});
169+
170+
test(
171+
'removes oldest files when max count is exceeded and maintains chronological order',
172+
() async {
173+
final maxBackups = 2;
174+
final handler = FileHandler(
175+
LogFileRotationPolicy.sized(10),
176+
formatter: formatter,
177+
path: logPath,
178+
fs: fs,
179+
maxBackupsCount: maxBackups,
180+
);
181+
182+
final record = RecordImpl(
183+
level: Level.info,
184+
message: 'a' * 20, // Ensure rotation
185+
timestamp: DateTime.now(),
186+
);
187+
188+
// Create multiple rotations with timestamps
189+
final createdFiles = <String>[];
190+
191+
for (var i = 0; i < maxBackups + 2; i++) {
192+
handler.handle(record);
193+
await Future<void>.delayed(Duration(seconds: 1));
194+
195+
final directory = fs.directory(path.dirname(logPath));
196+
final files = directory
197+
.listSync()
198+
.where((f) => f.path.endsWith('.log'))
199+
.map((f) => f.path)
200+
.toList();
201+
202+
// Store newly created backup file
203+
for (final file in files) {
204+
if (!createdFiles.contains(file)) {
205+
createdFiles.add(file);
206+
}
207+
}
208+
}
209+
210+
await handler.close();
211+
212+
final directory = fs.directory(path.dirname(logPath));
213+
final remainingFiles = directory
214+
.listSync()
215+
.where((f) => f.path.endsWith('.log'))
216+
.map((f) => f.path)
217+
.toList();
218+
219+
// Extract timestamps from filenames to verify chronological order
220+
List<String> extractTimestamps(List<String> files) {
221+
return files
222+
.where((f) => f != logPath) // Exclude current log file
223+
.map((f) {
224+
final match = RegExp(r'_(\d{14})\.log$').firstMatch(f);
225+
return match?.group(1) ?? '';
226+
})
227+
.where((t) => t.isNotEmpty)
228+
.toList();
229+
}
230+
231+
final timestamps = extractTimestamps(remainingFiles);
232+
final sortedTimestamps = [...timestamps]..sort();
233+
234+
// Verify total number of files (current + backups)
235+
expect(remainingFiles.length, equals(maxBackups + 1));
236+
237+
// Verify timestamps are in ascending order (older to newer)
238+
expect(timestamps, equals(sortedTimestamps),
239+
reason: 'Backup files should be sorted from old to new');
240+
241+
// Verify the oldest files were removed
242+
final allTimestamps = extractTimestamps(createdFiles);
243+
final removedTimestamps =
244+
allTimestamps.take(allTimestamps.length - maxBackups);
245+
246+
for (final timestamp in removedTimestamps) {
247+
expect(timestamps.contains(timestamp), isFalse,
248+
reason: 'Older timestamp $timestamp should have been removed');
249+
}
250+
251+
// Verify the newest files were kept
252+
final keptTimestamps =
253+
allTimestamps.skip(allTimestamps.length - maxBackups);
254+
for (final timestamp in keptTimestamps) {
255+
expect(timestamps.contains(timestamp), isTrue,
256+
reason: 'Newer timestamp $timestamp should have been kept');
257+
}
258+
});
259+
});
260+
}

0 commit comments

Comments
 (0)