bots-garden/mini-mepublic Fork 0
d72271127802973540c648bfb372176cdaaa8e4f
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

💾 Saved. d722711 · on d72271127802973540c648bfb372176cdaaa8e4f · k33g · 6h ago
fileedit_test.go · 307 lines · 10.2 KBGo Blame HistoryRaw
  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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
// The cases below are the `edit` CLI's own tests (tools/edit/internal/*),
// ported: what they guard is that the built-in tools refuse exactly what the
// CLI refuses, in words a model can act on — otherwise the A/B comparison of
// this part would measure two different tools.
package fileedit

import (
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

func inDir(t *testing.T, name, content string) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), name)
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
	return path
}

func fileOf(t *testing.T, path string) string {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	return string(data)
}

func TestApply(t *testing.T) {
	cases := []struct {
		name  string
		body  string
		edits []Edit
		want  string
	}{
		{"one replacement", "const port = 3000;\n",
			[]Edit{{"const port = 3000;", "const port = envPort(3000);"}}, "const port = envPort(3000);\n"},
		{"two independent edits, resolved against the original", "HOST = \"localhost\"\nPORT = 3000\n",
			[]Edit{{`HOST = "localhost"`, `HOST = env("HOST", "localhost")`}, {`PORT = 3000`, `PORT = envInt("PORT", 3000)`}},
			"HOST = env(\"HOST\", \"localhost\")\nPORT = envInt(\"PORT\", 3000)\n"},
		{"the order of the edits does not change the result", "a\nb\nc\n",
			[]Edit{{"c", "C"}, {"a", "A"}}, "A\nb\nC\n"},
		{"an empty new deletes", "keep\nto delete\nkeep too\n",
			[]Edit{{"to delete\n", ""}}, "keep\nkeep too\n"},
		{"the old text spans several lines", "func f() {\n\treturn 1\n}\n",
			[]Edit{{"func f() {\n\treturn 1\n}", "func f() int {\n\treturn 2\n}"}}, "func f() int {\n\treturn 2\n}\n"},
		{"an edit is NOT re-read by the next one", "x\ny\n",
			[]Edit{{"x", "y"}, {"y", "z"}}, "y\nz\n"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := apply(tc.body, tc.edits)
			if err != nil {
				t.Fatalf("apply: %v", err)
			}
			if got != tc.want {
				t.Errorf("apply = %q, want %q", got, tc.want)
			}
		})
	}
}

// TestApplyRefuses guards the rules that make an edit verifiable. Each message
// is read by a MODEL, which must be able to fix the call without guessing: the
// test also checks that the message carries the fix.
func TestApplyRefuses(t *testing.T) {
	cases := []struct {
		name  string
		body  string
		edits []Edit
		says  string
	}{
		{"text not found", "a\n", []Edit{{"b", "c"}}, "not found"},
		{"ambiguous text", "x = 1\nx = 1\n", []Edit{{"x = 1", "x = 2"}}, "appears 2 times — add the surrounding lines until it is unique"},
		{"overlapping regions", "func hello() {\n\treturn \"hello\"\n}\n",
			[]Edit{{"func hello() {\n\treturn \"hello\"\n}", "func hello() {\n\treturn \"bonjour\"\n}"}, {"return \"hello\"", "return \"salut\""}},
			"overlap"},
		{"empty old", "a\n", []Edit{{"", "b"}}, "empty old text"},
		{"an edit that changes nothing", "a\n", []Edit{{"a", "a"}}, "changes nothing"},
		{"no edit at all", "a\n", nil, "no edit given"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := apply(tc.body, tc.edits)
			if err == nil {
				t.Fatalf("apply succeeded, want an error — result %q", got)
			}
			if !strings.Contains(err.Error(), tc.says) {
				t.Errorf("error = %q, want it to contain %q", err, tc.says)
			}
			if got != "" {
				t.Errorf("apply returned %q along with the error: nothing must be written", got)
			}
		})
	}
}

