go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/legacy/annotee/step_test.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package annotee
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/url"
    21  	"testing"
    22  
    23  	"google.golang.org/protobuf/types/known/structpb"
    24  	"google.golang.org/protobuf/types/known/timestamppb"
    25  
    26  	pb "go.chromium.org/luci/buildbucket/proto"
    27  	annotpb "go.chromium.org/luci/luciexe/legacy/annotee/proto"
    28  
    29  	. "github.com/smartystreets/goconvey/convey"
    30  	. "go.chromium.org/luci/common/testing/assertions"
    31  )
    32  
    33  var annotationStep = &annotpb.Step{
    34  	Substep: asSubSteps(
    35  		&annotpb.Step{
    36  			Name:   "running step",
    37  			Status: annotpb.Status_RUNNING,
    38  		},
    39  		&annotpb.Step{
    40  			Name:    "successful step",
    41  			Status:  annotpb.Status_SUCCESS,
    42  			Started: &timestamppb.Timestamp{Seconds: 1400000000},
    43  			Ended:   &timestamppb.Timestamp{Seconds: 1400001000},
    44  		},
    45  		&annotpb.Step{
    46  			Name:    "failed step",
    47  			Status:  annotpb.Status_FAILURE,
    48  			Started: &timestamppb.Timestamp{Seconds: 1400000000},
    49  			Ended:   &timestamppb.Timestamp{Seconds: 1400001000},
    50  		},
    51  		&annotpb.Step{
    52  			Name:           "infra-failed step",
    53  			Status:         annotpb.Status_FAILURE,
    54  			FailureDetails: &annotpb.FailureDetails{Type: annotpb.FailureDetails_EXCEPTION},
    55  			Started:        &timestamppb.Timestamp{Seconds: 1400000000},
    56  			Ended:          &timestamppb.Timestamp{Seconds: 1400001000},
    57  		},
    58  		&annotpb.Step{
    59  			Name:           "with failure details text",
    60  			Status:         annotpb.Status_FAILURE,
    61  			FailureDetails: &annotpb.FailureDetails{Text: "failure_details_text"},
    62  			Started:        &timestamppb.Timestamp{Seconds: 1400000000},
    63  			Ended:          &timestamppb.Timestamp{Seconds: 1400001000},
    64  		},
    65  		&annotpb.Step{
    66  			Name: "with text",
    67  			Text: []string{"text1", "text2"},
    68  		},
    69  		&annotpb.Step{
    70  			Name:         "with stdio",
    71  			StdoutStream: &annotpb.LogdogStream{Name: "steps/setup_build/0/stdout"},
    72  			StderrStream: &annotpb.LogdogStream{Name: "steps/setup_build/0/stderr"},
    73  		},
    74  		&annotpb.Step{
    75  			Name: "other links",
    76  			OtherLinks: []*annotpb.AnnotationLink{
    77  				{
    78  					Label: "logdog link",
    79  					Value: &annotpb.AnnotationLink_LogdogStream{
    80  						LogdogStream: &annotpb.LogdogStream{Name: "steps/setup_build/0/logs/run_recipe/0"},
    81  					},
    82  				},
    83  				{
    84  					Label: "1",
    85  					Value: &annotpb.AnnotationLink_Url{
    86  						Url: "https://example.com/1(foo)",
    87  					},
    88  				},
    89  				{
    90  					Label: "with-ampersand",
    91  					Value: &annotpb.AnnotationLink_Url{
    92  						Url: "https://example.com?a=1&timestamp=2",
    93  					},
    94  				},
    95  			},
    96  		},
    97  		&annotpb.Step{
    98  			Name: "substeps",
    99  			// This time will be overridden by children.
   100  			Started: &timestamppb.Timestamp{Seconds: 1500000500},
   101  			Ended:   &timestamppb.Timestamp{Seconds: 1500000501},
   102  			Substep: asSubSteps(
   103  				&annotpb.Step{
   104  					Name:   "child",
   105  					Status: annotpb.Status_SUCCESS,
   106  					Substep: asSubSteps(
   107  						&annotpb.Step{
   108  							Name:    "descendant0",
   109  							Status:  annotpb.Status_FAILURE,
   110  							Started: &timestamppb.Timestamp{Seconds: 1500000000},
   111  							Ended:   &timestamppb.Timestamp{Seconds: 1500001000},
   112  						},
   113  						&annotpb.Step{
   114  							Name:           "descendant1",
   115  							Status:         annotpb.Status_FAILURE,
   116  							FailureDetails: &annotpb.FailureDetails{Type: annotpb.FailureDetails_EXCEPTION},
   117  							Started:        &timestamppb.Timestamp{Seconds: 1500001000},
   118  							Ended:          &timestamppb.Timestamp{Seconds: 1500002000},
   119  						},
   120  					),
   121  				},
   122  				&annotpb.Step{
   123  					Name:    "child2",
   124  					Status:  annotpb.Status_SUCCESS,
   125  					Started: &timestamppb.Timestamp{Seconds: 1500002000},
   126  					Ended:   &timestamppb.Timestamp{Seconds: 1500003000},
   127  				},
   128  				&annotpb.Step{
   129  					Name:    "child3_unfinished",
   130  					Status:  annotpb.Status_RUNNING,
   131  					Started: &timestamppb.Timestamp{Seconds: 1500003000},
   132  				},
   133  			),
   134  		},
   135  		&annotpb.Step{
   136  			Name: "started_parent",
   137  			Substep: asSubSteps(
   138  				&annotpb.Step{
   139  					Name:    "descendant",
   140  					Status:  annotpb.Status_RUNNING,
   141  					Started: &timestamppb.Timestamp{Seconds: 1500000000},
   142  				},
   143  			),
   144  		},
   145  		&annotpb.Step{
   146  			Name:         "duplicate_log_name",
   147  			StdoutStream: &annotpb.LogdogStream{Name: "steps/duplicate_log_name/0/stdout"},
   148  			StderrStream: &annotpb.LogdogStream{Name: "steps/duplicate_log_name/0/stderr"},
   149  			OtherLinks: []*annotpb.AnnotationLink{
   150  				{
   151  					Label: "stdout",
   152  					Value: &annotpb.AnnotationLink_LogdogStream{
   153  						LogdogStream: &annotpb.LogdogStream{Name: "steps/duplicate_log_name/0/stdout"},
   154  					},
   155  				},
   156  			},
   157  		},
   158  		&annotpb.Step{Name: "dup step name"},
   159  		&annotpb.Step{Name: "dup step name"},
   160  		&annotpb.Step{
   161  			Name: "parent_prefix",
   162  			Substep: asSubSteps(
   163  				&annotpb.Step{
   164  					Name: "parent_prefix.child",
   165  					Substep: asSubSteps(
   166  						&annotpb.Step{
   167  							Name: "parent_prefix.child.grandchild",
   168  						},
   169  					),
   170  				},
   171  			),
   172  		},
   173  		&annotpb.Step{
   174  			Name:    "start time is a bit greater than end time",
   175  			Status:  annotpb.Status_SUCCESS,
   176  			Started: &timestamppb.Timestamp{Seconds: 1500000000, Nanos: 2},
   177  			Ended:   &timestamppb.Timestamp{Seconds: 1500000000, Nanos: 1},
   178  		},
   179  	),
   180  }
   181  
   182  type calcURLFunc func(logName string) string
   183  
   184  var expectedStepsFn = func(urlFunc, viewerURLFunc calcURLFunc) []*pb.Step {
   185  	return []*pb.Step{
   186  		{
   187  			Name:   "running step",
   188  			Status: pb.Status_SCHEDULED,
   189  		},
   190  		{
   191  			Name:      "successful step",
   192  			Status:    pb.Status_SUCCESS,
   193  			StartTime: &timestamppb.Timestamp{Seconds: 1400000000},
   194  			EndTime:   &timestamppb.Timestamp{Seconds: 1400001000},
   195  		},
   196  		{
   197  			Name:      "failed step",
   198  			Status:    pb.Status_FAILURE,
   199  			StartTime: &timestamppb.Timestamp{Seconds: 1400000000},
   200  			EndTime:   &timestamppb.Timestamp{Seconds: 1400001000},
   201  		},
   202  		{
   203  			Name:      "infra-failed step",
   204  			Status:    pb.Status_INFRA_FAILURE,
   205  			StartTime: &timestamppb.Timestamp{Seconds: 1400000000},
   206  			EndTime:   &timestamppb.Timestamp{Seconds: 1400001000},
   207  		},
   208  		{
   209  			Name:            "with failure details text",
   210  			Status:          pb.Status_FAILURE,
   211  			SummaryMarkdown: "failure_details_text",
   212  			StartTime:       &timestamppb.Timestamp{Seconds: 1400000000},
   213  			EndTime:         &timestamppb.Timestamp{Seconds: 1400001000},
   214  		},
   215  		{
   216  			Name:            "with text",
   217  			Status:          pb.Status_SCHEDULED,
   218  			SummaryMarkdown: "\n\n<div>text1 text2</div>\n\n",
   219  		},
   220  		{
   221  			Name:   "with stdio",
   222  			Status: pb.Status_SCHEDULED,
   223  			Logs: []*pb.Log{
   224  				{
   225  					Name:    "stdout",
   226  					Url:     urlFunc("steps/setup_build/0/stdout"),
   227  					ViewUrl: viewerURLFunc("steps/setup_build/0/stdout"),
   228  				},
   229  				{
   230  					Name:    "stderr",
   231  					Url:     urlFunc("steps/setup_build/0/stderr"),
   232  					ViewUrl: viewerURLFunc("steps/setup_build/0/stderr"),
   233  				},
   234  			},
   235  		},
   236  		{
   237  			Name:            "other links",
   238  			Status:          pb.Status_SCHEDULED,
   239  			SummaryMarkdown: "* [1](https://example.com/1\\(foo\\))\n* [with-ampersand](https://example.com?a=1&amp;timestamp=2)",
   240  			Logs: []*pb.Log{
   241  				{
   242  					Name:    "logdog link",
   243  					Url:     urlFunc("steps/setup_build/0/logs/run_recipe/0"),
   244  					ViewUrl: viewerURLFunc("steps/setup_build/0/logs/run_recipe/0"),
   245  				},
   246  			},
   247  		},
   248  		{
   249  			Name:      "substeps",
   250  			Status:    pb.Status_STARTED,
   251  			StartTime: &timestamppb.Timestamp{Seconds: 1500000000},
   252  		},
   253  		{
   254  			Name:      "substeps|child",
   255  			Status:    pb.Status_STARTED,
   256  			StartTime: &timestamppb.Timestamp{Seconds: 1500000000},
   257  		},
   258  		{
   259  			Name:      "substeps|child|descendant0",
   260  			Status:    pb.Status_FAILURE,
   261  			StartTime: &timestamppb.Timestamp{Seconds: 1500000000},
   262  			EndTime:   &timestamppb.Timestamp{Seconds: 1500001000},
   263  		},
   264  		{
   265  			Name:      "substeps|child|descendant1",
   266  			Status:    pb.Status_INFRA_FAILURE,
   267  			StartTime: &timestamppb.Timestamp{Seconds: 1500001000},
   268  			EndTime:   &timestamppb.Timestamp{Seconds: 1500002000},
   269  		},
   270  		{
   271  			Name:      "substeps|child2",
   272  			Status:    pb.Status_SUCCESS,
   273  			StartTime: &timestamppb.Timestamp{Seconds: 1500002000},
   274  			EndTime:   &timestamppb.Timestamp{Seconds: 1500003000},
   275  		},
   276  		{
   277  			Name:      "substeps|child3_unfinished",
   278  			Status:    pb.Status_STARTED,
   279  			StartTime: &timestamppb.Timestamp{Seconds: 1500003000},
   280  		},
   281  		{
   282  			Name:      "started_parent",
   283  			Status:    pb.Status_STARTED,
   284  			StartTime: &timestamppb.Timestamp{Seconds: 1500000000},
   285  		},
   286  		{
   287  			Name:      "started_parent|descendant",
   288  			Status:    pb.Status_STARTED,
   289  			StartTime: &timestamppb.Timestamp{Seconds: 1500000000},
   290  		},
   291  		{
   292  			Name:   "duplicate_log_name",
   293  			Status: pb.Status_SCHEDULED,
   294  			Logs: []*pb.Log{
   295  				{
   296  					Name:    "stdout",
   297  					Url:     urlFunc("steps/duplicate_log_name/0/stdout"),
   298  					ViewUrl: viewerURLFunc("steps/duplicate_log_name/0/stdout"),
   299  				},
   300  				{
   301  					Name:    "stderr",
   302  					Url:     urlFunc("steps/duplicate_log_name/0/stderr"),
   303  					ViewUrl: viewerURLFunc("steps/duplicate_log_name/0/stderr"),
   304  				},
   305  			},
   306  		},
   307  		{
   308  			Name:   "dup step name",
   309  			Status: pb.Status_SCHEDULED,
   310  		},
   311  		{
   312  			Name:   "dup step name (2)",
   313  			Status: pb.Status_SCHEDULED,
   314  		},
   315  		{
   316  			Name:   "parent_prefix",
   317  			Status: pb.Status_SCHEDULED,
   318  		},
   319  		{
   320  			Name:   "parent_prefix|child",
   321  			Status: pb.Status_SCHEDULED,
   322  		},
   323  		{
   324  			Name:   "parent_prefix|child|grandchild",
   325  			Status: pb.Status_SCHEDULED,
   326  		},
   327  		{
   328  			Name:      "start time is a bit greater than end time",
   329  			Status:    pb.Status_SUCCESS,
   330  			StartTime: &timestamppb.Timestamp{Seconds: 1500000000, Nanos: 1},
   331  			EndTime:   &timestamppb.Timestamp{Seconds: 1500000000, Nanos: 2},
   332  		},
   333  	}
   334  
   335  }
   336  
   337  func TestConvertBuildStep(t *testing.T) {
   338  	t.Parallel()
   339  
   340  	Convey("convert", t, func() {
   341  		Convey("with LogDog URL constructed", func() {
   342  			host := "logdog.example.com"
   343  			prefix := "project/prefix"
   344  
   345  			actual, err := ConvertBuildSteps(context.Background(), annotationStep.Substep, true, host, prefix)
   346  			So(err, ShouldBeNil)
   347  			expected := expectedStepsFn(
   348  				func(logName string) string {
   349  					return fmt.Sprintf("logdog://%s/%s/+/%s", host, prefix, logName)
   350  				},
   351  				func(logName string) string {
   352  					return fmt.Sprintf("https://%s/v/?s=%s", host, url.QueryEscape(prefix+"/+/"+logName))
   353  				},
   354  			)
   355  			So(actual, ShouldResembleProto, expected)
   356  		})
   357  		Convey("without LogDog URL constructed", func() {
   358  			actual, err := ConvertBuildSteps(context.Background(), annotationStep.Substep, false, "", "")
   359  			So(err, ShouldBeNil)
   360  			expected := expectedStepsFn(
   361  				func(logName string) string { return logName },
   362  				func(logName string) string { return "" },
   363  			)
   364  			So(actual, ShouldResembleProto, expected)
   365  		})
   366  	})
   367  }
   368  
   369  func TestConvertRootStep(t *testing.T) {
   370  	t.Parallel()
   371  
   372  	Convey("convert", t, func() {
   373  		rootStep := &annotpb.Step{
   374  			Started: &timestamppb.Timestamp{Seconds: 1400000000},
   375  			Ended:   &timestamppb.Timestamp{Seconds: 1500000000},
   376  			Status:  annotpb.Status_SUCCESS,
   377  			Substep: asSubSteps(
   378  				&annotpb.Step{
   379  					Name:    "cool step",
   380  					Status:  annotpb.Status_SUCCESS,
   381  					Started: &timestamppb.Timestamp{Seconds: 1400000000},
   382  					Ended:   &timestamppb.Timestamp{Seconds: 1500000000},
   383  					Property: []*annotpb.Step_Property{
   384  						{
   385  							Name:  "string_prop",
   386  							Value: "\"baz\"",
   387  						},
   388  					},
   389  				},
   390  			),
   391  			StdoutStream: &annotpb.LogdogStream{Name: "build/stdout"},
   392  			OtherLinks: []*annotpb.AnnotationLink{
   393  				{
   394  					Label: "awesome_log",
   395  					Value: &annotpb.AnnotationLink_LogdogStream{
   396  						LogdogStream: &annotpb.LogdogStream{Name: "build/awesome"},
   397  					},
   398  				},
   399  			},
   400  			Text: []string{
   401  				"text one",
   402  				"text two",
   403  			},
   404  			Property: []*annotpb.Step_Property{
   405  				{
   406  					Name:  "map_prop",
   407  					Value: "{\"foo\" : \"bar\"}",
   408  				},
   409  			},
   410  		}
   411  
   412  		expectedBuild := &pb.Build{
   413  			StartTime: &timestamppb.Timestamp{Seconds: 1400000000},
   414  			EndTime:   &timestamppb.Timestamp{Seconds: 1500000000},
   415  			Status:    pb.Status_SUCCESS,
   416  			Steps: []*pb.Step{
   417  				{
   418  					Name:      "cool step",
   419  					Status:    pb.Status_SUCCESS,
   420  					StartTime: &timestamppb.Timestamp{Seconds: 1400000000},
   421  					EndTime:   &timestamppb.Timestamp{Seconds: 1500000000},
   422  				},
   423  			},
   424  			SummaryMarkdown: "\n\n<div>text one text two</div>\n\n",
   425  			Output: &pb.Build_Output{
   426  				Logs: []*pb.Log{
   427  					{
   428  						Name: "stdout",
   429  						Url:  "build/stdout",
   430  					},
   431  					{
   432  						Name: "awesome_log",
   433  						Url:  "build/awesome",
   434  					},
   435  				},
   436  				Properties: &structpb.Struct{
   437  					Fields: map[string]*structpb.Value{
   438  						"map_prop": {
   439  							Kind: &structpb.Value_StructValue{
   440  								StructValue: &structpb.Struct{
   441  									Fields: map[string]*structpb.Value{
   442  										"foo": {
   443  											Kind: &structpb.Value_StringValue{
   444  												StringValue: "bar",
   445  											},
   446  										},
   447  									},
   448  								},
   449  							},
   450  						},
   451  						"string_prop": {
   452  							Kind: &structpb.Value_StringValue{
   453  								StringValue: "baz",
   454  							},
   455  						},
   456  					},
   457  				},
   458  				SummaryMarkdown: "\n\n<div>text one text two</div>\n\n",
   459  				Status:          pb.Status_SUCCESS,
   460  			},
   461  		}
   462  
   463  		var test = func() {
   464  			actual, err := ConvertRootStep(context.Background(), rootStep)
   465  			So(err, ShouldBeNil)
   466  			So(actual, ShouldResembleProto, expectedBuild)
   467  		}
   468  
   469  		test()
   470  
   471  		Convey("infra failure build", func() {
   472  			rootStep.Status = annotpb.Status_FAILURE
   473  			rootStep.FailureDetails = &annotpb.FailureDetails{
   474  				Type: annotpb.FailureDetails_INFRA,
   475  				Text: "bad infra failure",
   476  			}
   477  
   478  			expectedBuild.Status = pb.Status_INFRA_FAILURE
   479  			expectedBuild.SummaryMarkdown = "bad infra failure\n\n" + expectedBuild.SummaryMarkdown
   480  			expectedBuild.Output.Status = pb.Status_INFRA_FAILURE
   481  			expectedBuild.Output.SummaryMarkdown = expectedBuild.SummaryMarkdown
   482  			test()
   483  		})
   484  		Convey("worst step status", func() {
   485  			rootStep.Substep[0].GetStep().Status = annotpb.Status_FAILURE
   486  
   487  			expectedBuild.Steps[0].Status = pb.Status_FAILURE
   488  			expectedBuild.Status = pb.Status_FAILURE
   489  			expectedBuild.Output.Status = pb.Status_FAILURE
   490  			test()
   491  		})
   492  		Convey("use largest step end time", func() {
   493  			rootStep.Substep[0].GetStep().Ended = &timestamppb.Timestamp{Seconds: 1600000000}
   494  
   495  			expectedBuild.Steps[0].EndTime = &timestamppb.Timestamp{Seconds: 1600000000}
   496  			expectedBuild.EndTime = &timestamppb.Timestamp{Seconds: 1600000000}
   497  			test()
   498  		})
   499  
   500  	})
   501  }
   502  
   503  func asSubSteps(subSteps ...*annotpb.Step) []*annotpb.Step_Substep {
   504  	ret := make([]*annotpb.Step_Substep, len(subSteps))
   505  	for i, subStep := range subSteps {
   506  		ret[i] = &annotpb.Step_Substep{
   507  			Substep: &annotpb.Step_Substep_Step{
   508  				Step: subStep,
   509  			},
   510  		}
   511  	}
   512  	return ret
   513  }