go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/host/buildmerge/build_state_test.go (about)

     1  // Copyright 2019 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 buildmerge
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"regexp"
    22  	"testing"
    23  
    24  	"github.com/golang/protobuf/ptypes"
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	bbpb "go.chromium.org/luci/buildbucket/proto"
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/logdog/api/logpb"
    30  	"go.chromium.org/luci/logdog/common/types"
    31  
    32  	. "github.com/smartystreets/goconvey/convey"
    33  
    34  	. "go.chromium.org/luci/common/testing/assertions"
    35  )
    36  
    37  func mkDgram(build *bbpb.Build) *logpb.LogEntry {
    38  	data, err := proto.Marshal(build)
    39  	So(err, ShouldBeNil)
    40  	return &logpb.LogEntry{
    41  		Content: &logpb.LogEntry_Datagram{Datagram: &logpb.Datagram{
    42  			Data: data,
    43  		}},
    44  	}
    45  }
    46  
    47  // cleanupProtoError inspects the SummaryMarkdown field of build proto
    48  // and if it contains proto err, it will strip out the proto error message.
    49  // The reason we are doing this is that the proto lib discourage us to perform
    50  // error string comparison. See:
    51  // https://github.com/protocolbuffers/protobuf-go/blob/cd108d00a8df3bba55927ef35ca07438b895d7aa/internal/errors/errors.go#L26-L34
    52  func cleanupProtoError(builds ...*bbpb.Build) {
    53  	re := regexp.MustCompile(`Error in build protocol:.*\sproto:`)
    54  	for _, build := range builds {
    55  		if build.Output.GetSummaryMarkdown() != "" {
    56  			So(build.Output.SummaryMarkdown, ShouldEqual, build.SummaryMarkdown)
    57  		}
    58  		if loc := re.FindStringIndex(build.SummaryMarkdown); loc != nil {
    59  			build.SummaryMarkdown = build.SummaryMarkdown[:loc[1]]
    60  			if build.Output.GetSummaryMarkdown() != "" {
    61  				build.Output.SummaryMarkdown = build.SummaryMarkdown
    62  			}
    63  		}
    64  	}
    65  }
    66  
    67  func assertStateEqual(actual, expected *buildState) {
    68  	cleanupProtoError(actual.build, expected.build)
    69  	So(actual.build, ShouldResembleProto, expected.build)
    70  	So(actual.closed, ShouldEqual, expected.closed)
    71  	So(actual.final, ShouldEqual, expected.final)
    72  	So(actual.invalid, ShouldEqual, expected.invalid)
    73  }
    74  
    75  func TestBuildState(t *testing.T) {
    76  	t.Parallel()
    77  
    78  	Convey(`buildState`, t, func() {
    79  		now, err := ptypes.TimestampProto(testclock.TestRecentTimeLocal)
    80  		So(err, ShouldBeNil)
    81  		ctx, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeLocal)
    82  		ctx, cancel := context.WithCancel(ctx)
    83  		defer cancel()
    84  
    85  		merger, err := New(ctx, "u/", &bbpb.Build{Output: &bbpb.Build_Output{}}, func(ns, stream types.StreamName) (url, viewURL string) {
    86  			return fmt.Sprintf("url://%s%s", ns, stream), fmt.Sprintf("view://%s%s", ns, stream)
    87  		})
    88  		So(err, ShouldBeNil)
    89  		defer merger.Close()
    90  
    91  		informChan := make(chan struct{}, 1)
    92  		merger.informNewData = func() {
    93  			informChan <- struct{}{}
    94  		}
    95  		wait := func() {
    96  			<-informChan
    97  		}
    98  
    99  		Convey(`opened in error state`, func() {
   100  			bs := newBuildStateTracker(ctx, merger, "ns/", false, errors.New("nope"))
   101  			wait() // for final build
   102  			So(bs.workClosed, ShouldBeTrue)
   103  			bs.Drain()
   104  			assertStateEqual(bs.latestState, &buildState{
   105  				build: &bbpb.Build{
   106  					SummaryMarkdown: "\n\nError in build protocol: nope",
   107  					Status:          bbpb.Status_INFRA_FAILURE,
   108  					UpdateTime:      now,
   109  					EndTime:         now,
   110  					Output: &bbpb.Build_Output{
   111  						Status:          bbpb.Status_INFRA_FAILURE,
   112  						SummaryMarkdown: "\n\nError in build protocol: nope",
   113  					},
   114  				},
   115  				closed: true,
   116  				final:  true,
   117  			})
   118  		})
   119  
   120  		Convey(`basic`, func() {
   121  			bs := newBuildStateTracker(ctx, merger, "ns/", false, nil)
   122  
   123  			Convey(`ignores updates when merger cancels context`, func() {
   124  				cancel()
   125  				wait() // cancel generated an 'informNewData'
   126  
   127  				bs.handleNewData(mkDgram(&bbpb.Build{
   128  					SummaryMarkdown: "some stuff",
   129  					Steps: []*bbpb.Step{
   130  						{Name: "Parent"},
   131  						{Name: "Parent|Child"},
   132  						{
   133  							Name: "Parent|Merge",
   134  							Logs: []*bbpb.Log{{
   135  								Name: "$build.proto",
   136  								Url:  "Parent/Merge/build.proto",
   137  							}},
   138  						},
   139  					},
   140  				}))
   141  				// No wait, because this handleNewData was ignored.
   142  				bs.Drain()
   143  				assertStateEqual(bs.latestState, &buildState{
   144  					build: &bbpb.Build{
   145  						EndTime:         now,
   146  						UpdateTime:      now,
   147  						Status:          bbpb.Status_INFRA_FAILURE,
   148  						SummaryMarkdown: "Never received any build data.",
   149  						Output: &bbpb.Build_Output{
   150  							Status:          bbpb.Status_INFRA_FAILURE,
   151  							SummaryMarkdown: "Never received any build data.",
   152  						},
   153  					},
   154  					closed: true,
   155  					final:  true,
   156  				})
   157  
   158  				Convey(`can still close, though`, func() {
   159  					bs.handleNewData(nil)
   160  					bs.Drain()
   161  					assertStateEqual(bs.latestState, &buildState{
   162  						closed: true,
   163  						final:  true,
   164  						build: &bbpb.Build{
   165  							EndTime:         now,
   166  							UpdateTime:      now,
   167  							Status:          bbpb.Status_INFRA_FAILURE,
   168  							SummaryMarkdown: "Never received any build data.",
   169  							Output: &bbpb.Build_Output{
   170  								Status:          bbpb.Status_INFRA_FAILURE,
   171  								SummaryMarkdown: "Never received any build data.",
   172  							},
   173  						},
   174  					})
   175  				})
   176  			})
   177  
   178  			Convey(`no updates`, func() {
   179  				Convey(`handleNewData(nil)`, func() {
   180  					bs.handleNewData(nil)
   181  					wait()
   182  					bs.Drain()
   183  					assertStateEqual(bs.latestState, &buildState{
   184  						closed: true,
   185  						final:  true,
   186  						build: &bbpb.Build{
   187  							EndTime:         now,
   188  							UpdateTime:      now,
   189  							Status:          bbpb.Status_INFRA_FAILURE,
   190  							SummaryMarkdown: "Never received any build data.",
   191  							Output: &bbpb.Build_Output{
   192  								Status:          bbpb.Status_INFRA_FAILURE,
   193  								SummaryMarkdown: "Never received any build data.",
   194  							},
   195  						}},
   196  					)
   197  
   198  					Convey(`convey close noop`, func() {
   199  						// not a valid state, but bs is closed, so handleNewData should do nothing
   200  						// when invoked.
   201  						bs.latestState = &buildState{
   202  							closed: true,
   203  							final:  true,
   204  							build:  &bbpb.Build{SummaryMarkdown: "wat"},
   205  						}
   206  						bs.handleNewData(nil)
   207  						assertStateEqual(bs.latestState, &buildState{
   208  							closed: true,
   209  							final:  true,
   210  							build:  &bbpb.Build{SummaryMarkdown: "wat"},
   211  						})
   212  					})
   213  				})
   214  			})
   215  
   216  			Convey(`valid update`, func() {
   217  				bs.handleNewData(mkDgram(&bbpb.Build{
   218  					SummaryMarkdown: "some stuff",
   219  					Steps: []*bbpb.Step{
   220  						{Name: "Parent"},
   221  						{Name: "Parent|Child"},
   222  						{
   223  							Name: "Parent|Merge",
   224  							Logs: []*bbpb.Log{{
   225  								Name: "$build.proto",
   226  								Url:  "Parent/Merge/build.proto",
   227  							}},
   228  						},
   229  					},
   230  					Output: &bbpb.Build_Output{
   231  						Logs: []*bbpb.Log{{
   232  							Name: "stderr",
   233  							Url:  "stderr",
   234  						}},
   235  					},
   236  				}))
   237  				wait()
   238  
   239  				So(bs.getLatestBuild(), ShouldResembleProto, &bbpb.Build{
   240  					SummaryMarkdown: "some stuff",
   241  					Steps: []*bbpb.Step{
   242  						{Name: "Parent"},
   243  						{Name: "Parent|Child"},
   244  						{
   245  							Name: "Parent|Merge",
   246  							Logs: []*bbpb.Log{},
   247  							MergeBuild: &bbpb.Step_MergeBuild{
   248  								FromLogdogStream: "url://ns/Parent/Merge/build.proto",
   249  							},
   250  						},
   251  					},
   252  					UpdateTime: now,
   253  					Output: &bbpb.Build_Output{
   254  						Logs: []*bbpb.Log{{
   255  							Name:    "stderr",
   256  							Url:     "url://ns/stderr",
   257  							ViewUrl: "view://ns/stderr",
   258  						}},
   259  					},
   260  				})
   261  
   262  				Convey(`followed by garbage`, func() {
   263  					bs.handleNewData(&logpb.LogEntry{
   264  						Content: &logpb.LogEntry_Datagram{Datagram: &logpb.Datagram{
   265  							Data: []byte("narpnarp"),
   266  						}},
   267  					})
   268  					wait()
   269  					wait() // for final build
   270  					bs.Drain()
   271  					assertStateEqual(bs.latestState, &buildState{
   272  						closed:  true,
   273  						final:   true,
   274  						invalid: true,
   275  						build: &bbpb.Build{
   276  							SummaryMarkdown: ("some stuff\n\n" +
   277  								"Error in build protocol: parsing Build: proto:"),
   278  							Status: bbpb.Status_INFRA_FAILURE,
   279  							Steps: []*bbpb.Step{
   280  								{Name: "Parent", EndTime: now, Status: bbpb.Status_CANCELED,
   281  									SummaryMarkdown: "step was never finalized; did the build crash?"},
   282  								{Name: "Parent|Child", EndTime: now, Status: bbpb.Status_CANCELED,
   283  									SummaryMarkdown: "step was never finalized; did the build crash?"},
   284  								{
   285  									Name: "Parent|Merge",
   286  									Logs: []*bbpb.Log{},
   287  									MergeBuild: &bbpb.Step_MergeBuild{
   288  										FromLogdogStream: "url://ns/Parent/Merge/build.proto",
   289  									},
   290  								},
   291  							},
   292  							UpdateTime: now,
   293  							EndTime:    now,
   294  							Output: &bbpb.Build_Output{
   295  								Logs: []*bbpb.Log{{
   296  									Name:    "stderr",
   297  									Url:     "url://ns/stderr",
   298  									ViewUrl: "view://ns/stderr",
   299  								}},
   300  								Status: bbpb.Status_INFRA_FAILURE,
   301  								SummaryMarkdown: ("some stuff\n\n" +
   302  									"Error in build protocol: parsing Build: proto:"),
   303  							},
   304  						},
   305  					})
   306  				})
   307  
   308  				Convey(`handleNewData(nil)`, func() {
   309  					bs.handleNewData(nil)
   310  					wait()
   311  					bs.Drain()
   312  					assertStateEqual(bs.latestState, &buildState{
   313  						closed: true,
   314  						final:  true,
   315  						build: &bbpb.Build{
   316  							SummaryMarkdown: ("some stuff\n\nError in build protocol: " +
   317  								"Expected a terminal build status, " +
   318  								"got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED."),
   319  							Status: bbpb.Status_INFRA_FAILURE,
   320  							Steps: []*bbpb.Step{
   321  								{Name: "Parent", EndTime: now, Status: bbpb.Status_CANCELED,
   322  									SummaryMarkdown: "step was never finalized; did the build crash?"},
   323  								{Name: "Parent|Child", EndTime: now, Status: bbpb.Status_CANCELED,
   324  									SummaryMarkdown: "step was never finalized; did the build crash?"},
   325  								{
   326  									Name: "Parent|Merge",
   327  									Logs: []*bbpb.Log{},
   328  									MergeBuild: &bbpb.Step_MergeBuild{
   329  										FromLogdogStream: "url://ns/Parent/Merge/build.proto",
   330  									},
   331  								},
   332  							},
   333  							EndTime:    now,
   334  							UpdateTime: now,
   335  							Output: &bbpb.Build_Output{
   336  								Logs: []*bbpb.Log{{
   337  									Name:    "stderr",
   338  									Url:     "url://ns/stderr",
   339  									ViewUrl: "view://ns/stderr",
   340  								}},
   341  								Status: bbpb.Status_INFRA_FAILURE,
   342  								SummaryMarkdown: ("some stuff\n\nError in build protocol: " +
   343  									"Expected a terminal build status, " +
   344  									"got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED."),
   345  							},
   346  						},
   347  					})
   348  				})
   349  			})
   350  
   351  			Convey(`invalid build data`, func() {
   352  				bs.handleNewData(&logpb.LogEntry{
   353  					Content: &logpb.LogEntry_Datagram{Datagram: &logpb.Datagram{
   354  						Data: []byte("narpnarp"),
   355  					}},
   356  				})
   357  				wait()
   358  				wait() // for final build
   359  				bs.Drain()
   360  				assertStateEqual(bs.latestState, &buildState{
   361  					closed:  true,
   362  					final:   true,
   363  					invalid: true,
   364  					build: &bbpb.Build{
   365  						SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"),
   366  						Status:          bbpb.Status_INFRA_FAILURE,
   367  						UpdateTime:      now,
   368  						EndTime:         now,
   369  						Output: &bbpb.Build_Output{
   370  							Status:          bbpb.Status_INFRA_FAILURE,
   371  							SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"),
   372  						},
   373  					},
   374  				})
   375  
   376  				Convey(`ignores further updates`, func() {
   377  					bs.handleNewData(mkDgram(&bbpb.Build{SummaryMarkdown: "hi"}))
   378  					bs.Drain()
   379  					assertStateEqual(bs.latestState, &buildState{
   380  						invalid: true,
   381  						final:   true,
   382  						closed:  true,
   383  						build: &bbpb.Build{
   384  							SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"),
   385  							Status:          bbpb.Status_INFRA_FAILURE,
   386  							UpdateTime:      now,
   387  							EndTime:         now,
   388  							Output: &bbpb.Build_Output{
   389  								Status:          bbpb.Status_INFRA_FAILURE,
   390  								SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"),
   391  							},
   392  						},
   393  					})
   394  				})
   395  			})
   396  
   397  			Convey(`accept absolute url`, func() {
   398  				bs.handleNewData(mkDgram(&bbpb.Build{
   399  					Steps: []*bbpb.Step{
   400  						{
   401  							Name: "hi",
   402  							Logs: []*bbpb.Log{{
   403  								Name: "foo",
   404  								Url:  "log/foo",
   405  							}},
   406  						},
   407  						{
   408  							Name: "heyo",
   409  							Logs: []*bbpb.Log{{
   410  								Name:    "bar",
   411  								Url:     "url://another_ns/log/bar", // absolute url populated
   412  								ViewUrl: "view://another_ns/log/bar",
   413  							}},
   414  						},
   415  					},
   416  					Output: &bbpb.Build_Output{
   417  						Logs: []*bbpb.Log{
   418  							{
   419  								Name: "stderr",
   420  								Url:  "stderr",
   421  							},
   422  							{
   423  								Name:    "another stderr",
   424  								Url:     "url://another_ns/stderr", // absolute url populated
   425  								ViewUrl: "view://another_ns/stderr",
   426  							},
   427  						},
   428  					},
   429  				}))
   430  				wait()
   431  
   432  				So(bs.getLatestBuild(), ShouldResembleProto, &bbpb.Build{
   433  					Steps: []*bbpb.Step{
   434  						{
   435  							Name: "hi",
   436  							Logs: []*bbpb.Log{{
   437  								Name:    "foo",
   438  								Url:     "url://ns/log/foo",
   439  								ViewUrl: "view://ns/log/foo",
   440  							}},
   441  						},
   442  						{
   443  							Name: "heyo",
   444  							Logs: []*bbpb.Log{{
   445  								Name:    "bar",
   446  								Url:     "url://another_ns/log/bar",
   447  								ViewUrl: "view://another_ns/log/bar",
   448  							}},
   449  						},
   450  					},
   451  					UpdateTime: now,
   452  					Output: &bbpb.Build_Output{
   453  						Logs: []*bbpb.Log{
   454  							{
   455  								Name:    "stderr",
   456  								Url:     "url://ns/stderr",
   457  								ViewUrl: "view://ns/stderr",
   458  							},
   459  							{
   460  								Name:    "another stderr",
   461  								Url:     "url://another_ns/stderr",
   462  								ViewUrl: "view://another_ns/stderr",
   463  							},
   464  						},
   465  					},
   466  				})
   467  			})
   468  
   469  			Convey(`bad log url`, func() {
   470  				Convey(`step log`, func() {
   471  					bs.handleNewData(mkDgram(&bbpb.Build{
   472  						Steps: []*bbpb.Step{
   473  							{
   474  								Name: "hi",
   475  								Logs: []*bbpb.Log{{
   476  									Name: "log",
   477  									Url:  "!!badnews!!",
   478  								}},
   479  							},
   480  						},
   481  					}))
   482  					wait()
   483  					wait() // for final build
   484  					bs.Drain()
   485  					assertStateEqual(bs.latestState, &buildState{
   486  						closed:  true,
   487  						final:   true,
   488  						invalid: true,
   489  						build: &bbpb.Build{
   490  							SummaryMarkdown: ("\n\nError in build protocol: " +
   491  								"step[\"hi\"].logs[\"log\"]: bad log url \"!!badnews!!\": " +
   492  								"segment (at 0) must begin with alphanumeric character"),
   493  							Steps: []*bbpb.Step{
   494  								{
   495  									Name:    "hi",
   496  									Status:  bbpb.Status_INFRA_FAILURE,
   497  									EndTime: now,
   498  									SummaryMarkdown: ("bad log url \"!!badnews!!\": " +
   499  										"segment (at 0) must begin with alphanumeric character"),
   500  									Logs: []*bbpb.Log{{
   501  										Name: "log",
   502  										Url:  "!!badnews!!",
   503  									}},
   504  								},
   505  							},
   506  							Status:     bbpb.Status_INFRA_FAILURE,
   507  							UpdateTime: now,
   508  							EndTime:    now,
   509  							Output: &bbpb.Build_Output{
   510  								Status: bbpb.Status_INFRA_FAILURE,
   511  								SummaryMarkdown: ("\n\nError in build protocol: " +
   512  									"step[\"hi\"].logs[\"log\"]: bad log url \"!!badnews!!\": " +
   513  									"segment (at 0) must begin with alphanumeric character"),
   514  							},
   515  						},
   516  					})
   517  				})
   518  
   519  				Convey(`build log`, func() {
   520  					bs.handleNewData(mkDgram(&bbpb.Build{
   521  						Output: &bbpb.Build_Output{
   522  							Logs: []*bbpb.Log{{
   523  								Name: "log",
   524  								Url:  "!!badnews!!",
   525  							}},
   526  						},
   527  					}))
   528  					wait()
   529  					wait() // for final build
   530  					bs.Drain()
   531  					assertStateEqual(bs.latestState, &buildState{
   532  						closed:  true,
   533  						final:   true,
   534  						invalid: true,
   535  						build: &bbpb.Build{
   536  							SummaryMarkdown: ("\n\nError in build protocol: " +
   537  								"build.output.logs[\"log\"]: bad log url \"!!badnews!!\": " +
   538  								"segment (at 0) must begin with alphanumeric character"),
   539  							Output: &bbpb.Build_Output{
   540  								Logs: []*bbpb.Log{{
   541  									Name: "log",
   542  									Url:  "!!badnews!!",
   543  								}},
   544  								Status: bbpb.Status_INFRA_FAILURE,
   545  								SummaryMarkdown: ("\n\nError in build protocol: " +
   546  									"build.output.logs[\"log\"]: bad log url \"!!badnews!!\": " +
   547  									"segment (at 0) must begin with alphanumeric character"),
   548  							},
   549  							Status:     bbpb.Status_INFRA_FAILURE,
   550  							UpdateTime: now,
   551  							EndTime:    now,
   552  						},
   553  					})
   554  				})
   555  			})
   556  		})
   557  	})
   558  }