736 lines
18 KiB
Go
736 lines
18 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package metadata
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/containers"
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/filters"
|
|
"github.com/containerd/containerd/log/logtest"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/containerd/typeurl"
|
|
"github.com/gogo/protobuf/types"
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
func init() {
|
|
typeurl.Register(&specs.Spec{}, "types.containerd.io/opencontainers/runtime-spec", "v1", "Spec")
|
|
}
|
|
|
|
func TestContainersList(t *testing.T) {
|
|
ctx, db, cancel := testEnv(t)
|
|
defer cancel()
|
|
|
|
store := NewContainerStore(NewDB(db, nil, nil))
|
|
|
|
spec := &specs.Spec{}
|
|
encoded, err := typeurl.MarshalAny(spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testset := map[string]*containers.Container{}
|
|
for i := 0; i < 4; i++ {
|
|
id := "container-" + fmt.Sprint(i)
|
|
testset[id] = &containers.Container{
|
|
ID: id,
|
|
Labels: map[string]string{
|
|
"idlabel": id,
|
|
"even": fmt.Sprint(i%2 == 0),
|
|
"odd": fmt.Sprint(i%2 != 0),
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
}
|
|
|
|
if err := db.Update(func(tx *bolt.Tx) error {
|
|
now := time.Now()
|
|
result, err := store.Create(WithTransactionContext(ctx, tx), *testset[id])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
checkContainerTimestamps(t, &result, now, true)
|
|
testset[id].UpdatedAt, testset[id].CreatedAt = result.UpdatedAt, result.CreatedAt
|
|
checkContainersEqual(t, &result, testset[id], "ensure that containers were created as expected for list")
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
for _, testcase := range []struct {
|
|
name string
|
|
filters []string
|
|
}{
|
|
{
|
|
name: "FullSet",
|
|
},
|
|
{
|
|
name: "FullSetFiltered", // full set, but because we have OR filter
|
|
filters: []string{"labels.even==true", "labels.odd==true"},
|
|
},
|
|
{
|
|
name: "Even",
|
|
filters: []string{"labels.even==true"},
|
|
},
|
|
{
|
|
name: "Odd",
|
|
filters: []string{"labels.odd==true"},
|
|
},
|
|
{
|
|
name: "ByID",
|
|
filters: []string{"id==container-0"},
|
|
},
|
|
{
|
|
name: "ByIDLabelEven",
|
|
filters: []string{"labels.idlabel==container-0,labels.even==true"},
|
|
},
|
|
{
|
|
name: "ByRuntime",
|
|
filters: []string{"runtime.name==testruntime"},
|
|
},
|
|
} {
|
|
t.Run(testcase.name, func(t *testing.T) {
|
|
testset := testset
|
|
if len(testcase.filters) > 0 {
|
|
fs, err := filters.ParseAll(testcase.filters...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
newtestset := make(map[string]*containers.Container, len(testset))
|
|
for k, v := range testset {
|
|
if fs.Match(adaptContainer(*v)) {
|
|
newtestset[k] = v
|
|
}
|
|
}
|
|
testset = newtestset
|
|
}
|
|
|
|
results, err := store.List(ctx, testcase.filters...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(results) == 0 { // all tests return a non-empty result set
|
|
t.Fatalf("not results returned")
|
|
}
|
|
|
|
if len(results) != len(testset) {
|
|
t.Fatalf("length of result does not match testset: %v != %v", len(results), len(testset))
|
|
}
|
|
|
|
for _, result := range results {
|
|
checkContainersEqual(t, &result, testset[result.ID], "list results did not match")
|
|
}
|
|
})
|
|
}
|
|
|
|
// delete everything to test it
|
|
for id := range testset {
|
|
if err := store.Delete(ctx, id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// try it again, get NotFound
|
|
if err := store.Delete(ctx, id); err == nil {
|
|
t.Fatalf("expected error deleting non-existent container")
|
|
} else if !errdefs.IsNotFound(err) {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestContainersUpdate ensures that updates are taken in an expected manner.
|
|
func TestContainersCreateUpdateDelete(t *testing.T) {
|
|
ctx, db, cancel := testEnv(t)
|
|
defer cancel()
|
|
|
|
store := NewContainerStore(NewDB(db, nil, nil))
|
|
|
|
spec := &specs.Spec{}
|
|
encoded, err := typeurl.MarshalAny(spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
spec.Annotations = map[string]string{"updated": "true"}
|
|
encodedUpdated, err := typeurl.MarshalAny(spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, testcase := range []struct {
|
|
name string
|
|
original containers.Container
|
|
createerr error
|
|
input containers.Container
|
|
fieldpaths []string
|
|
expected containers.Container
|
|
cause error
|
|
}{
|
|
{
|
|
name: "UpdateIDFail",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
ID: "newid",
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
fieldpaths: []string{"id"},
|
|
cause: errdefs.ErrNotFound,
|
|
},
|
|
{
|
|
name: "UpdateRuntimeFail",
|
|
original: containers.Container{
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntimedifferent",
|
|
},
|
|
},
|
|
fieldpaths: []string{"runtime"},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "UpdateRuntimeClearFail",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
},
|
|
fieldpaths: []string{"runtime"},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "UpdateSpec",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Spec: encodedUpdated,
|
|
},
|
|
fieldpaths: []string{"spec"},
|
|
expected: containers.Container{
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Spec: encodedUpdated,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateSnapshot",
|
|
original: containers.Container{
|
|
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
SnapshotKey: "test2-snapshot-key",
|
|
},
|
|
fieldpaths: []string{"snapshotkey"},
|
|
expected: containers.Container{
|
|
|
|
Spec: encoded,
|
|
SnapshotKey: "test2-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateImage",
|
|
original: containers.Container{
|
|
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Image: "test2 image",
|
|
},
|
|
fieldpaths: []string{"image"},
|
|
expected: containers.Container{
|
|
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test2 image",
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateLabel",
|
|
original: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "two",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Labels: map[string]string{
|
|
"bar": "baz",
|
|
},
|
|
},
|
|
fieldpaths: []string{"labels.bar"},
|
|
expected: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "baz",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "DeleteAllLabels",
|
|
original: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "two",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Labels: nil,
|
|
},
|
|
fieldpaths: []string{"labels"},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "DeleteLabel",
|
|
original: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
"bar": "two",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
input: containers.Container{
|
|
Labels: map[string]string{
|
|
"bar": "",
|
|
},
|
|
},
|
|
fieldpaths: []string{"labels.bar"},
|
|
expected: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
},
|
|
Spec: encoded,
|
|
SnapshotKey: "test-snapshot-key",
|
|
Snapshotter: "snapshotter",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Image: "test image",
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateSnapshotKeyImmutable",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "",
|
|
Snapshotter: "",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
SnapshotKey: "something",
|
|
Snapshotter: "something",
|
|
},
|
|
fieldpaths: []string{"snapshotkey", "snapshotter"},
|
|
cause: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "SnapshotKeyWithoutSnapshot",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
SnapshotKey: "/nosnapshot",
|
|
Snapshotter: "",
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
},
|
|
createerr: errdefs.ErrInvalidArgument,
|
|
},
|
|
{
|
|
name: "UpdateExtensionsFull",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateExtensionsNotInFieldpath",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"labels"},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateExtensionsFieldPath",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Labels: map[string]string{
|
|
"foo": "one",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"extensions"},
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("world"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateExtensionsFieldPathIsolated",
|
|
original: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
// leaves hello in place.
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"),
|
|
},
|
|
},
|
|
},
|
|
input: containers.Container{
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("universe"), // this will be ignored
|
|
},
|
|
"bar": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("foo"), // this will be added
|
|
},
|
|
},
|
|
},
|
|
fieldpaths: []string{"extensions.bar"}, //
|
|
expected: containers.Container{
|
|
Spec: encoded,
|
|
Runtime: containers.RuntimeInfo{
|
|
Name: "testruntime",
|
|
},
|
|
Extensions: map[string]types.Any{
|
|
"hello": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("hello"), // remains as world
|
|
},
|
|
"bar": {
|
|
TypeUrl: "test.update.extensions",
|
|
Value: []byte("foo"), // this will be added
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(testcase.name, func(t *testing.T) {
|
|
testcase.original.ID = testcase.name
|
|
if testcase.input.ID == "" {
|
|
testcase.input.ID = testcase.name
|
|
}
|
|
testcase.expected.ID = testcase.name
|
|
|
|
now := time.Now().UTC()
|
|
|
|
result, err := store.Create(ctx, testcase.original)
|
|
if !errors.Is(err, testcase.createerr) {
|
|
if testcase.createerr == nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
} else {
|
|
t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Cause(err), testcase.createerr)
|
|
}
|
|
} else if testcase.createerr != nil {
|
|
return
|
|
}
|
|
|
|
checkContainerTimestamps(t, &result, now, true)
|
|
|
|
// ensure that createdat is never tampered with
|
|
testcase.original.CreatedAt = result.CreatedAt
|
|
testcase.expected.CreatedAt = result.CreatedAt
|
|
testcase.original.UpdatedAt = result.UpdatedAt
|
|
testcase.expected.UpdatedAt = result.UpdatedAt
|
|
|
|
checkContainersEqual(t, &result, &testcase.original, "unexpected result on container update")
|
|
|
|
now = time.Now()
|
|
result, err = store.Update(ctx, testcase.input, testcase.fieldpaths...)
|
|
if !errors.Is(err, testcase.cause) {
|
|
if testcase.cause == nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
} else {
|
|
t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Cause(err), testcase.cause)
|
|
}
|
|
} else if testcase.cause != nil {
|
|
return
|
|
}
|
|
|
|
checkContainerTimestamps(t, &result, now, false)
|
|
testcase.expected.UpdatedAt = result.UpdatedAt
|
|
checkContainersEqual(t, &result, &testcase.expected, "updated failed to get expected result")
|
|
|
|
result, err = store.Get(ctx, testcase.original.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
checkContainersEqual(t, &result, &testcase.expected, "get after failed to get expected result")
|
|
})
|
|
}
|
|
}
|
|
|
|
func checkContainerTimestamps(t *testing.T, c *containers.Container, now time.Time, oncreate bool) {
|
|
if c.UpdatedAt.IsZero() || c.CreatedAt.IsZero() {
|
|
t.Fatalf("timestamps not set")
|
|
}
|
|
|
|
if oncreate {
|
|
if !c.CreatedAt.Equal(c.UpdatedAt) {
|
|
t.Fatal("timestamps should be equal on create")
|
|
}
|
|
|
|
} else {
|
|
// ensure that updatedat is always after createdat
|
|
if !c.UpdatedAt.After(c.CreatedAt) {
|
|
t.Fatalf("timestamp for updatedat not after createdat: %v <= %v", c.UpdatedAt, c.CreatedAt)
|
|
}
|
|
}
|
|
|
|
if c.UpdatedAt.Before(now) {
|
|
t.Fatal("createdat time incorrect should be after the start of the operation")
|
|
}
|
|
}
|
|
|
|
func checkContainersEqual(t *testing.T, a, b *containers.Container, format string, args ...interface{}) {
|
|
if !reflect.DeepEqual(a, b) {
|
|
t.Fatalf("containers not equal \n\t%v != \n\t%v: "+format, append([]interface{}{a, b}, args...)...)
|
|
}
|
|
}
|
|
|
|
func testEnv(t *testing.T) (context.Context, *bolt.DB, func()) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
ctx = namespaces.WithNamespace(ctx, "testing")
|
|
ctx = logtest.WithT(ctx, t)
|
|
|
|
dirname, err := ioutil.TempDir("", strings.Replace(t.Name(), "/", "_", -1)+"-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
db, err := bolt.Open(filepath.Join(dirname, "meta.db"), 0644, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return ctx, db, func() {
|
|
db.Close()
|
|
if err := os.RemoveAll(dirname); err != nil {
|
|
t.Log("failed removing temp dir", err)
|
|
}
|
|
cancel()
|
|
}
|
|
}
|