Skip to content

Commit 0f7b13d

Browse files
committed
attach coverage to a gRPC error detail on failure
Signed-off-by: Kartik Joshi <karikjoshi21@gmail.com>
1 parent d2983f5 commit 0f7b13d

7 files changed

Lines changed: 496 additions & 51 deletions

File tree

cmd/frontend/coverage.go

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@ import (
1010
"runtime/coverage"
1111

1212
gwclient "github.com/moby/buildkit/frontend/gateway/client"
13-
)
14-
15-
const (
16-
frontendCoverageOptKey = "dalec.coverage"
17-
frontendCovMetaKey = "dalec.coverage.frontend.meta.gz"
18-
frontendCovCountersKey = "dalec.coverage.frontend.counters.gz"
13+
"github.com/project-dalec/dalec/internal/frontendcoverage"
1914
)
2015

2116
func isNoMetaErr(err error) bool {
@@ -26,14 +21,9 @@ func isNoMetaErr(err error) bool {
2621
return strings.Contains(strings.ToLower(err.Error()), "no meta-data available")
2722
}
2823

29-
// Enabled per solve via SolveRequest.FrontendOpt["dalec.coverage"]="1"
24+
// Enabled per solve via SolveRequest.FrontendOpt[frontendcoverage.OptKey]="1"
3025
func wantFrontendCoverage(c gwclient.Client) bool {
31-
v, ok := c.BuildOpts().Opts[frontendCoverageOptKey]
32-
if !ok {
33-
return false
34-
}
35-
v = strings.ToLower(strings.TrimSpace(v))
36-
return v == "1" || v == "true" || v == "yes" || v == "on"
26+
return frontendcoverage.Want(c.BuildOpts().Opts)
3727
}
3828

3929
func gzipBytes(in []byte) ([]byte, error) {
@@ -49,65 +39,74 @@ func gzipBytes(in []byte) ([]byte, error) {
4939
return buf.Bytes(), nil
5040
}
5141

52-
func attachFrontendCoverage(c gwclient.Client, res *gwclient.Result) error {
53-
if res == nil || !wantFrontendCoverage(c) {
54-
return nil
55-
}
56-
if res.Metadata == nil {
57-
res.Metadata = map[string][]byte{}
58-
}
42+
var frontendCoverageCollector = collectFrontendCoveragePayload
5943

44+
func collectFrontendCoveragePayload() (*frontendcoverage.Payload, error) {
6045
var metaBuf, ctrBuf bytes.Buffer
6146

6247
if err := coverage.WriteMeta(&metaBuf); err != nil {
6348
if isNoMetaErr(err) {
64-
return nil
49+
return nil, nil
6550
}
66-
return err
51+
return nil, err
6752
}
6853
if err := coverage.WriteCounters(&ctrBuf); err != nil {
6954
if isNoMetaErr(err) {
70-
return nil
55+
return nil, nil
7156
}
72-
return err
57+
return nil, err
7358
}
7459

7560
metaGz, err := gzipBytes(metaBuf.Bytes())
7661
if err != nil {
77-
return err
62+
return nil, err
7863
}
7964
ctrGz, err := gzipBytes(ctrBuf.Bytes())
8065
if err != nil {
81-
return err
66+
return nil, err
8267
}
8368

84-
res.Metadata[frontendCovMetaKey] = metaGz
85-
res.Metadata[frontendCovCountersKey] = ctrGz
86-
8769
// Avoid cross-solve accumulation if the frontend process is reused.
8870
// Only works for binaries built with -cover (and typically atomic counters).
8971
_ = coverage.ClearCounters()
9072

91-
return nil
73+
return &frontendcoverage.Payload{
74+
MetaGz: metaGz,
75+
CountersGz: ctrGz,
76+
}, nil
9277
}
9378

9479
func wrapWithCoverage(next gwclient.BuildFunc) gwclient.BuildFunc {
9580
return func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) {
9681
res, err := next(ctx, c)
97-
98-
// If coverage is requested, make sure we have a result object to attach
99-
// metadata to even on error paths.
100-
if wantFrontendCoverage(c) && res == nil {
101-
res = gwclient.NewResult()
82+
if !wantFrontendCoverage(c) {
83+
return res, err
10284
}
10385

104-
if covErr := attachFrontendCoverage(c, res); covErr != nil {
86+
payload, covErr := frontendCoverageCollector()
87+
if covErr != nil {
10588
if err != nil {
10689
return res, errors.Join(err, covErr)
10790
}
10891
return res, covErr
10992
}
93+
if payload == nil {
94+
return res, err
95+
}
96+
97+
if err != nil {
98+
errWithCoverage, attachErr := payload.AttachToError(err)
99+
if attachErr != nil {
100+
return res, errors.Join(err, attachErr)
101+
}
102+
return res, errWithCoverage
103+
}
104+
105+
if res == nil {
106+
res = gwclient.NewResult()
107+
}
108+
payload.AttachToResult(res)
110109

111-
return res, err
110+
return res, nil
112111
}
113112
}

cmd/frontend/coverage_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"testing"
8+
9+
"github.com/moby/buildkit/client/llb"
10+
"github.com/moby/buildkit/client/llb/sourceresolver"
11+
gwclient "github.com/moby/buildkit/frontend/gateway/client"
12+
"github.com/moby/buildkit/solver/pb"
13+
digest "github.com/opencontainers/go-digest"
14+
"github.com/project-dalec/dalec/internal/frontendcoverage"
15+
)
16+
17+
func TestWrapWithCoverageAttachesResultMetadataOnSuccess(t *testing.T) {
18+
t.Cleanup(setFrontendCoverageCollectorForTest(func() (*frontendcoverage.Payload, error) {
19+
return &frontendcoverage.Payload{
20+
MetaGz: []byte("meta-gz"),
21+
CountersGz: []byte("counters-gz"),
22+
}, nil
23+
}))
24+
25+
res, err := wrapWithCoverage(func(context.Context, gwclient.Client) (*gwclient.Result, error) {
26+
return nil, nil
27+
})(context.Background(), &fakeGatewayClient{
28+
opts: map[string]string{frontendcoverage.OptKey: "1"},
29+
})
30+
if err != nil {
31+
t.Fatalf("expected nil error, got %v", err)
32+
}
33+
if res == nil {
34+
t.Fatal("expected result to be created when coverage is enabled")
35+
}
36+
37+
payload, payloadErr := frontendcoverage.PayloadFromSolve(res, nil)
38+
if payloadErr != nil {
39+
t.Fatalf("expected nil payload error, got %v", payloadErr)
40+
}
41+
if payload == nil {
42+
t.Fatal("expected payload to be attached to result metadata")
43+
}
44+
if !bytes.Equal(payload.MetaGz, []byte("meta-gz")) {
45+
t.Fatalf("unexpected meta payload: %q", payload.MetaGz)
46+
}
47+
if !bytes.Equal(payload.CountersGz, []byte("counters-gz")) {
48+
t.Fatalf("unexpected counters payload: %q", payload.CountersGz)
49+
}
50+
}
51+
52+
func TestWrapWithCoverageAttachesGRPCDetailOnError(t *testing.T) {
53+
t.Cleanup(setFrontendCoverageCollectorForTest(func() (*frontendcoverage.Payload, error) {
54+
return &frontendcoverage.Payload{
55+
MetaGz: []byte("meta-gz"),
56+
CountersGz: []byte("counters-gz"),
57+
}, nil
58+
}))
59+
60+
frontendErr := errors.New("frontend failed")
61+
res, err := wrapWithCoverage(func(context.Context, gwclient.Client) (*gwclient.Result, error) {
62+
return nil, frontendErr
63+
})(context.Background(), &fakeGatewayClient{
64+
opts: map[string]string{frontendcoverage.OptKey: "1"},
65+
})
66+
if res != nil {
67+
t.Fatal("expected nil result on error path")
68+
}
69+
if err == nil {
70+
t.Fatal("expected error from wrapped frontend")
71+
}
72+
if !errors.Is(err, frontendErr) {
73+
t.Fatalf("expected wrapped error to preserve original error, got %v", err)
74+
}
75+
if err.Error() != frontendErr.Error() {
76+
t.Fatalf("expected wrapped error message %q, got %q", frontendErr.Error(), err.Error())
77+
}
78+
79+
payload, payloadErr := frontendcoverage.PayloadFromError(err)
80+
if payloadErr != nil {
81+
t.Fatalf("expected nil payload error, got %v", payloadErr)
82+
}
83+
if payload == nil {
84+
t.Fatal("expected payload to be attached to error details")
85+
}
86+
if !bytes.Equal(payload.MetaGz, []byte("meta-gz")) {
87+
t.Fatalf("unexpected meta payload: %q", payload.MetaGz)
88+
}
89+
if !bytes.Equal(payload.CountersGz, []byte("counters-gz")) {
90+
t.Fatalf("unexpected counters payload: %q", payload.CountersGz)
91+
}
92+
}
93+
94+
func setFrontendCoverageCollectorForTest(f func() (*frontendcoverage.Payload, error)) func() {
95+
previous := frontendCoverageCollector
96+
frontendCoverageCollector = f
97+
return func() {
98+
frontendCoverageCollector = previous
99+
}
100+
}
101+
102+
type fakeGatewayClient struct {
103+
opts map[string]string
104+
}
105+
106+
func (c *fakeGatewayClient) Solve(context.Context, gwclient.SolveRequest) (*gwclient.Result, error) {
107+
panic("unexpected call to Solve")
108+
}
109+
110+
func (c *fakeGatewayClient) ResolveImageConfig(context.Context, string, sourceresolver.Opt) (string, digest.Digest, []byte, error) {
111+
panic("unexpected call to ResolveImageConfig")
112+
}
113+
114+
func (c *fakeGatewayClient) ResolveSourceMetadata(context.Context, *pb.SourceOp, sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
115+
panic("unexpected call to ResolveSourceMetadata")
116+
}
117+
118+
func (c *fakeGatewayClient) BuildOpts() gwclient.BuildOpts {
119+
return gwclient.BuildOpts{Opts: c.opts}
120+
}
121+
122+
func (c *fakeGatewayClient) Inputs(context.Context) (map[string]llb.State, error) {
123+
panic("unexpected call to Inputs")
124+
}
125+
126+
func (c *fakeGatewayClient) NewContainer(context.Context, gwclient.NewContainerRequest) (gwclient.Container, error) {
127+
panic("unexpected call to NewContainer")
128+
}
129+
130+
func (c *fakeGatewayClient) Warn(context.Context, digest.Digest, string, gwclient.WarnOpts) error {
131+
panic("unexpected call to Warn")
132+
}

0 commit comments

Comments
 (0)