forked from rxi/log.lua
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest.lua
More file actions
634 lines (497 loc) · 18.4 KB
/
test.lua
File metadata and controls
634 lines (497 loc) · 18.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
-- test.lua — unit tests for log.lua
local log = require("log")
local passed = 0
local failed = 0
local function assert_eq(label, got, expected)
if got == expected then
print(" PASS " .. label)
passed = passed + 1
else
print(" FAIL " .. label)
print(" expected: " .. tostring(expected))
print(" got: " .. tostring(got))
failed = failed + 1
end
end
local function assert_true(label, v)
assert_eq(label, not not v, true)
end
local function assert_false(label, v)
assert_eq(label, not not v, false)
end
-- Capture console output for inspection
local captured = {}
local real_stdout = io.stdout
local real_stderr = io.stderr
local mock_io = { write = function(_, s) captured[#captured + 1] = s end }
local function capture_start()
captured = {}
io.stdout = mock_io
io.stderr = mock_io
end
local function capture_stop()
io.stdout = real_stdout
io.stderr = real_stderr
end
-- ── Helpers ──────────────────────────────────────────────────────────────────
local function reset_log()
log.level = "trace"
log.usecolor = true
log.outfile = nil
log.stderr = false
log.tostr = nil
end
-- Strip ANSI escape codes so we can assert on plain text
local function strip_ansi(s)
return s:gsub("\27%[%d+m", "")
end
-- ── Suite: level filtering ────────────────────────────────────────────────────
print("\n── level filtering ──")
do
reset_log()
log.level = "warn"
capture_start()
log.trace("t"); log.debug("d"); log.info("i")
capture_stop()
assert_eq("levels below threshold produce no output", #captured, 0)
capture_start()
log.warn("w"); log.error("e"); log.fatal("f")
capture_stop()
assert_eq("levels at/above threshold produce output", #captured, 3)
end
do
reset_log()
log.level = "error"
capture_start()
log.warn("suppressed")
capture_stop()
assert_eq("warn suppressed when level=error", #captured, 0)
capture_start()
log.error("shown"); log.fatal("shown")
capture_stop()
assert_eq("error and fatal shown when level=error", #captured, 2)
end
do
reset_log()
log.level = "trace"
capture_start()
for _, fn in ipairs({ "trace", "debug", "info", "warn", "error", "fatal" }) do
log[fn]("x")
end
capture_stop()
assert_eq("all 6 levels shown when level=trace", #captured, 6)
end
-- ── Suite: output format ──────────────────────────────────────────────────────
print("\n── output format ──")
do
reset_log()
log.usecolor = false
capture_start()
log.info("hello world")
capture_stop()
local line = captured[1]
assert_true("output contains level label INFO", line:find("%[INFO"))
assert_true("output contains HH:MM:SS timestamp", line:find("%d%d:%d%d:%d%d"))
assert_true("output contains the message", line:find("hello world"))
assert_true("output contains source:line", line:find("[^:]+:%d+"))
end
do
reset_log()
log.usecolor = true
capture_start()
log.warn("colored")
capture_stop()
assert_true("ANSI color codes present when usecolor=true",
captured[1]:find("\27%["))
log.usecolor = false
capture_start()
log.warn("plain")
capture_stop()
assert_false("no ANSI codes when usecolor=false",
captured[1]:find("\27%["))
end
do
reset_log()
log.usecolor = false
-- Each level should use its own uppercased label
local labels = { "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" }
local fns = { "trace", "debug", "info", "warn", "error", "fatal" }
for k, fn in ipairs(fns) do
capture_start()
log[fn]("x")
capture_stop()
assert_true("level label " .. labels[k], captured[1]:find(labels[k]))
end
end
-- ── Suite: number rounding ────────────────────────────────────────────────────
print("\n── number rounding ──")
do
reset_log()
log.usecolor = false
capture_start()
log.info(1.23456)
capture_stop()
assert_true("float rounded to 2dp", strip_ansi(captured[1]):find("1.23"))
assert_false("float not printed beyond 2dp", strip_ansi(captured[1]):find("1.2345"))
capture_start()
log.info(1.236)
capture_stop()
assert_true("float rounds up correctly", strip_ansi(captured[1]):find("1.24"))
capture_start()
log.info(-1.23456)
capture_stop()
assert_true("negative float rounded to 2dp", strip_ansi(captured[1]):find("-1.23"))
end
-- ── Suite: multi-argument concatenation ──────────────────────────────────────
print("\n── multi-argument concatenation ──")
do
reset_log()
log.usecolor = false
capture_start()
log.info("a", "b", "c")
capture_stop()
assert_true("multiple args joined with spaces", captured[1]:find("a b c"))
capture_start()
log.info("val:", 42)
capture_stop()
assert_true("string and number concatenated", captured[1]:find("val: 42"))
end
-- ── Suite: outfile ────────────────────────────────────────────────────────────
print("\n── outfile ──")
do
local tmpfile = os.tmpname()
reset_log()
log.usecolor = false
log.outfile = tmpfile
log.warn("written to file")
log.outfile = nil -- stop further writes
local f = assert(io.open(tmpfile, "r"))
local contents = f:read("*a")
f:close()
os.remove(tmpfile)
assert_true("outfile contains level label", contents:find("WARN"))
assert_true("outfile contains the message", contents:find("written to file"))
assert_false("outfile has no ANSI codes", contents:find("\27%["))
end
do
-- Bad path should raise an error (fail loudly on misconfigured outfile)
reset_log()
log.outfile = "/nonexistent_dir/nope.log"
local ok, err = pcall(function() log.info("safe") end)
log.outfile = nil
assert_false("bad outfile path raises an error", ok)
assert_true("bad outfile error message contains OS reason", err and err:find("No such") ~= nil)
end
-- ── Suite: invalid level ─────────────────────────────────────────────────────
print("\n── invalid level ──")
do
-- setting an invalid level on the global logger raises immediately
reset_log()
local ok, err = pcall(function() log.level = "verbose" end)
reset_log()
assert_false("invalid level on global logger raises an error", ok)
assert_true("error message names the invalid value", err and err:find("verbose") ~= nil)
end
do
-- same check for an instance
local inst = log { level = "trace" }
local ok, err = pcall(function() inst.level = "verbose" end)
assert_false("invalid level on instance raises an error", ok)
assert_true("instance error message names the invalid value", err and err:find("verbose") ~= nil)
end
do
-- creating an instance with an invalid level raises immediately
local ok, err = pcall(function() log { level = "verbose" } end)
assert_false("log{level='invalid'} raises an error", ok)
assert_true("creation error message names the invalid value", err and err:find("verbose") ~= nil)
end
-- ── Suite: logger instances ───────────────────────────────────────────────────
print("\n── logger instances ──")
do
-- log is callable and returns a table with all 6 log methods
local inst = log {}
assert_eq("log{} returns a table", type(inst), "table")
for _, fn in ipairs({ "trace", "debug", "info", "warn", "error", "fatal" }) do
assert_eq("instance has method " .. fn, type(inst[fn]), "function")
end
end
do
-- instance level filtering is independent from the global logger
reset_log()
log.level = "trace"
local inst = log { level = "error" }
capture_start()
inst.trace("t"); inst.debug("d"); inst.info("i"); inst.warn("w")
capture_stop()
assert_eq("instance level=error suppresses trace/debug/info/warn", #captured, 0)
capture_start()
inst.error("e"); inst.fatal("f")
capture_stop()
assert_eq("instance level=error passes error/fatal", #captured, 2)
-- global logger must be unaffected
capture_start()
log.trace("global logger trace")
capture_stop()
assert_eq("global logger level unaffected by instance level", #captured, 1)
end
do
-- instance inherits global logger defaults when options are omitted
reset_log()
log.level = "warn"
log.usecolor = false
local inst = log {}
assert_eq("instance inherits level from global logger", inst.level, "warn")
assert_eq("instance inherits usecolor from global logger", inst.usecolor, false)
reset_log()
end
do
-- instance config is isolated: mutating the instance does not affect global logger
reset_log()
local inst = log { level = "trace", usecolor = false }
inst.level = "fatal"
inst.usecolor = true
assert_eq("global logger level unchanged after instance mutation", log.level, "trace")
assert_eq("global logger usecolor unchanged after instance mutation", log.usecolor, true)
end
do
-- instance level is mutable after creation
reset_log()
local inst = log { level = "error", usecolor = false }
capture_start()
inst.info("before")
capture_stop()
assert_eq("info suppressed before level change", #captured, 0)
inst.level = "info"
capture_start()
inst.info("after")
capture_stop()
assert_eq("info shown after level lowered to info", #captured, 1)
end
do
-- instance usecolor is respected
reset_log()
local colored = log { usecolor = true, level = "trace" }
local plain = log { usecolor = false, level = "trace" }
capture_start(); colored.info("c"); capture_stop()
assert_true("instance usecolor=true emits ANSI codes", captured[1]:find("\27%["))
capture_start(); plain.info("p"); capture_stop()
assert_false("instance usecolor=false emits no ANSI codes", captured[1]:find("\27%["))
end
-- ── Suite: instance name ──────────────────────────────────────────────────────
print("\n── instance name ──")
do
-- name appears in console output
reset_log()
local inst = log { name = "mymod", usecolor = false }
capture_start()
inst.info("hello")
capture_stop()
assert_true("name appears in console output", captured[1]:find("mymod"))
end
do
-- global logger output has no name prefix
reset_log()
log.usecolor = false
capture_start()
log.info("hello")
capture_stop()
-- output format is "[INFO HH:MM:SS] src:line: msg" — nothing before src:line
assert_true("global logger output matches expected format",
strip_ansi(captured[1]):match("%[%u+%s+%d+:%d+:%d+%] [^%s]+:%d+:"))
end
do
-- unnamed instance output has no name prefix either
reset_log()
local inst = log { usecolor = false }
capture_start()
inst.info("hello")
capture_stop()
assert_true("unnamed instance matches same format as global logger",
strip_ansi(captured[1]):match("%[%u+%s+%d+:%d+:%d+%] [^%s]+:%d+:"))
end
do
-- name appears in outfile output
local tmpfile = os.tmpname()
local inst = log { name = "mymod", usecolor = false, outfile = tmpfile }
inst.warn("to file")
local f = assert(io.open(tmpfile, "r"))
local contents = f:read("*a")
f:close()
os.remove(tmpfile)
assert_true("name appears in outfile output", contents:find("mymod"))
end
do
-- name is read-only after creation
local inst = log { name = "mymod" }
local ok, err = pcall(function() inst.name = "other" end)
assert_false("assigning name after creation raises an error", ok)
assert_true("error message mentions name is read-only", err and err:find("read%-only"))
assert_eq("name unchanged after failed assignment", inst.name, "mymod")
end
-- ── Suite: noop optimization ─────────────────────────────────────────────────
print("\n── noop optimization ──")
do
-- all disabled levels share the same noop function reference
reset_log()
log.level = "fatal"
assert_true("disabled levels share one noop function",
log.trace == log.debug and log.debug == log.info and
log.info == log.warn and log.warn == log.error)
assert_false("enabled level is not the noop", log.trace == log.fatal)
reset_log()
end
do
-- after raising the level, newly disabled levels become noop
reset_log()
local was_info = log.info -- real impl at level=trace
log.level = "error"
assert_false("info becomes noop after level raised to error", log.info == was_info)
assert_true("info and warn are now the same noop", log.info == log.warn)
reset_log()
end
do
-- after lowering the level, previously disabled levels become real again
reset_log()
log.level = "fatal"
local noop_ref = log.info -- captured noop
log.level = "trace"
assert_false("info is no longer noop after level lowered to trace", log.info == noop_ref)
reset_log()
end
do
-- same noop optimization applies to instances
local inst = log { level = "warn" }
assert_true("instance: disabled levels share one noop",
inst.trace == inst.debug and inst.debug == inst.info)
assert_false("instance: warn (enabled) is not noop", inst.trace == inst.warn)
local noop_ref = inst.trace
inst.level = "trace"
assert_false("instance: trace is real impl after level lowered", inst.trace == noop_ref)
end
-- ── Suite: tostr ─────────────────────────────────────────────────────────────
print("\n── tostr ──")
do
-- nil by default: existing behavior unchanged
reset_log()
log.usecolor = false
assert_eq("tostr is nil by default on global logger", log.tostr, nil)
capture_start()
log.info("plain")
capture_stop()
assert_true("nil tostr still produces output", captured[1]:find("plain"))
end
do
-- custom tostr is called for each argument
reset_log()
log.usecolor = false
local calls = {}
log.tostr = function(v)
calls[#calls + 1] = v; return "x"
end
capture_start()
log.info("a", "b")
capture_stop()
assert_eq("custom tostr called once per arg", #calls, 2)
assert_true("custom tostr return value appears in output", captured[1]:find("x x"))
end
do
-- custom tostr bypasses number rounding
log.tostr = function(v) return tostring(v) end
capture_start()
log.info(1.23456)
capture_stop()
assert_true("custom tostr bypasses number rounding", strip_ansi(captured[1]):find("1.23456"))
end
do
-- custom tostr on an instance, does not affect global logger
reset_log()
log.usecolor = false
local inst = log { tostr = function(v) return "T:" .. tostring(v) end, usecolor = false }
capture_start()
inst.info("hello")
capture_stop()
assert_true("instance tostr wraps value", captured[1]:find("T:hello"))
capture_start()
log.info("hello")
capture_stop()
assert_false("global logger unaffected by instance tostr", captured[1]:find("T:hello"))
end
do
-- instance inherits tostr from global logger
reset_log()
log.tostr = function(v) return "G:" .. tostring(v) end
local inst = log { usecolor = false }
log.usecolor = false
capture_start()
inst.info("hello")
capture_stop()
assert_true("instance inherits tostr from global logger", captured[1]:find("G:hello"))
end
do
-- instance-level tostr overrides inherited global tostr
reset_log()
log.tostr = function(v) return "G:" .. tostring(v) end
local inst = log { tostr = function(v) return "I:" .. tostring(v) end, usecolor = false }
capture_start()
inst.info("hello")
capture_stop()
assert_true("instance tostr overrides global tostr", captured[1]:find("I:hello"))
assert_false("global tostr not used when instance tostr set", captured[1]:find("G:hello"))
end
-- ── Suite: stderr ────────────────────────────────────────────────────────────
print("\n── stderr ──")
do
-- stderr=false by default: output goes to stdout
reset_log()
log.usecolor = false
assert_eq("stderr is false by default", log.stderr, false)
end
do
-- stderr=false: output goes to stdout, nothing to stderr
reset_log()
log.usecolor = false
local stdout_captured = {}
local stderr_captured = {}
io.stdout = { write = function(_, s) stdout_captured[#stdout_captured + 1] = s end }
io.stderr = { write = function(_, s) stderr_captured[#stderr_captured + 1] = s end }
log.info("to stdout")
io.stdout = real_stdout
io.stderr = real_stderr
assert_eq("stderr=false: stdout receives output", #stdout_captured, 1)
assert_true("stderr=false: stdout contains message", stdout_captured[1]:find("to stdout"))
assert_eq("stderr=false: stderr receives nothing", #stderr_captured, 0)
end
do
-- stderr=true: output goes to stderr, nothing to stdout
reset_log()
log.usecolor = false
log.stderr = true
local stdout_captured = {}
local stderr_captured = {}
io.stdout = { write = function(_, s) stdout_captured[#stdout_captured + 1] = s end }
io.stderr = { write = function(_, s) stderr_captured[#stderr_captured + 1] = s end }
log.info("to stderr")
io.stdout = real_stdout
io.stderr = real_stderr
assert_eq("stderr=true: stderr receives output", #stderr_captured, 1)
assert_true("stderr=true: stderr contains message", stderr_captured[1]:find("to stderr"))
assert_eq("stderr=true: stdout receives nothing", #stdout_captured, 0)
end
do
-- instance inherits stderr from global logger
reset_log()
log.stderr = true
local inst = log { usecolor = false }
assert_eq("instance inherits stderr from global logger", inst.stderr, true)
reset_log()
end
do
-- instance stderr overrides global
reset_log()
log.stderr = false
local inst = log { stderr = true, usecolor = false }
assert_eq("instance stderr overrides global", inst.stderr, true)
end
-- ── Summary ──────────────────────────────────────────────────────────────────
print(string.format("\n%d passed, %d failed", passed, failed))
if failed > 0 then os.exit(1) end