diff --git a/core/itg/graph/update.go b/core/itg/graph/update.go index 9d0849b9..5e3ffa72 100644 --- a/core/itg/graph/update.go +++ b/core/itg/graph/update.go @@ -75,9 +75,14 @@ func (g *OptimizedGraph) UpdateGraph( for id, target := range g.ExternalRuleTargets { name := g.TargetIDToString[id] if _, exists := targets[name]; !exists { + deps := make([]string, 0, len(target.Deps)) + for depID := range target.Deps { + deps = append(deps, g.TargetIDToString[depID]) + } targets[name] = &targethasher.Target{ Name: name, RuleType: targethasher.ExternalRuleType, + Deps: deps, Hash: target.Hash, HashWithoutDeps: target.HashWithoutDeps, External: target.External, diff --git a/core/itg/graph/update_test.go b/core/itg/graph/update_test.go index 19b79b34..da4d63ee 100644 --- a/core/itg/graph/update_test.go +++ b/core/itg/graph/update_test.go @@ -234,6 +234,61 @@ func TestComputeHashes(t *testing.T) { }) } +// --- UpdateGraph: external rule target dep preservation --- + +func TestUpdateGraphPreservesExternalRuleTargetDeps(t *testing.T) { + t.Parallel() + + t.Run("carried-over external rule target keeps its dependency edges", func(t *testing.T) { + t.Parallel() + g := OptimizeGraph(map[string]*targethasher.Target{ + "//pkg:dep": {Name: "//pkg:dep", RuleType: "go_library", HashWithoutDeps: []byte{0x01}, Hash: []byte{0x01}}, + "//external:repo": { + Name: "//external:repo", + RuleType: targethasher.ExternalRuleType, + Deps: []string{"//pkg:dep"}, + Hash: []byte{0xCA, 0xFE}, + HashWithoutDeps: []byte{0xCA, 0xFE}, + }, + }) + depID := g.TargetNameToID["//pkg:dep"] + externalID := g.TargetNameToID["//external:repo"] + require.True(t, g.OptimizedTargets[externalID].Deps.Contains(depID), + "test setup: external target should start with the dep edge") + + // A query result with no targets at all: //external:repo is not + // rediscovered fresh, so UpdateGraph must reconstruct it from + // g.ExternalRuleTargets rather than dropping it. + err := g.UpdateGraph(context.Background(), &fakeSourceHasher{}, UpdateGraphInput{ + QueryResult: &buildpb.QueryResult{}, + }) + require.NoError(t, err) + + assert.True(t, g.OptimizedTargets[externalID].Deps.Contains(depID), + "external rule target should keep its dependency edge after being carried over unchanged") + }) + + t.Run("carried-over external rule target with no deps stays empty without error", func(t *testing.T) { + t.Parallel() + g := OptimizeGraph(map[string]*targethasher.Target{ + "//external:repo": { + Name: "//external:repo", + RuleType: targethasher.ExternalRuleType, + Hash: []byte{0xCA, 0xFE}, + HashWithoutDeps: []byte{0xCA, 0xFE}, + }, + }) + externalID := g.TargetNameToID["//external:repo"] + + err := g.UpdateGraph(context.Background(), &fakeSourceHasher{}, UpdateGraphInput{ + QueryResult: &buildpb.QueryResult{}, + }) + require.NoError(t, err) + + assert.Empty(t, g.OptimizedTargets[externalID].Deps) + }) +} + func TestComputeInvalidatedHashesCycleOrderInvariance(t *testing.T) { t.Parallel()