diff --git a/cmd/prune-junit-xml/prunexml.go b/cmd/prune-junit-xml/prunexml.go new file mode 100644 index 00000000000..f4a5421736b --- /dev/null +++ b/cmd/prune-junit-xml/prunexml.go @@ -0,0 +1,150 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/xml" + "flag" + "fmt" + "io" + "os" +) + +// JUnitTestSuites is a collection of JUnit test suites. +type JUnitTestSuites struct { + XMLName xml.Name `xml:"testsuites"` + Suites []JUnitTestSuite `xml:"testsuite"` +} + +// JUnitTestSuite is a single JUnit test suite which may contain many +// testcases. +type JUnitTestSuite struct { + XMLName xml.Name `xml:"testsuite"` + Tests int `xml:"tests,attr"` + Failures int `xml:"failures,attr"` + Time string `xml:"time,attr"` + Name string `xml:"name,attr"` + Properties []JUnitProperty `xml:"properties>property,omitempty"` + TestCases []JUnitTestCase `xml:"testcase"` + Timestamp string `xml:"timestamp,attr"` +} + +// JUnitTestCase is a single test case with its result. +type JUnitTestCase struct { + XMLName xml.Name `xml:"testcase"` + Classname string `xml:"classname,attr"` + Name string `xml:"name,attr"` + Time string `xml:"time,attr"` + SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"` + Failure *JUnitFailure `xml:"failure,omitempty"` +} + +// JUnitSkipMessage contains the reason why a testcase was skipped. +type JUnitSkipMessage struct { + Message string `xml:"message,attr"` +} + +// JUnitProperty represents a key/value pair used to define properties. +type JUnitProperty struct { + Name string `xml:"name,attr"` + Value string `xml:"value,attr"` +} + +// JUnitFailure contains data related to a failed test. +type JUnitFailure struct { + Message string `xml:"message,attr"` + Type string `xml:"type,attr"` + Contents string `xml:",chardata"` +} + +func main() { + maxTextSize := flag.Int("max-text-size", 1, "maximum size of attribute or text (in MB)") + flag.Parse() + + if flag.NArg() > 0 { + for _, path := range flag.Args() { + fmt.Printf("processing junit xml file : %s\n", path) + xmlReader, err := os.Open(path) + if err != nil { + panic(err) + } + defer xmlReader.Close() + suites, err := fetchXML(xmlReader) // convert MB into bytes (roughly!) + if err != nil { + panic(err) + } + + pruneXML(suites, *maxTextSize*1e6) // convert MB into bytes (roughly!) + + xmlWriter, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + panic(err) + } + defer xmlWriter.Close() + err = streamXML(xmlWriter, suites) + if err != nil { + panic(err) + } + fmt.Println("done.") + } + } +} + +func pruneXML(suites *JUnitTestSuites, maxBytes int) { + for _, suite := range suites.Suites { + for _, testcase := range suite.TestCases { + if testcase.SkipMessage != nil { + if len(testcase.SkipMessage.Message) > maxBytes { + fmt.Printf("clipping skip message in test case : %s\n", testcase.Name) + testcase.SkipMessage.Message = "[... clipped...]" + + testcase.SkipMessage.Message[len(testcase.SkipMessage.Message)-maxBytes:] + } + } + if testcase.Failure != nil { + if len(testcase.Failure.Contents) > maxBytes { + fmt.Printf("clipping failure message in test case : %s\n", testcase.Name) + testcase.Failure.Contents = "[... clipped...]" + + testcase.Failure.Contents[len(testcase.Failure.Contents)-maxBytes:] + } + } + } + } +} + +func fetchXML(xmlReader io.Reader) (*JUnitTestSuites, error) { + decoder := xml.NewDecoder(xmlReader) + var suites JUnitTestSuites + err := decoder.Decode(&suites) + if err != nil { + return nil, err + } + return &suites, nil +} + +func streamXML(writer io.Writer, in *JUnitTestSuites) error { + _, err := writer.Write([]byte("\n")) + if err != nil { + return err + } + encoder := xml.NewEncoder(writer) + encoder.Indent("", "\t") + err = encoder.Encode(in) + if err != nil { + return err + } + return encoder.Flush() +} diff --git a/cmd/prune-junit-xml/prunexml_test.go b/cmd/prune-junit-xml/prunexml_test.go new file mode 100644 index 00000000000..6f9aef6ee7b --- /dev/null +++ b/cmd/prune-junit-xml/prunexml_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "bytes" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestPruneXML(t *testing.T) { + sourceXML := ` + + + + + + + + + + + /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport/transport.go:169 +0x147 k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport.(*transportReader).Read(0xc0e5f8edb0, {0xc0efe16f88?, 0xc1169d3a88?, 0x1804787?}) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport/transport.go:483 +0x32 io.ReadAtLeast({0x55c5720, 0xc0e5f8edb0}, {0xc0efe16f88, 0x5, 0x5}, 0x5) /usr/local/go/src/io/io.go:331 +0x9a io.ReadFull(...) /usr/local/go/src/io/io.go:350 k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport.(*Stream).Read(0xc0f3cd67e0, {0xc0efe16f88, 0x5, 0x5}) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/internal/transport/transport.go:467 +0xa5 k8s.io/kubernetes/vendor/google.golang.org/grpc.(*parser).recvMsg(0xc0efe16f78, 0x7fffffff) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/rpc_util.go:559 +0x47 k8s.io/kubernetes/vendor/google.golang.org/grpc.recvAndDecompress(0xc1169d3c58?, 0xc0f3cd67e0, {0x0, 0x0}, 0x7fffffff, 0x0, {0x0, 0x0}) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/rpc_util.go:690 +0x66 k8s.io/kubernetes/vendor/google.golang.org/grpc.recv(0x172b28f?, {0x7f837c291d58, 0x7f84350}, 0x6f5a274d6e8f284c?, {0x0?, 0x0?}, {0x4be7d40, 0xc0f8c01d50}, 0x0?, 0x0, ...) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/rpc_util.go:758 +0x6e k8s.io/kubernetes/vendor/google.golang.org/grpc.(*csAttempt).recvMsg(0xc0eb72d800, {0x4be7d40?, 0xc0f8c01d50}, 0x2?) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:970 +0x2b0 k8s.io/kubernetes/vendor/google.golang.org/grpc.(*clientStream).RecvMsg.func1(0x4be7d40?) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:821 +0x25 k8s.io/kubernetes/vendor/google.golang.org/grpc.(*clientStream).withRetry(0xc0f3cd65a0, 0xc1169d3e78, 0xc1169d3e48) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:675 +0x2f6 k8s.io/kubernetes/vendor/google.golang.org/grpc.(*clientStream).RecvMsg(0xc0f3cd65a0, {0x4be7d40?, 0xc0f8c01d50?}) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/google.golang.org/grpc/stream.go:820 +0x11f k8s.io/kubernetes/vendor/github.com/grpc-ecosystem/go-grpc-prometheus.(*monitoredClientStream).RecvMsg(0xc0efe16f90, {0x4be7d40?, 0xc0f8c01d50?}) /home/prow/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/vendor/github.com/grpc-ecosystem/go-grpc-prometheus/client_metrics.go:160 + + +` + + outputXML := ` + + + + + + + + + + + [... clipped...]prometheus/client_metrics.go:160 + + +` + suites, _ := fetchXML(strings.NewReader(sourceXML)) + pruneXML(suites, 32) + var output bytes.Buffer + writer := bufio.NewWriter(&output) + _ = streamXML(writer, suites) + _ = writer.Flush() + assert.Equal(t, outputXML, string(output.Bytes()), "xml was not pruned correctly") +} diff --git a/hack/make-rules/test.sh b/hack/make-rules/test.sh index 4c0015ebd62..33e8c311dc7 100755 --- a/hack/make-rules/test.sh +++ b/hack/make-rules/test.sh @@ -240,6 +240,14 @@ produceJUnitXMLReport() { rm "${junit_filename_prefix}"*.stdout fi + if ! command -v prune-junit-xml >/dev/null 2>&1; then + kube::log::status "prune-junit-xml not found; installing from hack/tools" + pushd "${KUBE_ROOT}/cmd/prune-junit-xml" >/dev/null + GO111MODULE=on go install . + popd >/dev/null + fi + prune-junit-xml "${junit_xml_filename}" + kube::log::status "Saved JUnit XML test report to ${junit_xml_filename}" }