k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/prune-junit-xml/prunexml.go (about)

     1  /*
     2  Copyright 2022 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 main
    18  
    19  import (
    20  	"encoding/xml"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"regexp"
    26  
    27  	"k8s.io/kubernetes/third_party/forked/gotestsum/junitxml"
    28  )
    29  
    30  func main() {
    31  	maxTextSize := flag.Int("max-text-size", 1, "maximum size of attribute or text (in MB)")
    32  	pruneTests := flag.Bool("prune-tests", true,
    33  		"prune's xml files to display only top level tests and failed sub-tests")
    34  	flag.Parse()
    35  	for _, path := range flag.Args() {
    36  		fmt.Printf("processing junit xml file : %s\n", path)
    37  		xmlReader, err := os.Open(path)
    38  		if err != nil {
    39  			panic(err)
    40  		}
    41  		defer xmlReader.Close()
    42  		suites, err := fetchXML(xmlReader) // convert MB into bytes (roughly!)
    43  		if err != nil {
    44  			panic(err)
    45  		}
    46  
    47  		pruneXML(suites, *maxTextSize*1e6) // convert MB into bytes (roughly!)
    48  		if *pruneTests {
    49  			pruneTESTS(suites)
    50  		}
    51  
    52  		xmlWriter, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
    53  		if err != nil {
    54  			panic(err)
    55  		}
    56  		defer xmlWriter.Close()
    57  		err = streamXML(xmlWriter, suites)
    58  		if err != nil {
    59  			panic(err)
    60  		}
    61  		fmt.Println("done.")
    62  	}
    63  }
    64  
    65  func pruneXML(suites *junitxml.JUnitTestSuites, maxBytes int) {
    66  	for _, suite := range suites.Suites {
    67  		for _, testcase := range suite.TestCases {
    68  			if testcase.SkipMessage != nil {
    69  				if len(testcase.SkipMessage.Message) > maxBytes {
    70  					fmt.Printf("clipping skip message in test case : %s\n", testcase.Name)
    71  					head := testcase.SkipMessage.Message[:maxBytes/2]
    72  					tail := testcase.SkipMessage.Message[len(testcase.SkipMessage.Message)-maxBytes/2:]
    73  					testcase.SkipMessage.Message = head + "[...clipped...]" + tail
    74  				}
    75  			}
    76  			if testcase.Failure != nil {
    77  				if len(testcase.Failure.Contents) > maxBytes {
    78  					fmt.Printf("clipping failure message in test case : %s\n", testcase.Name)
    79  					head := testcase.Failure.Contents[:maxBytes/2]
    80  					tail := testcase.Failure.Contents[len(testcase.Failure.Contents)-maxBytes/2:]
    81  					testcase.Failure.Contents = head + "[...clipped...]" + tail
    82  				}
    83  			}
    84  		}
    85  	}
    86  }
    87  
    88  // This function condenses the junit xml to have package name as top level identifier
    89  // and nesting under that.
    90  func pruneTESTS(suites *junitxml.JUnitTestSuites) {
    91  	var updatedTestsuites []junitxml.JUnitTestSuite
    92  
    93  	for _, suite := range suites.Suites {
    94  		var updatedTestcases []junitxml.JUnitTestCase
    95  		var updatedTestcase junitxml.JUnitTestCase
    96  		var updatedTestcaseFailure junitxml.JUnitFailure
    97  		failflag := false
    98  		name := suite.Name
    99  		regex := regexp.MustCompile(`^(.*?)/([^/]+)/?$`)
   100  		match := regex.FindStringSubmatch(name)
   101  		updatedTestcase.Classname = match[1]
   102  		updatedTestcase.Name = match[2]
   103  		updatedTestcase.Time = suite.Time
   104  		for _, testcase := range suite.TestCases {
   105  			// The top level testcase element in a JUnit xml file does not have the / character.
   106  			if testcase.Failure != nil {
   107  				failflag = true
   108  				updatedTestcaseFailure.Message = updatedTestcaseFailure.Message + testcase.Failure.Message + ";"
   109  				updatedTestcaseFailure.Contents = updatedTestcaseFailure.Contents + testcase.Failure.Contents + ";"
   110  				updatedTestcaseFailure.Type = updatedTestcaseFailure.Type + testcase.Failure.Type
   111  			}
   112  		}
   113  		if failflag {
   114  			updatedTestcase.Failure = &updatedTestcaseFailure
   115  		}
   116  		suite.TestCases = append(updatedTestcases, updatedTestcase)
   117  		updatedTestsuites = append(updatedTestsuites, suite)
   118  	}
   119  	suites.Suites = updatedTestsuites
   120  }
   121  
   122  func fetchXML(xmlReader io.Reader) (*junitxml.JUnitTestSuites, error) {
   123  	decoder := xml.NewDecoder(xmlReader)
   124  	var suites junitxml.JUnitTestSuites
   125  	err := decoder.Decode(&suites)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	return &suites, nil
   130  }
   131  
   132  func streamXML(writer io.Writer, in *junitxml.JUnitTestSuites) error {
   133  	_, err := writer.Write([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"))
   134  	if err != nil {
   135  		return err
   136  	}
   137  	encoder := xml.NewEncoder(writer)
   138  	encoder.Indent("", "\t")
   139  	err = encoder.Encode(in)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	return encoder.Flush()
   144  }