| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package TAP::Formatter::GitHubActions; |
|
2
|
|
|
|
|
|
|
|
|
3
|
1
|
|
|
1
|
|
260060
|
use strict; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
32
|
|
|
4
|
1
|
|
|
1
|
|
3
|
use warnings; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
60
|
|
|
5
|
1
|
|
|
1
|
|
14
|
use v5.16; |
|
|
1
|
|
|
|
|
2
|
|
|
6
|
1
|
|
|
1
|
|
5
|
use base 'TAP::Formatter::File'; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
381
|
|
|
7
|
|
|
|
|
|
|
|
|
8
|
|
|
|
|
|
|
our $VERSION = '0.3.4'; |
|
9
|
|
|
|
|
|
|
|
|
10
|
1
|
|
|
1
|
|
13813
|
use TAP::Formatter::GitHubActions::Error; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
25
|
|
|
11
|
1
|
|
|
1
|
|
330
|
use TAP::Formatter::GitHubActions::ErrorGroup; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
23
|
|
|
12
|
1
|
|
|
1
|
|
5
|
use TAP::Formatter::GitHubActions::Utils; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
46
|
|
|
13
|
1
|
|
|
1
|
|
384
|
use TAP::Formatter::GitHubActions::ErrorAggregate; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
1050
|
|
|
14
|
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
my $MONOPHASIC_REGEX = qr/^Failed\stest .+\nat/; |
|
16
|
|
|
|
|
|
|
my $RUNNING_IN_GHA = $ENV{GITHUB_ACTIONS}; |
|
17
|
|
|
|
|
|
|
my $GHA_SKIP_SUMMARY = $ENV{GHA_SKIP_SUMMARY}; |
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
sub _stash_line_for_current_test { |
|
20
|
77
|
|
|
77
|
|
180
|
my ($self, $parser, $line) = @_; |
|
21
|
77
|
|
|
|
|
195
|
my $test_num = $parser->{tests_run}; |
|
22
|
77
|
|
100
|
|
|
476
|
my $stash = ($parser->{_msgs_for_test}{$test_num} //= []); |
|
23
|
77
|
|
|
|
|
159
|
push @{$stash}, $line; |
|
|
77
|
|
|
|
|
317
|
|
|
24
|
|
|
|
|
|
|
} |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
sub _normalize_stash { |
|
27
|
18
|
|
|
18
|
|
34
|
my $stash = shift; |
|
28
|
18
|
|
|
|
|
29
|
my @new_stash; |
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
# First phase: Join all output, and segment it by "Failed test". |
|
31
|
18
|
|
|
|
|
33
|
my @chunks = split(/(Failed test)/, join("\n", @{$stash})); |
|
|
18
|
|
|
|
|
211
|
|
|
32
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
# Second phase, heal the splits so we end up with every string in the stash |
|
34
|
|
|
|
|
|
|
# beginning with "Failed test". |
|
35
|
18
|
|
|
|
|
62
|
while (scalar @chunks) { |
|
36
|
49
|
|
|
|
|
89
|
my $chunk = shift @chunks; |
|
37
|
|
|
|
|
|
|
# skip empty lines (consequence of split) |
|
38
|
49
|
100
|
|
|
|
128
|
next unless $chunk; |
|
39
|
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
# if we hit the separator |
|
41
|
31
|
50
|
50
|
|
|
228
|
if ($chunk eq 'Failed test' && scalar @chunks) { |
|
42
|
|
|
|
|
|
|
# Grab a new chunk |
|
43
|
31
|
|
|
|
|
86
|
$chunk .= shift @chunks; |
|
44
|
|
|
|
|
|
|
# Cleanup the newlines for named tests. |
|
45
|
31
|
100
|
|
|
|
515
|
$chunk =~ s/\n/ / if $chunk =~ qr/$MONOPHASIC_REGEX/; |
|
46
|
|
|
|
|
|
|
} |
|
47
|
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
# kill off any rouge newlines |
|
49
|
31
|
|
|
|
|
106
|
chomp($chunk); |
|
50
|
31
|
|
|
|
|
164
|
my $error = TAP::Formatter::GitHubActions::Error->from_output($chunk); |
|
51
|
31
|
50
|
|
|
|
129
|
next unless $error; |
|
52
|
|
|
|
|
|
|
|
|
53
|
31
|
|
|
|
|
94
|
push @new_stash, $error; |
|
54
|
|
|
|
|
|
|
} |
|
55
|
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
# Return it |
|
57
|
18
|
|
|
|
|
73
|
return @new_stash; |
|
58
|
|
|
|
|
|
|
} |
|
59
|
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
sub open_test { |
|
61
|
5
|
|
|
5
|
1
|
130303
|
my ($self, $test, $parser) = @_; |
|
62
|
5
|
|
|
|
|
124
|
my $session = $self->SUPER::open_test($test, $parser); |
|
63
|
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
# force verbosity to be able to read comments, yamls & unknowns. |
|
65
|
5
|
|
|
|
|
1327
|
$self->verbosity(1); |
|
66
|
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
# We'll use the parser as a vessel, afaics there's one parser instance per |
|
68
|
|
|
|
|
|
|
# parallel job. |
|
69
|
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
# We'll keep track of all output of a test with this. |
|
71
|
5
|
|
|
|
|
59
|
$parser->{_msgs_for_test} = {}; |
|
72
|
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
# In an ideal world, we'd just need to listen to `comment` and that should |
|
74
|
|
|
|
|
|
|
# suffice, but `throws_ok` & `lives_ok` report via `unknown`... |
|
75
|
|
|
|
|
|
|
# But this is real life... |
|
76
|
|
|
|
|
|
|
# so... |
|
77
|
|
|
|
|
|
|
my $handler = sub { |
|
78
|
159
|
|
|
159
|
|
2198507
|
my $result = shift->raw; |
|
79
|
|
|
|
|
|
|
# Skip Subtests |
|
80
|
159
|
100
|
|
|
|
1077
|
return if $result =~ /Subtest/; |
|
81
|
|
|
|
|
|
|
# Ignore anything that's not a comment (plans & tests) |
|
82
|
154
|
100
|
|
|
|
756
|
return unless $result =~ /^\s*#/; |
|
83
|
|
|
|
|
|
|
# Skip trailing end of tests/subtests |
|
84
|
67
|
100
|
|
|
|
312
|
return if $result =~ m/Looks like you/; |
|
85
|
|
|
|
|
|
|
# Cleanup comment start |
|
86
|
58
|
|
|
|
|
373
|
$result =~ s/\s*# //; |
|
87
|
|
|
|
|
|
|
# Test::Exception doens't indent the messages 🤡, excluding those keywords |
|
88
|
|
|
|
|
|
|
# and anything that doesn't begin with a space. |
|
89
|
58
|
50
|
|
|
|
323
|
return unless $result =~ m/^( |died|found|expecting)/; |
|
90
|
|
|
|
|
|
|
# Skip lines only having whitespaces |
|
91
|
58
|
50
|
|
|
|
306
|
return if $result =~ m/^ +$/; |
|
92
|
|
|
|
|
|
|
# Cleanup indent (2 spaces) |
|
93
|
58
|
|
|
|
|
208
|
$result =~ s/^ //; |
|
94
|
58
|
|
|
|
|
219
|
$self->_stash_line_for_current_test($parser, $result); |
|
95
|
5
|
|
|
|
|
140
|
}; |
|
96
|
|
|
|
|
|
|
|
|
97
|
5
|
|
|
|
|
100
|
$parser->callback(comment => $handler); |
|
98
|
5
|
|
|
|
|
590
|
$parser->callback(unknown => $handler); |
|
99
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
# Enable YAML Support |
|
101
|
5
|
|
|
|
|
158
|
$parser->version(13); |
|
102
|
|
|
|
|
|
|
$parser->callback(yaml => sub { |
|
103
|
27
|
|
|
27
|
|
41042
|
my $yaml = shift->data; |
|
104
|
|
|
|
|
|
|
# skip notes. |
|
105
|
27
|
100
|
|
|
|
137
|
return if grep { $yaml->{emitter} eq $_ } qw(Test::More::note Test::More::diag); |
|
|
54
|
|
|
|
|
216
|
|
|
106
|
|
|
|
|
|
|
# skip empty messages (prolly never happens?) |
|
107
|
19
|
50
|
|
|
|
73
|
return unless $yaml->{message}; |
|
108
|
19
|
|
|
|
|
88
|
$self->_stash_line_for_current_test($parser, $yaml->{message}); |
|
109
|
5
|
|
|
|
|
90
|
}); |
|
110
|
|
|
|
|
|
|
|
|
111
|
5
|
|
|
|
|
162
|
return $session; |
|
112
|
|
|
|
|
|
|
} |
|
113
|
|
|
|
|
|
|
|
|
114
|
|
|
|
0
|
0
|
|
sub header { } |
|
115
|
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
sub _output_report_notice { |
|
117
|
4
|
|
|
4
|
|
15
|
my ($self, $test) = @_; |
|
118
|
4
|
|
|
|
|
8
|
my $workflow_url = '%WORKFLOW_URL%'; |
|
119
|
|
|
|
|
|
|
|
|
120
|
12
|
|
|
|
|
54
|
my @workflow_vars = grep { $_ } @ENV{ |
|
121
|
4
|
|
|
|
|
33
|
qw( |
|
122
|
|
|
|
|
|
|
GITHUB_SERVER_URL |
|
123
|
|
|
|
|
|
|
GITHUB_REPOSITORY |
|
124
|
|
|
|
|
|
|
GITHUB_RUN_ID |
|
125
|
|
|
|
|
|
|
) |
|
126
|
|
|
|
|
|
|
}; |
|
127
|
|
|
|
|
|
|
|
|
128
|
4
|
0
|
33
|
|
|
3080
|
if (!$GHA_SKIP_SUMMARY && !!@workflow_vars) { |
|
129
|
0
|
|
|
|
|
0
|
$workflow_url = sprintf("%s/%s/actions/runs/%s", @workflow_vars); |
|
130
|
|
|
|
|
|
|
} |
|
131
|
|
|
|
|
|
|
|
|
132
|
4
|
|
|
|
|
62
|
my $file_marker_line = TAP::Formatter::GitHubActions::Utils::log_annotation_line( |
|
133
|
|
|
|
|
|
|
type => 'notice', |
|
134
|
|
|
|
|
|
|
filename => $test, |
|
135
|
|
|
|
|
|
|
line => 1, |
|
136
|
|
|
|
|
|
|
title => 'More details', |
|
137
|
|
|
|
|
|
|
body => "See the full report in: $workflow_url" |
|
138
|
|
|
|
|
|
|
); |
|
139
|
|
|
|
|
|
|
|
|
140
|
4
|
|
|
|
|
35
|
$self->_output("$file_marker_line\n"); |
|
141
|
|
|
|
|
|
|
} |
|
142
|
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
sub summary { |
|
144
|
5
|
|
|
5
|
1
|
564977
|
my ($self, $aggregate, $interrupted) = @_; |
|
145
|
5
|
|
|
|
|
53
|
$self->SUPER::summary($aggregate, $interrupted); |
|
146
|
|
|
|
|
|
|
|
|
147
|
5
|
|
|
|
|
3302
|
my $total = $aggregate->total; |
|
148
|
5
|
|
|
|
|
24
|
my $passed = $aggregate->passed; |
|
149
|
|
|
|
|
|
|
|
|
150
|
5
|
100
|
66
|
|
|
66
|
return if ($total == $passed && !$aggregate->has_problems); |
|
151
|
|
|
|
|
|
|
|
|
152
|
4
|
|
|
|
|
16
|
$self->_output("\n= GitHub Actions Report =\n"); |
|
153
|
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
# First print a mark at the beginning of the files reporting errors with a |
|
155
|
|
|
|
|
|
|
# link to the workflow run for the full report. |
|
156
|
|
|
|
|
|
|
# |
|
157
|
|
|
|
|
|
|
# This is a workaround due to the fact that there's a hard limit on the |
|
158
|
|
|
|
|
|
|
# amount of annotations rendered by GitHub on Pull requests. |
|
159
|
|
|
|
|
|
|
# |
|
160
|
|
|
|
|
|
|
# As of writting is a max of 10 annotations per step, 50 per workflow. |
|
161
|
|
|
|
|
|
|
# To overcome this we'll write here to the Workflow Summary File. |
|
162
|
|
|
|
|
|
|
# and link back in an anotation. |
|
163
|
|
|
|
|
|
|
# see [0] & [1] & [2]. |
|
164
|
4
|
|
|
|
|
44
|
foreach my $test ($aggregate->descriptions) { |
|
165
|
4
|
|
|
|
|
88
|
my ($parser) = $aggregate->parsers($test); |
|
166
|
4
|
50
|
33
|
|
|
75
|
next if $parser->passed == $parser->tests_run && !$parser->exit; |
|
167
|
|
|
|
|
|
|
|
|
168
|
4
|
|
|
|
|
60
|
$self->_output_report_notice($test); |
|
169
|
|
|
|
|
|
|
} |
|
170
|
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
# Now print all error annotations |
|
172
|
4
|
|
|
|
|
74
|
foreach my $test ($aggregate->descriptions) { |
|
173
|
4
|
|
|
|
|
43
|
my ($parser) = $aggregate->parsers($test); |
|
174
|
4
|
50
|
33
|
|
|
82
|
next if $parser->passed == $parser->tests_run && !$parser->exit; |
|
175
|
|
|
|
|
|
|
|
|
176
|
4
|
|
|
|
|
66
|
$self->_dump_test_parser($test, $parser); |
|
177
|
|
|
|
|
|
|
} |
|
178
|
|
|
|
|
|
|
} |
|
179
|
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
sub _dump_test_parser { |
|
181
|
4
|
|
|
4
|
|
14
|
my ($self, $test, $parser) = @_; |
|
182
|
|
|
|
|
|
|
|
|
183
|
4
|
|
|
|
|
75
|
my $error_aggregate = TAP::Formatter::GitHubActions::ErrorAggregate->new(); |
|
184
|
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
# Transform messages into error objects and feed them into the error |
|
186
|
|
|
|
|
|
|
# aggregate. |
|
187
|
4
|
|
|
|
|
8
|
foreach my $test_num (sort keys %{$parser->{_msgs_for_test}}) { |
|
|
4
|
|
|
|
|
34
|
|
|
188
|
18
|
|
|
|
|
47
|
my $stash = $parser->{_msgs_for_test}->{$test_num}; |
|
189
|
18
|
|
|
|
|
48
|
$error_aggregate->add(_normalize_stash($stash)); |
|
190
|
|
|
|
|
|
|
} |
|
191
|
|
|
|
|
|
|
|
|
192
|
4
|
|
|
|
|
48
|
my @error_groups = $error_aggregate->as_sorted_array(); |
|
193
|
|
|
|
|
|
|
# Print in GHA Annotation format |
|
194
|
4
|
|
|
|
|
76
|
$self->_output($_->as_gha_summary_for($test)) for (@error_groups); |
|
195
|
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# Skip if not running in GitHub Actions |
|
197
|
4
|
50
|
33
|
|
|
156
|
return if !($RUNNING_IN_GHA && !$GHA_SKIP_SUMMARY); |
|
198
|
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
# Write full report on GHA Step Summary [2] |
|
200
|
0
|
0
|
0
|
|
|
|
open(my $summary_fd, '>>', $ENV{GITHUB_STEP_SUMMARY}) or die "Unable to open " . $ENV{GITHUB_STEP_SUMMARY} . ": $!" if $RUNNING_IN_GHA; |
|
201
|
0
|
|
|
|
|
|
print $summary_fd "## Failures in `$test`\n"; |
|
202
|
0
|
|
|
|
|
|
print $summary_fd $_->as_markdown_summary() for (@error_groups); |
|
203
|
0
|
|
|
|
|
|
print $summary_fd "\n\n"; |
|
204
|
|
|
|
|
|
|
} |
|
205
|
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
# [0]: https://github.com/orgs/community/discussions/26680#discussioncomment-3252835 |
|
207
|
|
|
|
|
|
|
# [1]: https://github.com/orgs/community/discussions/68471 |
|
208
|
|
|
|
|
|
|
# [2]: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary |
|
209
|
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
1; |
|
211
|
|
|
|
|
|
|
__END__ |