Correctly overwrite local state if remote state is newer (#1008)

## Changes

A bug in the code that pulls the remote state could cause the local
state to be empty instead of a copy of the remote state. This happened
only if the local state was present and stale when compared to the
remote version.

We correctly checked for the state serial to see if the local state had
to be replaced but didn't seek back on the remote state before writing
it out. Because the staleness check would read the remote state in full,
copying from the same reader would immediately yield an EOF.

## Tests

* Unit tests for state pull and push mutators that rely on a mocked
filer.
* An integration test that deploys the same bundle from multiple paths,
triggering the staleness logic.

Both failed prior to the fix and now pass.
This commit is contained in:
Pieter Noordhuis 2023-11-24 12:15:46 +01:00 committed by GitHub
parent d985601d30
commit 6187803007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 572 additions and 85 deletions

4
NOTICE
View File

@ -16,6 +16,10 @@ go-ini/ini - https://github.com/go-ini/ini
Copyright ini authors
License - https://github.com/go-ini/ini/blob/main/LICENSE
uber-go/mock - https://go.uber.org/mock
Copyright Google Inc.
License - https://github.com/uber-go/mock/blob/main/LICENSE
—--
This software contains code from the following open source projects, licensed under the MPL 2.0 license:

View File

@ -0,0 +1,14 @@
package terraform
import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/filer"
)
// filerFunc is a function that returns a filer.Filer.
type filerFunc func(b *bundle.Bundle) (filer.Filer, error)
// stateFiler returns a filer.Filer that can be used to read/write state files.
func stateFiler(b *bundle.Bundle) (filer.Filer, error) {
return filer.NewWorkspaceFilesClient(b.WorkspaceClient(), b.Config.Workspace.StatePath)
}

View File

