Skip to content

Commit 62c1aa0

Browse files
authored
Merge pull request kubernetes#127929 from pohly/prune-junit-xml-failure
prune-junit-xml: simplify failure message
2 parents 7f55109 + fba2bcf commit 62c1aa0

File tree

5 files changed

+480
-16
lines changed

5 files changed

+480
-16
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package logparse provides a parser for the klog text format:
18+
//
19+
// I1007 13:16:55.727802 1146763 example.go:57] "Key/value encoding" logger="example" foo="bar" duration="1s" int=42 float=3.14 string="hello world" quotedString="hello \"world\"" multiLinString=<
20+
// hello
21+
// world
22+
// >
23+
// E1007 15:20:04.343375 1157755 example.go:41] Log using Errorf, err: fail
24+
//
25+
// It also supports the klog/ktesting unit test log output:
26+
//
27+
// === RUN TestKlogr
28+
// example_test.go:45: I1007 13:28:21.908998] hello world
29+
// example_test.go:46: E1007 13:28:21.909034] failed err="failed: some error"
30+
// example_test.go:47: I1007 13:28:21.909042] verbosity 1
31+
// example_test.go:48: I1007 13:28:21.909047] main/helper: with prefix
32+
// example_test.go:50: I1007 13:28:21.909076] key/value pairs int=1 float=2 pair="(1, 2)" raw={"Name":"joe","NS":"kube-system"} slice=[1,2,3,"str"] map={"a":1,"b":2} kobj="kube-system/joe" string="hello world" quotedString="hello \"world\"" multiLineString=<
33+
// hello
34+
// world
35+
// >
36+
// example_test.go:63: I1007 13:28:21.909085] info message level 4
37+
// example_test.go:64: I1007 13:28:21.909089] info message level 5
38+
// --- PASS: TestKlogr (0.00s)
39+
// PASS
40+
// ok k8s.io/klog/v2/ktesting/example (cached)
41+
//
42+
// Arbitrary indention with space or tab is supported. All lines of
43+
// a klog log entry must be indented the same way.
44+
package logparse
45+
46+
import (
47+
"bufio"
48+
"errors"
49+
"fmt"
50+
"io"
51+
"iter"
52+
"regexp"
53+
"strings"
54+
)
55+
56+
// Parse splits log output provided by the reader into individual log entries.
57+
// The original log can be reconstructed verbatim by concatenating these
58+
// entries. If the reader fails with an error, the last log entry will
59+
// capture that error.
60+
func Parse(in io.Reader) []Entry {
61+
var log []Entry
62+
for entry := range All(in) {
63+
log = append(log, entry)
64+
}
65+
return log
66+
}
67+
68+
// All is like Parse except that it can be used in a for/range loop:
69+
//
70+
// for entry := range logparse.All(reader) {
71+
// // entry is the next log entry.
72+
// }
73+
func All(in io.Reader) iter.Seq[Entry] {
74+
return func(yield func(Entry) bool) {
75+
parse(in, yield)
76+
}
77+
}
78+
79+
// Entry is the base type for a log entry.
80+
//
81+
// Use type assertions to check for additional information. All
82+
// of the instances behind this interface are pointers.
83+
type Entry interface {
84+
// LogData returns a verbatim copy of the original log chunk,
85+
// including one or more line breaks. Concatenating these chunks
86+
// from all entries will reconstruct the parsed log.
87+
LogData() string
88+
}
89+
90+
// ErrorEntry captures the error encountered while reading the log input.
91+
type ErrorEntry struct {
92+
Err error
93+
}
94+
95+
var _ Entry = &ErrorEntry{}
96+
var _ fmt.Stringer = &ErrorEntry{}
97+
98+
func (e *ErrorEntry) LogData() string { return "" }
99+
func (e *ErrorEntry) String() string { return e.Err.Error() }
100+
101+
// OtherEntry captures some log line which is not part of a klog log entry.
102+
type OtherEntry struct {
103+
Data string
104+
}
105+
106+
var _ Entry = &OtherEntry{}
107+
var _ fmt.Stringer = &OtherEntry{}
108+
109+
func (e *OtherEntry) LogData() string { return e.Data }
110+
func (e *OtherEntry) String() string { return e.Data }
111+
112+
// LogEntry captures some log entry which was recognized as coming from klog.
113+
type KlogEntry struct {
114+
Data string
115+
Severity Severity
116+
}
117+
118+
type Severity byte
119+
120+
const (
121+
SeverityInfo = Severity('I')
122+
SeverityWarning = Severity('W')
123+
SeverityError = Severity('E')
124+
SeverityFatal = Severity('F')
125+
)
126+
127+
var _ Entry = &KlogEntry{}
128+
var _ fmt.Stringer = &KlogEntry{}
129+
130+
func (e *KlogEntry) LogData() string { return e.Data }
131+
func (e *KlogEntry) String() string { return e.Data }
132+
133+
func parse(in io.Reader, yield func(Entry) bool) {
134+
// Read lines using arbitrary length, which can't be done with a
135+
// bufio.Scanner.
136+
reader := bufio.NewReader(in)
137+
line, err := reader.ReadString('\n')
138+
for {
139+
// First deliver the current line. This may need to look
140+
// ahead and thus returns the next line.
141+
var nextLine string
142+
var nextErr error
143+
if len(line) > 0 {
144+
var cont bool
145+
nextLine, cont, nextErr = parseLine(reader, line, yield)
146+
if !cont {
147+
return
148+
}
149+
} else {
150+
nextLine, nextErr = reader.ReadString('\n')
151+
}
152+
153+
// Finalize parsing?
154+
switch {
155+
case err == nil:
156+
// Okay.
157+
case errors.Is(err, io.EOF):
158+
return
159+
default:
160+
yield(&ErrorEntry{Err: err})
161+
return
162+
}
163+
164+
line = nextLine
165+
err = nextErr
166+
}
167+
}
168+
169+
const (
170+
pid = `(?<pid>[[:digit:]]+)`
171+
source = `(?<source>[^:]+:[[:digit:]]+)`
172+
severity = `(?<severity>[IWEF])`
173+
datetime = `(?<month>[[:digit:]]{2})(?<day>[[:digit:]]{2}) (?<hour>[[:digit:]]{2}):(?<minutes>[[:digit:]]{2}):(?<seconds>[[:digit:]]{2})\.(?<microseconds>[[:digit:]]{6})`
174+
)
175+
176+
var (
177+
klogPrefix = regexp.MustCompile(`^(?<indention>[[:blank:]]*)` +
178+
`(?:` + source + `: )?` + // `go test` source code
179+
severity +
180+
datetime +
181+
`(?: +` + pid + ` ` + source + `)?` + // klog pid + source code
182+
`\] `)
183+
184+
indentionIndex = lookupSubmatch("indention")
185+
severityIndex = lookupSubmatch("severity")
186+
)
187+
188+
func lookupSubmatch(name string) int {
189+
names := klogPrefix.SubexpNames()
190+
for i, n := range names {
191+
if n == name {
192+
return i
193+
}
194+
}
195+
panic(fmt.Errorf("named submatch %q not found in %q", name, klogPrefix.String()))
196+
}
197+
198+
// parseLine deals with one non-empty line. It returns the result of yield and
199+
// potentially the next line and/or a read error. If it doesn't have any new
200+
// data to process, it returns the empty string and a nil error.
201+
func parseLine(reader *bufio.Reader, line string, yield func(Entry) bool) (string, bool, error) {
202+
match := klogPrefix.FindStringSubmatchIndex(line)
203+
if match == nil {
204+
cont := yield(&OtherEntry{Data: line})
205+
return "", cont, nil
206+
}
207+
208+
e := &KlogEntry{
209+
Data: line,
210+
Severity: Severity(line[match[2*severityIndex]]),
211+
// TODO (?): store more of the fields that have been identified
212+
}
213+
// Deal with potential line continuation of multi-line string value,
214+
// if necessary.
215+
if !strings.HasSuffix(line, "=<\n") {
216+
return "", yield(e), nil
217+
}
218+
indention := line[match[2*indentionIndex]:match[2*indentionIndex+1]]
219+
for {
220+
var err error
221+
line, err = reader.ReadString('\n')
222+
if !strings.HasPrefix(line, indention) ||
223+
!strings.HasPrefix(line[len(indention):], "\t") && !strings.HasPrefix(line[len(indention):], " >") {
224+
// Some other line (wrong indention or wrong continuation).
225+
// Yield what we have so far and the go back to processing that new line.
226+
cont := yield(e)
227+
return line, cont, err
228+
}
229+
e.Data += line
230+
if err != nil {
231+
return "", yield(e), err
232+
}
233+
}
234+
}

0 commit comments

Comments
 (0)