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 }