func TestRead(t *testing.T) {
	path := inDir(t, "f.txt", "one\ntwo\nthree\n")

	got, err := Read(path, 0, 0, false)
	if err != nil || got != "one\ntwo\nthree\n" {
		t.Errorf("Read = (%q, %v)", got, err)
	}
	got, err = Read(path, 2, 2, true)
	if err != nil || got != "     2  two\n" {
		t.Errorf("Read 2-2 numbered = (%q, %v)", got, err)
	}
	if _, err := Read(filepath.Join(filepath.Dir(path), "nope.txt"), 0, 0, false); err == nil || !strings.Contains(err.Error(), "does not exist") {
		t.Errorf("Read of a missing file = %v", err)
	}
	if _, err := Read(path, 99, 0, false); err == nil || !strings.Contains(err.Error(), "past the end") {
		t.Errorf("Read start 99 = %v", err)
	}
}

func TestWriteCreatesAndReports(t *testing.T) {
	path := filepath.Join(t.TempDir(), "notes.md")

	r, err := Write(path, "# Notes")
	if err != nil {
		t.Fatal(err)
	}
	if got := fileOf(t, path); got != "# Notes\n" {
		t.Errorf("file = %q, want %q — the final newline is missing", got, "# Notes\n")
	}
	if !r.Created || !strings.Contains(r.Headline, "created") {
		t.Errorf("Result = %+v, want it to announce the creation", r)
	}
	// Rewriting the same content must do nothing, and SAY so.
	r, err = Write(path, "# Notes")
	if err != nil || r.Changed || !strings.Contains(r.Headline, "unchanged") {
		t.Errorf("identical Write = (%+v, %v)", r, err)
	}
}

func TestReplace(t *testing.T) {
	path := inDir(t, "server.go", "port := 3000\nhost := \"local\"\n")

	r, err := Replace(path, []Edit{{"port := 3000", "port := envPort(3000)"}}, false)
	if err != nil {
		t.Fatal(err)
	}
	if got := fileOf(t, path); got != "port := envPort(3000)\nhost := \"local\"\n" {
		t.Errorf("file = %q", got)
	}
	if r.FirstChangedLine != 1 || !strings.Contains(r.Headline, "first change at line 1") || !strings.Contains(r.Diff, "+ port := envPort(3000)") {
		t.Errorf("Result = %+v", r)
	}
}

// TestReplaceIsAllOrNothing: the second edit is impossible, so the first must
// not have happened. A half-edited file is the worst result — it looks like a
// success.
func TestReplaceIsAllOrNothing(t *testing.T) {
	const before = "a = 1\nb = 2\n"
	path := inDir(t, "f.txt", before)
	_, err := Replace(path, []Edit{{"a = 1", "a = 10"}, {"absent", "x"}}, false)
	if err == nil || !strings.Contains(err.Error(), "not found") {
		t.Errorf("err = %v", err)
	}
	if got := fileOf(t, path); got != before {
		t.Errorf("file = %q, want unchanged %q", got, before)
	}
}

func TestReplaceRefusesAMissingFile(t *testing.T) {
	_, err := Replace(filepath.Join(t.TempDir(), "nope.txt"), []Edit{{"a", "b"}}, false)
	if err == nil || !strings.Contains(err.Error(), "does not exist") {
		t.Errorf("err = %v", err)
	}
}

func TestDryRunWritesNothing(t *testing.T) {
	const before = "a = 1\n"
	path := inDir(t, "f.txt", before)
	r, err := Replace(path, []Edit{{"a = 1", "a = 2"}}, true)
	if err != nil || !r.DryRun || !strings.Contains(r.Headline, "dry run") || !strings.Contains(r.Diff, "+ a = 2") {
		t.Errorf("dry run = (%+v, %v)", r, err)
	}
	if got := fileOf(t, path); got != before {
		t.Errorf("file = %q, want unchanged", got)
	}
}

// TestShapeSurvives: BOM and CRLF are removed for the edit and put back on
// write, else every line of a Windows file would show up as changed.
func TestShapeSurvives(t *testing.T) {
	cases := []struct{ name, before, old, new, after string }{
		{"CRLF kept", "a\r\nb\r\n", "b", "B", "a\r\nB\r\n"},
		{"BOM kept", "\uFEFFa\n", "a", "A", "\uFEFFA\n"},
		{"plain LF", "a\n", "a", "A", "A\n"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			path := inDir(t, "f.txt", tc.before)
			if _, err := Replace(path, []Edit{{tc.old, tc.new}}, false); err != nil {
				t.Fatal(err)
			}
			if got := fileOf(t, path); got != tc.after {
				t.Errorf("file = %q, want %q", got, tc.after)
			}
		})
	}
}

