go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/requirement/diff.go (about)

     1  // Copyright 2022 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 requirement
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"sort"
    21  
    22  	"google.golang.org/protobuf/proto"
    23  
    24  	"go.chromium.org/luci/common/data/cmpbin"
    25  
    26  	"go.chromium.org/luci/cv/internal/tryjob"
    27  )
    28  
    29  // DefinitionMapping is a mapping between two definitions.
    30  type DefinitionMapping map[*tryjob.Definition]*tryjob.Definition
    31  
    32  // Has reports whether the given tryjob definition is in the mapping as a key.
    33  func (dm DefinitionMapping) Has(def *tryjob.Definition) bool {
    34  	_, ok := dm[def]
    35  	return ok
    36  }
    37  
    38  // DiffResult contains a diff between two Tryjob requirements.
    39  type DiffResult struct {
    40  	// RemovedDefs contains the definitions in `base` that are no longer present
    41  	// in `target`.
    42  	//
    43  	// Only the keys will be populated, the values will be nil.
    44  	RemovedDefs DefinitionMapping
    45  	// ChangedDefs contains mapping between definitions in `base` and `target`
    46  	// that have changed.
    47  	//
    48  	// The categorization of changes may vary across different backend. For
    49  	// buildbucket tryjob, a definition is considered changed if the main builder
    50  	// stays the same and other properties like equivalent builder, reuse config
    51  	// have changed.
    52  	ChangedDefs DefinitionMapping
    53  	// ChangedDefsReverse is a reverse mapping of `ChangedDefs`
    54  	ChangedDefsReverse DefinitionMapping
    55  	// AddedDefs contains the definitions that are added to `target` but are not
    56  	// in `base`.
    57  	//
    58  	// Only the keys will be populated, the values will be nil.
    59  	AddedDefs DefinitionMapping
    60  	// UnchangedDefs is like `ChangeDefs` but contain unchanged definitions.
    61  	//
    62  	// This comes handy when trying to locate the corresponding definition in the
    63  	// `target` when iterating through `base`.
    64  	UnchangedDefs DefinitionMapping
    65  	// UnchangedDefsReverse is a reverse mapping of `UnchangedDefs`
    66  	UnchangedDefsReverse DefinitionMapping
    67  
    68  	// RetryConfigChanged indicates the retry configuration has changed.
    69  	RetryConfigChanged bool
    70  }
    71  
    72  // Diff computes the diff between two Tryjob Requirements.
    73  func Diff(base, target *tryjob.Requirement) DiffResult {
    74  	sortedBaseDefs := toSortedTryjobDefs(base.GetDefinitions())
    75  	sortedTargetDefs := toSortedTryjobDefs(target.GetDefinitions())
    76  
    77  	res := DiffResult{
    78  		AddedDefs:            make(DefinitionMapping),
    79  		ChangedDefs:          make(DefinitionMapping),
    80  		ChangedDefsReverse:   make(DefinitionMapping),
    81  		UnchangedDefs:        make(DefinitionMapping),
    82  		UnchangedDefsReverse: make(DefinitionMapping),
    83  		RemovedDefs:          make(DefinitionMapping),
    84  	}
    85  	for len(sortedBaseDefs) > 0 && len(sortedTargetDefs) > 0 {
    86  		baseDef, targetDef := sortedBaseDefs[0].def, sortedTargetDefs[0].def
    87  		switch bytes.Compare(sortedBaseDefs[0].sortKey, sortedTargetDefs[0].sortKey) {
    88  		case 0:
    89  			if !proto.Equal(baseDef, targetDef) {
    90  				res.ChangedDefs[baseDef] = targetDef
    91  				res.ChangedDefsReverse[targetDef] = baseDef
    92  			} else {
    93  				res.UnchangedDefs[baseDef] = targetDef
    94  				res.UnchangedDefsReverse[targetDef] = baseDef
    95  			}
    96  			sortedBaseDefs, sortedTargetDefs = sortedBaseDefs[1:], sortedTargetDefs[1:]
    97  		case -1:
    98  			// The head of the sortedBaseDefs is lower than the head of ,
    99  			// sortedTargetDefs, this implies that the definition at the head of
   100  			// sortedBaseDefs has been removed.
   101  			res.RemovedDefs[baseDef] = nil
   102  			sortedBaseDefs = sortedBaseDefs[1:]
   103  		case 1:
   104  			// Converse case of the above.
   105  			res.AddedDefs[targetDef] = nil
   106  			sortedTargetDefs = sortedTargetDefs[1:]
   107  		}
   108  	}
   109  	// Add the remaining definitions.
   110  	for _, def := range sortedBaseDefs {
   111  		res.RemovedDefs[def.def] = nil
   112  	}
   113  	for _, def := range sortedTargetDefs {
   114  		res.AddedDefs[def.def] = nil
   115  	}
   116  
   117  	if !proto.Equal(base.GetRetryConfig(), target.GetRetryConfig()) {
   118  		res.RetryConfigChanged = true
   119  	}
   120  	return res
   121  }
   122  
   123  type comparableTryjobDef struct {
   124  	def *tryjob.Definition
   125  	// sortKey is comparable binary encoding of critical part of definition.
   126  	//
   127  	// For example, for Buildbucket tryjob, the key will be encode(host
   128  	// + fully qualified builder name).
   129  	sortKey []byte
   130  }
   131  
   132  func toSortedTryjobDefs(defs []*tryjob.Definition) []comparableTryjobDef {
   133  	if len(defs) == 0 {
   134  		return nil // Optimization: avoid initialize empty slice.
   135  	}
   136  	ret := make([]comparableTryjobDef, len(defs))
   137  	for i, def := range defs {
   138  		if def.GetBuildbucket() == nil {
   139  			panic(fmt.Errorf("only support buildbucket backend; got %T", def.GetBackend()))
   140  		}
   141  		buf := &bytes.Buffer{}
   142  		encodeBuildbucketDef(buf, def.GetBuildbucket())
   143  		ret[i] = comparableTryjobDef{
   144  			def:     def,
   145  			sortKey: buf.Bytes(),
   146  		}
   147  	}
   148  	sort.Slice(ret, func(i, j int) bool {
   149  		return bytes.Compare(ret[i].sortKey, ret[j].sortKey) < 0
   150  	})
   151  	return ret
   152  }
   153  
   154  func encodeBuildbucketDef(buf cmpbin.WriteableBytesBuffer, bbdef *tryjob.Definition_Buildbucket) {
   155  	mustCmpbinWriteString(buf, bbdef.GetHost())
   156  	builder := bbdef.GetBuilder()
   157  	mustCmpbinWriteString(buf, builder.GetProject())
   158  	mustCmpbinWriteString(buf, builder.GetBucket())
   159  	mustCmpbinWriteString(buf, builder.GetBuilder())
   160  }
   161  
   162  func mustCmpbinWriteString(buf cmpbin.WriteableBytesBuffer, str string) {
   163  	_, err := cmpbin.WriteString(buf, str)
   164  	if err != nil {
   165  		panic(err)
   166  	}
   167  }