mtoohey.com/vimv2@v0.0.0-20240419154935-8326cddb67ee/main_test.go (about) 1 //go:generate go build -o testdata testdata/mockeditor.go 2 3 package main 4 5 import ( 6 "errors" 7 "fmt" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "runtime" 12 "sort" 13 "testing" 14 ) 15 16 const prompt = "[\033[1;31me\033[0mdit existing/edit " + 17 "\033[1;31mn\033[0mew/\033[1;31mq\033[0muit]: " 18 19 func Test_main(t *testing.T) { 20 // assumes the tests are run from the root of the repository 21 cwd, err := os.Getwd() 22 requireNoError(t, err) 23 // restore cwd, since subtests will change it 24 t.Cleanup(func() { requireNoError(t, os.Chdir(cwd)) }) 25 26 mockEditorPath := filepath.Join(cwd, "testdata", "mockeditor") 27 if runtime.GOOS == "windows" { 28 mockEditorPath += ".exe" 29 } 30 31 t.Log(mockEditorPath) 32 33 // generate the mock editor if it doesn't already exist 34 _, err = os.Stat(mockEditorPath) 35 if err != nil && errors.Is(err, os.ErrNotExist) { 36 requireNoError(t, exec.Command("go", "generate").Run()) 37 } else { 38 requireNoError(t, err) 39 } 40 41 nonExecutableEditorPath := filepath.Join(t.TempDir(), "nonexecutable") 42 requireNoError(t, os.WriteFile(nonExecutableEditorPath, nil, 0o644)) 43 44 tests := []struct { 45 description string 46 47 preTest func(t *testing.T) 48 stdin string 49 createdFiles []string 50 51 expectedFiles []string 52 expectedStdout, expectedStderr string 53 expectedExitCode int 54 }{ 55 { 56 description: "happy path simple", 57 preTest: func(t *testing.T) { 58 t.Setenv("EDITOR", mockEditorPath) 59 countFile := filepath.Join(t.TempDir(), "count") 60 requireNoError(t, os.WriteFile(countFile, []byte{'0'}, 0o644)) 61 t.Setenv("MOCK_EDITOR_COUNT_FILE", countFile) 62 t.Setenv("MOCK_EDITOR_OUTPUT_0", `d file 63 e file 64 f file 65 `) 66 t.Setenv("MOCK_EDITOR_EXIT_CODE_0", "0") 67 }, 68 createdFiles: []string{ 69 "a file", 70 "b file", 71 "c file", 72 }, 73 expectedFiles: []string{ 74 "d file", 75 "e file", 76 "f file", 77 }, 78 expectedStdout: "mock editor run 0\n", 79 expectedStderr: "mock editor run 0\n", 80 }, 81 82 { 83 description: "no editor", 84 expectedStderr: "self: no editor found, please set $EDITOR or $VISUAL\n", 85 expectedExitCode: 1, 86 }, 87 { 88 description: "editor not executable, $VISUAL respected", 89 preTest: func(t *testing.T) { 90 t.Setenv("VISUAL", nonExecutableEditorPath) 91 }, 92 expectedStderr: fmt.Sprintf("self: running editor command failed: "+ 93 "fork/exec %s: permission denied\n", nonExecutableEditorPath), 94 expectedExitCode: 1, 95 }, 96 { 97 description: "editor exits with non-zero", 98 preTest: func(t *testing.T) { 99 t.Setenv("EDITOR", mockEditorPath) 100 countFile := filepath.Join(t.TempDir(), "count") 101 requireNoError(t, os.WriteFile(countFile, []byte{'0'}, 0o644)) 102 t.Setenv("MOCK_EDITOR_COUNT_FILE", countFile) 103 t.Setenv("MOCK_EDITOR_OUTPUT_0", "") 104 t.Setenv("MOCK_EDITOR_EXIT_CODE_0", "15") 105 }, 106 expectedStdout: "mock editor run 0\n", 107 expectedStderr: `mock editor run 0 108 self: running editor command failed: exit status 15 109 `, 110 expectedExitCode: 1, 111 }, 112 { 113 description: "too many lines, invalid selection, prompt EOF", 114 preTest: func(t *testing.T) { 115 t.Setenv("EDITOR", mockEditorPath) 116 countFile := filepath.Join(t.TempDir(), "count") 117 requireNoError(t, os.WriteFile(countFile, []byte{'0'}, 0o644)) 118 t.Setenv("MOCK_EDITOR_COUNT_FILE", countFile) 119 t.Setenv("MOCK_EDITOR_OUTPUT_0", `d file 120 e file 121 f file 122 g file 123 `) 124 t.Setenv("MOCK_EDITOR_EXIT_CODE_0", "0") 125 }, 126 stdin: "?", 127 createdFiles: []string{ 128 "a file", 129 "b file", 130 "c file", 131 }, 132 expectedFiles: []string{ 133 "a file", 134 "b file", 135 "c file", 136 }, 137 expectedStdout: "mock editor run 0\n", 138 expectedStderr: `mock editor run 0 139 self: tmpfile contains too many lines 140 ` + prompt + `? 141 self: invalid selection '?' 142 ` + prompt + ` 143 self: user exited 144 `, 145 expectedExitCode: 1, 146 }, 147 { 148 description: "duplicate destination, retried with new, too few, retried with existing, empty, quit", 149 preTest: func(t *testing.T) { 150 t.Setenv("EDITOR", mockEditorPath) 151 countFile := filepath.Join(t.TempDir(), "count") 152 requireNoError(t, os.WriteFile(countFile, []byte{'0'}, 0o644)) 153 t.Setenv("MOCK_EDITOR_COUNT_FILE", countFile) 154 t.Setenv("MOCK_EDITOR_OUTPUT_0", `d file 155 e file 156 e file 157 `) 158 t.Setenv("MOCK_EDITOR_OUTPUT_1", `d file 159 e file 160 `) 161 t.Setenv("MOCK_EDITOR_OUTPUT_2", "") 162 t.Setenv("MOCK_EDITOR_EXIT_CODE_0", "0") 163 t.Setenv("MOCK_EDITOR_EXIT_CODE_1", "0") 164 t.Setenv("MOCK_EDITOR_EXIT_CODE_2", "0") 165 }, 166 stdin: "nEq", 167 createdFiles: []string{ 168 "a file", 169 "b file", 170 "c file", 171 }, 172 expectedFiles: []string{ 173 "a file", 174 "b file", 175 "c file", 176 }, 177 expectedStdout: `mock editor run 0 178 mock editor run 1 179 mock editor run 2 180 `, 181 expectedStderr: `mock editor run 0 182 self: duplicate destination "e file" 183 ` + prompt + `n 184 mock editor run 1 185 self: tmpfile contains too few lines 186 ` + prompt + `E 187 mock editor run 2 188 self: tmpfile contains too few lines 189 ` + prompt + `q 190 self: user exited 191 `, 192 expectedExitCode: 1, 193 }, 194 } 195 196 // prevent external env from polluting tests 197 for _, envVar := range [2]string{"EDITOR", "VISUAL"} { 198 oldVal, found := os.LookupEnv(envVar) 199 if found { 200 requireNoError(t, os.Unsetenv(envVar)) 201 t.Cleanup(func() { 202 requireNoError(t, os.Setenv(envVar, oldVal)) 203 }) 204 } 205 } 206 207 // clear args 208 mockVar(t, &os.Args, []string{"self"}) 209 210 // exit mocking 211 var actualExitCode int 212 mockVar(t, &exit, func(exitCode int) { 213 actualExitCode = exitCode 214 }) 215 216 for _, test := range tests { 217 t.Run(test.description, func(t *testing.T) { 218 // setup 219 220 tempDir := t.TempDir() 221 222 // cwd will be restored by at the end of the test as a whole 223 os.Chdir(tempDir) 224 225 for _, file := range test.createdFiles { 226 requireNoError(t, os.WriteFile(file, nil, 0o644)) 227 } 228 229 // clear actualExitCode so we can tell when it didn't get set 230 actualExitCode = -1 231 232 if test.preTest != nil { 233 test.preTest(t) 234 } 235 236 // SUT with stdin/stdout/stderr mocking 237 actualStdout, actualStderr := redirectRecover(t, main, test.stdin) 238 239 // a ssertions 240 if test.expectedExitCode != actualExitCode { 241 t.Errorf("expected exit code: %d did not match actual exit "+ 242 "code: %d", test.expectedExitCode, actualExitCode) 243 } 244 if test.expectedStdout != actualStdout { 245 t.Errorf("expected stdout:\n%s\ndid not match actual "+ 246 "stdout:\n%s", test.expectedStdout, actualStdout) 247 } 248 if test.expectedStderr != actualStderr { 249 t.Errorf("expected stderr:\n%s\ndid not match actual "+ 250 "stderr:\n%s", test.expectedStderr, actualStderr) 251 } 252 253 actualEntries, err := os.ReadDir(".") 254 requireNoError(t, err) 255 256 actualFiles := make([]string, len(actualEntries)) 257 for i, e := range actualEntries { 258 actualFiles[i] = e.Name() 259 } 260 261 if len(test.expectedFiles) == len(actualFiles) { 262 sort.Strings(test.expectedFiles) 263 sort.Strings(actualFiles) 264 for i, expectedFile := range test.expectedFiles { 265 if expectedFile != actualFiles[i] { 266 t.Errorf( 267 "expected files: %v didn't match actual files: %v", 268 test.expectedFiles, actualFiles) 269 } 270 } 271 } else { 272 t.Errorf("expected files: %v didn't match actual files: %v", 273 test.expectedFiles, actualFiles) 274 } 275 }) 276 } 277 }