diff --git a/internal/mapper/BUILD.bazel b/internal/mapper/BUILD.bazel index 5b988d0f..65bf5afd 100644 --- a/internal/mapper/BUILD.bazel +++ b/internal/mapper/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "mapper", srcs = [ "build_description.go", + "changed_targets.go", "errors.go", "target_graph.go", ], diff --git a/internal/mapper/changed_targets.go b/internal/mapper/changed_targets.go new file mode 100644 index 00000000..8b28d185 --- /dev/null +++ b/internal/mapper/changed_targets.go @@ -0,0 +1,40 @@ +package mapper + +import ( + "github.com/uber/tango/entity" + "github.com/uber/tango/tangopb" +) + +// ChangedTargetsResponseToProto converts an entity.GetChangedTargetsResponse +// to its proto equivalent for gRPC streaming. +func ChangedTargetsResponseToProto(resp *entity.GetChangedTargetsResponse) *tangopb.GetChangedTargetsResponse { + if resp.Metadata != nil { + return &tangopb.GetChangedTargetsResponse{ + Item: &tangopb.GetChangedTargetsResponse_Metadata{ + Metadata: metadataToProto(resp.Metadata), + }, + } + } + changed := make([]*tangopb.ChangedTarget, len(resp.ChangedTargets)) + for i := range resp.ChangedTargets { + ct := &resp.ChangedTargets[i] + changed[i] = &tangopb.ChangedTarget{ + ChangeType: tangopb.ChangeType(int32(ct.ChangeType)), + OldTarget: optionalTargetToProto(ct.OldTarget), + NewTarget: optionalTargetToProto(ct.NewTarget), + Distance: ct.Distance, + } + } + return &tangopb.GetChangedTargetsResponse{ + Item: &tangopb.GetChangedTargetsResponse_ChangedTargets{ + ChangedTargets: &tangopb.ChangedTargets{ChangedTargets: changed}, + }, + } +} + +func optionalTargetToProto(t *entity.OptimizedTarget) *tangopb.OptimizedTarget { + if t == nil { + return nil + } + return optimizedTargetToProto(t) +} diff --git a/internal/mapper/target_graph.go b/internal/mapper/target_graph.go index 45717d82..4ff3aa28 100644 --- a/internal/mapper/target_graph.go +++ b/internal/mapper/target_graph.go @@ -24,3 +24,48 @@ func ProtoToGetTargetGraphRequest(req *tangopb.GetTargetGraphRequest) (entity.Ge BypassCache: req.GetBypassCache(), }, nil } + +// GetTargetGraphResponseToProto converts an entity.GetTargetGraphResponse to +// the corresponding proto GetTargetGraphResponse. +func GetTargetGraphResponseToProto(chunk *entity.GetTargetGraphResponse) *tangopb.GetTargetGraphResponse { + if chunk.Metadata != nil { + return &tangopb.GetTargetGraphResponse{ + Item: &tangopb.GetTargetGraphResponse_Metadata{ + Metadata: metadataToProto(chunk.Metadata), + }, + } + } + targets := make([]*tangopb.OptimizedTarget, len(chunk.Targets)) + for i := range chunk.Targets { + targets[i] = optimizedTargetToProto(&chunk.Targets[i]) + } + return &tangopb.GetTargetGraphResponse{ + Item: &tangopb.GetTargetGraphResponse_Targets{ + Targets: &tangopb.OptimizedTargets{Targets: targets}, + }, + } +} + +// metadataToProto converts an entity.Metadata to a proto Metadata. +func metadataToProto(m *entity.Metadata) *tangopb.Metadata { + return &tangopb.Metadata{ + TargetIdMapping: m.TargetIDMapping, + RuleTypeMapping: m.RuleTypeMapping, + TagMapping: m.TagMapping, + AttributeNameMapping: m.AttributeNameMapping, + AttributeStringValueMapping: m.AttributeStringValueMapping, + } +} + +func optimizedTargetToProto(t *entity.OptimizedTarget) *tangopb.OptimizedTarget { + return &tangopb.OptimizedTarget{ + Id: t.ID, + Hash: t.Hash, + DirectDependencies: t.DirectDependencies, + RuleType: t.RuleType, + Tags: t.Tags, + Root: t.Root, + External: t.External, + Attributes: t.Attributes, + } +} diff --git a/internal/mapper/target_graph_test.go b/internal/mapper/target_graph_test.go index 0d0d92fb..ebec3962 100644 --- a/internal/mapper/target_graph_test.go +++ b/internal/mapper/target_graph_test.go @@ -86,3 +86,41 @@ func TestProtoToGetTargetGraphRequest(t *testing.T) { }) } } + +func TestGetTargetGraphResponseToProto_Targets(t *testing.T) { + t.Parallel() + + chunk := entity.GetTargetGraphResponse{ + Targets: []entity.OptimizedTarget{ + {ID: 1, Hash: "ab", DirectDependencies: []int32{2}, RuleType: 10, Root: true}, + {ID: 2, Hash: "cd", Tags: []int32{5, 6}, External: true}, + }, + } + + proto := GetTargetGraphResponseToProto(&chunk) + targets := proto.GetItem().(*tangopb.GetTargetGraphResponse_Targets) + require.Len(t, targets.Targets.GetTargets(), 2) + + got := targets.Targets.GetTargets()[0] + assert.Equal(t, int32(1), got.GetId()) + assert.Equal(t, "ab", got.GetHash()) + assert.Equal(t, []int32{2}, got.GetDirectDependencies()) + assert.Equal(t, int32(10), got.GetRuleType()) + assert.True(t, got.GetRoot()) +} + +func TestGetTargetGraphResponseToProto_Metadata(t *testing.T) { + t.Parallel() + + chunk := entity.GetTargetGraphResponse{ + Metadata: &entity.Metadata{ + TargetIDMapping: map[int32]string{1: "//pkg:a", 2: "//pkg:b"}, + RuleTypeMapping: map[int32]string{10: "go_library"}, + }, + } + + proto := GetTargetGraphResponseToProto(&chunk) + meta := proto.GetItem().(*tangopb.GetTargetGraphResponse_Metadata) + assert.Equal(t, map[int32]string{1: "//pkg:a", 2: "//pkg:b"}, meta.Metadata.GetTargetIdMapping()) + assert.Equal(t, map[int32]string{10: "go_library"}, meta.Metadata.GetRuleTypeMapping()) +} diff --git a/mapper/BUILD.bazel b/mapper/BUILD.bazel new file mode 100644 index 00000000..ea640a95 --- /dev/null +++ b/mapper/BUILD.bazel @@ -0,0 +1,25 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "mapper", + srcs = ["mapper.go"], + importpath = "github.com/uber/tango/mapper", + visibility = ["//visibility:public"], + deps = [ + "//core/targethasher", + "//entity", + "//internal/mapper/idmapper", + "@com_github_bazelbuild_buildtools//build_proto", + ], +) + +go_test( + name = "mapper_test", + srcs = ["mapper_test.go"], + embed = [":mapper"], + deps = [ + "//core/targethasher", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/mapper/mapper.go b/mapper/mapper.go new file mode 100644 index 00000000..92fa0f54 --- /dev/null +++ b/mapper/mapper.go @@ -0,0 +1,97 @@ +package mapper + +import ( + "context" + "encoding/hex" + + buildpb "github.com/bazelbuild/buildtools/build_proto" + "github.com/uber/tango/core/targethasher" + "github.com/uber/tango/entity" + "github.com/uber/tango/internal/mapper/idmapper" +) + +const cancelCheckInterval = 4096 + +// ResultToTargetGraph converts a targethasher.Result into ID-mapped entity +// types. Returns a flat list of targets and their accompanying metadata. +// No chunking or proto conversion is performed. +func ResultToTargetGraph(ctx context.Context, result targethasher.Result) ([]entity.OptimizedTarget, *entity.Metadata, error) { + targetNamesMapping := make(map[string]int32, len(result.TargetNames)) + for i, name := range result.TargetNames { + targetNamesMapping[name] = int32(i + 1) + } + + ruleTypeMapper := idmapper.NewMapper() + tagMapper := idmapper.NewMapper() + attrNameMapper := idmapper.NewMapper() + attrStrValMapper := idmapper.NewMapper() + + targets := make([]entity.OptimizedTarget, 0, len(result.TargetNames)) + + n := 0 + for _, name := range result.TargetNames { + t, ok := result.Targets[name] + if !ok { + continue + } + if n%cancelCheckInterval == 0 { + if err := ctx.Err(); err != nil { + return nil, nil, err + } + } + n++ + + depIDs := make([]int32, 0, len(t.Deps)) + for _, depName := range t.Deps { + if depID, ok := targetNamesMapping[depName]; ok { + depIDs = append(depIDs, depID) + } + } + + idt := entity.OptimizedTarget{ + ID: targetNamesMapping[name], + Hash: hex.EncodeToString(t.Hash), + DirectDependencies: depIDs, + Root: t.Root, + External: t.External, + } + if t.RuleType != "" { + idt.RuleType = ruleTypeMapper.ID(t.RuleType) + } + if len(t.Tags) > 0 { + tagIDs := make([]int32, 0, len(t.Tags)) + for _, tag := range t.Tags { + tagIDs = append(tagIDs, tagMapper.ID(tag)) + } + idt.Tags = tagIDs + } + if len(t.Attributes) > 0 { + attrs := make(map[int32]int32, len(t.Attributes)) + for _, attr := range t.Attributes { + if attr.GetType() == buildpb.Attribute_STRING && attr.Name != nil && attr.StringValue != nil { + attrs[attrNameMapper.ID(*attr.Name)] = attrStrValMapper.ID(*attr.StringValue) + } + } + if len(attrs) > 0 { + idt.Attributes = attrs + } + } + + targets = append(targets, idt) + } + + targetIDToName := make(map[int32]string, len(targetNamesMapping)) + for s, id := range targetNamesMapping { + targetIDToName[id] = s + } + + meta := &entity.Metadata{ + TargetIDMapping: targetIDToName, + RuleTypeMapping: ruleTypeMapper.Invert(), + TagMapping: tagMapper.Invert(), + AttributeNameMapping: attrNameMapper.Invert(), + AttributeStringValueMapping: attrStrValMapper.Invert(), + } + + return targets, meta, nil +} diff --git a/mapper/mapper_test.go b/mapper/mapper_test.go new file mode 100644 index 00000000..3641f7ea --- /dev/null +++ b/mapper/mapper_test.go @@ -0,0 +1,19 @@ +package mapper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/uber/tango/core/targethasher" +) + +func TestResultToTargetGraph_EmptyResult(t *testing.T) { + t.Parallel() + + targets, meta, err := ResultToTargetGraph(t.Context(), targethasher.Result{}) + require.NoError(t, err) + assert.Empty(t, targets) + assert.NotNil(t, meta) +}