From e5e6ac7bc13643f7135df415fa364b8d9a6935cc Mon Sep 17 00:00:00 2001 From: Eric Hauser Date: Wed, 13 Apr 2022 12:50:44 -0600 Subject: [PATCH] Fixes #1 - use streamed_output to get around 2GB limitation (#10) --- README.md | 1 - internal/BUILD.bazel | 11 ++++++-- internal/bazel.go | 19 ++++--------- internal/bazel_client.go | 2 +- internal/proto_delimited.go | 47 +++++++++++++++++++++++++++++++ internal/proto_delimited_test.go | 23 +++++++++++++++ internal/query.protodelim | Bin 0 -> 112442 bytes 7 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 internal/proto_delimited.go create mode 100644 internal/proto_delimited_test.go create mode 100644 internal/query.protodelim diff --git a/README.md b/README.md index a82a0c8..32f2229 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ A couple reasons: * Some of the short codes for the CLI are different (only supporting one character instead of two) * Due to some minor implementation differences, the actual hashes generated by the two programs are different -* `bazel-differ` isn't using `streamed_proto` output from Bazel query (I'm not sure if there is a Go implemntation of this?). In some minimal testing against some large repositories, `bazel-differ` still seems to outperform `bazel-diff` by 2x. 3. What target types has this been tested against? diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel index 68cb107..efe5683 100644 --- a/internal/BUILD.bazel +++ b/internal/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "filesystem.go", "git_client.go", "io_utils.go", + "proto_delimited.go", "target_hashing_client.go", ], importpath = "github.com/ewhauser/bazel-differ/internal", @@ -24,9 +25,15 @@ go_library( go_test( name = "internal_test", - srcs = ["target_hashing_client_test.go"], + srcs = [ + "proto_delimited_test.go", + "target_hashing_client_test.go", + ], + data = [ + "query.protodelim", # keep + ], + embed = [":internal"], deps = [ - ":internal", "//mocks", "@com_github_golang_mock//gomock", ], diff --git a/internal/bazel.go b/internal/bazel.go index 042fd3d..f07f2cd 100644 --- a/internal/bazel.go +++ b/internal/bazel.go @@ -18,8 +18,6 @@ import ( "bytes" "context" "errors" - "fmt" - "google.golang.org/protobuf/proto" "io" "log" "os" @@ -122,7 +120,7 @@ type Bazel interface { WriteToStderr(v bool) WriteToStdout(v bool) Info() (map[string]string, error) - Query(args ...string) (*QueryResult, error) + Query(args ...string) ([]*Target, error) Build(args ...string) (*bytes.Buffer, error) Test(args ...string) (*bytes.Buffer, error) Run(args ...string) (*exec.Cmd, *bytes.Buffer, error) @@ -286,8 +284,8 @@ func (b *bazel) processInfo(info string) (map[string]string, error) { // or to find a dependency path between //path/to/package:target and //dependency: // // res, err := b.Query('somepath(//path/to/package:target, //dependency)') -func (b *bazel) Query(args ...string) (*QueryResult, error) { - blazeArgs := append([]string(nil), "--output=proto", "--order_output=no", "--color=no") +func (b *bazel) Query(args ...string) ([]*Target, error) { + blazeArgs := append([]string(nil), "--output=streamed_proto", "--order_output=no", "--color=no") blazeArgs = append(blazeArgs, args...) b.WriteToStderr(true) @@ -302,14 +300,9 @@ func (b *bazel) Query(args ...string) (*QueryResult, error) { return b.processQuery(stdoutBuffer.Bytes()) } -func (b *bazel) processQuery(out []byte) (*QueryResult, error) { - var qr QueryResult - if err := proto.Unmarshal(out, &qr); err != nil { - fmt.Fprintf(os.Stderr, "Could not read blaze query response. Error: %s\nOutput: %s\n", err, out) - return nil, err - } - - return &qr, nil +func (b *bazel) processQuery(out []byte) ([]*Target, error) { + reader := NewReader(bytes.NewReader(out)) + return reader.ReadTargets() } func (b *bazel) Build(args ...string) (*bytes.Buffer, error) { diff --git a/internal/bazel_client.go b/internal/bazel_client.go index 94f3537..b40d71c 100644 --- a/internal/bazel_client.go +++ b/internal/bazel_client.go @@ -146,5 +146,5 @@ func (b bazelClient) performBazelQuery(query string) ([]*Target, error) { if err != nil { return nil, err } - return result.Target, nil + return result, nil } diff --git a/internal/proto_delimited.go b/internal/proto_delimited.go new file mode 100644 index 0000000..eccc418 --- /dev/null +++ b/internal/proto_delimited.go @@ -0,0 +1,47 @@ +package internal + +import ( + "bufio" + "encoding/binary" + "fmt" + "google.golang.org/protobuf/proto" + "io" +) + +type ProtoDelimitedReader struct { + buf *bufio.Reader +} + +func NewReader(r io.Reader) *ProtoDelimitedReader { + return &ProtoDelimitedReader{ + buf: bufio.NewReader(r), + } +} + +func (r *ProtoDelimitedReader) Next() ([]byte, error) { + size, err := binary.ReadUvarint(r.buf) + if err != nil { + return nil, err + } + data := make([]byte, size) + if _, err := io.ReadFull(r.buf, data); err != nil { + return nil, err + } + return data, nil +} + +func (r *ProtoDelimitedReader) ReadTargets() ([]*Target, error) { + var targets []*Target + var err error + for buf, err := r.Next(); err == nil; buf, err = r.Next() { + var target Target + if err := proto.Unmarshal(buf, &target); err != nil { + return nil, fmt.Errorf("failed to unmarshal Target message: %w", err) + } + targets = append(targets, &target) + } + if err != nil && err != io.EOF { + return nil, fmt.Errorf("error while reading stdout from bazel command: %w", err) + } + return targets, nil +} diff --git a/internal/proto_delimited_test.go b/internal/proto_delimited_test.go new file mode 100644 index 0000000..e2e07ad --- /dev/null +++ b/internal/proto_delimited_test.go @@ -0,0 +1,23 @@ +package internal + +import ( + "bytes" + "io/ioutil" + "testing" +) + +func TestDelimitedReader(t *testing.T) { + protoBytes, err := ioutil.ReadFile("query.protodelim") + if err != nil { + t.Errorf("Error reading file") + } + reader := NewReader(bytes.NewReader(protoBytes)) + targets, err := reader.ReadTargets() + if err != nil { + t.Errorf("error marshalling: %s", err) + t.FailNow() + } + if len(targets) != 49 { + t.Errorf("Expecting 49 targets but got %d", len(targets)) + } +} diff --git a/internal/query.protodelim b/internal/query.protodelim new file mode 100644 index 0000000000000000000000000000000000000000..c483fa1fb22e6d9e2dc220ac61d2ddac290e1ae5 GIT binary patch literal 112442 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 ib@&f%_`l?umWi5{z^lVwujs