github.com/GoogleCloudPlatform/testgrid@v0.0.174/resultstore/resultstore.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 resultstore fetches and process results from ResultStore. 18 package resultstore 19 20 import ( 21 "fmt" 22 "net/url" 23 "time" 24 25 durationpb "github.com/golang/protobuf/ptypes/duration" 26 timestamppb "github.com/golang/protobuf/ptypes/timestamp" 27 wrapperspb "github.com/golang/protobuf/ptypes/wrappers" 28 resultstore "google.golang.org/genproto/googleapis/devtools/resultstore/v2" 29 ) 30 31 // Invocation represents a flatted ResultStore invocation 32 type Invocation struct { 33 // Name of the invocation, immutable after creation. 34 Name string 35 // Project in GCP that owns this invocation. 36 Project string 37 38 // Details describing the invocation. 39 Details string 40 // Duration of the invocation 41 Duration time.Duration 42 // Start time of the invocation 43 Start time.Time 44 45 // Files for this invocation (InvocationLog in particular) 46 Files []File 47 // Properties of the invocation, currently appears to be useless. 48 Properties []Property 49 50 // Status indicating whether the invocation completed successfully. 51 Status Status 52 // Description of the status. 53 Description string 54 } 55 56 // URL returns the Resultstore URL for a given resource as a string. 57 func URL(resourceName string) string { 58 u := url.URL{ 59 Scheme: "https", 60 Host: "source.cloud.google.com", 61 Path: "results/" + resourceName, 62 } 63 return u.String() 64 } 65 66 func fromInvocation(rsi *resultstore.Invocation) Invocation { 67 i := Invocation{ 68 Name: rsi.Name, 69 Files: fromFiles(rsi.Files), 70 Properties: fromProperties(rsi.Properties), 71 } 72 if ia := rsi.InvocationAttributes; ia != nil { 73 i.Project = ia.ProjectId 74 i.Description = ia.Description 75 } 76 if rsi.Timing != nil { 77 i.Start, i.Duration = fromTiming(rsi.Timing) 78 } 79 i.Status, i.Description = fromStatus(rsi.StatusAttributes) 80 return i 81 } 82 83 // To converts the invocation into a ResultStore Invoation proto. 84 func (i Invocation) To() *resultstore.Invocation { 85 inv := resultstore.Invocation{ 86 Name: i.Name, 87 Timing: timing(i.Start, i.Duration), 88 StatusAttributes: status(i.Status, i.Description), 89 Files: Files(i.Files), 90 Properties: properties(i.Properties), 91 } 92 if i.Project != "" || i.Details != "" { 93 inv.InvocationAttributes = &resultstore.InvocationAttributes{ 94 ProjectId: i.Project, 95 Description: i.Details, 96 } 97 } 98 return &inv 99 } 100 101 // Timing 102 103 func dur(d time.Duration) *durationpb.Duration { 104 return &durationpb.Duration{ 105 Seconds: int64(d / time.Second), 106 Nanos: int32(d % time.Second), 107 } 108 } 109 110 func stamp(when time.Time) *timestamppb.Timestamp { 111 if when.IsZero() { 112 return nil 113 } 114 return ×tamppb.Timestamp{ 115 Seconds: when.Unix(), 116 Nanos: int32(when.UnixNano() % int64(time.Second)), 117 } 118 } 119 120 func protoTimeToGoTime(t *timestamppb.Timestamp) time.Time { 121 return time.Unix(t.Seconds, int64(t.Nanos)) 122 } 123 124 func protoDurationToGoDuration(d *durationpb.Duration) time.Duration { 125 return time.Duration(d.Seconds)*time.Second + time.Duration(d.Nanos)*time.Nanosecond 126 } 127 128 func fromTiming(t *resultstore.Timing) (time.Time, time.Duration) { 129 var when time.Time 130 var dur time.Duration 131 if t == nil { 132 return when, dur 133 } 134 if s := t.StartTime; s != nil { 135 when = protoTimeToGoTime(s) 136 } 137 if d := t.Duration; d != nil { 138 dur = protoDurationToGoDuration(d) 139 } 140 return when, dur 141 } 142 143 func timing(when time.Time, d time.Duration) *resultstore.Timing { 144 never := when.IsZero() 145 if never && d == 0 { 146 return nil 147 } 148 rst := resultstore.Timing{} 149 if !never { 150 rst.StartTime = stamp(when) 151 } 152 if d > 0 { 153 rst.Duration = dur(d) 154 } 155 return &rst 156 } 157 158 // TestFailure == Failure 159 160 // Failure describes the encountered problem. 161 type Failure struct { 162 // Message is the failure message. 163 Message string 164 // Type is the type/type/class of error, currently appears useless. 165 Type string 166 // Stack represents the call stack, separated by new lines. 167 Stack string 168 // Expected represents what we expected, often just one value. 169 Expected []string 170 // Actual represents what we actually got. 171 Actual []string 172 } 173 174 func fromFailures(tfs []*resultstore.TestFailure) []Failure { 175 var ret []Failure 176 for _, tf := range tfs { 177 ret = append(ret, Failure{ 178 Message: tf.FailureMessage, 179 Type: tf.ExceptionType, 180 Stack: tf.StackTrace, 181 Expected: tf.Expected, 182 Actual: tf.Actual, 183 }) 184 } 185 return ret 186 } 187 188 // To converts the failure into a ResultStore TestFailure proto 189 func (f Failure) To() *resultstore.TestFailure { 190 return &resultstore.TestFailure{ 191 FailureMessage: f.Message, 192 ExceptionType: f.Type, 193 StackTrace: f.Stack, 194 Expected: f.Expected, 195 Actual: f.Actual, 196 } 197 } 198 199 func failures(fs []Failure) []*resultstore.TestFailure { 200 var rstfs []*resultstore.TestFailure 201 for _, f := range fs { 202 rstfs = append(rstfs, f.To()) 203 } 204 return rstfs 205 } 206 207 // TestError == Error 208 209 // Error describes what prevented completion. 210 type Error struct { 211 // Message of the error 212 Message string 213 // Type of error, currently useless. 214 Type string 215 // Stack trace, separated by new lines. 216 Stack string 217 } 218 219 // To returns the corresponding ResultStore TestError message 220 func (e Error) To() *resultstore.TestError { 221 return &resultstore.TestError{ 222 ErrorMessage: e.Message, 223 ExceptionType: e.Type, 224 StackTrace: e.Stack, 225 } 226 } 227 228 func fromErrors(tes []*resultstore.TestError) []Error { 229 var ret []Error 230 for _, te := range tes { 231 ret = append(ret, Error{ 232 Message: te.ErrorMessage, 233 Type: te.ExceptionType, 234 Stack: te.StackTrace, 235 }) 236 } 237 return ret 238 } 239 240 func errors(es []Error) []*resultstore.TestError { 241 var rstes []*resultstore.TestError 242 for _, e := range es { 243 rstes = append(rstes, e.To()) 244 } 245 return rstes 246 } 247 248 // Property 249 250 // Properties converts key, value pairs into a property list. 251 func Properties(pairs ...string) []Property { 252 if len(pairs)%2 == 1 { 253 panic(fmt.Sprintf("unbalanced properties: %v", pairs)) 254 } 255 var out []Property 256 for i := 0; i < len(pairs); i += 2 { 257 out = append(out, Property{Key: pairs[i], Value: pairs[i+1]}) 258 } 259 return out 260 } 261 262 // Property represents a key-value pairing. 263 type Property = resultstore.Property 264 265 func properties(ps []Property) []*Property { 266 var out []*Property 267 for _, p := range ps { 268 p2 := p 269 out = append(out, &p2) 270 } 271 return out 272 } 273 274 func fromProperties(ps []*Property) []Property { 275 var out []Property 276 for _, p := range ps { 277 out = append(out, *p) 278 } 279 return out 280 } 281 282 // File 283 284 // The following logs cause ResultStore to do additional processing 285 const ( 286 // BuildLog appears in the invocation log 287 BuildLog = "build.log" 288 289 // Stdout of a build action, which isn't useful right now. 290 Stdout = "stdout" 291 // Stderr of a build action, which also isn't useful. 292 Stderr = "stderr" 293 294 // TestLog appears in the Target Log tab. 295 TestLog = "test.log" 296 // TestXML causes ResultStore to process this junit.xml to add cases automatically (we aren't using). 297 TestXML = "test.xml" 298 299 // TestCov provides line coverage, currently we're not using this. 300 TestCov = "test.lcov" 301 // BaselineCov provides original line coverage, currently we're not using this. 302 BaselineCov = "baseline.lcov" 303 ) 304 305 // ResultStore will display the following logs inline. 306 const ( 307 // InvocationLog is a more obvious name for the invocation log 308 InvocationLog = BuildLog 309 // TargetLog is a more obvious name for the target log. 310 TargetLog = TestLog 311 ) 312 313 // File represents a file stored in GCS 314 type File struct { 315 // Unique name within the set 316 ID string 317 318 // ContentType tells the browser how to render 319 ContentType string 320 // Length if complete and known 321 Length int64 322 // URL to file in Google Cloud Storage, such as gs://bucket/path/foo 323 URL string 324 } 325 326 func wrap64(v int64) *wrapperspb.Int64Value { 327 if v == 0 { 328 return nil 329 } 330 return &wrapperspb.Int64Value{Value: v} 331 } 332 333 func unwrap64(w *wrapperspb.Int64Value) int64 { 334 if w == nil { 335 return 0 336 } 337 return w.Value 338 } 339 340 // To converts the file to the corresponding ResultStore File proto. 341 func (f File) To() *resultstore.File { 342 return &resultstore.File{ 343 Uid: f.ID, 344 Uri: f.URL, 345 Length: wrap64(f.Length), 346 ContentType: f.ContentType, 347 } 348 } 349 350 // Files converts a list of files. 351 func Files(fs []File) []*resultstore.File { 352 var rsfs []*resultstore.File 353 for _, f := range fs { 354 rsfs = append(rsfs, f.To()) 355 } 356 return rsfs 357 } 358 359 func fromFiles(fs []*resultstore.File) []File { 360 var out []File 361 for _, f := range fs { 362 out = append(out, File{ 363 ID: f.Uid, 364 URL: f.Uri, 365 Length: unwrap64(f.Length), 366 ContentType: f.ContentType, 367 }) 368 } 369 return out 370 } 371 372 // Case == TestCase 373 374 // Result specifies whether the test passed. 375 type Result = resultstore.TestCase_Result 376 377 // Common constants. 378 const ( 379 // Completed cases finished, producing failures if it failed. 380 Completed = resultstore.TestCase_COMPLETED 381 // Cancelled cases did not complete (should have an error). 382 Cancelled = resultstore.TestCase_CANCELLED 383 // Skipped cases did not run. 384 Skipped = resultstore.TestCase_SKIPPED 385 ) 386 387 // Case represents the completion of a test case/method. 388 type Case struct { 389 // Name identifies the test within its class. 390 Name string 391 392 // Class is the container holding one or more names. 393 Class string 394 // Result indicates whether it ran and to completion. 395 Result Result 396 397 // Duration of the case. 398 Duration time.Duration 399 // Errors preventing the case from completing. 400 Errors []Error 401 // Failures encountered upon completion. 402 Failures []Failure 403 // Files specific to this case 404 Files []File 405 // Properties of the case 406 Properties []Property 407 // Start time of the case. 408 Start time.Time 409 } 410 411 func fromCase(tc *resultstore.TestCase) Case { 412 c := Case{ 413 Name: tc.CaseName, 414 Class: tc.ClassName, 415 Result: tc.Result, 416 Properties: fromProperties(tc.Properties), 417 Errors: fromErrors(tc.Errors), 418 Failures: fromFailures(tc.Failures), 419 } 420 c.Start, c.Duration = fromTiming(tc.Timing) 421 return c 422 } 423 424 // To converts the case to the corresponding ResultStore TestCase proto. 425 func (c Case) To() *resultstore.TestCase { 426 return &resultstore.TestCase{ 427 CaseName: c.Name, 428 ClassName: c.Class, 429 Errors: errors(c.Errors), 430 Failures: failures(c.Failures), 431 Result: c.Result, 432 Timing: timing(c.Start, c.Duration), 433 Properties: properties(c.Properties), 434 } 435 } 436 437 // TestAction == Test 438 439 // Status represents the status of the action/target/invocation. 440 type Status = resultstore.Status 441 442 // Common statuses 443 const ( 444 // Running means incomplete. 445 Running = resultstore.Status_TESTING 446 // Passed means successful. 447 Passed = resultstore.Status_PASSED 448 // Failed means unsuccessful. 449 Failed = resultstore.Status_FAILED 450 ) 451 452 // Test represents a test action, containing action, suite and warnings. 453 type Test struct { 454 // Action holds generic metadata about the test 455 Action 456 // Suite holds a variety of case and sub-suite data. 457 Suite 458 // Warnings, appear to be useless. 459 Warnings []string 460 } 461 462 // To converts the test into the corresponding ResultStore Action proto 463 func (t Test) To() *resultstore.Action { 464 a := t.Action.to() 465 a.ActionType = &resultstore.Action_TestAction{ 466 TestAction: &resultstore.TestAction{ 467 Warnings: warnings(t.Warnings), 468 TestSuite: t.Suite.To(), 469 }, 470 } 471 a.Files = Files(t.Files) 472 a.Properties = properties(t.Properties) 473 return a 474 } 475 476 func fromTestAction(ta *resultstore.TestAction) (Suite, []string) { 477 if ta == nil { 478 return Suite{}, nil 479 } 480 return fromSuite(ta.TestSuite), fromWarnings(ta.Warnings) 481 } 482 483 func fromTest(a *resultstore.Action) Test { 484 t := Test{ 485 Action: fromAction(a), 486 } 487 t.Suite, t.Warnings = fromTestAction(a.GetTestAction()) 488 return t 489 } 490 491 // Action rerepresents a step in the target, such as a container or command. 492 type Action struct { 493 // StatusAttributes 494 // Description of the status. 495 Description string 496 // Status indicates whether the action completed successfully. 497 Status Status 498 499 // Timing 500 // Start of the action. 501 Start time.Time 502 // Duration of the action. 503 Duration time.Duration 504 505 // Node or machine on which the test ran. 506 Node string 507 // ExitCode of the command 508 ExitCode int 509 510 // TODO(fejta): deps, coverage 511 } 512 513 func (act Action) to() *resultstore.Action { 514 return &resultstore.Action{ 515 StatusAttributes: status(act.Status, act.Description), 516 Timing: timing(act.Start, act.Duration), 517 ActionAttributes: actionAttributes(act.Node, act.ExitCode), 518 } 519 } 520 521 func actionAttributes(node string, exit int) *resultstore.ActionAttributes { 522 if node == "" && exit == 0 { 523 return nil 524 } 525 return &resultstore.ActionAttributes{ 526 Hostname: node, 527 ExitCode: int32(exit), 528 } 529 } 530 531 func fromActionAttributes(aa *resultstore.ActionAttributes) (string, int) { 532 if aa == nil { 533 return "", 0 534 } 535 return aa.Hostname, int(aa.ExitCode) 536 } 537 538 func fromAction(a *resultstore.Action) Action { 539 var ret Action 540 ret.Status, ret.Description = fromStatus(a.StatusAttributes) 541 ret.Start, ret.Duration = fromTiming(a.Timing) 542 ret.Node, ret.ExitCode = fromActionAttributes(a.ActionAttributes) 543 return ret 544 } 545 546 func status(s Status, d string) *resultstore.StatusAttributes { 547 return &resultstore.StatusAttributes{ 548 Status: s, 549 Description: d, 550 } 551 } 552 553 func fromStatus(sa *resultstore.StatusAttributes) (Status, string) { 554 if sa == nil { 555 return 0, "" 556 } 557 return sa.Status, sa.Description 558 } 559 560 func warnings(ws []string) []*resultstore.TestWarning { 561 var rstws []*resultstore.TestWarning 562 for _, w := range ws { 563 rstws = append(rstws, &resultstore.TestWarning{WarningMessage: w}) 564 } 565 return rstws 566 } 567 568 func fromWarnings(ws []*resultstore.TestWarning) []string { 569 var ret []string 570 for _, w := range ws { 571 ret = append(ret, w.WarningMessage) 572 } 573 return ret 574 } 575 576 // TestSuite == Suite 577 578 // Suite represents testing details. 579 type Suite struct { 580 // Name of the suite, such as the tested class. 581 Name string 582 583 // Cases holds details about each case in the suite. 584 Cases []Case 585 // Duration of the entire suite. 586 Duration time.Duration 587 // Errors that prevented the suite from completing. 588 Errors []Error 589 // Failures detected during the suite. 590 Failures []Failure 591 // Files outputted by the suite. 592 Files []File 593 // Properties of the suite. 594 Properties []Property 595 // Result determines whether the suite ran and finished. 596 Result Result 597 // Time the suite started 598 Start time.Time 599 // Suites hold details about child suites. 600 Suites []Suite 601 } 602 603 func (s Suite) tests() []*resultstore.Test { 604 var ts []*resultstore.Test 605 for _, suite := range s.Suites { 606 ts = append(ts, &resultstore.Test{ 607 TestType: &resultstore.Test_TestSuite{ 608 TestSuite: suite.To(), 609 }, 610 }) 611 } 612 for _, c := range s.Cases { 613 ts = append(ts, &resultstore.Test{ 614 TestType: &resultstore.Test_TestCase{ 615 TestCase: c.To(), 616 }, 617 }) 618 } 619 return ts 620 } 621 622 func (s *Suite) fromTests(tests []*resultstore.Test) { 623 for _, t := range tests { 624 if tc := t.GetTestCase(); tc != nil { 625 s.Cases = append(s.Cases, fromCase(tc)) 626 } 627 if ts := t.GetTestSuite(); ts != nil { 628 s.Suites = append(s.Suites, fromSuite(ts)) 629 } 630 } 631 } 632 633 // To converts a suite into the corresponding ResultStore TestSuite proto. 634 func (s Suite) To() *resultstore.TestSuite { 635 return &resultstore.TestSuite{ 636 Errors: errors(s.Errors), 637 Failures: failures(s.Failures), 638 Properties: properties(s.Properties), 639 SuiteName: s.Name, 640 Tests: s.tests(), 641 Timing: timing(s.Start, s.Duration), 642 Files: Files(s.Files), 643 } 644 } 645 646 func fromSuite(ts *resultstore.TestSuite) Suite { 647 s := Suite{ 648 Errors: fromErrors(ts.Errors), 649 Failures: fromFailures(ts.Failures), 650 Properties: fromProperties(ts.Properties), 651 Name: ts.SuiteName, 652 Files: fromFiles(ts.Files), 653 } 654 s.fromTests(ts.Tests) 655 s.Start, s.Duration = fromTiming(ts.Timing) 656 return s 657 } 658 659 // Target represents a set of commands run inside the same pod. 660 type Target struct { 661 // Name of the target, immutable. 662 Name string 663 664 // Start time of the target. 665 Start time.Time 666 // Duration the target ran. 667 Duration time.Duration 668 669 // Status specifying whether the target completed successfully. 670 Status Status 671 // Description of the status 672 Description string 673 674 // Tags are metadata for the target (like github labels). 675 Tags []string 676 // Properties of the target 677 Properties []Property 678 } 679 680 func fromTarget(t *resultstore.Target) Target { 681 tgt := Target{ 682 Name: t.Name, 683 Properties: fromProperties(t.Properties), 684 } 685 if t.TargetAttributes != nil { 686 tgt.Tags = make([]string, len(t.TargetAttributes.Tags)) 687 copy(tgt.Tags, t.TargetAttributes.Tags) 688 } 689 tgt.Start, tgt.Duration = fromTiming(t.Timing) 690 tgt.Status, tgt.Description = fromStatus(t.StatusAttributes) 691 return tgt 692 } 693 694 // To converts a target into the corresponding ResultStore Target proto. 695 func (t Target) To() *resultstore.Target { 696 tgt := resultstore.Target{ 697 Timing: timing(t.Start, t.Duration), 698 StatusAttributes: status(t.Status, t.Description), 699 Visible: true, 700 Properties: properties(t.Properties), 701 } 702 if t.Tags != nil { 703 tgt.TargetAttributes = &resultstore.TargetAttributes{ 704 Tags: t.Tags, 705 } 706 } 707 return &tgt 708 }