// TestWriteKeepsTheMode: an executable script that gets edited must stay
// executable.
func TestWriteKeepsTheMode(t *testing.T) {
	path := inDir(t, "s.sh", "echo a\n")
	if err := os.Chmod(path, 0o755); err != nil {
		t.Fatal(err)
	}
	if _, err := Write(path, "echo b\n"); err != nil {
		t.Fatal(err)
	}
	info, _ := os.Stat(path)
	if info.Mode().Perm() != 0o755 {
		t.Errorf("mode = %v, want 755", info.Mode().Perm())
	}
}

func TestWriteCreatesParentDirs(t *testing.T) {
	path := filepath.Join(t.TempDir(), "a", "b", "notes.md")
	if _, err := Write(path, "# Notes\n"); err != nil {
		t.Fatalf("Write: %v", err)
	}
	if got := fileOf(t, path); got != "# Notes\n" {
		t.Errorf("file = %q", got)
	}
}

func TestFirstChanged(t *testing.T) {
	cases := []struct {
		name     string
		old, new string
		want     int
	}{
		{"nothing changed", "a\nb\n", "a\nb\n", 0},
		{"the second line", "a\nb\nc\n", "a\nB\nc\n", 2},
		{"an addition at the top", "a\n", "zero\na\n", 1},
		{"a deletion", "a\nb\nc\n", "a\nc\n", 2},
		{"a created file", "", "a\n", 1},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := compare(tc.old, tc.new).firstChanged(); got != tc.want {
				t.Errorf("firstChanged = %d, want %d", got, tc.want)
			}
		})
	}
}

func TestRenderHasNoANSI(t *testing.T) {
	out := compare("a\nb\n", "a\nB\n").render()
	if strings.Contains(out, "\033") || !strings.Contains(out, "- b") || !strings.Contains(out, "+ B") {
		t.Errorf("render = %q", out)
	}
}

// TestUnifiedMatchesGitApply is the test that counts: a patch that "looks
// like" a unified patch is useless. git APPLIES it, and refuses anything that
// is not exact — headers, line numbers, counts, end-of-file marker.
func TestUnifiedMatchesGitApply(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git absent")
	}
	cases := []struct{ name, old, new string }{
		{"one line in the middle", "a\nb\nc\nd\ne\n", "a\nb\nC\nd\ne\n"},
		{"two distant areas", "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n", "1\n2\nTHREE\n4\n5\n6\n7\n8\n9\n10\n11\n12\nTHIRTEEN\n14\n15\n"},
		{"two neighbouring areas: one hunk", "1\n2\n3\n4\n5\n6\n7\n8\n", "1\n2\nTHREE\n4\nFIVE\n6\n7\n8\n"},
		{"addition at the top", "a\nb\n", "zero\na\nb\n"},
		{"deletion at the tail", "a\nb\nc\n", "a\nb\n"},
		{"everything replaced", "a\nb\n", "x\ny\nz\n"},
		{"no final newline", "a\nb\nc", "a\nB\nc"},
		{"final newline added", "a\nb", "a\nb\n"},
		{"file emptied", "a\nb\n", ""},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			patch := compare(tc.old, tc.new).unified("f.txt")
			if patch == "" {
				t.Fatal("empty patch")
			}
			dir := t.TempDir()
			run := func(args ...string) {
				t.Helper()
				cmd := exec.Command("git", args...)
				cmd.Dir = dir
				if out, err := cmd.CombinedOutput(); err != nil {
					t.Fatalf("git %s: %v\n%s\npatch:\n%s", strings.Join(args, " "), err, out, patch)
				}
			}
			run("init", "-q")
			os.WriteFile(filepath.Join(dir, "f.txt"), []byte(tc.old), 0o644)
			os.WriteFile(filepath.Join(dir, "p.diff"), []byte(patch), 0o644)
			run("apply", "p.diff")
			if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {
				t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)
			}
		})
	}
}