1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
|
package gotest
import (
"fmt"
"sort"
"strings"
"time"
"github.com/jstemmer/go-junit-report/v2/gtr"
"github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/collector"
)
const (
globalID = 0
)
// reportBuilder helps build a test Report from a collection of events.
//
// The reportBuilder delegates to the packageBuilder for creating packages from
// basic test events, but keeps track of build errors itself. The reportBuilder
// is also responsible for generating unique test id's.
//
// Test output is collected by the output collector, which also keeps track of
// the currently active test so output is automatically associated with the
// correct test.
type reportBuilder struct {
packageBuilders map[string]*packageBuilder
buildErrors map[int]gtr.Error
nextID int // next free unused id
output *collector.Output // output collected for each id
packages []gtr.Package // completed packages
// options
packageName string
subtestMode SubtestMode
timestampFunc func() time.Time
}
// newReportBuilder creates a new reportBuilder.
func newReportBuilder() *reportBuilder {
return &reportBuilder{
packageBuilders: make(map[string]*packageBuilder),
buildErrors: make(map[int]gtr.Error),
nextID: 1,
output: collector.New(),
timestampFunc: time.Now,
}
}
// getPackageBuilder returns the packageBuilder for the given packageName. If
// no packageBuilder exists for the given package, a new one is created.
func (b *reportBuilder) getPackageBuilder(packageName string) *packageBuilder {
pb, ok := b.packageBuilders[packageName]
if !ok {
output := b.output
if packageName != "" {
output = collector.New()
}
pb = newPackageBuilder(b.generateID, output)
b.packageBuilders[packageName] = pb
}
return pb
}
// ProcessEvent takes a test event and adds it to the report.
func (b *reportBuilder) ProcessEvent(ev Event) {
switch ev.Type {
case "run_test":
b.getPackageBuilder(ev.Package).CreateTest(ev.Name)
case "pause_test":
b.getPackageBuilder(ev.Package).PauseTest(ev.Name)
case "cont_test":
b.getPackageBuilder(ev.Package).ContinueTest(ev.Name)
case "end_test":
b.getPackageBuilder(ev.Package).EndTest(ev.Name, ev.Result, ev.Duration, ev.Indent)
case "run_benchmark":
b.getPackageBuilder(ev.Package).CreateTest(ev.Name)
case "benchmark":
b.getPackageBuilder(ev.Package).BenchmarkResult(ev.Name, ev.Iterations, ev.NsPerOp, ev.MBPerSec, ev.BytesPerOp, ev.AllocsPerOp)
case "end_benchmark":
b.getPackageBuilder(ev.Package).EndTest(ev.Name, ev.Result, 0, 0)
case "status":
b.getPackageBuilder(ev.Package).End()
case "summary":
// The summary marks the end of a package. We can now create the actual
// package from all the events we've processed so far for this package.
b.packages = append(b.packages, b.CreatePackage(ev.Package, ev.Name, ev.Result, ev.Duration, ev.Data))
case "coverage":
b.getPackageBuilder(ev.Package).Coverage(ev.CovPct, ev.CovPackages)
case "build_output":
b.CreateBuildError(ev.Name)
case "output":
if ev.Package != "" {
b.getPackageBuilder(ev.Package).Output(ev.Data)
} else {
b.output.Append(ev.Data)
}
default:
// This shouldn't happen, but just in case print a warning and ignore
// this event.
fmt.Printf("reportBuilder: unhandled event type: %v\n", ev.Type)
}
}
// newID returns a new unique id.
func (b *reportBuilder) generateID() int {
id := b.nextID
b.nextID++
return id
}
// Build returns the new Report containing all the tests, build errors and
// their output created from the processed events.
func (b *reportBuilder) Build() gtr.Report {
// Create packages for any leftover package builders.
for name, pb := range b.packageBuilders {
if pb.IsEmpty() {
continue
}
b.packages = append(b.packages, b.CreatePackage(name, b.packageName, "", 0, ""))
}
// Create packages for any leftover build errors.
for _, buildErr := range b.buildErrors {
b.packages = append(b.packages, b.CreatePackage("", buildErr.Name, "", 0, ""))
}
return gtr.Report{Packages: b.packages}
}
// CreateBuildError creates a new build error and marks it as active.
func (b *reportBuilder) CreateBuildError(packageName string) {
id := b.generateID()
b.output.SetActiveID(id)
b.buildErrors[id] = gtr.Error{ID: id, Name: packageName}
}
// CreatePackage returns a new package containing all the build errors, output,
// tests and benchmarks created so far. The optional packageName is used to
// find the correct reportBuilder. The newPackageName is the actual package
// name that will be given to the returned package, which should be used in
// case the packageName was unknown until this point.
func (b *reportBuilder) CreatePackage(packageName, newPackageName, result string, duration time.Duration, data string) gtr.Package {
pkg := gtr.Package{
Name: newPackageName,
Duration: duration,
Timestamp: b.timestampFunc(),
}
// First check if this package contained a build error. If that's the case,
// we won't find any tests in this package.
for id, buildErr := range b.buildErrors {
if buildErr.Name == newPackageName || strings.TrimSuffix(buildErr.Name, "_test") == newPackageName {
pkg.BuildError = buildErr
pkg.BuildError.ID = id
pkg.BuildError.Duration = duration
pkg.BuildError.Cause = data
pkg.BuildError.Output = b.output.Get(id)
delete(b.buildErrors, id)
b.output.SetActiveID(0)
return pkg
}
}
// Get the packageBuilder for this package and make sure it's deleted, so
// future events for this package will use a new packageBuilder.
pb := b.getPackageBuilder(packageName)
delete(b.packageBuilders, packageName)
pb.output.SetActiveID(0)
// If the packageBuilder is empty, we never received any events for this
// package so there's no need to continue.
if pb.IsEmpty() {
// However, we should at least report an error if the result says we
// failed.
if parseResult(result) == gtr.Fail {
pkg.RunError = gtr.Error{
Name: newPackageName,
}
}
return pkg
}
// If we've collected output, but there were no tests, then this package
// had a runtime error or it simply didn't have any tests.
if pb.output.Contains(globalID) && len(pb.tests) == 0 {
if parseResult(result) == gtr.Fail {
pkg.RunError = gtr.Error{
Name: newPackageName,
Output: pb.output.Get(globalID),
}
} else {
pkg.Output = pb.output.Get(globalID)
}
pb.output.Clear(globalID)
return pkg
}
// If the summary result says we failed, but there were no failing tests
// then something else must have failed.
if parseResult(result) == gtr.Fail && len(pb.tests) > 0 && !pb.containsFailures() {
pkg.RunError = gtr.Error{
Name: newPackageName,
Output: pb.output.Get(globalID),
}
pb.output.Clear(globalID)
}
// Collect tests for this package
var tests []gtr.Test
for id, t := range pb.tests {
if pb.isParent(id) {
if b.subtestMode == IgnoreParentResults {
t.Result = gtr.Pass
} else if b.subtestMode == ExcludeParents {
pb.output.Merge(id, globalID)
continue
}
}
t.Output = pb.output.Get(id)
tests = append(tests, t)
}
tests = groupBenchmarksByName(tests, b.output)
// Sort packages by id to ensure we maintain insertion order.
sort.Slice(tests, func(i, j int) bool {
return tests[i].ID < tests[j].ID
})
pkg.Tests = groupBenchmarksByName(tests, pb.output)
pkg.Coverage = pb.coverage
pkg.Output = pb.output.Get(globalID)
pb.output.Clear(globalID)
return pkg
}
// parseResult returns a gtr.Result for the given result string r.
func parseResult(r string) gtr.Result {
switch r {
case "PASS":
return gtr.Pass
case "FAIL":
return gtr.Fail
case "SKIP":
return gtr.Skip
case "BENCH":
return gtr.Pass
default:
return gtr.Unknown
}
}
// groupBenchmarksByName groups tests with the Benchmark prefix if they have
// the same name and combines their output.
func groupBenchmarksByName(tests []gtr.Test, output *collector.Output) []gtr.Test {
if len(tests) == 0 {
return nil
}
var grouped []gtr.Test
byName := make(map[string][]gtr.Test)
for _, test := range tests {
if !strings.HasPrefix(test.Name, "Benchmark") {
// If this test is not a benchmark, we won't group it by name but
// just add it to the final result.
grouped = append(grouped, test)
continue
}
if _, ok := byName[test.Name]; !ok {
grouped = append(grouped, gtr.NewTest(test.ID, test.Name))
}
byName[test.Name] = append(byName[test.Name], test)
}
for i, group := range grouped {
if !strings.HasPrefix(group.Name, "Benchmark") {
continue
}
var (
ids []int
total Benchmark
count int
)
for _, test := range byName[group.Name] {
ids = append(ids, test.ID)
if test.Result != gtr.Pass {
continue
}
if bench, ok := GetBenchmarkData(test); ok {
total.Iterations += bench.Iterations
total.NsPerOp += bench.NsPerOp
total.MBPerSec += bench.MBPerSec
total.BytesPerOp += bench.BytesPerOp
total.AllocsPerOp += bench.AllocsPerOp
count++
}
}
group.Duration = combinedDuration(byName[group.Name])
group.Result = groupResults(byName[group.Name])
group.Output = output.GetAll(ids...)
if count > 0 {
total.Iterations /= int64(count)
total.NsPerOp /= float64(count)
total.MBPerSec /= float64(count)
total.BytesPerOp /= int64(count)
total.AllocsPerOp /= int64(count)
SetBenchmarkData(&group, total)
}
grouped[i] = group
}
return grouped
}
// combinedDuration returns the sum of the durations of the given tests.
func combinedDuration(tests []gtr.Test) time.Duration {
var total time.Duration
for _, test := range tests {
total += test.Duration
}
return total
}
// groupResults returns the result we should use for a collection of tests.
func groupResults(tests []gtr.Test) gtr.Result {
var result gtr.Result
for _, test := range tests {
if test.Result == gtr.Fail {
return gtr.Fail
}
if result != gtr.Pass {
result = test.Result
}
}
return result
}
// packageBuilder helps build a gtr.Package from a collection of test events.
type packageBuilder struct {
generateID func() int
output *collector.Output
tests map[int]gtr.Test
parentIDs map[int]struct{} // set of test id's that contain subtests
coverage float64 // coverage percentage
}
// newPackageBuilder creates a new packageBuilder. New tests will be assigned
// an ID returned by the generateID function. The activeIDSetter is called to
// set or reset the active test id.
func newPackageBuilder(generateID func() int, output *collector.Output) *packageBuilder {
return &packageBuilder{
generateID: generateID,
output: output,
tests: make(map[int]gtr.Test),
parentIDs: make(map[int]struct{}),
}
}
// IsEmpty returns true if this package builder does not have any tests and has
// not collected any global output.
func (b packageBuilder) IsEmpty() bool {
return len(b.tests) == 0 && !b.output.Contains(0)
}
// CreateTest adds a test with the given name to the package, marks it as
// active and returns its generated id.
func (b *packageBuilder) CreateTest(name string) int {
if parentID, ok := b.findTestParentID(name); ok {
b.parentIDs[parentID] = struct{}{}
}
id := b.generateID()
b.output.SetActiveID(id)
b.tests[id] = gtr.NewTest(id, name)
return id
}
// PauseTest marks the test with the given name no longer active. Any results
// or output added to the package after calling PauseTest will no longer be
// associated with this test.
func (b *packageBuilder) PauseTest(name string) {
b.output.SetActiveID(0)
}
// ContinueTest finds the test with the given name and marks it as active. If
// more than one test exist with this name, the most recently created test will
// be used.
func (b *packageBuilder) ContinueTest(name string) {
id, _ := b.findTest(name)
b.output.SetActiveID(id)
}
// EndTest finds the test with the given name, sets the result, duration and
// level. If more than one test exists with this name, the most recently
// created test will be used. If no test exists with this name, a new test is
// created. The test is then marked as no longer active.
func (b *packageBuilder) EndTest(name, result string, duration time.Duration, level int) {
id, ok := b.findTest(name)
if !ok {
// test did not exist, create one
// TODO: Likely reason is that the user ran go test without the -v
// flag, should we report this somewhere?
id = b.CreateTest(name)
}
t := b.tests[id]
t.Result = parseResult(result)
t.Duration = duration
t.Level = level
b.tests[id] = t
b.output.SetActiveID(0)
}
// End resets the active test.
func (b *packageBuilder) End() {
b.output.SetActiveID(0)
}
// BenchmarkResult updates an existing or adds a new test with the given
// results and marks it as active. If an existing test with this name exists
// but without result, then that one is updated. Otherwise a new one is added
// to the report.
func (b *packageBuilder) BenchmarkResult(name string, iterations int64, nsPerOp, mbPerSec float64, bytesPerOp, allocsPerOp int64) {
id, ok := b.findTest(name)
if !ok || b.tests[id].Result != gtr.Unknown {
id = b.CreateTest(name)
}
b.output.SetActiveID(id)
benchmark := Benchmark{iterations, nsPerOp, mbPerSec, bytesPerOp, allocsPerOp}
test := gtr.NewTest(id, name)
test.Result = gtr.Pass
test.Duration = benchmark.ApproximateDuration()
SetBenchmarkData(&test, benchmark)
b.tests[id] = test
}
// Coverage sets the code coverage percentage.
func (b *packageBuilder) Coverage(pct float64, packages []string) {
b.coverage = pct
}
// Output appends data to the output of this package.
func (b *packageBuilder) Output(data string) {
b.output.Append(data)
}
// findTest returns the id of the most recently created test with the given
// name if it exists.
func (b *packageBuilder) findTest(name string) (int, bool) {
var maxid int
for id, test := range b.tests {
if maxid < id && test.Name == name {
maxid = id
}
}
return maxid, maxid > 0
}
// findTestParentID searches the existing tests in this package for a parent of
// the test with the given name, and returns its id if one is found.
func (b *packageBuilder) findTestParentID(name string) (int, bool) {
parent := dropLastSegment(name)
for parent != "" {
if id, ok := b.findTest(parent); ok {
return id, true
}
parent = dropLastSegment(parent)
}
return 0, false
}
// isParent returns true if the test with the given id has sub tests.
func (b *packageBuilder) isParent(id int) bool {
_, ok := b.parentIDs[id]
return ok
}
// dropLastSegment strips the last `/` and everything following it from the
// given name. If no `/` was found, the empty string is returned.
func dropLastSegment(name string) string {
if idx := strings.LastIndexByte(name, '/'); idx >= 0 {
return name[:idx]
}
return ""
}
// containsFailures return true if this package contains at least one failing
// test or a test with an unknown result.
func (b *packageBuilder) containsFailures() bool {
for _, test := range b.tests {
if test.Result == gtr.Fail || test.Result == gtr.Unknown {
return true
}
}
return false
}
|