File Coverage

blib/lib/Log/Fmt/Test.pm
Criterion Covered Total %
statement 213 214 99.5
branch 3 6 50.0
condition 1 2 50.0
subroutine 15 15 100.0
pod 0 2 0.0
total 232 239 97.0


line stmt bran cond sub pod time code
1             package Log::Fmt::Test 3.101;
2              
3 1     1   107322 use v5.22.0; # lexical subs too buggy before this
  1         3  
4 1     1   3 use warnings;
  1         5  
  1         44  
5              
6 1     1   389 use experimental qw(lexical_subs postderef signatures);
  1         2847  
  1         4  
7 1     1   607 use utf8;
  1         198  
  1         3  
8              
9 1     1   357 use JSON::MaybeXS;
  1         7003  
  1         85  
10 1     1   462 use Log::Dispatchouli;
  1         3  
  1         36  
11 1     1   4 use Test::More 0.88;
  1         23  
  1         7  
12 1     1   843 use Test::Deep;
  1         6548  
  1         5  
13              
14             my sub messages_ok {
15 18     18   1110 my ($logger, $lines, $desc) = @_;
16              
17 18         44 local $Test::Builder::Level = $Test::Builder::Level+1;
18              
19 18         41 my @messages = map {; $_->{message} } $logger->events->@*;
  22         50  
20              
21 18         52 my $ok = cmp_deeply(
22             \@messages,
23             $lines,
24             $desc,
25             );
26              
27 18         29123 $logger->clear_events;
28              
29 18 50       28 unless ($ok) {
30 0         0 diag "GOT: $_" for @messages;
31             }
32              
33 18         164 return $ok;
34             }
35              
36             my sub event_logs_ok {
37 11     11   32 my ($event_type, $data, $line, $desc) = @_;
38              
39 11         23 local $Test::Builder::Level = $Test::Builder::Level+1;
40              
41 11         121 my $logger = Log::Dispatchouli::LogFmtTester->new_tester({
42             log_pid => 0,
43             ident => 't/basic.t',
44             });
45              
46 11         63 $logger->log_event($event_type, $data);
47              
48 11         35 messages_ok($logger, [$line], $desc);
49             }
50              
51             my sub parse_event_ok {
52 12     12   28 my ($event_string, $expect, $desc) = @_;
53              
54 12         21 local $Test::Builder::Level = $Test::Builder::Level+1;
55              
56 12         48 my $result = Log::Fmt->parse_event_string($event_string);
57              
58 12 50       29 cmp_deeply(
59             $result,
60             $expect,
61             $desc,
62             ) or note explain $result;
63             }
64              
65             sub logger_trio {
66 7     7 0 39 my $logger = Log::Dispatchouli::LogFmtTester->new_tester({
67             log_pid => 0,
68             ident => 't/basic.t',
69             });
70              
71 7         54 my $proxy1 = $logger->proxy({ proxy_ctx => { 'outer' => 'proxy' } });
72 7         27 my $proxy2 = $proxy1->proxy({ proxy_ctx => { 'inner' => 'proxy' } });
73              
74 7         24 return ($logger, $proxy1, $proxy2);
75             }
76              
77 1     1 0 250092 sub test_logfmt_implementation ($self, $logfmt_package) {
  1         3  
  1         2  
  1         1  
78 1         2 local $Log::Dispatchouli::LogFmtTester::LOG_FMT_PACKAGE = $logfmt_package;
79              
80             subtest "testing logfmt implementation $logfmt_package" => sub {
81             subtest "very basic stuff" => sub {
82 1         1434 event_logs_ok(
83             'world-series' => [ phl => 1, hou => 0, games => [ 'done', 'in-progress' ] ],
84             'event=world-series phl=1 hou=0 games.0=done games.1=in-progress',
85             "basic data with an arrayref value",
86             );
87              
88 1         8 parse_event_ok(
89             'event=world-series phl=1 hou=0 games.0=done games.1=in-progress',
90             [
91             event => 'world-series',
92             phl => 1,
93             hou => 0,
94             'games.0' => 'done',
95             'games.1' => 'in-progress',
96             ],
97             'we can parse something we produced'
98             );
99              
100 1         1177 event_logs_ok(
101             'programmer-sleepiness' => {
102             weary => 8.62,
103             excited => 3.2,
104             motto => q{Never say "never" ever again.},
105             },
106             'event=programmer-sleepiness excited=3.2 motto="Never say \\"never\\" ever again." weary=8.62',
107             "basic data as a hashref",
108             );
109              
110             {
111 1         4 my %kv = (
  1         3  
112             weary => 8.62,
113             excited => 3.2,
114             motto => q{Never say "never" ever again.},
115             );
116              
117 1         25 my $line = $logfmt_package->format_event_string([%kv]);
118              
119 1         5 cmp_deeply(
120             $logfmt_package->parse_event_string_as_hash($line),
121             \%kv,
122             "parse_event_string_as_hash works",
123             );
124             }
125              
126             parse_event_ok(
127 1         5424 'event=programmer-sleepiness excited=3.2 motto="Never say \\"never\\" ever again." weary=8.62',
128             [
129             event => 'programmer-sleepiness',
130             excited => '3.2',
131             motto => q{Never say "never" ever again.},
132             weary => '8.62',
133             ],
134             "parse an event with simple quotes",
135             );
136              
137 1         1172 event_logs_ok(
138             'rich-structure' => [
139             array => [
140             { name => [ qw(Ricardo Signes) ], limbs => { arms => 2, legs => 2 } },
141             [ 2, 4, 6 ],
142             ],
143             ],
144             join(q{ }, qw(
145             event=rich-structure
146             array.0.limbs.arms=2
147             array.0.limbs.legs=2
148             array.0.name.0=Ricardo
149             array.0.name.1=Signes
150             array.1.0=2
151             array.1.1=4
152             array.1.2=6
153             )),
154             "a structured nested a few levels",
155             );
156              
157 1         7 event_logs_ok(
158             'empty-key' => { '' => 'disgusting' },
159             'event=empty-key ~=disgusting',
160             "cope with jerks putting empty keys into the data structure",
161             );
162              
163 1         6 event_logs_ok(
164             'bogus-subkey' => { valid => { 'foo bar' => 'revolting' } },
165             'event=bogus-subkey valid.foo?bar=revolting',
166             "cope with bogus key characters in recursion",
167             );
168              
169 1         6 event_logs_ok(
170             'has-tab' => { tabby => "\tx = 1;" },
171             'event=has-tab tabby="\\tx = 1;"',
172             "tabs become \\t",
173             );
174              
175 1         5 parse_event_ok(
176             'event=has-tab tabby="\\tx = 1;"',
177             [ event => 'has-tab', tabby => "\tx = 1;" ],
178             "\\t becomes a tab",
179             );
180              
181 1         1124 event_logs_ok(
182             'has-eq' => { equals => "0=1" },
183             'event=has-eq equals="0=1"',
184             "including an = gets you quoted",
185             );
186              
187 1         7 parse_event_ok(
188             'event=has-eq equals="0=1"',
189             [ event => 'has-eq', equals => "0=1" ],
190             "= in input is fine",
191             );
192              
193 1         1782 event_logs_ok(
194             'has-backslash' => { revsol => "foo\\bar" },
195             'event=has-backslash revsol="foo\\\\bar"',
196             "including a \\ gets you quoted",
197             );
198              
199 1         7 event_logs_ok(
200             'key-has-backslash' => { 'a\\b' => "foo" },
201             'event=key-has-backslash a?b=foo',
202             "backslash in a key becomes question mark",
203             );
204              
205 1         10 parse_event_ok(
206             'event=has-backslash revsol="foo\\\\bar"',
207             [ event => 'has-backslash', revsol => "foo\\bar" ],
208             "\\ in input is fine",
209             );
210              
211 1         2132 event_logs_ok(
212             # Note that the ë at the end becomes UTF-8 encoded into octets.
213             ctrlctl => [ string => qq{NL \x0a CR \x0d "Q" ZWJ \x{200D} \\nothing ë}, ],
214             'event=ctrlctl string="NL \\n CR \\r \\"Q\\" ZWJ \\x{e2}\\x{80}\\x{8d} \\\\nothing ' . "\xc3\xab" . '"',
215             'control characters and otherwise',
216             );
217              
218 1         9 parse_event_ok(
219             'event=ctrlctl string="NL \\n CR \\r \\"Q\\" ZWJ \\x{e2}\\x{80}\\x{8d} \\\\nothing ' . "\xc3\xab" . '"',
220             [
221             event => 'ctrlctl',
222             string => qq{NL \x0a CR \x0d "Q" ZWJ \x{200D} \\nothing ë},
223             ],
224             "parse an event with simple quotes",
225             );
226              
227 1         2028 event_logs_ok(
228             spacey => [ string => qq{line \x{2028} spacer} ],
229             'event=spacey string="line \x{e2}\x{80}\x{a8} spacer"',
230             'non-control non-ascii vertical whitespace is also escaped',
231             );
232              
233 1         5 parse_event_ok(
234             'event=spacey string="line \x{e2}\x{80}\x{a8} spacer"',
235             [
236             event => 'spacey',
237             string => qq{line \x{2028} spacer}
238             ],
239             "parse an that has an escaped vertical whitespace cahracter",
240             );
241 1         1602 };
242              
243             subtest "parsing junk input" => sub {
244 1         920 parse_event_ok(
245             'junkword',
246             [ junk => 'junkword' ],
247             "bare word with no = becomes junk",
248             );
249              
250 1         1086 parse_event_ok(
251             'foo=bar bareword foo=baz',
252             [ foo => 'bar', junk => 'bareword', foo => 'baz' ],
253             "junk among valid pairs is captured with key 'junk'",
254             );
255              
256 1         1098 parse_event_ok(
257             'key=',
258             [ junk => 'key=' ],
259             "key= with no value is junk",
260             );
261              
262 1         1050 parse_event_ok(
263             'key="unclosed',
264             [ junk => 'key="unclosed' ],
265             "unclosed quoted string is junk",
266             );
267 1         3816 };
268              
269             subtest "parsing empty quoted value" => sub {
270 1         836 parse_event_ok(
271             'key=""',
272             [ key => '' ],
273             "empty quoted string parses to empty string",
274             );
275 1         2621 };
276              
277             subtest "very basic proxy operation" => sub {
278 1         821 my ($logger, $proxy1, $proxy2) = logger_trio();
279              
280 1         5 $proxy2->log_event(pie_picnic => [
281             pies_eaten => 1.2,
282             joy_harvested => 6,
283             ]);
284              
285 1         94 messages_ok(
286             $logger,
287             [
288             'event=pie_picnic outer=proxy inner=proxy pies_eaten=1.2 joy_harvested=6'
289             ],
290             'got the expected log output from events',
291             );
292 1         2380 };
293              
294             subtest "debugging in the proxies" => sub {
295 1         813 my ($logger, $proxy1, $proxy2) = logger_trio();
296              
297 1         5 $proxy1->set_debug(1);
298              
299 1         9 $logger->log_debug_event(0 => [ seq => 0 ]);
300 1         5 $proxy1->log_debug_event(1 => [ seq => 1 ]);
301 1         74 $proxy2->log_debug_event(2 => [ seq => 2 ]);
302              
303 1         59 $proxy2->set_debug(0);
304              
305 1         4 $logger->log_debug_event(0 => [ seq => 3 ]);
306 1         3 $proxy1->log_debug_event(1 => [ seq => 4 ]);
307 1         81 $proxy2->log_debug_event(2 => [ seq => 5 ]);
308              
309 1         4 messages_ok(
310             $logger,
311             [
312             # 'event=0 seq=0', # not logged, debugging
313             'event=1 outer=proxy seq=1',
314             'event=2 outer=proxy inner=proxy seq=2',
315             # 'event=0 seq=3', # not logged, debugging
316             'event=1 outer=proxy seq=4',
317             # 'event=2 outer=proxy inner=proxy seq=5', # not logged, debugging
318             ],
319             'got the expected log output from events',
320             );
321 1         1370 };
322              
323             # NOT TESTED HERE: "mute" and "unmute", which rjbs believes are probably
324             # broken already. Their tests don't appear to test the important case of "root
325             # logger muted, proxy explicitly unmuted".
326              
327             subtest "recursive structure" => sub {
328 1         831 my ($logger, $proxy1, $proxy2) = logger_trio();
329              
330 1         2 my $struct = {};
331              
332 1         3 $struct->{recurse} = $struct;
333              
334 1         4 $logger->log_event('recursive-thing' => [ recursive => $struct ]);
335              
336 1         4 messages_ok(
337             $logger,
338             [
339             'event=recursive-thing recursive.recurse=&recursive',
340             ],
341             "an event with recursive stuff terminates",
342             );
343 1         1375 };
344              
345             subtest "lazy values" => sub {
346 1         830 my ($logger) = logger_trio();
347              
348 1         4 my $called = 0;
349 1         4 my $callback = sub { $called++; return 'X' };
  2         3  
  2         3  
350              
351 1         5 $logger->log_event('sub-caller' => [
352             once => $callback,
353             twice => $callback,
354             ]);
355              
356 1         5 $logger->log_debug_event('sub-caller' => [
357             d_once => $callback,
358             d_twice => $callback,
359             ]);
360              
361 1         4 messages_ok(
362             $logger,
363             [
364             'event=sub-caller once=X twice=X',
365             ],
366             "we call sublike arguments to lazily compute",
367             );
368              
369 1         5 is($called, 2, "only called twice; debug events did not call sub");
370 1         1376 };
371              
372             subtest "lazy values in proxy context" => sub {
373 1         816 my ($logger) = logger_trio();
374              
375 1         5 my $called_A = 0;
376 1         4 my $callback_A = sub { $called_A++; return 'X' };
  1         2  
  1         2  
377              
378 1         2 my $called_B = 0;
379 1         2 my $callback_B = sub { $called_B++; return 'X' };
  1         2  
  1         2  
380              
381 1         5 my $proxy1 = $logger->proxy({ proxy_ctx => [ outer => $callback_A ] });
382 1         3 my $proxy2 = $proxy1->proxy({ proxy_ctx => [ inner => $callback_B ] });
383              
384 1         5 $proxy1->log_event('outer-event' => [ guitar => 'electric' ]);
385              
386 1         71 is($called_A, 1, "outer proxy did log, called outer callback");
387 1         376 is($called_B, 0, "outer proxy did log, didn't call inner callback");
388              
389 1         331 $proxy2->log_event('inner-event' => [ mandolin => 'bluegrass' ]);
390              
391 1         95 is($called_A, 1, "inner proxy did log, didn't re-call outer callback");
392 1         332 is($called_B, 1, "inner proxy did log, did call inner callback");
393              
394 1         332 $proxy2->log_event('inner-second' => [ snare => 'infinite' ]);
395              
396 1         72 messages_ok(
397             $logger,
398             [
399             'event=outer-event outer=X guitar=electric',
400             'event=inner-event outer=X inner=X mandolin=bluegrass',
401             'event=inner-second outer=X inner=X snare=infinite',
402             ],
403             "all our laziness didn't change our results",
404             );
405 1         1760 };
406              
407             subtest "reused JSON booleans" => sub {
408             # It's not that this is extremely special, but we mostly don't want to
409             # recurse into the same reference value multiple times, but we also don't
410             # want the infuriating "reused boolean variable" you get from Dumper. This
411             # is just to make sure I don't accidentally break this case.
412 1         830 my ($logger, $proxy1, $proxy2) = logger_trio();
413              
414 1         8 my $struct = {
415             b => [ JSON::MaybeXS::true(), JSON::MaybeXS::false() ],
416             f => [ (JSON::MaybeXS::false()) x 3 ],
417             t => [ (JSON::MaybeXS::true()) x 3 ],
418             };
419              
420 1         5 $logger->log_event('tf-thing' => [ cond => $struct ]);
421              
422 1         4 messages_ok(
423             $logger,
424             [
425             'event=tf-thing cond.b.0=1 cond.b.1=0 cond.f.0=0 cond.f.1=0 cond.f.2=0 cond.t.0=1 cond.t.1=1 cond.t.2=1',
426             ],
427             "JSON bools do what we expect",
428             );
429 1         1636 };
430              
431             subtest "JSON-ification of refrefs" => sub {
432 1         833 my ($logger, $proxy1, $proxy2) = logger_trio();
433              
434 1         7 $logger->log_event('json-demo' => [
435             foo => { a => 1 },
436             bar => \{ a => 1 },
437             baz => \[ 12, 34 ],
438             ]);
439              
440 1         5 my @messages = map {; $_->{message} } $logger->events->@*;
  1         4  
441              
442 1         5 messages_ok(
443             $logger,
444             [
445             # XS and PP versions of JSON differ on space, so we need "12, 34" and
446             # "12,34" both. Then things get weird, because the version with no
447             # spaces (pure perl, at least as of today) doesn't need to be quoted to
448             # be used as a logfmt value, so the quotes are now optional. Wild.
449             # -- rjbs, 2023-09-02
450             any(
451             'event=json-demo foo.a=1 bar="{{{\"a\": 1}}}" baz="{{[12, 34]}}"',
452             'event=json-demo foo.a=1 bar="{{{\"a\": 1}}}" baz={{[12,34]}}',
453             ),
454             ],
455             "refref becomes JSON flogged",
456             );
457              
458 1         32 my $result = $logfmt_package->parse_event_string($messages[0]);
459              
460 1         4 cmp_deeply(
461             $result,
462             [
463             event => 'json-demo',
464             'foo.a' => 1,
465             bar => "{{{\"a\": 1}}}",
466             baz => any("{{[12, 34]}}", "{{[12,34]}}"),
467             ],
468             "parsing gets us JSON string out, because it is just strings",
469             );
470              
471 1         1230 my ($json_string) = $result->[5] =~ /\A\{\{(.+)\}\}\z/;
472 1         9 my $json_struct = decode_json($json_string);
473              
474 1         4 cmp_deeply(
475             $json_struct,
476             { a => 1 },
477             "we can round trip that JSON",
478             );
479 1         1367 };
480              
481             my sub kvstrs_ok {
482 28         2347 my ($pairs, $expected, $desc) = @_;
483 28         40 local $Test::Builder::Level = $Test::Builder::Level + 1;
484              
485 28         120 my $got = $logfmt_package->_pairs_to_kvstr_aref($pairs);
486 28 50       71 cmp_deeply($got, $expected, $desc) or diag explain $got;
487             }
488              
489             subtest "simple key=value pairs" => sub {
490 1         863 kvstrs_ok(
491             [ foo => 'bar' ],
492             [ 'foo=bar' ],
493             "simple bare value",
494             );
495              
496 1         1065 kvstrs_ok(
497             [ foo => 'bar', baz => 'quux' ],
498             [ 'foo=bar', 'baz=quux' ],
499             "multiple simple pairs",
500             );
501              
502 1         1043 kvstrs_ok(
503             [ phl => 1, hou => 0 ],
504             [ 'phl=1', 'hou=0' ],
505             "numeric values",
506             );
507 1         2783 };
508              
509             subtest "values needing quoting" => sub {
510 1         834 kvstrs_ok(
511             [ msg => 'hello world' ],
512             [ 'msg="hello world"' ],
513             "value with space gets quoted",
514             );
515              
516 1         1092 kvstrs_ok(
517             [ eq => '0=1' ],
518             [ 'eq="0=1"' ],
519             "value with = gets quoted",
520             );
521              
522 1         1064 kvstrs_ok(
523             [ q => 'say "hi"' ],
524             [ 'q="say \\"hi\\""' ],
525             "value with double quotes gets escaped",
526             );
527              
528 1         1058 kvstrs_ok(
529             [ bs => 'foo\\bar' ],
530             [ 'bs="foo\\\\bar"' ],
531             "value with backslash gets escaped",
532             );
533              
534 1         1053 kvstrs_ok(
535             [ tabby => "\tx = 1;" ],
536             [ 'tabby="\\tx = 1;"' ],
537             "tab becomes \\t",
538             );
539              
540 1         1042 kvstrs_ok(
541             [ nl => "line1\nline2" ],
542             [ 'nl="line1\\nline2"' ],
543             "newline becomes \\n",
544             );
545              
546 1         1104 kvstrs_ok(
547             [ cr => "a\rb" ],
548             [ 'cr="a\\rb"' ],
549             "carriage return becomes \\r",
550             );
551 1         2519 };
552              
553             subtest "empty and invalid keys" => sub {
554 1         874 kvstrs_ok(
555             [ '' => 'val' ],
556             [ '~=val' ],
557             "empty key becomes ~",
558             );
559              
560 1         1087 kvstrs_ok(
561             [ 'foo bar' => 'val' ],
562             [ 'foo?bar=val' ],
563             "space in key becomes ?",
564             );
565              
566 1         1050 kvstrs_ok(
567             [ 'a=b' => 'val' ],
568             [ 'a?b=val' ],
569             "= in key becomes ?",
570             );
571              
572 1         1071 kvstrs_ok(
573             [ 'a"b' => 'val' ],
574             [ 'a?b=val' ],
575             "double quote in key becomes ?",
576             );
577              
578 1         1074 kvstrs_ok(
579             [ "a\\b" => 'val' ],
580             [ 'a?b=val' ],
581             "backslash in key becomes ?",
582             );
583 1         2823 };
584              
585             subtest "undef values" => sub {
586 1         838 kvstrs_ok(
587             [ key => undef ],
588             [ 'key=~missing~' ],
589             "undef value becomes ~missing~",
590             );
591 1         2682 };
592              
593             subtest "nested arrayrefs" => sub {
594 1         830 kvstrs_ok(
595             [ games => [ 'done', 'in-progress' ] ],
596             [ 'games.0=done', 'games.1=in-progress' ],
597             "arrayref values get flattened with numeric indices",
598             );
599              
600 1         1096 kvstrs_ok(
601             [ arr => [ 'a', 'b', 'c' ] ],
602             [ 'arr.0=a', 'arr.1=b', 'arr.2=c' ],
603             "three-element array",
604             );
605 1         2373 };
606              
607             subtest "nested hashrefs" => sub {
608 1         827 kvstrs_ok(
609             [ data => { alpha => 1, beta => 2 } ],
610             [ 'data.alpha=1', 'data.beta=2' ],
611             "hashref values get flattened with sorted keys",
612             );
613 1         2527 };
614              
615             subtest "deeply nested structures" => sub {
616 1         825 kvstrs_ok(
617             [
618             array => [
619             { name => [ 'Ricardo', 'Signes' ], limbs => { arms => 2, legs => 2 } },
620             [ 2, 4, 6 ],
621             ],
622             ],
623             [
624             'array.0.limbs.arms=2',
625             'array.0.limbs.legs=2',
626             'array.0.name.0=Ricardo',
627             'array.0.name.1=Signes',
628             'array.1.0=2',
629             'array.1.1=4',
630             'array.1.2=6',
631             ],
632             "deeply nested array/hash structure",
633             );
634 1         2444 };
635              
636             subtest "recursive structures" => sub {
637 1         822 my $struct = {};
638 1         4 $struct->{recurse} = $struct;
639              
640 1         4 kvstrs_ok(
641             [ recursive => $struct ],
642             [ 'recursive.recurse=&recursive' ],
643             "recursive hashref produces backreference",
644             );
645 1         2508 };
646              
647             subtest "coderef (lazy) values" => sub {
648 1         1010 my $called = 0;
649 1         5 my $cb = sub { $called++; return 'lazy_val' };
  1         3  
  1         3  
650              
651 1         5 kvstrs_ok(
652             [ key => $cb ],
653             [ 'key=lazy_val' ],
654             "coderef is called to produce value",
655             );
656              
657 1         1223 is($called, 1, "coderef called exactly once");
658 1         2431 };
659              
660             subtest "ref-to-ref (String::Flogger)" => sub {
661 1         1077 kvstrs_ok(
662             [ bar => \{ a => 1 } ],
663             [ re(qr/^bar=/) ],
664             "refref produces flogged output",
665             );
666 1         2364 };
667              
668             subtest "UTF-8 values" => sub {
669             # ë (U+00EB) is a safe non-ASCII character — should appear as UTF-8 bytes
670             # in the output without \x{} escaping
671 1         1060 kvstrs_ok(
672             [ name => "Jürgen" ],
673             [ "name=\"J\xc3\xbcrgen\"" ],
674             "safe non-ASCII chars (ü) are UTF-8 encoded directly",
675             );
676 1         3256 };
677              
678             subtest "control characters and special escapes" => sub {
679             # ZWJ (U+200D) is a Cf character — gets \x{} escaped
680 1         1059 kvstrs_ok(
681             [ string => "NL \x0a CR \x0d \"Q\" ZWJ \x{200D} \\nothing \x{00EB}" ],
682             [ 'string="NL \\n CR \\r \\"Q\\" ZWJ \\x{e2}\\x{80}\\x{8d} \\\\nothing ' . "\xc3\xab" . '"' ],
683             "control chars, ZWJ, quotes, backslash, and ë all handled correctly",
684             );
685 1         3095 };
686              
687             subtest "vertical whitespace" => sub {
688             # LINE SEPARATOR (U+2028) should be escaped to its UTF-8 bytes
689 1         1551 kvstrs_ok(
690             [ string => "line \x{2028} spacer" ],
691             [ "string=\"line \\x{e2}\\x{80}\\x{a8} spacer\"" ],
692             "LINE SEPARATOR is escaped via UTF-8 byte \x{} sequences",
693             );
694 1         3835 };
695              
696             subtest "empty value" => sub {
697 1         1649 kvstrs_ok(
698             [ key => '' ],
699             [ 'key=""' ],
700             "empty string gets quoted",
701             );
702 1         5376 };
703              
704             subtest "bogus subkey characters" => sub {
705 1         1321 kvstrs_ok(
706             [ valid => { 'foo bar' => 'revolting' } ],
707             [ 'valid.foo?bar=revolting' ],
708             "bogus key chars in recursion become ?",
709             );
710 1         5004 };
711              
712             subtest "prefix handling" => sub {
713             # Test with explicit prefix argument
714 1         1041 my $got = Log::Fmt->_pairs_to_kvstr_aref(
715             [ alpha => 1, beta => 2 ],
716             {},
717             'pfx',
718             );
719 1         6 cmp_deeply(
720             $got,
721             [ 'pfx.alpha=1', 'pfx.beta=2' ],
722             "explicit prefix prepended to keys",
723             );
724 1         3075 };
725              
726             subtest "match full format_event_string output" => sub {
727             # Verify XS output matches what format_event_string produces
728 1         1045 my @test_cases = (
729             {
730             input => [ phl => 1, hou => 0, games => [ 'done', 'in-progress' ] ],
731             expected => 'phl=1 hou=0 games.0=done games.1=in-progress',
732             desc => 'basic data with arrayref',
733             },
734             {
735             input => [ tabby => "\tx = 1;" ],
736             expected => 'tabby="\\tx = 1;"',
737             desc => 'tab escape',
738             },
739             {
740             input => [ equals => "0=1" ],
741             expected => 'equals="0=1"',
742             desc => 'equals sign quoted',
743             },
744             {
745             input => [ revsol => "foo\\bar" ],
746             expected => 'revsol="foo\\\\bar"',
747             desc => 'backslash quoted',
748             },
749             );
750              
751 1         4 for my $tc (@test_cases) {
752 4         1337 my $got = $logfmt_package->format_event_string($tc->{input});
753 4         18 is($got, $tc->{expected}, "format_event_string: $tc->{desc}");
754             }
755 1         3029 };
756             }
757 1         15 }
758              
759             package Log::Dispatchouli::LogFmtTester 3.101 {
760 1     1   2951 use parent 'Log::Dispatchouli';
  1         1  
  1         9  
761             our $LOG_FMT_PACKAGE;
762 28   50 28   183 sub _log_fmt_package { $LOG_FMT_PACKAGE // die "no package supplied" }
763             }
764              
765             1;
766              
767             __END__
768              
769             =pod
770              
771             =encoding UTF-8
772              
773             =head1 NAME
774              
775             Log::Fmt::Test
776              
777             =head1 VERSION
778              
779             version 3.101
780              
781             =head1 PERL VERSION
782              
783             This library should run on perls released even a long time ago. It should
784             work on any version of perl released in the last five years.
785              
786             Although it may work on older versions of perl, no guarantee is made that the
787             minimum required version will not be increased. The version may be increased
788             for any reason, and there is no promise that patches will be accepted to
789             lower the minimum required perl.
790              
791             =head1 AUTHOR
792              
793             Ricardo SIGNES <cpan@semiotic.systems>
794              
795             =head1 COPYRIGHT AND LICENSE
796              
797             This software is copyright (c) 2026 by Ricardo SIGNES.
798              
799             This is free software; you can redistribute it and/or modify it under
800             the same terms as the Perl 5 programming language system itself.
801              
802             =cut