~bigbes/sr-ht-dolt

ref: ecfc6bb21417a3997754a5399a2cd2a73a542037 sr-ht-dolt/storage/move_test.go -rw-r--r-- 5.6 KiB
ecfc6bb2 — Eugene Blikh graph: a read schema for dolt.sr.ht at /query 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package storage

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// A moved store must still be a store: MoveStore relocates the directory whole,
// so the same chunks open at the new path.
func TestMoveStoreMovesAServableStore(t *testing.T) {
	ctx := context.Background()
	root := t.TempDir()
	src := RepoDiskPath(root, "alice", "widgets")
	dst := RepoDiskPath(root, "alice", "gadgets")
	require.NoError(t, InitStore(ctx, src, "alice", "a@b.test"))

	require.NoError(t, MoveStore(ctx, root, src, dst))

	_, err := os.Stat(src)
	assert.True(t, os.IsNotExist(err), "the old path must be gone, stat err = %v", err)

	cache := NewCache(func(context.Context, string, string) (string, error) { return dst, nil })
	defer cache.Close()
	cs, err := cache.Get(ctx, "~alice/gadgets", nbfVer)
	require.NoError(t, err, "the moved store must open at its new path")
	assert.NotNil(t, cs)
}

// The web rename evicts the memoized handle *after* the directory has moved,
// so eviction has to survive closing a store whose files are no longer where it
// opened them. If it did not, every rename of a database that had been served
// once would end in the eviction-failure branch — a 500 on the routine path
// instead of on the exceptional one.
func TestEvictAfterMoveClosesCleanly(t *testing.T) {
	ctx := context.Background()
	root := t.TempDir()
	src := RepoDiskPath(root, "alice", "widgets")
	dst := RepoDiskPath(root, "alice", "gadgets")
	require.NoError(t, InitStore(ctx, src, "alice", "a@b.test"))

	cache := NewCache(func(_ context.Context, _, name string) (string, error) {
		if name == "widgets" {
			return src, nil
		}
		return dst, nil
	})
	defer cache.Close()

	_, err := cache.Get(ctx, "~alice/widgets", nbfVer) // memoize a handle on the old path
	require.NoError(t, err)

	require.NoError(t, MoveStore(ctx, root, src, dst))
	require.NoError(t, cache.Evict(src), "evicting a handle whose store has moved must not fail")

	// And the moved store is servable again through its new path.
	_, err = cache.Get(ctx, "~alice/gadgets", nbfVer)
	require.NoError(t, err)
}

// os.Rename onto an existing empty directory succeeds and swallows it, so the
// destination is checked before anything is touched.
func TestMoveStoreRefusesAnExistingDestination(t *testing.T) {
	ctx := context.Background()
	root := t.TempDir()
	src := RepoDiskPath(root, "alice", "widgets")
	dst := RepoDiskPath(root, "alice", "gadgets")
	require.NoError(t, InitStore(ctx, src, "alice", "a@b.test"))

	for _, tc := range []struct {
		name  string
		setup func(t *testing.T)
	}{
		{"empty directory", func(t *testing.T) { require.NoError(t, os.MkdirAll(dst, 0o755)) }},
		{"another store", func(t *testing.T) { require.NoError(t, InitStore(ctx, dst, "alice", "a@b.test")) }},
	} {
		t.Run(tc.name, func(t *testing.T) {
			require.NoError(t, os.RemoveAll(dst))
			tc.setup(t)

			require.Error(t, MoveStore(ctx, root, src, dst), "MoveStore must refuse an occupied destination")
			assert.DirExists(t, src, "a refused move must leave the source where it was")
			assert.DirExists(t, dst, "a refused move must leave the destination alone")
		})
	}
}

// A source that is not there means the rename would leave the metadata row
// pointing at nothing, so it is an error rather than a silent success.
func TestMoveStoreRefusesAMissingSource(t *testing.T) {
	ctx := context.Background()
	root := t.TempDir()

	err := MoveStore(ctx, root, RepoDiskPath(root, "alice", "ghost"), RepoDiskPath(root, "alice", "gadgets"))
	require.Error(t, err)
	assert.NoDirExists(t, RepoDiskPath(root, "alice", "gadgets"))
}

func TestMoveStoreRefusesTheSamePath(t *testing.T) {
	ctx := context.Background()
	root := t.TempDir()
	src := RepoDiskPath(root, "alice", "widgets")
	require.NoError(t, InitStore(ctx, src, "alice", "a@b.test"))

	require.Error(t, MoveStore(ctx, root, src, src))
	assert.DirExists(t, src)
}

// Both ends of the move are contained: neither a source dragged in from outside
// the repos root nor a destination pointed outside it may be acted on.
func TestMoveStoreRootEscapeGuard(t *testing.T) {
	ctx := context.Background()
	root := t.TempDir()
	outside := t.TempDir()

	canary := filepath.Join(outside, "keep")
	require.NoError(t, os.WriteFile(canary, []byte("x"), 0o644))

	inside := RepoDiskPath(root, "alice", "widgets")
	require.NoError(t, InitStore(ctx, inside, "alice", "a@b.test"))

	for name, tc := range map[string]struct{ src, dst string }{
		"source outside the root":     {outside, RepoDiskPath(root, "alice", "gadgets")},
		"destination outside":         {inside, filepath.Join(outside, "stolen")},
		"traversal destination":       {inside, filepath.Join(root, "..", filepath.Base(outside), "stolen")},
		"the root as a source":        {root, RepoDiskPath(root, "alice", "gadgets")},
		"the root as a destination":   {inside, root},
		"prefix-not-subdir dest":      {inside, root + "-evil"},
		"relative source":             {"rel/path", RepoDiskPath(root, "alice", "gadgets")},
		"relative destination":        {inside, "rel/path"},
		"relative root with abs ends": {inside, RepoDiskPath(root, "alice", "gadgets")},
	} {
		t.Run(name, func(t *testing.T) {
			useRoot := root
			if name == "relative root with abs ends" {
				useRoot = "rel/root"
			}
			require.Error(t, MoveStore(ctx, useRoot, tc.src, tc.dst),
				"MoveStore(%q, %q) should have been refused", tc.src, tc.dst)
		})
	}

	assert.FileExists(t, canary, "a refused move disturbed something outside the repos root")
	assert.DirExists(t, inside, "a refused move disturbed the store it named")
	assert.NoDirExists(t, RepoDiskPath(root, "alice", "gadgets"))
}