github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/tool/tool_test.go (about)

     1  package tool
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"testing"
    10  
    11  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/command"
    12  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
    13  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes"
    14  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/outerr"
    15  	"github.com/google/go-cmp/cmp"
    16  	"google.golang.org/protobuf/encoding/prototext"
    17  
    18  	cpb "github.com/bazelbuild/remote-apis-sdks/go/api/command"
    19  	repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    20  )
    21  
    22  var fooProperties = &cpb.NodeProperties{Properties: []*cpb.NodeProperty{{Name: "fooName", Value: "fooValue"}}}
    23  
    24  func TestTool_DownloadActionResult(t *testing.T) {
    25  	e, cleanup := fakes.NewTestEnv(t)
    26  	defer cleanup()
    27  	cmd := &command.Command{
    28  		Args:        []string{"tool"},
    29  		ExecRoot:    e.ExecRoot,
    30  		InputSpec:   &command.InputSpec{},
    31  		OutputFiles: []string{"a/b/out"},
    32  	}
    33  	opt := command.DefaultExecutionOptions()
    34  	output := "output"
    35  	_, acDg, _, _ := e.Set(cmd, opt, &command.Result{Status: command.CacheHitResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: output},
    36  		fakes.StdOut("stdout"), fakes.StdErr("stderr"))
    37  
    38  	toolClient := &Client{GrpcClient: e.Client.GrpcClient}
    39  	tmpDir := t.TempDir()
    40  	if err := toolClient.DownloadActionResult(context.Background(), acDg.String(), tmpDir); err != nil {
    41  		t.Fatalf("DownloadActionResult(%v,%v) failed: %v", acDg.String(), tmpDir, err)
    42  	}
    43  	verifyData := map[string]string{
    44  		filepath.Join(tmpDir, "a/b/out"): "output",
    45  		filepath.Join(tmpDir, "stdout"):  "stdout",
    46  		filepath.Join(tmpDir, "stderr"):  "stderr",
    47  	}
    48  	for fp, want := range verifyData {
    49  		c, err := os.ReadFile(fp)
    50  		if err != nil {
    51  			t.Fatalf("Unable to read downloaded output file %v: %v", fp, err)
    52  		}
    53  		got := string(c)
    54  		if got != want {
    55  			t.Fatalf("Incorrect content in downloaded file %v, want %v, got %v", fp, want, got)
    56  		}
    57  	}
    58  }
    59  
    60  func TestTool_ShowAction(t *testing.T) {
    61  	e, cleanup := fakes.NewTestEnv(t)
    62  	defer cleanup()
    63  	cmd := &command.Command{
    64  		Args:     []string{"tool"},
    65  		ExecRoot: e.ExecRoot,
    66  		InputSpec: &command.InputSpec{
    67  			Inputs: []string{
    68  				"a/b/input.txt",
    69  				"a/b/input2.txt",
    70  			},
    71  			InputNodeProperties: map[string]*cpb.NodeProperties{"a/b/input2.txt": fooProperties},
    72  		},
    73  		OutputFiles: []string{"a/b/out"},
    74  	}
    75  
    76  	opt := command.DefaultExecutionOptions()
    77  	_, acDg, _, _ := e.Set(cmd, opt, &command.Result{Status: command.CacheHitResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "output"},
    78  		fakes.StdOut("stdout"), fakes.StdErr("stderr"), &fakes.InputFile{Path: "a/b/input.txt", Contents: "input"}, &fakes.InputFile{Path: "a/b/input2.txt", Contents: "input2"})
    79  
    80  	toolClient := &Client{GrpcClient: e.Client.GrpcClient}
    81  	got, err := toolClient.ShowAction(context.Background(), acDg.String())
    82  	if err != nil {
    83  		t.Fatalf("ShowAction(%v) failed: %v", acDg.String(), err)
    84  	}
    85  	want := fmt.Sprintf(`Command
    86  =======
    87  Command Digest: 76a608e419da9ed3673f59b8b903f21dbf7cc3178281029151a090cac02d9e4d/15
    88  	tool
    89  
    90  Platform
    91  ========
    92  
    93  Inputs
    94  ======
    95  [Root directory digest: 456e94a43b31b158fa7b3fe8d3a8cd6f0b66ef8a6a05ab8350e03df83b9740b6/75]
    96  a/b/input.txt: [File digest: c96c6d5be8d08a12e7b5cdc1b207fa6b2430974c86803d8891675e76fd992c20/5]
    97  a/b/input2.txt: [File digest: 124d8541ff3d7a18b95432bdfbecd86816b86c8265bff44ef629765afb25f06b/6] [Node properties: %s]
    98  
    99  ------------------------------------------------------------------------
   100  Action Result
   101  
   102  Exit code: 0
   103  stdout digest: 63d42d26156fcc761e57da4128e9881d5bdf3bf933f0f6e9c93d6e26b9b90ae7/6
   104  stderr digest: 7e6b710b765404cccbad9eedcff7615fc37b269d6db12cd81a58be541d93083c/6
   105  
   106  Output Files
   107  ============
   108  a/b/out, digest: e0ee8bb50685e05fa0f47ed04203ae953fdfd055f5bd2892ea186504254f8c3a/6
   109  
   110  Output Files From Directories
   111  =============================
   112  `, prototext.MarshalOptions{Multiline: false}.Format(fooProperties))
   113  	if diff := cmp.Diff(want, got); diff != "" {
   114  		t.Fatalf("ShowAction(%v) returned diff (-want +got): %v\n\ngot: %v\n\nwant: %v\n", acDg.String(), diff, got, want)
   115  	}
   116  }
   117  
   118  func TestTool_CheckDeterminism(t *testing.T) {
   119  	e, cleanup := fakes.NewTestEnv(t)
   120  	defer cleanup()
   121  	cmd := &command.Command{
   122  		Args:        []string{"foo", "bar", "baz"},
   123  		ExecRoot:    e.ExecRoot,
   124  		InputSpec:   &command.InputSpec{Inputs: []string{"i1", "i2"}},
   125  		OutputFiles: []string{"a/b/out"},
   126  	}
   127  	_, acDg, _, _ := e.Set(cmd, command.DefaultExecutionOptions(), &command.Result{Status: command.SuccessResultStatus}, &fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "i2", Contents: "i2"}, &fakes.OutputFile{Path: "a/b/out", Contents: "out"})
   128  
   129  	client := &Client{GrpcClient: e.Client.GrpcClient}
   130  	if err := client.CheckDeterminism(context.Background(), acDg.String(), "", 2); err != nil {
   131  		t.Errorf("CheckDeterminism returned an error: %v", err)
   132  	}
   133  	// Now execute again and return a different output.
   134  	testOnlyStartDeterminismExec = func() {
   135  		e.Set(cmd, command.DefaultExecutionOptions(), &command.Result{Status: command.SuccessResultStatus}, &fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "i2", Contents: "i2"}, &fakes.OutputFile{Path: "a/b/out", Contents: "out2"})
   136  	}
   137  	defer func() { testOnlyStartDeterminismExec = func() {} }()
   138  	if err := client.CheckDeterminism(context.Background(), acDg.String(), "", 2); err == nil {
   139  		t.Errorf("CheckDeterminism returned nil, want error")
   140  	}
   141  }
   142  
   143  func TestTool_DownloadAction(t *testing.T) {
   144  	e, cleanup := fakes.NewTestEnv(t)
   145  	defer cleanup()
   146  	cmd := &command.Command{
   147  		Args:        []string{"foo", "bar", "baz"},
   148  		ExecRoot:    e.ExecRoot,
   149  		InputSpec:   &command.InputSpec{Inputs: []string{"i1", "a/b/i2"}, InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties}},
   150  		OutputFiles: []string{"a/b/out"},
   151  		Platform: map[string]string{
   152  			"container-image": "foo",
   153  		},
   154  	}
   155  	_, acDg, _, _ := e.Set(cmd, command.DefaultExecutionOptions(), &command.Result{Status: command.SuccessResultStatus}, &fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "a/b/i2", Contents: "i2"})
   156  
   157  	client := &Client{GrpcClient: e.Client.GrpcClient}
   158  	tmpDir := filepath.Join(t.TempDir(), "action_root")
   159  	os.MkdirAll(tmpDir, os.ModePerm)
   160  	if err := client.DownloadAction(context.Background(), acDg.String(), tmpDir, true); err != nil {
   161  		t.Errorf("error DownloadAction: %v", err)
   162  	}
   163  
   164  	reCmdPb := &repb.Command{
   165  		Arguments:   []string{"foo", "bar", "baz"},
   166  		OutputFiles: []string{"a/b/out"},
   167  		Platform: &repb.Platform{
   168  			Properties: []*repb.Platform_Property{
   169  				&repb.Platform_Property{
   170  					Name:  "container-image",
   171  					Value: "foo",
   172  				},
   173  			},
   174  		},
   175  	}
   176  	acPb := &repb.Action{
   177  		CommandDigest:   digest.TestNewFromMessage(reCmdPb).ToProto(),
   178  		InputRootDigest: &repb.Digest{Hash: "01dc5b6fa6d16d30bf80a7d88ff58f13fe801c4060b9782253b94e793444df05", SizeBytes: 176},
   179  	}
   180  	ipPb := &cpb.InputSpec{InputNodeProperties: cmd.InputSpec.InputNodeProperties}
   181  	expectedContents := []struct {
   182  		path     string
   183  		contents string
   184  	}{
   185  		{
   186  			path:     "ac.textproto",
   187  			contents: prototext.Format(acPb),
   188  		},
   189  		{
   190  			path:     "cmd.textproto",
   191  			contents: prototext.Format(reCmdPb),
   192  		},
   193  		{
   194  			path:     "input_node_properties.textproto",
   195  			contents: prototext.Format(ipPb),
   196  		},
   197  		{
   198  			path:     "input/i1",
   199  			contents: "i1",
   200  		},
   201  		{
   202  			path:     "input/a/b/i2",
   203  			contents: "i2",
   204  		},
   205  	}
   206  	for _, ec := range expectedContents {
   207  		fp := filepath.Join(tmpDir, ec.path)
   208  		got, err := os.ReadFile(fp)
   209  		if err != nil {
   210  			t.Fatalf("Unable to read downloaded action file %v: %v", fp, err)
   211  		}
   212  		if diff := cmp.Diff(ec.contents, string(got)); diff != "" {
   213  			t.Errorf("Incorrect content in downloaded file %v: diff (-want +got): %v\n\ngot: %s\n\nwant: %v\n", fp, diff, got, ec.contents)
   214  		}
   215  	}
   216  }
   217  
   218  func TestTool_ExecuteAction(t *testing.T) {
   219  	e, cleanup := fakes.NewTestEnv(t)
   220  	defer cleanup()
   221  	cmd := &command.Command{
   222  		Args:        []string{"foo", "bar", "baz"},
   223  		ExecRoot:    e.ExecRoot,
   224  		InputSpec:   &command.InputSpec{Inputs: []string{"i1", "i2"}, InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties}},
   225  		OutputFiles: []string{"a/b/out"},
   226  		Platform: map[string]string{
   227  			"container-image": "foo",
   228  		},
   229  	}
   230  	opt := &command.ExecutionOptions{AcceptCached: false, DownloadOutputs: true, DownloadOutErr: true}
   231  	_, acDg, _, _ := e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out"},
   232  		&fakes.InputFile{Path: "i1", Contents: "i1"}, &fakes.InputFile{Path: "i2", Contents: "i2"}, fakes.StdOut("stdout"), fakes.StdErr("stderr"))
   233  
   234  	client := &Client{GrpcClient: e.Client.GrpcClient}
   235  	oe := outerr.NewRecordingOutErr()
   236  	if _, err := client.ExecuteAction(context.Background(), acDg.String(), "", "", oe); err != nil {
   237  		t.Errorf("error executeAction: %v", err)
   238  	}
   239  	if string(oe.Stderr()) != "stderr" {
   240  		t.Errorf("Incorrect stderr %v, expected \"stderr\"", oe.Stderr())
   241  	}
   242  	if string(oe.Stdout()) != "stdout" {
   243  		t.Errorf("Incorrect stdout %v, expected \"stdout\"", oe.Stdout())
   244  	}
   245  	// Now execute again with changed inputs.
   246  	tmpDir := filepath.Join(t.TempDir(), "action_root")
   247  	os.MkdirAll(tmpDir, os.ModePerm)
   248  	inputRoot := filepath.Join(tmpDir, "input")
   249  	if err := client.DownloadAction(context.Background(), acDg.String(), tmpDir, true); err != nil {
   250  		t.Errorf("error DownloadAction: %v", err)
   251  	}
   252  	if err := os.WriteFile(filepath.Join(inputRoot, "i1"), []byte("i11"), 0644); err != nil {
   253  		t.Fatalf("failed overriding input file: %v", err)
   254  	}
   255  	if err := os.WriteFile(filepath.Join(inputRoot, "i2"), []byte("i22"), 0644); err != nil {
   256  		t.Fatalf("failed overriding input file: %v", err)
   257  	}
   258  	cmd.ExecRoot = inputRoot
   259  	_, acDg2, _, _ := e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out2"},
   260  		fakes.StdOut("stdout2"), fakes.StdErr("stderr2"))
   261  	if diff := cmp.Diff(acDg, acDg2); diff == "" {
   262  		t.Fatalf("expected action digest to change after input change, got %v\n", acDg)
   263  	}
   264  	oe = outerr.NewRecordingOutErr()
   265  	if _, err := client.ExecuteAction(context.Background(), acDg2.String(), "", tmpDir, oe); err != nil {
   266  		t.Errorf("error executeAction: %v", err)
   267  	}
   268  
   269  	fp := filepath.Join(tmpDir, "a/b/out")
   270  	c, err := os.ReadFile(fp)
   271  	if err != nil {
   272  		t.Fatalf("Unable to read downloaded output %v: %v", fp, err)
   273  	}
   274  	if string(c) != "out2" {
   275  		t.Fatalf("Incorrect content in downloaded file %v, want \"out2\", got %s", fp, c)
   276  	}
   277  	if string(oe.Stderr()) != "stderr2" {
   278  		t.Errorf("Incorrect stderr %v, expected \"stderr2\"", oe.Stderr())
   279  	}
   280  	if string(oe.Stdout()) != "stdout2" {
   281  		t.Errorf("Incorrect stdout %v, expected \"stdout2\"", oe.Stdout())
   282  	}
   283  	// Now execute again without node properties.
   284  	fp = filepath.Join(tmpDir, "input_node_properties.textproto")
   285  	if err := os.Remove(fp); err != nil {
   286  		t.Fatalf("Unable to remove %v: %v", fp, err)
   287  	}
   288  	cmd.InputSpec.InputNodeProperties = nil
   289  	_, acDg3, _, _ := e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out3"},
   290  		fakes.StdOut("stdout3"), fakes.StdErr("stderr3"))
   291  	if diff := cmp.Diff(acDg2, acDg3); diff == "" {
   292  		t.Errorf("Expected action digests to be different when input node properties change, got: %v\n", acDg2)
   293  	}
   294  	oe = outerr.NewRecordingOutErr()
   295  	if _, err := client.ExecuteAction(context.Background(), acDg3.String(), "", tmpDir, oe); err != nil {
   296  		t.Errorf("error executeAction: %v", err)
   297  	}
   298  
   299  	fp = filepath.Join(tmpDir, "a/b/out")
   300  	c, err = os.ReadFile(fp)
   301  	if err != nil {
   302  		t.Fatalf("Unable to read downloaded output %v: %v", fp, err)
   303  	}
   304  	if string(c) != "out3" {
   305  		t.Fatalf("Incorrect content in downloaded file %v, want \"out3\", got %s", fp, c)
   306  	}
   307  	if string(oe.Stderr()) != "stderr3" {
   308  		t.Errorf("Incorrect stderr %v, expected \"stderr3\"", oe.Stderr())
   309  	}
   310  	if string(oe.Stdout()) != "stdout3" {
   311  		t.Errorf("Incorrect stdout %v, expected \"stdout3\"", oe.Stdout())
   312  	}
   313  }
   314  
   315  func TestTool_ExecuteActionFromRoot(t *testing.T) {
   316  	e, cleanup := fakes.NewTestEnv(t)
   317  	defer cleanup()
   318  	cmd := &command.Command{
   319  		Args:        []string{"foo", "bar", "baz"},
   320  		ExecRoot:    e.ExecRoot,
   321  		InputSpec:   &command.InputSpec{Inputs: []string{"i1", "i2"}, InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties}},
   322  		OutputFiles: []string{"a/b/out"},
   323  	}
   324  	// Create files necessary for the fake
   325  	if err := os.WriteFile(filepath.Join(e.ExecRoot, "i1"), []byte("i1"), 0644); err != nil {
   326  		t.Fatalf("failed creating input file: %v", err)
   327  	}
   328  	if err := os.WriteFile(filepath.Join(e.ExecRoot, "i2"), []byte("i2"), 0644); err != nil {
   329  		t.Fatalf("failed creating input file: %v", err)
   330  	}
   331  	opt := &command.ExecutionOptions{AcceptCached: false, DownloadOutputs: false, DownloadOutErr: true}
   332  	e.Set(cmd, opt, &command.Result{Status: command.SuccessResultStatus}, &fakes.OutputFile{Path: "a/b/out", Contents: "out"},
   333  		fakes.StdOut("stdout"), fakes.StdErr("stderr"))
   334  
   335  	client := &Client{GrpcClient: e.Client.GrpcClient}
   336  	oe := outerr.NewRecordingOutErr()
   337  	// Construct the action root
   338  	os.Mkdir(filepath.Join(e.ExecRoot, "input"), os.ModePerm)
   339  	if err := os.WriteFile(filepath.Join(e.ExecRoot, "input", "i1"), []byte("i1"), 0644); err != nil {
   340  		t.Fatalf("failed creating input file: %v", err)
   341  	}
   342  	if err := os.WriteFile(filepath.Join(e.ExecRoot, "input", "i2"), []byte("i2"), 0644); err != nil {
   343  		t.Fatalf("failed creating input file: %v", err)
   344  	}
   345  	ipPb := &cpb.InputSpec{
   346  		InputNodeProperties: map[string]*cpb.NodeProperties{"i1": fooProperties},
   347  	}
   348  	if err := os.WriteFile(filepath.Join(e.ExecRoot, "input_node_properties.textproto"), []byte(prototext.Format(ipPb)), 0644); err != nil {
   349  		t.Fatalf("failed creating input node properties file: %v", err)
   350  	}
   351  	reCmdPb := &repb.Command{
   352  		Arguments:   []string{"foo", "bar", "baz"},
   353  		OutputFiles: []string{"a/b/out"},
   354  	}
   355  	if err := os.WriteFile(filepath.Join(e.ExecRoot, "cmd.textproto"), []byte(prototext.Format(reCmdPb)), 0644); err != nil {
   356  		t.Fatalf("failed creating command file: %v", err)
   357  	}
   358  	// The tool will embed the Command proto digest into the Action proto, so the `command_digest` field is effectively ignored:
   359  	if err := os.WriteFile(filepath.Join(e.ExecRoot, "ac.textproto"), []byte(`command_digest: {hash: "whatever"}`), 0644); err != nil {
   360  		t.Fatalf("failed creating action file: %v", err)
   361  	}
   362  	if _, err := client.ExecuteAction(context.Background(), "", e.ExecRoot, "", oe); err != nil {
   363  		t.Errorf("error executeAction: %v", err)
   364  	}
   365  	if string(oe.Stderr()) != "stderr" {
   366  		t.Errorf("Incorrect stderr %v, expected \"stderr\"", string(oe.Stderr()))
   367  	}
   368  	if string(oe.Stdout()) != "stdout" {
   369  		t.Errorf("Incorrect stdout %v, expected \"stdout\"", oe.Stdout())
   370  	}
   371  }
   372  
   373  func TestTool_DownloadBlob(t *testing.T) {
   374  	e, cleanup := fakes.NewTestEnv(t)
   375  	defer cleanup()
   376  	cas := e.Server.CAS
   377  	dg := cas.Put([]byte("hello"))
   378  
   379  	toolClient := &Client{GrpcClient: e.Client.GrpcClient}
   380  	got, err := toolClient.DownloadBlob(context.Background(), dg.String(), "")
   381  	if err != nil {
   382  		t.Fatalf("DownloadBlob(%v) failed: %v", dg.String(), err)
   383  	}
   384  	want := "hello"
   385  	if diff := cmp.Diff(want, got); diff != "" {
   386  		t.Fatalf("DownloadBlob(%v) returned diff (-want +got): %v\n\ngot: %v\n\nwant: %v\n", dg.String(), diff, got, want)
   387  	}
   388  	// Now download into a specified location.
   389  	tmpFile, err := os.CreateTemp(t.TempDir(), "")
   390  	if err != nil {
   391  		t.Fatalf("TempFile failed: %v", err)
   392  	}
   393  	if err := tmpFile.Close(); err != nil {
   394  		t.Fatalf("TempFile Close failed: %v", err)
   395  	}
   396  	fp := tmpFile.Name()
   397  	got, err = toolClient.DownloadBlob(context.Background(), dg.String(), fp)
   398  	if err != nil {
   399  		t.Fatalf("DownloadBlob(%v) failed: %v", dg.String(), err)
   400  	}
   401  	if got != "" {
   402  		t.Fatalf("DownloadBlob(%v) returned %v, expected empty: ", dg.String(), got)
   403  	}
   404  	c, err := os.ReadFile(fp)
   405  	if err != nil {
   406  		t.Fatalf("Unable to read downloaded output file %v: %v", fp, err)
   407  	}
   408  	got = string(c)
   409  	if got != want {
   410  		t.Fatalf("Incorrect content in downloaded file %v, want %v, got %v", fp, want, got)
   411  	}
   412  }
   413  
   414  func TestTool_UploadBlob(t *testing.T) {
   415  	e, cleanup := fakes.NewTestEnv(t)
   416  	defer cleanup()
   417  	cas := e.Server.CAS
   418  
   419  	tmpFile := path.Join(t.TempDir(), "blob")
   420  	if err := os.WriteFile(tmpFile, []byte("Hello, World!"), 0777); err != nil {
   421  		t.Fatalf("Could not create temp blob: %v", err)
   422  	}
   423  
   424  	dg, err := digest.NewFromFile(tmpFile)
   425  	if err != nil {
   426  		t.Fatalf("digest.NewFromFile('%v') failed: %v", tmpFile, err)
   427  	}
   428  
   429  	toolClient := &Client{GrpcClient: e.Client.GrpcClient}
   430  	if err := toolClient.UploadBlob(context.Background(), tmpFile); err != nil {
   431  		t.Fatalf("UploadBlob('%v', '%v') failed: %v", dg.String(), tmpFile, err)
   432  	}
   433  
   434  	// First request should upload the blob.
   435  	if cas.BlobWrites(dg) != 1 {
   436  		t.Fatalf("Expected 1 write for blob '%v', got %v", dg.String(), cas.BlobWrites(dg))
   437  	}
   438  
   439  	// Retries should check whether the blob already exists and skip uploading if it does.
   440  	if err := toolClient.UploadBlob(context.Background(), tmpFile); err != nil {
   441  		t.Fatalf("UploadBlob('%v', '%v') failed: %v", dg.String(), tmpFile, err)
   442  	}
   443  	if cas.BlobWrites(dg) != 1 {
   444  		t.Fatalf("Expected 1 write for blob '%v', got %v", dg.String(), cas.BlobWrites(dg))
   445  	}
   446  }