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 &timestamppb.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  }