@ -1,6 +1,7 @@
package terraform
import (
"bytes"
"context"
"errors"
"io"
@ -13,14 +14,38 @@ import (
"github.com/databricks/cli/libs/log"
)
type statePull struct{}
type statePull struct {
filerFunc
}
func (l *statePull) Name() string {
return "terraform:state-pull"
}
func (l *statePull) remoteState(ctx context.Context, f filer.Filer) (*bytes.Buffer, error) {
// Download state file from filer to local cache directory.
remote, err := f.Read(ctx, TerraformStateFileName)
if err != nil {
// On first deploy this state file doesn't yet exist.
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
return nil, err
}
defer remote.Close()
var buf bytes.Buffer
_, err = io.Copy(&buf, remote)
if err != nil {
return nil, err
}
return &buf, nil
}
func (l *statePull) Apply(ctx context.Context, b *bundle.Bundle) error {
f, err := filer.NewWorkspaceFilesClient(b.WorkspaceClient(), b.Config.Workspace.StatePath)
f, err := l.filerFunc(b)
if err != nil {
return err
}
@ -32,15 +57,15 @@ func (l *statePull) Apply(ctx context.Context, b *bundle.Bundle) error {
// Download state file from filer to local cache directory.
log.Infof(ctx, "Opening remote state file")
remote, err := f.Read(ctx, TerraformStateFileName)
remote, err := l.remoteState(ctx, f)
if err != nil {
// On first deploy this state file doesn't yet exist.
if errors.Is(err, fs.ErrNotExist) {
log.Infof(ctx, "Remote state file does not exist")
return nil
}
log.Infof(ctx, "Unable to open remote state file: %s", err)
return err
}
if remote == nil {
log.Infof(ctx, "Remote state file does not exist")
return nil
}
// Expect the state file to live under dir.
local, err := os.OpenFile(filepath.Join(dir, TerraformStateFileName), os.O_CREATE|os.O_RDWR, 0600)
@ -49,7 +74,7 @@ func (l *statePull) Apply(ctx context.Context, b *bundle.Bundle) error {
}
defer local.Close()
if !IsLocalStateStale(local, remote) {
if !IsLocalStateStale(local, bytes.NewReader(remote.Bytes())) {
log.Infof(ctx, "Local state is the same or newer, ignoring remote state")
return nil
}
@ -60,7 +85,7 @@ func (l *statePull) Apply(ctx context.Context, b *bundle.Bundle) error {
// Write file to disk.
log.Infof(ctx, "Writing remote state file to local cache directory")
_, err = io.Copy(local, remote)
_, err = io.Copy(local, bytes.NewReader(remote.Bytes()))
if err != nil {
return err
}
@ -69,5 +94,5 @@ func (l *statePull) Apply(ctx context.Context, b *bundle.Bundle) error {
}
func StatePull() bundle.Mutator {
return &statePull{}
return &statePull{stateFiler}
}

View File

@ -0,0 +1,128 @@
package terraform
import (
"bytes"
"context"
"encoding/json"
"io"
"io/fs"
"os"
"testing"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
mock "github.com/databricks/cli/internal/mocks/libs/filer"
"github.com/databricks/cli/libs/filer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func mockStateFilerForPull(t *testing.T, contents map[string]int, merr error) filer.Filer {
buf, err := json.Marshal(contents)
require.NoError(t, err)
ctrl := gomock.NewController(t)
mock := mock.NewMockFiler(ctrl)
mock.
EXPECT().
Read(gomock.Any(), gomock.Eq(TerraformStateFileName)).
Return(io.NopCloser(bytes.NewReader(buf)), merr).
Times(1)
return mock
}
func statePullTestBundle(t *testing.T) *bundle.Bundle {
return &bundle.Bundle{
Config: config.Root{
Bundle: config.Bundle{
Target: "default",
},
Path: t.TempDir(),
},
}
}
func TestStatePullLocalMissingRemoteMissing(t *testing.T) {
m := &statePull{
identityFiler(mockStateFilerForPull(t, nil, os.ErrNotExist)),
}
ctx := context.Background()
b := statePullTestBundle(t)
err := bundle.Apply(ctx, b, m)
assert.NoError(t, err)
// Confirm that no local state file has been written.
_, err = os.Stat(localStateFile(t, ctx, b))
assert.ErrorIs(t, err, fs.ErrNotExist)
}
func TestStatePullLocalMissingRemotePresent(t *testing.T) {
m := &statePull{
identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5}, nil)),
}
ctx := context.Background()
b := statePullTestBundle(t)
err := bundle.Apply(ctx, b, m)
assert.NoError(t, err)
// Confirm that the local state file has been updated.
localState := readLocalState(t, ctx, b)
assert.Equal(t, map[string]int{"serial": 5}, localState)
}
func TestStatePullLocalStale(t *testing.T) {
m := &statePull{
identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5}, nil)),
}
ctx := context.Background()
b := statePullTestBundle(t)
// Write a stale local state file.
writeLocalState(t, ctx, b, map[string]int{"serial": 4})
err := bundle.Apply(ctx, b, m)
assert.NoError(t, err)
// Confirm that the local state file has been updated.
localState := readLocalState(t, ctx, b)
assert.Equal(t, map[string]int{"serial": 5}, localState)
}
func TestStatePullLocalEqual(t *testing.T) {
m := &statePull{
identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5, "some_other_key": 123}, nil)),
}
ctx := context.Background()
b := statePullTestBundle(t)
// Write a local state file with the same serial as the remote.
writeLocalState(t, ctx, b, map[string]int{"serial": 5})
err := bundle.Apply(ctx, b, m)
assert.NoError(t, err)
// Confirm that the local state file has not been updated.
localState := readLocalState(t, ctx, b)
assert.Equal(t, map[string]int{"serial": 5}, localState)
}
func TestStatePullLocalNewer(t *testing.T) {
m := &statePull{
identityFiler(mockStateFilerForPull(t, map[string]int{"serial": 5, "some_other_key": 123}, nil)),
}
ctx := context.Background()
b := statePullTestBundle(t)
// Write a local state file with a newer serial as the remote.
writeLocalState(t, ctx, b, map[string]int{"serial": 6})
err := bundle.Apply(ctx, b, m)
assert.NoError(t, err)
// Confirm that the local state file has not been updated.
localState := readLocalState(t, ctx, b)
assert.Equal(t, map[string]int{"serial": 6}, localState)
}

View File

