k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/pkg/benchmarkjunit/parse.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 main
    18  
    19  import (
    20  	"fmt"
    21  	"path"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/GoogleCloudPlatform/testgrid/metadata/junit"
    28  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  var (
    32  	// reSuiteStart identifies the start of a new TestSuite and captures the package path.
    33  	// Matches lines like "pkg: k8s.io/test-infra/experiment/dummybenchmarks"
    34  	reSuiteStart = regexp.MustCompile(`^pkg:\s+(\S+)\s*$`)
    35  	// reSuiteEnd identifies the end of a TestSuite and captures the overall result, package path, and runtime.
    36  	// Matches lines like:
    37  	// "ok  	k8s.io/test-infra/experiment/dummybenchmarks/subpkg	1.490s"
    38  	// "FAIL	k8s.io/test-infra/experiment/dummybenchmarks	17.829s"
    39  	reSuiteEnd = regexp.MustCompile(`^(ok|FAIL)\s+(\S+)\s+(\S+)\s*$`)
    40  	// reBenchMetrics identifies lines with metrics for successful Benchmarks and captures the name, op count, and metric values.
    41  	// Matches lines like:
    42  	// "Benchmark-4                 	20000000	       77.9 ns/op"
    43  	// "BenchmarkAllocsAndBytes-4   	10000000	       131 ns/op	 152.50 MB/s	     112 B/op	       2 allocs/op"
    44  	reBenchMetrics = regexp.MustCompile(`^(Benchmark\S*)\s+(\d+)\s+([\d\.]+) ns/op(?:\s+([\d\.]+) MB/s)?(?:\s+([\d\.]+) B/op)?(?:\s+([\d\.]+) allocs/op)?\s*$`)
    45  	// reActionLine identifies lines that start with "--- " and denote the start of log output and/or a skipped or failed Benchmark.
    46  	// Matches lines like:
    47  	// "--- BENCH: BenchmarkLog-4"
    48  	// "--- SKIP: BenchmarkSkip"
    49  	// "--- FAIL: BenchmarkFatal"
    50  	reActionLine = regexp.MustCompile(`^--- (BENCH|SKIP|FAIL):\s+(\S+)\s*$`)
    51  )
    52  
    53  func truncate(str string, n int) string {
    54  	if len(str) <= n {
    55  		return str
    56  	}
    57  	if n > 3 {
    58  		return str[:n-3] + "..."
    59  	}
    60  	return str[:n]
    61  }
    62  
    63  func recordLogText(s *junit.Suite, text string) {
    64  	if len(s.Results) == 0 {
    65  		logrus.Error("Tried to record Benchmark log text before any Benchmarks were found for the package!")
    66  		return
    67  	}
    68  	result := &s.Results[len(s.Results)-1]
    69  	// TODO: make text truncation configurable.
    70  	text = truncate(text, 1000)
    71  
    72  	switch {
    73  	case result.Failure != nil:
    74  		result.Failure.Value = text
    75  		// Also add failure text to "categorized_fail" property for TestGrid.
    76  		result.SetProperty("categorized_fail", text)
    77  
    78  	case result.Skipped != nil:
    79  		result.Skipped.Value = text
    80  
    81  	default:
    82  		result.Output = &text
    83  	}
    84  }
    85  
    86  // applyPropertiesFromMatch sets the properties and test duration for Result based on a Benchmark metric line match (reBenchMetrics).
    87  func applyPropertiesFromMatch(result *junit.Result, match []string) error {
    88  	opCount, err := strconv.ParseFloat(match[2], 64)
    89  	if err != nil {
    90  		return fmt.Errorf("error parsing opcount %q: %w", match[2], err)
    91  	}
    92  	opDuration, err := strconv.ParseFloat(match[3], 64)
    93  	if err != nil {
    94  		return fmt.Errorf("error parsing ns/op %q: %w", match[3], err)
    95  	}
    96  	result.Time = opCount * opDuration / 1000000000 // convert from ns to s.
    97  	result.SetProperty("op count", match[2])
    98  	result.SetProperty("avg op duration (ns/op)", match[3])
    99  	if len(match[4]) > 0 {
   100  		result.SetProperty("MB/s", match[4])
   101  	}
   102  	if len(match[5]) > 0 {
   103  		result.SetProperty("alloced B/op", match[5])
   104  	}
   105  	if len(match[6]) > 0 {
   106  		result.SetProperty("allocs/op", match[6])
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  func parse(raw []byte) (*junit.Suites, error) {
   113  	lines := strings.Split(string(raw), "\n")
   114  
   115  	var suites junit.Suites
   116  	var suite junit.Suite
   117  	var logText string
   118  	for _, line := range lines {
   119  		// First handle multi-line log text aggregation
   120  		if strings.HasPrefix(line, "    ") {
   121  			logText += strings.TrimPrefix(line, "    ") + "\n"
   122  			continue
   123  		} else if len(logText) > 0 {
   124  			recordLogText(&suite, logText)
   125  			logText = ""
   126  		}
   127  
   128  		switch {
   129  		case reSuiteStart.MatchString(line):
   130  			match := reSuiteStart.FindStringSubmatch(line)
   131  			suite = junit.Suite{
   132  				Name: match[1],
   133  			}
   134  
   135  		case reSuiteEnd.MatchString(line):
   136  			match := reSuiteEnd.FindStringSubmatch(line)
   137  			if match[2] != suite.Name {
   138  				// "Mismatched package summary for match[2] with suite.Name testsuites.
   139  				// This is normal in scenarios where the line matching pkg <packagename>
   140  				// is missing (running as a bazel test).
   141  				suite.Name = match[2]
   142  			}
   143  			duration, err := time.ParseDuration(match[3])
   144  			if err != nil {
   145  				return nil, fmt.Errorf("failed to parse package test time %q: %w", match[3], err)
   146  			}
   147  			suite.Time = duration.Seconds()
   148  			suites.Suites = append(suites.Suites, suite)
   149  			suite = junit.Suite{}
   150  
   151  		case reBenchMetrics.MatchString(line):
   152  			match := reBenchMetrics.FindStringSubmatch(line)
   153  			result := junit.Result{
   154  				ClassName: path.Base(suite.Name),
   155  				Name:      match[1],
   156  			}
   157  			if err := applyPropertiesFromMatch(&result, match); err != nil {
   158  				return nil, fmt.Errorf("error parsing benchmark metric values: %w", err)
   159  			}
   160  			suite.Results = append(suite.Results, result)
   161  			suite.Tests += 1
   162  
   163  		case reActionLine.MatchString(line):
   164  			match := reActionLine.FindStringSubmatch(line)
   165  			emptyText := "" // Will be replaced with real text once output is read.
   166  			if match[1] == "SKIP" {
   167  				suite.Results = append(suite.Results, junit.Result{
   168  					ClassName: path.Base(suite.Name),
   169  					Name:      match[2],
   170  					Skipped: &junit.Skipped{
   171  						Value: emptyText,
   172  					},
   173  				})
   174  			} else if match[1] == "FAIL" {
   175  				suite.Results = append(suite.Results, junit.Result{
   176  					ClassName: path.Base(suite.Name),
   177  					Name:      match[2],
   178  					Failure: &junit.Failure{
   179  						Type:  "failure",
   180  						Value: emptyText,
   181  					},
   182  				})
   183  				suite.Failures += 1
   184  				suite.Tests += 1
   185  			}
   186  		}
   187  	}
   188  	return &suites, nil
   189  }