github.com/GoogleCloudPlatform/testgrid@v0.0.174/metadata/junit/junit.go (about)

     1  /*
     2  Copyright 2019 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 junit describes the test-infra definition of "junit", and provides
    18  // utilities to parse it.
    19  package junit
    20  
    21  import (
    22  	"bytes"
    23  	"encoding/xml"
    24  	"fmt"
    25  	"io"
    26  	"strings"
    27  	"unicode/utf8"
    28  )
    29  
    30  type suiteOrSuites struct {
    31  	suites Suites
    32  }
    33  
    34  func (s *suiteOrSuites) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    35  	switch start.Name.Local {
    36  	case "testsuites":
    37  		d.DecodeElement(&s.suites, &start)
    38  	case "testsuite":
    39  		var suite Suite
    40  		d.DecodeElement(&suite, &start)
    41  		s.suites.Suites = append(s.suites.Suites, suite)
    42  	default:
    43  		return fmt.Errorf("bad element name: %q", start.Name)
    44  	}
    45  	s.suites.Truncate(10000)
    46  	return nil
    47  }
    48  
    49  // Suites holds a <testsuites/> list of Suite results
    50  type Suites struct {
    51  	XMLName xml.Name `xml:"testsuites"`
    52  	Suites  []Suite  `xml:"testsuite"`
    53  }
    54  
    55  // Truncate ensures that strings do not exceed the specified length.
    56  func (s *Suites) Truncate(max int) {
    57  	for i := range s.Suites {
    58  		s.Suites[i].Truncate(max)
    59  	}
    60  }
    61  
    62  // Suite holds <testsuite/> results
    63  type Suite struct {
    64  	XMLName    xml.Name    `xml:"testsuite"`
    65  	Suites     []Suite     `xml:"testsuite"`
    66  	Name       string      `xml:"name,attr"`
    67  	Properties *Properties `xml:"properties,omitempty"`
    68  	Time       float64     `xml:"time,attr"` // Seconds
    69  	TimeStamp  string      `xml:"timestamp,attr"`
    70  	Failures   int         `xml:"failures,attr"`
    71  	Tests      int         `xml:"tests,attr"`
    72  	Disabled   int         `xml:"disabled,attr"`
    73  	Skipped    int         `xml:"skipped,attr"`
    74  	Errors     int         `xml:"errors,attr"`
    75  	Results    []Result    `xml:"testcase"`
    76  }
    77  
    78  // Truncate ensures that strings do not exceed the specified length.
    79  func (s *Suite) Truncate(max int) {
    80  	for i := range s.Suites {
    81  		s.Suites[i].Truncate(max)
    82  	}
    83  	for i := range s.Results {
    84  		s.Results[i].Truncate(max)
    85  	}
    86  }
    87  
    88  // Property defines the xml element that stores additional metrics about each benchmark.
    89  type Property struct {
    90  	Name  string `xml:"name,attr"`
    91  	Value string `xml:"value,attr"`
    92  }
    93  
    94  // Properties defines the xml element that stores the list of properties that are associated with one benchmark.
    95  type Properties struct {
    96  	PropertyList []Property `xml:"property"`
    97  }
    98  
    99  // Result holds <testcase/> results
   100  type Result struct {
   101  	Name       string      `xml:"name,attr"`
   102  	Time       float64     `xml:"time,attr"`
   103  	ClassName  string      `xml:"classname,attr"`
   104  	Output     *string     `xml:"system-out,omitempty"`
   105  	Error      *string     `xml:"system-err,omitempty"`
   106  	Errored    *Errored    `xml:"error,omitempty"`
   107  	Failure    *Failure    `xml:"failure,omitempty"`
   108  	Skipped    *Skipped    `xml:"skipped,omitempty"`
   109  	Status     string      `xml:"status,attr"`
   110  	Properties *Properties `xml:"properties,omitempty"`
   111  }
   112  
   113  // Errored holds <error/> elements.
   114  type Errored struct {
   115  	Message string `xml:"message,attr"`
   116  	Type    string `xml:"type,attr"`
   117  	Value   string `xml:",chardata"`
   118  }
   119  
   120  // Failure holds <failure/> elements.
   121  type Failure struct {
   122  	Message string `xml:"message,attr"`
   123  	Type    string `xml:"type,attr"`
   124  	Value   string `xml:",chardata"`
   125  }
   126  
   127  // Skipped holds <skipped/> elements.
   128  type Skipped struct {
   129  	Message string `xml:"message,attr"`
   130  	Value   string `xml:",chardata"`
   131  }
   132  
   133  // SetProperty adds the specified property to the Result or replaces the
   134  // existing value if a property with that name already exists.
   135  func (r *Result) SetProperty(name, value string) {
   136  	if r.Properties == nil {
   137  		r.Properties = &Properties{}
   138  	}
   139  	for i, existing := range r.Properties.PropertyList {
   140  		if existing.Name == name {
   141  			r.Properties.PropertyList[i].Value = value
   142  			return
   143  		}
   144  	}
   145  	// Didn't find an existing property. Add a new one.
   146  	r.Properties.PropertyList = append(
   147  		r.Properties.PropertyList,
   148  		Property{
   149  			Name:  name,
   150  			Value: value,
   151  		},
   152  	)
   153  }
   154  
   155  // Message extracts the message for the junit test case.
   156  //
   157  // Will use the first non-empty <error/>, <failure/>, <skipped/>, <system-err/>, <system-out/> value.
   158  func (r Result) Message(max int) string {
   159  	var msg string
   160  	switch {
   161  	case r.Errored != nil && (r.Errored.Message != "" || r.Errored.Value != ""):
   162  		msg = composeMessage(r.Errored.Message, r.Errored.Value)
   163  	case r.Failure != nil && (r.Failure.Message != "" || r.Failure.Value != ""):
   164  		msg = composeMessage(r.Failure.Message, r.Failure.Value)
   165  	case r.Skipped != nil && (r.Skipped.Message != "" || r.Skipped.Value != ""):
   166  		msg = composeMessage(r.Skipped.Message, r.Skipped.Value)
   167  	case r.Error != nil && *r.Error != "":
   168  		msg = *r.Error
   169  	case r.Output != nil && *r.Output != "":
   170  		msg = *r.Output
   171  	}
   172  	msg = truncate(msg, max)
   173  	if utf8.ValidString(msg) {
   174  		return msg
   175  	}
   176  	return fmt.Sprintf("invalid utf8: %s", strings.ToValidUTF8(msg, "?"))
   177  }
   178  
   179  func composeMessage(messages ...string) string {
   180  	nonEmptyMessages := []string{}
   181  	for _, m := range messages {
   182  		if m != "" {
   183  			nonEmptyMessages = append(nonEmptyMessages, m)
   184  		}
   185  	}
   186  	messageBuilder := strings.Builder{}
   187  	for i, m := range nonEmptyMessages {
   188  		messageBuilder.WriteString(m)
   189  		if i+1 < len(nonEmptyMessages) {
   190  			messageBuilder.WriteRune('\n')
   191  		}
   192  	}
   193  	return messageBuilder.String()
   194  }
   195  
   196  func truncate(s string, max int) string {
   197  	if max <= 0 {
   198  		return s
   199  	}
   200  	l := len(s)
   201  	if l < max {
   202  		return s
   203  	}
   204  	h := max / 2
   205  	return s[:h] + "..." + s[l-h:]
   206  }
   207  
   208  func truncatePointer(str *string, max int) {
   209  	if str == nil {
   210  		return
   211  	}
   212  	s := truncate(*str, max)
   213  	str = &s
   214  }
   215  
   216  // Truncate ensures that strings do not exceed the specified length.
   217  func (r Result) Truncate(max int) {
   218  	var errorVal, failureVal, skippedVal string
   219  	if r.Errored != nil {
   220  		errorVal = r.Errored.Value
   221  	}
   222  	if r.Failure != nil {
   223  		failureVal = r.Failure.Value
   224  	}
   225  	if r.Skipped != nil {
   226  		skippedVal = r.Skipped.Value
   227  	}
   228  	for _, s := range []*string{&errorVal, &failureVal, &skippedVal, r.Error, r.Output} {
   229  		truncatePointer(s, max)
   230  	}
   231  }
   232  
   233  func unmarshalXML(reader io.Reader, i interface{}) error {
   234  	dec := xml.NewDecoder(reader)
   235  	dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
   236  		switch charset {
   237  		case "UTF-8", "utf8", "":
   238  			// utf8 is not recognized by golang, but our coalesce.py writes a utf8 doc, which python accepts.
   239  			return input, nil
   240  		default:
   241  			return nil, fmt.Errorf("unknown charset: %s", charset)
   242  		}
   243  	}
   244  	return dec.Decode(i)
   245  }
   246  
   247  // Parse returns the Suites representation of these XML bytes.
   248  func Parse(buf []byte) (*Suites, error) {
   249  	if len(buf) == 0 {
   250  		return &Suites{}, nil
   251  	}
   252  	reader := bytes.NewReader(buf)
   253  	return ParseStream(reader)
   254  }
   255  
   256  // ParseStream reads bytes into a Suites object.
   257  func ParseStream(reader io.Reader) (*Suites, error) {
   258  	// Try to parse it as a <testsuites/> object
   259  	var s suiteOrSuites
   260  	err := unmarshalXML(reader, &s)
   261  	if err != nil && err != io.EOF {
   262  		return nil, err
   263  	}
   264  	return &s.suites, nil
   265  }