@ -10,14 +10,16 @@ import (
"github.com/databricks/cli/libs/log"
)
type statePush struct{}
type statePush struct {
filerFunc
}
func (l *statePush) Name() string {
return "terraform:state-push"
}
func (l *statePush) Apply(ctx context.Context, b *bundle.Bundle) error {
f, err := filer.NewWorkspaceFilesClient(b.WorkspaceClient(), b.Config.Workspace.StatePath)
f, err := l.filerFunc(b)
if err != nil {
return err
}
@ -45,5 +47,5 @@ func (l *statePush) Apply(ctx context.Context, b *bundle.Bundle) error {
}
func StatePush() bundle.Mutator {
return &statePush{}
return &statePush{stateFiler}
}

View File

@ -0,0 +1,63 @@
package terraform
import (
"context"
"encoding/json"
"io"
"testing"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
mock "github.com/databricks/cli/internal/mocks/libs/filer"
"github.com/databricks/cli/libs/filer"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func mockStateFilerForPush(t *testing.T, fn func(body io.Reader)) filer.Filer {
ctrl := gomock.NewController(t)
mock := mock.NewMockFiler(ctrl)
mock.
EXPECT().
Write(gomock.Any(), gomock.Any(), gomock.Any(), filer.CreateParentDirectories, filer.OverwriteIfExists).
Do(func(ctx context.Context, path string, reader io.Reader, mode ...filer.WriteMode) error {
fn(reader)
return nil
}).
Return(nil).
Times(1)
return mock
}
func statePushTestBundle(t *testing.T) *bundle.Bundle {
return &bundle.Bundle{
Config: config.Root{
Bundle: config.Bundle{
Target: "default",
},
Path: t.TempDir(),
},
}
}
func TestStatePush(t *testing.T) {
mock := mockStateFilerForPush(t, func(body io.Reader) {
dec := json.NewDecoder(body)
var contents map[string]int
err := dec.Decode(&contents)
assert.NoError(t, err)
assert.Equal(t, map[string]int{"serial": 4}, contents)
})
m := &statePush{
identityFiler(mock),
}
ctx := context.Background()
b := statePushTestBundle(t)
// Write a stale local state file.
writeLocalState(t, ctx, b, map[string]int{"serial": 4})
err := bundle.Apply(ctx, b, m)
assert.NoError(t, err)
}

View File

@ -0,0 +1,48 @@
package terraform
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/filer"
"github.com/stretchr/testify/require"
)
// identityFiler returns a filerFunc that returns the specified filer.
func identityFiler(f filer.Filer) filerFunc {
return func(_ *bundle.Bundle) (filer.Filer, error) {
return f, nil
}
}
func localStateFile(t *testing.T, ctx context.Context, b *bundle.Bundle) string {
dir, err := Dir(ctx, b)
require.NoError(t, err)
return filepath.Join(dir, TerraformStateFileName)
}
func readLocalState(t *testing.T, ctx context.Context, b *bundle.Bundle) map[string]int {
f, err := os.Open(localStateFile(t, ctx, b))
require.NoError(t, err)
defer f.Close()
var contents map[string]int
dec := json.NewDecoder(f)
err = dec.Decode(&contents)
require.NoError(t, err)
return contents
}
func writeLocalState(t *testing.T, ctx context.Context, b *bundle.Bundle, contents map[string]int) {
f, err := os.Create(localStateFile(t, ctx, b))
require.NoError(t, err)
defer f.Close()
enc := json.NewEncoder(f)
err = enc.Encode(contents)
require.NoError(t, err)
}

View File

@ -2,92 +2,39 @@ package terraform
import (
"fmt"
"io"
"strings"
"testing"
"testing/iotest"
"github.com/stretchr/testify/assert"
)
type mockedReader struct {
content string
}
func (r *mockedReader) Read(p []byte) (n int, err error) {
content := []byte(r.content)
n = copy(p, content)
return n, io.EOF
}
func TestLocalStateIsNewer(t *testing.T) {
local := &mockedReader{content: `
{
"serial": 5
}
`}
remote := &mockedReader{content: `
{
"serial": 4
}
`}
stale := IsLocalStateStale(local, remote)
assert.False(t, stale)
local := strings.NewReader(`{"serial": 5}`)
remote := strings.NewReader(`{"serial": 4}`)
assert.False(t, IsLocalStateStale(local, remote))
}
func TestLocalStateIsOlder(t *testing.T) {
local := &mockedReader{content: `
{
"serial": 5
}
`}
remote := &mockedReader{content: `
{
"serial": 6
}
`}
stale := IsLocalStateStale(local, remote)
assert.True(t, stale)
local := strings.NewReader(`{"serial": 5}`)
remote := strings.NewReader(`{"serial": 6}`)
assert.True(t, IsLocalStateStale(local, remote))
}
func TestLocalStateIsTheSame(t *testing.T) {
local := &mockedReader{content: `
{
"serial": 5
}
`}
remote := &mockedReader{content: `
{
"serial": 5
}
`}
stale := IsLocalStateStale(local, remote)
assert.False(t, stale)
local := strings.NewReader(`{"serial": 5}`)
remote := strings.NewReader(`{"serial": 5}`)
assert.False(t, IsLocalStateStale(local, remote))
}
func TestLocalStateMarkStaleWhenFailsToLoad(t *testing.T) {
local := iotest.ErrReader(fmt.Errorf("Random error"))
remote := &mockedReader{content: `
{
"serial": 5
}
`}
stale := IsLocalStateStale(local, remote)
assert.True(t, stale)
remote := strings.NewReader(`{"serial": 5}`)
assert.True(t, IsLocalStateStale(local, remote))
}
func TestLocalStateMarkNonStaleWhenRemoteFailsToLoad(t *testing.T) {
local := &mockedReader{content: `
{
"serial": 5
}
`}
local := strings.NewReader(`{"serial": 5}`)
remote := iotest.ErrReader(fmt.Errorf("Random error"))
stale := IsLocalStateStale(local, remote)
assert.False(t, stale)
assert.False(t, IsLocalStateStale(local, remote))
}

5
go.mod
View File

@ -31,7 +31,10 @@ require (
gopkg.in/ini.v1 v1.67.0 // Apache 2.0
)
require gopkg.in/yaml.v3 v3.0.1
require (
go.uber.org/mock v0.3.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cloud.google.com/go/compute v1.23.1 // indirect

2
go.sum
View File

@ -158,6 +158,8 @@ github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA
github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View File

@ -0,0 +1,16 @@
{
"properties": {
"unique_id": {
"type": "string",
"description": "Unique ID for job name"
},
"spark_version": {
"type": "string",
"description": "Spark version used for job cluster"
},
"node_type_id": {
"type": "string",
"description": "Node type id for job cluster"
}
}
}

View File

@ -0,0 +1,18 @@
bundle:
name: basic
workspace:
root_path: "~/.bundle/{{.unique_id}}"
resources:
jobs:
foo:
name: test-job-basic-{{.unique_id}}
tasks:
- task_key: my_notebook_task
new_cluster:
num_workers: 1
spark_version: "{{.spark_version}}"
node_type_id: "{{.node_type_id}}"
spark_python_task:
python_file: ./hello_world.py

View File

@ -0,0 +1 @@
print("Hello World!")

View File

@ -0,0 +1,70 @@
package bundle
import (
"context"
"testing"
"github.com/databricks/cli/internal"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/listing"
"github.com/databricks/databricks-sdk-go/service/jobs"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAccLocalStateStaleness(t *testing.T) {
env := internal.GetEnvOrSkipTest(t, "CLOUD_ENV")
t.Log(env)
w, err := databricks.NewWorkspaceClient()
require.NoError(t, err)
// The approach for this test is as follows:
// 1) First deploy of bundle instance A
// 2) First deploy of bundle instance B
// 3) Second deploy of bundle instance A
// Because of deploy (2), the locally cached state of bundle instance A should be stale.
// Then for deploy (3), it must use the remote state over the stale local state.
nodeTypeId := internal.GetNodeTypeId(env)
uniqueId := uuid.New().String()
initialize := func() string {
root, err := initTestTemplate(t, "basic", map[string]any{
"unique_id": uniqueId,
"node_type_id": nodeTypeId,
"spark_version": "13.2.x-snapshot-scala2.12",
})
require.NoError(t, err)
t.Cleanup(func() {
err = destroyBundle(t, root)
require.NoError(t, err)
})
return root
}
bundleA := initialize()
bundleB := initialize()
// 1) Deploy bundle A
err = deployBundle(t, bundleA)
require.NoError(t, err)
// 2) Deploy bundle B
err = deployBundle(t, bundleB)
require.NoError(t, err)
// 3) Deploy bundle A again
err = deployBundle(t, bundleA)
require.NoError(t, err)
// Assert that there is only a single job in the workspace corresponding to this bundle.
iter := w.Jobs.List(context.Background(), jobs.ListJobsRequest{
Name: "test-job-basic-" + uniqueId,
})
jobs, err := listing.ToSlice(context.Background(), iter)
require.NoError(t, err)
assert.Len(t, jobs, 1)
}

7
internal/mocks/README.md Normal file
View File

@ -0,0 +1,7 @@
# Interface mocking
Use this directory to store mocks for interfaces in this repository.
Please use the same package structure for the mocks as the interface it is mocking.
See https://github.com/uber-go/mock for more information on how to generate mocks.

View File

@ -0,0 +1,139 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/databricks/cli/libs/filer (interfaces: Filer)
//
// Generated by this command:
//
// mockgen -destination filer_mock.go github.com/databricks/cli/libs/filer Filer
//
// Package mock_filer is a generated GoMock package.
package mock_filer
import (
context "context"
io "io"
fs "io/fs"
reflect "reflect"
filer "github.com/databricks/cli/libs/filer"
gomock "go.uber.org/mock/gomock"
)
// MockFiler is a mock of Filer interface.
type MockFiler struct {
ctrl *gomock.Controller
recorder *MockFilerMockRecorder
}
// MockFilerMockRecorder is the mock recorder for MockFiler.
type MockFilerMockRecorder struct {
mock *MockFiler
}
// NewMockFiler creates a new mock instance.
func NewMockFiler(ctrl *gomock.Controller) *MockFiler {
mock := &MockFiler{ctrl: ctrl}
mock.recorder = &MockFilerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFiler) EXPECT() *MockFilerMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockFiler) Delete(arg0 context.Context, arg1 string, arg2 ...filer.DeleteMode) error {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Delete", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockFilerMockRecorder) Delete(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockFiler)(nil).Delete), varargs...)
}
// Mkdir mocks base method.
func (m *MockFiler) Mkdir(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mkdir", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Mkdir indicates an expected call of Mkdir.
func (mr *MockFilerMockRecorder) Mkdir(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mkdir", reflect.TypeOf((*MockFiler)(nil).Mkdir), arg0, arg1)
}
// Read mocks base method.
func (m *MockFiler) Read(arg0 context.Context, arg1 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", arg0, arg1)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockFilerMockRecorder) Read(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockFiler)(nil).Read), arg0, arg1)
}
// ReadDir mocks base method.
func (m *MockFiler) ReadDir(arg0 context.Context, arg1 string) ([]fs.DirEntry, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDir", arg0, arg1)
ret0, _ := ret[0].([]fs.DirEntry)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDir indicates an expected call of ReadDir.
func (mr *MockFilerMockRecorder) ReadDir(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDir", reflect.TypeOf((*MockFiler)(nil).ReadDir), arg0, arg1)
}
// Stat mocks base method.
func (m *MockFiler) Stat(arg0 context.Context, arg1 string) (fs.FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stat", arg0, arg1)
ret0, _ := ret[0].(fs.FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Stat indicates an expected call of Stat.
func (mr *MockFilerMockRecorder) Stat(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockFiler)(nil).Stat), arg0, arg1)
}
// Write mocks base method.
func (m *MockFiler) Write(arg0 context.Context, arg1 string, arg2 io.Reader, arg3 ...filer.WriteMode) error {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Write", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Write indicates an expected call of Write.
func (mr *MockFilerMockRecorder) Write(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockFiler)(nil).Write), varargs...)
}

View File

@ -10,14 +10,14 @@ import (
type WriteMode int
const (
OverwriteIfExists WriteMode = iota
CreateParentDirectories = iota << 1
OverwriteIfExists WriteMode = 1 << iota
CreateParentDirectories
)
type DeleteMode int
const (
DeleteRecursively DeleteMode = iota
DeleteRecursively DeleteMode = 1 << iota
)
type FileAlreadyExistsError struct {