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  }