3
# logformatter - highlight a Cirrus test log (ginkgo or bats)
5
# Adapted from https://raw.githubusercontent.com/edsantiago/greasemonkey/podman-ginkgo-highlight
7
package LibPod::CI::LogFormatter;
12
# Grumble. CI system doesn't have 'open'
13
binmode STDIN, ':utf8';
14
binmode STDOUT, ':utf8';
15
binmode STDERR, ':utf8';
20
(our $ME = $0) =~ s|.*/||;
27
# For debugging, show data structures using DumpTree($var)
28
#use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0;
30
###############################################################################
31
# BEGIN user-customizable section
33
# Stylesheet for highlighting or de-highlighting parts of lines
34
our $CSS = <<'END_CSS';
35
/* wrap long lines - don't require user to scroll right */
36
pre { line-break: normal; overflow-wrap: normal; white-space: pre-wrap; }
38
/* synopsis table at top */
39
table.synopsis { border: none; border-collapse: collapse; margin-left: 2em; margin-top: 2ex; }
40
.synopsis th { font-weight: normal; font-size: 110%; text-align: right; }
41
.synopsis td { font-weight: bold; font-size: 120%; font-family: monospace; }
44
.testname { font-size: 125%; color: #444; }
45
.boring { color: #999; }
46
.timestamp { color: #999; }
47
.log-debug { color: #999; }
48
.log-info { color: #333; }
49
.log-warning { color: #f60; }
50
.log-error { background: #fee; color: #900; font-weight: bold; }
51
.error-retry { color: #c33; font-size: 125%; font-weight: bold; border-top: 1px solid #f00; }
52
.log-passed { color: #393; }
53
.log-failed { color: #F00; font-weight: bold; font-size: 150%; }
54
.log-skipped { color: #F90; }
55
.log-flakey { background: #f93; font-weight: bold; font-size: 150%; }
56
.log-slow { background: #FF0; color: #000; font-weight: bold; }
57
.subtest { background: #eee; }
58
.subsubtest { color: #F39; font-weight: bold; }
59
.string { color: #00c; }
60
.command { font-weight: bold; color: #000; }
61
.changed { color: #000; font-weight: bold; }
63
/* Ginkgo "Enter/Exit [foo]": visually separate from each other */
64
.ginkgo-timeline { margin-top: 1ex; margin-bottom: 1ex; }
66
/* BeforeEach and AfterEach tend to be boring. Gray them out. */
67
.ginkgo-beforeeach { background: #f0f0f0; color: #999; }
68
.ginkgo-aftereach { background: #f0f0f0; color: #999; }
69
.ginkgo-beforeeach b { color: #000; }
70
.ginkgo-aftereach b { color: #000; }
72
/* [It] is where the test happens but we don't need any special highlights */
75
/* Final summary line at bottom */
76
.ginkgo-final-success { background: #393; font-weight: bold; font-size: 150%; }
77
.ginkgo-final-fail { background: #f00; font-weight: bold; font-size: 150%; }
79
/* links to source files: not as prominent as links to errors */
80
a.codelink:link { color: #000; }
81
a.codelink:visited { color: #666; }
82
a.codelink:hover { background: #000; color: #999; }
84
/* The timing tests at bottom: remove underline, it's too cluttery. */
85
a.timing { text-decoration: none; }
86
.timing:hover { background: #FF9; } /* highlight row for easy reading */
89
.bats-passed { color: #393; }
90
.bats-failed { color: #F00; font-weight: bold; }
91
.bats-flaked { color: #F93; font-weight: bold; }
92
.bats-skipped { color: #F90; }
93
.bats-log { color: #933; }
94
.bats-log-failblock { color: #b00; background-color: #fee; display: inline-flex; margin: 0 -500%; padding: 0 500% !important; }
96
.bats-summary { font-size: 150%; }
98
/* error titles: display next to timestamp, not on separate line */
99
h2,h3 { display: inline; }
102
# END user-customizable section
103
###############################################################################
105
###############################################################################
106
# BEGIN boilerplate args checking, usage messages
110
Usage: $ME [OPTIONS] TEST_NAME
112
$ME is a filter; it HTMLifies an input stream (presumably
113
Ginkgo or BATS log results), writing HTML results to an output file
114
but passing stdin unmodified to stdout. It is intended to run in
115
the Cirrus CI environment.
119
TEST_NAME descriptive name; output file will be TEST_NAME.log.html
123
--help display this message
124
--man display program man page
125
--version display program name and version
131
# Command-line options. Note that this operates directly on @ARGV !
135
our $NOT = ''; # print "blahing the blah$NOT\n" if $debug
140
'dry-run|n!' => sub { $NOT = ' [NOT]' },
142
'verbose|v' => \$verbose,
145
version => sub { print "$ME version $VERSION\n"; exit 0 },
146
) or die "Try `$ME --help' for help\n";
149
# END boilerplate args checking, usage messages
150
###############################################################################
152
############################## CODE BEGINS HERE ###############################
154
# The term is "modulino".
155
__PACKAGE__->main() unless caller();
159
# Note that we operate directly on @ARGV, not on function parameters.
160
# This is deliberate: it's because Getopt::Long only operates on @ARGV
161
# and there's no clean way to make it use @_.
162
handle_opts(); # will set package globals
164
# In case someone is tempted to run us on the command line
165
die "$ME: this is a filter, not an interactive script\n" if -t *STDIN;
167
# Fetch command-line arguments. Barf if too many.
168
my $test_name = shift(@ARGV)
169
or die "$ME: missing TEST_NAME argument; try $ME --help\n";
170
warn "$ME: Too many arguments; ignoring extras. try $ME --help\n" if @ARGV;
172
format_log($test_name);
177
my $test_name = shift; # in: e.g. 'integration_test'
179
my $outfile = "$test_name.log.html";
180
my $out_tmp = "$outfile.tmp.$$";
181
open my $out_fh, '>:utf8', $out_tmp
182
or warn "$ME: Cannot create $out_tmp: $!\n";
184
# Boilerplate: HTML headers for output file
185
print { $out_fh } <<"END_HTML" if $out_fh;
186
<?xml version="1.0" encoding="utf-8"?>
188
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
189
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
190
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US">
192
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
193
<title>$test_name</title>
194
<style type="text/css">
198
<!-- on page load, go to bottom: that's where the error summary is -->
199
<script language="javascript">
200
function scrollToBottom() {
201
if (window.scrollY < 10) {
202
window.scrollTo(0, document.body.scrollHeight);
205
window.addEventListener("load", scrollToBottom, false);
211
# Synopsis of this job: show job environment, links to PR and Cirrus
212
print { $out_fh } "<h2>Synopsis</h2>\n<hr/>\n",
213
job_synopsis($test_name), "<hr/>\n";
215
# FOR DEBUGGING: dump environment, but in HTML comments to not clutter
216
# This is safe. There is a TOKEN envariable, but it's not sensitive.
217
# There are no sensitive/secret values in our execution environment,
218
# but we're careful anyway. $SECRET_ENV_RE is set in lib.sh
219
my $filter_re = $ENV{SECRET_ENV_RE} || 'ACCOUNT|GC[EP]|PASSW|SECRET|TOKEN';
220
$filter_re .= '|BASH_FUNC'; # These are long and un-useful
222
print { $out_fh } "<!-- Environment: -->\n";
223
for my $e (sort keys %ENV) {
224
next if $e =~ /$filter_re/;
226
my $val = escapeHTML($ENV{$e});
227
$val =~ s/--/--/g; # double dash not valid in comments
228
printf { $out_fh } "<!-- %-20s %s -->\n", $e, $val;
232
my $previous_timestamp = ''; # timestamp of previous line
233
my $previous_timestamp_fine; # fine-grain timestamp (BATS only)
234
my $cirrus_task; # Cirrus task number, used for linking
235
my $git_commit; # git SHA, used for linking to source files
236
my $subtest_status; # pass, fail, skip, flake - for each subtest
237
my $subtest_name; # assembled from two or more Describe()/It()s
238
my $in_failure; # binary flag: are we in an error dump?
239
my $in_timing; # binary flag: are we in the timing section?
240
my $after_divider = 999; # Count of lines after seeing '-----'
241
my $current_output; # for removing duplication
242
my $looks_like_bats; # binary flag: for detecting BATS results
243
my $looks_like_python; # " " " : for colorizing python tests
244
my %bats_count; # For summary line: count of pass/fail/skip
246
# When running in cirrus, we have the commit SHA
247
$git_commit = $ENV{CIRRUS_CHANGE_IN_REPO};
249
print { $out_fh } "<pre> <!-- begin processed output -->\n";
251
# Assume rootful prompt, check for rootless (here and in log itself, below)
253
$Prompt = '$' if $test_name =~ /rootless/;
255
# Main loop: read input, one line at a time, and write out reformatted
257
while (my $line = <STDIN>) {
258
# ARGH. Some log files have NUL characters , apparently because
259
# certain tests write tar to stdout. Bleagh. Although it seems
260
# rude to strip those from our (purportedly untouched) log,
261
# it's worse to read log files with random NULs.
264
print $line; # Immediately dump back to stdout
266
$Prompt = '$' if $line =~ /Runner executing .* as rootless /;
268
# Remain robust in face of errors: always write stdout even if no HTML
269
next LINE if ! $out_fh;
272
$line = escapeHTML($line);
274
# Temporarily strip off leading timestamp
275
$line =~ s/^(\[\+\d+s\]\s)//;
276
my $timestamp = $1 || '';
277
if ($previous_timestamp && $timestamp eq $previous_timestamp) {
278
$timestamp = ' ' x length($timestamp);
281
$previous_timestamp = $timestamp;
284
# Helper function for printing a formatted line. This should almost
285
# always be followed by 'next LINE'.
286
my $print_line = sub {
289
print { $out_fh } "<span class=\"timestamp\">$timestamp</span>"
291
print { $out_fh } "<span class='$css'>" if $css;
292
print { $out_fh } $line;
293
print { $out_fh } "</span>" if $css;
294
print { $out_fh } "\n";
297
# ARGH! Special case for tests that run 'cat /proc/self/attr/current:
298
# that file terminates with NUL, so ginkgo logs concatenate the next
299
# output line, which is usually "Running: next-podman-command".
300
# This makes the log readable by splitting into two lines.
301
if ($line =~ /^(\s*)(\S+_u:\S+_t:\S+:c\S+)(Running:.*)/) {
302
my ($indent, $selinux, $nextline) = ($1||'', $2, $3);
303
# Print the SELinux line, unmodified...
304
$line = $indent . $selinux;
306
# ...then forget that, and continue processing (fall through)
307
# with the 'Running' line.
308
$line = $indent . $nextline;
311
# Try to identify the git commit we're working with...
312
if ($line =~ m!/define.gitCommit=([0-9a-f]+)!) {
315
# ...so we can link to specific lines in source files
317
# 1 12 3 34 4 5 526 6
318
$line =~ s{^(.*)(\/(containers\/[^/]+)(\/\S+):(\d+))(.*)$}
319
{$1<a class="codelink" href='https://github.com/$3/blob/$git_commit$4#L$5'>$2</a>$6};
321
# Same, for python errors
323
$line =~ s{^(.*)(\/(containers\/[^/]+)(\/\S+\.py).*,\s+line\s+(\d+))(,\s+in.*)$}
324
{$1<a class="codelink" href='https://github.com/$3/blob/$git_commit$4#L$5'>$2</a>$6};
326
# And, sigh, Macintosh always has to be different
328
$line =~ s{^(.*/ci/task-\d+)((/\S+):(\d+))(.*)$}
329
{$1<a class="codelink" href="https://github.com/containers/podman/blob/$git_commit$3#L$4">$2</a>$5};
333
$line =~ s{^(.*/Local/cirrus-ci-build/repo)((/\S+):(\d+))(.*)$}
334
{$1<a class="codelink" href="https://github.com/containers/podman/blob/$git_commit$3#L$4">$2</a>$5}
337
# Try to identify the cirrus task
338
if ($line =~ /cirrus-task-(\d+)/) {
342
# logrus messages, always highlighted
343
# 1 2 2 13 34 4 5 56 6 7 78 8
344
if ($line =~ /^(\s*(#\s)?)(time=)(.*) (level=)(\S+) (msg=)(.*)/) {
345
my $span = "<span class='log-$6'>";
346
$line = "$1$3$span$4</span> $5$span$6</span> $7$span$8</span>";
349
# BATS handling. This will recognize num_tests both at start and end
350
if ($line =~ /^1\.\.(\d+)$/) {
351
$looks_like_bats = 1;
352
$bats_count{expected_total} = $1;
353
undef $looks_like_python;
355
# Since the number of tests can't always be predicted, recognize
356
# some leading text strings that indicate BATS output to come.
357
elsif ($line =~ /^TAP\s+version\s/ || $line =~ m!/test-apiv2!) {
358
$looks_like_bats = 1;
359
$bats_count{expected_total} = -1; # Expect to be overridden at end!
360
undef $looks_like_python;
363
# 'python -m unittest' means we're starting some pythony stuff
364
elsif ($line =~ m!/python.*\sunittest\s!) {
365
$looks_like_python = 1;
366
undef $looks_like_bats;
368
elsif ($looks_like_python && $line =~ m!Ran\s+(\d+)\s+tests\s+in\s!) {
369
# End of python tests. However, we're still likely to see a
370
# summary line saying 'OK' or 'FAILED'. Deal with that by
371
# resetting $looks_like_python to 0, which the next elsif catches
372
$bats_count{expected_total} += $1;
373
$looks_like_python = 0;
374
print { $out_fh } "</div>\n" if $in_failure;
377
elsif (defined($looks_like_python) && !$looks_like_python) {
378
# The final python summary line. Show it in its appropriate color.
379
if ($line =~ /^\s*(OK|FAILED)\s+\(/) {
380
undef $looks_like_python;
381
my $css = ($1 eq 'OK' ? 'passed' : 'failed');
382
$print_line->("bats-$css");
387
if ($looks_like_bats) {
390
# 2023-05-16 run_podman() now displays NS-precision timestamps
391
# on commands and their output. This is cluttery, so if we
392
# see these, strip them and display in the left-hand (coarse)
393
# timestamp column instead. Hovering will display the full time.
396
if ($line =~ s{^(#\s)\[((\d+):(\d+):(\d+\.\d+))\]\s}{$1}) {
397
my ($full, $h, $m, $s) = ($2, $3, $4, $5);
398
my $timestamp_fine = $h * 3600.0 + $m * 60.0 + $s;
400
if ($previous_timestamp_fine) {
401
# This will fail if we do a midnight wraparound. NBD.
402
my $delta = $timestamp_fine - $previous_timestamp_fine;
404
# Human-readable format
406
$short = sprintf("%04ds", $delta);
409
$short = sprintf("%03.2fs", $delta);
411
elsif ($delta > 0.001) {
412
$short = sprintf("%03dms", $delta * 1000.0);
416
$short = sprintf("%03dns", $delta * 1_000_000.0);
423
# left-hand timestamp [+0001s] becomes <+013ms>
424
$timestamp = "<span title=\"$full\"><+$short></span> ";
425
$previous_timestamp_fine = $timestamp_fine;
428
# Readability: /long/path/to/podman -> podman (hover for full path)
429
# Also make it boldface, to make commands stand out
430
$line =~ s{^(#\s+(#|\$)\s+)(\S+/)(podman\S*)(\s.*)}
431
{$1<b><span title="$3$4">$4</span>$5</b>};
433
if ($line =~ /^ok\s.*\s# skip/) { $css = 'skipped' }
434
elsif ($line =~ /^ok\s/) { $css = 'passed' }
435
elsif ($line =~ /^not\s+ok\s/) { $css = 'failed' }
436
elsif ($line =~ /^# #(\/v|\| |\\\^)/) { $css = 'log-failblock' }
437
elsif ($line =~ /^#\s/) { $css = 'log' }
439
# Link to source file. This is ugly: we have to hardcode 'podman'
440
# and 'test/system' because there's no way to get them from log.
443
$line =~ s{(in(\stest)?\s+file\s+\S+/)(\S+\.(bats|bash)),\s+line\s+(\d+)}{$1<a class="codelink" href="https://github.com/containers/podman/blob/$git_commit/test/system/$3#L$5">$3, line $5</a>};
446
# Make it linkable, e.g. foo.html#t--00001
447
if ($line =~ /^(not\s+)?ok\s+(\d+)\s+(.*)/) {
448
$line = sprintf("<a name='t--%05d'>%s</a>", $2, $line);
450
push @{$bats_count{__fail_list}}, [ $2, $3 ] if $1;
459
elsif ($looks_like_python) {
462
if ($line =~ /\s\.\.\.\sskipped/) { $css = 'skipped' }
463
elsif ($line =~ /\s\.\.\.\sok\s*$/) { $css = 'passed' }
464
elsif ($line =~ /\s\.\.\.\sFAIL/) { $css = 'failed' }
465
elsif ($line =~ /^\s*={40}/) {
466
# Begins a block of multiple lines including a stack trace
467
print { $out_fh } "<div class='log-error'>\n" unless $in_failure;
482
if ($line =~ s!^(\s*)(&[gl]t;)\s+(Enter|Exit)\s+\[(\w+)\]!!) {
483
my ($indent, $arrow, $action, $block) = ($1, $2, $3, $4);
484
if ($action eq 'Enter') {
485
printf { $out_fh } "<div class=\"ginkgo-timeline ginkgo-%s\">",
487
$line = "$indent→ Enter [<b>$block</b>]$line";
490
# machine tests, run without -p, don't have a blank line
491
# separating headers from logs; but this works just fine.
492
$after_divider = 999;
495
# Can be a div within a div
498
print { $out_fh } "</div>";
501
$line = "$indent← Exit [$block]$line";
504
print { $out_fh } "</div>";
510
# Ginkgo v2 nicely lumps all retries for the same given test
511
if ($line =~ /^\s*Attempt\s+\#\d+\s+Failed\.\s+Retr/) {
512
$print_line->("error-retry");
516
# Timing section at the bottom of the page
517
if ($line =~ / timing results\s*$/) {
521
if ($line =~ /^\s*(\S.*\S)\s+(\d+\.\d+)\s*$/) {
522
my ($name, $time) = ($1, $2);
523
my $id = make_id($1, 'timing');
525
# Try to column-align the timing numbers. Some test names
526
# will be longer than our max - oh well.
527
my $spaces = 90 - length(unescapeHTML($name));
528
$spaces = 1 if $spaces < 1;
529
$spaces++ if $time < 10;
530
my $spacing = ' ' x $spaces;
531
$line = qq{<span class="timing"><a href="#t--$id">$name</a>$spacing$time</span>};
538
# Ginkgo summary line. Colorize Passed, Failed, Flaked, Skipped
539
if ($line =~ /^(\s*)(FAIL|SUCCESS)!(\s+--\s+)(.*\d+\sPassed.*)/) {
540
my ($indent, $status, $dashes, $rhs) = ($1, $2, $3, $4);
541
my @counts = split('\|', $rhs);
543
for my $c (@counts) {
544
$c =~ /^(\s*)(\d+)\s+(\w+)(\s*)$/
545
or warn "$ME: line $.: WEIRD: '$c' in '$rhs' from '$line'";
546
my ($lhs, $n, $category, $rhs) = ($1||'', $2, $3, $4||'');
547
# Only highlight if count > 0, so "0 Failed" doesn't yell
549
# Yes, we use 'bats-xxxx' classes even though we're ginkgo.
550
push @formatted, sprintf("%s<span class=\"bats-%s\"><b>%d</b> %s</span>%s",
551
$lhs, lc($category), $n, $category, $rhs);
554
# Zero. Leave unhighlighted.
558
$line = sprintf("%s<span class=\"ginkgo-final-%s\">%s!</span>%s%s",
560
lc($status), $status,
561
$dashes, join('|', @formatted));
567
# Ginkgo error reformatting
569
if ($line =~ /^\s*\[(FAILED|PANICKED)\]/) {
570
# Begins a block of multiple lines including a stack trace
571
print { $out_fh } "<div class='log-error'>\n";
574
elsif ($line =~ /^-----------/) {
576
# Ends a stack trace block
578
print { $out_fh } "</div>\n";
581
$subtest_status = 'passed'; # until proven otherwise
584
print { $out_fh } "</pre>\n<hr />\n<pre>\n";
585
# Always show timestamp at start of each new test
586
$previous_timestamp = '';
589
# (bindings test sometimes emits 'Running' with leading bullet char)
590
elsif ($line =~ s!^•?(\s*)Running:!<span class="boring">$1$Prompt</span>!) {
591
# Highlight the important (non-boilerplate) podman command.
592
$line =~ s/\s+--remote\s+/ /g; # --remote takes no args
593
# Strip out the global podman options, but show them on hover
594
$line =~ s{(\S+\/podman(-remote)?)((\s+--(root|runroot|runtime|tmpdir|storage-opt|conmon|cgroup-manager|\S+-backend|network-config-dir|storage-driver|url) \S+)*)(\s.*)}{
595
my ($full_path, $remote, $options, $args) = ($1, $2||'', $3, $6);
597
$options =~ s/^\s+//;
598
# Separate each '--foo bar' with newlines for readability
599
$options =~ s/ --/\n--/g;
600
qq{<span title="$full_path"><b>podman$remote</b></span> <span class=\"boring\" title=\"$options\">[options]</span><b>$args</b>};
603
# Quadlet output messages have a weird "... with FOO=path"
604
# addition that's hard to read. Make it friendlier.
606
$line =~ s{(\S+/bin/(quadlet))(\s.*)(\s+with\s+Q\S+?=)(\S+)}
607
{<span title="$1"><b>$2$3</b></span><span class="boring">$4</span><b>$5</b>};
609
$current_output = '';
613
elsif ($line =~ /^(\s*)(C:\\Users.*\\podman\.exe)\s(.*)$/) {
614
$line = qq{$1C> <span title="$2"><b>podman.exe</b></span> <b>$3</b>};
616
elsif ($line =~ /^\s*Error:/ || $line =~ / level=(warning|error) /) {
617
$line = "<span class='log-warning'>" . $line . "</span>";
619
elsif ($line =~ /^panic:/) {
620
$line = "<span class='log-error'>" . $line . "</span>";
623
$current_output .= ' ' . $line;
626
# One line after each divider, there's a status/timing line.
627
if ($after_divider == 1) {
628
# When run with -p, ginkgo emits timing information on the
629
# first line after the divider. (Without -p, it's at the end).
630
# Recognize this, because it affects our recognition (below)
632
$line =~ s{(\[(\d+)\.\d+\s+seconds\])}{
633
if ($2 > 5) { "<b><span class='log-slow'>$1</span></b>" }
635
}e && --$after_divider;
637
# Make FAILED and SKIPPING visible. (Only works with ginkgo -p;
638
# without -p, status is at the bottom of the test block)
639
if ($line =~ s!^(.*\[(SKIPPED|FAILED|FLAKEY).*\].*)!<span class="log-\L$2\E">$1</span>!) {
640
$subtest_status = lc($2);
643
# FIXME: gray out entire block if it's skipped?
646
# Test name recognition, linking, and highlighting.
647
# The lines after each divider, until the first empty line, are
648
# one or more test name specifiers, a nested level of Description()
651
# * blah [N seconds] <<<---- only when run with -p
653
# /var/tmp/go/src/path/to/this/file.go:lineno
655
# /var/tmp/go/src/path/to/this/file.go:lineno
657
# There may even be three nested levels (e.g., quadlet). We
658
# look for non-path lines and assemble them -- "Podman foo",
659
# " Podman foo podman bar" -- giving each an HTML anchor
660
# to which we can link from the summary section at bottom.
661
if ($after_divider <= 10) { # Assume no more than ~5 levels
662
if ($line =~ /^\s*$/) {
663
# Stop looking when we get to the blank line
664
$after_divider = 999;
666
elsif ($line =~ /span.*class=.boring./) {
667
# Sigh. Bindings tests run without -p, and have no blank line
668
# separator. Use the first command to signal real logs.
669
# FIXME: can we solve this with ReportBeforeEach()?
670
# https://onsi.github.io/ginkgo/#generating-reports-programmatically
671
$after_divider = 999;
673
elsif ($line =~ m!^\s*(/\S+|C:/)!) {
674
# Source code path: skip
676
elsif ($line =~ /<b/) {
677
# Already-formatted line (like the "N seconds" one): skip
679
elsif ($line =~ /^\s*(\[It\]\s*)?(.*)/) {
680
# Test description or name! Remove the "It", and make an anchor
681
$subtest_name .= " " if $subtest_name;
683
my $id = make_id($subtest_name, 'anchor');
684
$line = "<a name='t--$id'><h2 class=\"log-$subtest_status\">$line</h2></a>";
686
# Special case for tests that fail outside of a Describe()
687
# block, e.g., global After/BeforeEach. We still want to
688
# highlight those in the <h2>, but do not include "TOP-LEVEL"
689
# in the anchor name for the actual test name (which will
690
# come later, a few lines down).
691
$subtest_name = '' if $subtest_name =~ /^\s*TOP-LEVEL/;
696
# Highlight test name when it appears in the middle of commands.
697
# But make it boring, because we already have the test name in large
698
# bold just above. (Except in skipped tests).
699
$line =~ s!^(\s*)(\[It\]\s+.*)!$1<span class="testname">$2</span>!;
701
# Failure name corresponds to a previously-seen block.
703
if ($line =~ /^(\s*\[(FAIL|PANIC!)\] (.*) \[(It|BeforeEach|AfterEach)\] )([A-Za-z-].*)/) {
704
my ($lhs, $type, $desc, $ginkgo_fluff, $testname) = ($1, $2, $3, $4, $5);
705
$desc =~ s/^TOP-LEVEL\s*//;
706
my $id = make_id("$desc $testname", 'link');
708
$line = "<span class=\"log-error\">$lhs<a href='#t--$id'>$testname</a></span>";
711
print { $out_fh } "<span class=\"timestamp\">$timestamp</span>"
713
print { $out_fh } $line, "\n";
716
my $have_formatted_log; # Set on success
719
# Summary line for BATS tests
720
if (keys %bats_count) {
721
print { $out_fh } "<hr/><span class='bats-summary'>Summary:";
724
for my $class (qw(passed failed skipped)) {
725
if (my $n = $bats_count{$class}) {
726
printf { $out_fh } "%s <span class='bats-%s'>%d %s</span>",
727
$comma, $class, $n, ucfirst($class);
733
printf { $out_fh } ". Total tests: $total";
734
if (my $expected_total = $bats_count{expected_total}) {
735
if ($total != $expected_total) {
736
print { $out_fh } " <span class='bats-failed'>(WARNING: expected $expected_total)</span>";
739
print { $out_fh } "</span>\n";
742
print { $out_fh } "</pre> <!-- end processed output -->\n";
744
# Did we find a cirrus task? Link back.
746
print { $out_fh } <<"END_HTML";
748
<h3>Cirrus <a href="https://cirrus-ci.com/task/$cirrus_task">task $cirrus_task</a></h3>
752
# FIXME: need a safe way to get TZ
753
printf { $out_fh } <<"END_HTML", scalar(CORE::localtime);
755
<small>Processed %s by $ME v$VERSION</small>
761
if (rename $out_tmp => $outfile) {
762
$have_formatted_log = 1;
765
warn "$ME: Could not rename $out_tmp: $!\n";
769
warn "$ME: Error writing $out_tmp: $!\n";
773
# Grumble. Github only shows the last N lines of the log... which is
774
# anti-helpful when you want a quick synopsis of what failed. Write a
775
# summary at the tail, to make it easier for humans to see what went wrong.
776
if (my $fails = $bats_count{__fail_list}) {
778
printf "Failed tests (%d):\n", scalar(@$fails);
779
printf " - %d %s\n", @$_ for @$fails;
782
# If Cirrus magic envariables are available, write a link to results.
783
# FIXME: it'd be so nice to make this a clickable live link.
785
# As of June 2022 we use the Cirrus API[1] as the source of our logs,
786
# instead of linking directly to googleapis.com. This will allow us
787
# to abstract cloud-specific details, so we can one day use Amazon cloud.
788
# See #14569 for more info.
790
# [1] https://cirrus-ci.org/guide/writing-tasks/#latest-build-artifacts
791
if ($have_formatted_log && $ENV{CIRRUS_TASK_ID}) {
792
my $URL_BASE = "https://api.cirrus-ci.com";
793
my $task_id = $ENV{CIRRUS_TASK_ID};
795
# Link by *taskID*, not buildID + taskname. First, this is shorter
796
# and less duplicaty. Second, and more important, buildID + taskname
797
# is non-unique, and a link to a flake log will be clobbered.
798
my $URL = "${URL_BASE}/v1/artifact/task/$task_id/html/${outfile}";
800
print "\n\nAnnotated results:\n $URL\n";
806
# make_id # Given a test name, generate an anchor link name
809
my $name = shift; # in: test title
810
my $type = shift; # in: differentiator (anchor, link)
814
$name =~ s/\r//g; # strip windows ^Ms
815
$name =~ s/^\s+|\s+$//g; # strip leading/trailing whitespace
816
$name =~ s/^\[It\]\s*//; # strip leading "[It] "
817
$name =~ s/\&#\d+;//g; # 'doesn't' -> 'doesnt'
818
$name =~ s/\"/-/g; # '"path"' -> '-path-'
819
$name =~ s/[^a-zA-Z0-9_-]/-/g; # Convert non-alphanumeric to dash
820
$name =~ s/-{3,}/-/g; # '------' to just '-'
822
# Keep a running tally of how many times we've seen this identifier
823
# for this given type! This lets us cross-match, in the bottom of
824
# the page, the first/second/third failure of a given test.
825
$name .= "--" . ++$counter{$type}{$name};
831
###############################################################################
832
# BEGIN job_synopsis and related helpers
835
# job_synopsis # Job details, links to github/cirrus
838
my $subtest_name = shift; # e.g. integration_test
840
my $s = <<"END_SYNOPSIS";
841
<table class="synopsis">
844
# PR 1234 - title of the pr
845
my $pr_title = escapeHTML(_env_replace("{CIRRUS_CHANGE_TITLE}"));
846
$s .= _tr("GitHub PR", sprintf("%s - %s",
847
_a("{CIRRUS_PR}", "https://{CIRRUS_REPO_CLONE_HOST}/{CIRRUS_REPO_FULL_NAME}/pull/{CIRRUS_PR}"),
850
# PR author, if signed-off-by
851
if (my $msg = _env_replace("{CIRRUS_COMMIT_MESSAGE}")) {
852
while ($msg =~ /^Signed-off-by:\s+(\S.*\S)$/gmi) {
853
$s .= _tr("Author", escapeHTML($1));
857
# eg "test fedora", "special_testing_rootless"
858
# WARNING: As of 2020-10-05, $CIRRUS_TASK_NAME reflects the same
859
# descriptive content as our $subtest_name argument (confirm via
860
# cross-checking runner.sh:logformatter() vs cirrus.yml:&std_name_fmt).
861
# If this ever becomes untrue, just add _tr("Subtest", $subtest_name).
862
my $test_name = _env_replace("{CIRRUS_TASK_NAME}");
863
# (Special-case cleanup: Cirrus\ quotes\ spaces; remove for readability).
864
$test_name =~ s/\\\s+/ /g;
865
$s .= _tr("Test name", $test_name);
867
# Macs always have to be different
868
my $is_mac = ($test_name =~ /darwin/);
870
# Link to further Cirrus results, e.g. other runs.
871
# Build is mostly boring, it's usually TASK that we want to see.
872
$s .= _tr("Cirrus", sprintf("<small>Build %s</small> / <b>Task %s</b>",
873
_a("{CIRRUS_BUILD_ID}", "https://cirrus-ci.com/build/{CIRRUS_BUILD_ID}"),
874
_a("{CIRRUS_TASK_ID}", "https://cirrus-ci.com/task/{CIRRUS_TASK_ID}")));
876
# Logs: link to original (unformatted) log; journal; and, if remote, server
878
push @logs, _a("main", sprintf("https://api.cirrus-ci.com/v1/task/{CIRRUS_TASK_ID}/logs/%s.log",
879
($is_mac ? 'test' : 'main')));
880
push @logs, _a("journal", "https://api.cirrus-ci.com/v1/task/{CIRRUS_TASK_ID}/logs/journal.log")
883
# System tests are single-threaded, and have a server log available
884
if ($test_name =~ /sys\s+remote\s/) {
885
push @logs, _a("remote server", "https://api.cirrus-ci.com/v1/artifact/task/{CIRRUS_TASK_ID}/server_log/podman-server.log");
887
$s .= _tr("Logs", join(" / ", @logs));
889
# BASE_SHA can tell us if our parent includes--or doesn't--a purported
890
# fix for a flake. Note that "commits", plural, links to a git history
891
# listing; if we used "commit", singular, that would be less useful.
892
$s .= _tr("Base commit", _a("{CIRRUS_BASE_SHA}", "https://{CIRRUS_REPO_CLONE_HOST}/{CIRRUS_REPO_FULL_NAME}/commits/{CIRRUS_BASE_SHA}"));
901
return "<tr><th>$th:</th><td>$td</td></tr>\n";
905
my ($name, $href) = map { _env_replace($_) } @_;
907
if ($href =~ /UNDEFINED/) {
908
return "$name ($href)";
910
return "<a href='$href'>$name</a>";
916
$s_in =~ s[\{(.*?)\}][$ENV{$1} || "[$1 UNDEFINED]"]ge;
921
# END job_synopsis and related helpers
922
###############################################################################
923
# BEGIN html-formatting helpers
929
%chars = ('&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''')
930
if keys(%chars) == 0;
931
my $class = join('', sort keys %chars);
932
$s =~ s/([$class])/$chars{$1}/ge;
940
# We don't actually care about the character, only its length
941
$s =~ s/\&\#?[a-z0-9]+;/./g;
946
# END html-formatting helpers
947
###############################################################################