github.com/naive/revgrep@v0.0.0-20240331191128-ab485935cedc/revgrep_test.go (about)

     1  package revgrep
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"reflect"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  	"testing"
    15  )
    16  
    17  func setup(t *testing.T, stage, subdir string) (string, []byte) {
    18  	t.Helper()
    19  
    20  	wd, err := os.Getwd()
    21  	if err != nil {
    22  		t.Fatalf("could not get working dir: %s", err)
    23  	}
    24  
    25  	testDataDir := filepath.Join(wd, "testdata")
    26  
    27  	// Execute make
    28  	cmd := exec.Command("bash", "./make.sh", stage)
    29  	cmd.Dir = testDataDir
    30  
    31  	gitOutput, err := cmd.CombinedOutput()
    32  	if err != nil {
    33  		t.Logf("%s: git setup: %s", stage, string(gitOutput))
    34  		t.Fatalf("could not run make.sh: %v", err)
    35  	}
    36  
    37  	gitDir := filepath.Join(testDataDir, "git")
    38  	t.Cleanup(func() {
    39  		_ = os.RemoveAll(gitDir)
    40  	})
    41  
    42  	cmd = exec.Command("go", "vet", "./...")
    43  	cmd.Dir = gitDir
    44  
    45  	goVetOutput, err := cmd.CombinedOutput()
    46  	if cmd.ProcessState.ExitCode() != 1 {
    47  		t.Logf("%s: go vet: %s", stage, string(goVetOutput))
    48  		t.Fatalf("could not run go vet: %v", err)
    49  	}
    50  
    51  	// chdir so the vcs exec commands read the correct testdata
    52  	err = os.Chdir(filepath.Join(gitDir, subdir))
    53  	if err != nil {
    54  		t.Fatalf("could not chdir: %v", err)
    55  	}
    56  
    57  	if stage == "11-abs-path" {
    58  		goVetOutput = regexp.MustCompile(`(.+\.go)`).
    59  			ReplaceAll(goVetOutput, []byte(filepath.Join(gitDir, "$1")))
    60  	}
    61  
    62  	// clean go vet output
    63  	goVetOutput = bytes.ReplaceAll(goVetOutput, []byte("."+string(filepath.Separator)), []byte(""))
    64  
    65  	t.Logf("%s: go vet clean: %s", stage, string(goVetOutput))
    66  
    67  	return wd, goVetOutput
    68  }
    69  
    70  func teardown(t *testing.T, wd string) {
    71  	t.Helper()
    72  
    73  	err := os.Chdir(wd)
    74  	if err != nil {
    75  		t.Fatalf("could not chdir: %v", err)
    76  	}
    77  }
    78  
    79  // TestCheckerRegexp tests line matching and extraction of issue.
    80  func TestCheckerRegexp(t *testing.T) {
    81  	tests := []struct {
    82  		regexp string
    83  		line   string
    84  		want   Issue
    85  	}{
    86  		{
    87  			line: "file.go:1:issue",
    88  			want: Issue{File: "file.go", LineNo: 1, HunkPos: 2, Issue: "file.go:1:issue", Message: "issue"},
    89  		},
    90  		{
    91  			line: "file.go:1:5:issue",
    92  			want: Issue{File: "file.go", LineNo: 1, ColNo: 5, HunkPos: 2, Issue: "file.go:1:5:issue", Message: "issue"},
    93  		},
    94  		{
    95  			line: "file.go:1:  issue",
    96  			want: Issue{File: "file.go", LineNo: 1, HunkPos: 2, Issue: "file.go:1:  issue", Message: "issue"},
    97  		},
    98  		{
    99  			regexp: `.*?:(.*?\.go):([0-9]+):()(.*)`,
   100  			line:   "prefix:file.go:1:issue",
   101  			want:   Issue{File: "file.go", LineNo: 1, HunkPos: 2, Issue: "prefix:file.go:1:issue", Message: "issue"},
   102  		},
   103  	}
   104  
   105  	diff := []byte(`--- a/file.go
   106  +++ b/file.go
   107  @@ -1,1 +1,1 @@
   108  -func Line() {}
   109  +func NewLine() {}`)
   110  
   111  	for _, test := range tests {
   112  		checker := Checker{
   113  			Patch:  bytes.NewReader(diff),
   114  			Regexp: test.regexp,
   115  		}
   116  
   117  		issues, err := checker.Check(bytes.NewReader([]byte(test.line)), io.Discard)
   118  		if err != nil {
   119  			t.Errorf("unexpected error: %v", err)
   120  		}
   121  
   122  		want := []Issue{test.want}
   123  		if !reflect.DeepEqual(issues, want) {
   124  			t.Errorf("unexpected issues for line: %q\nhave: %#v\nwant: %#v", test.line, issues, want)
   125  		}
   126  	}
   127  }
   128  
   129  // TestWholeFile tests Checker.WholeFiles will report any issues in files that have changes, even if
   130  // they are outside the diff.
   131  func TestWholeFiles(t *testing.T) {
   132  	tests := []struct {
   133  		name    string
   134  		line    string
   135  		matches bool
   136  	}{
   137  		{
   138  			name:    "inside diff",
   139  			line:    "file.go:1:issue",
   140  			matches: true,
   141  		},
   142  		{
   143  			name:    "outside diff",
   144  			line:    "file.go:10:5:issue",
   145  			matches: true,
   146  		},
   147  		{
   148  			name: "different file",
   149  			line: "file2.go:1:issue",
   150  		},
   151  	}
   152  
   153  	diff := []byte(`--- a/file.go
   154  +++ b/file.go
   155  @@ -1,1 +1,1 @@
   156  -func Line() {}
   157  +func NewLine() {}`)
   158  
   159  	for _, test := range tests {
   160  		t.Run(test.name, func(t *testing.T) {
   161  			checker := Checker{
   162  				Patch:      bytes.NewReader(diff),
   163  				WholeFiles: true,
   164  			}
   165  
   166  			issues, err := checker.Check(bytes.NewReader([]byte(test.line)), io.Discard)
   167  			if err != nil {
   168  				t.Fatalf("unexpected error: %v", err)
   169  			}
   170  			if test.matches && len(issues) != 1 {
   171  				t.Fatalf("expected one issue to be returned, but got %#v", issues)
   172  			}
   173  			if !test.matches && len(issues) != 0 {
   174  				t.Fatalf("expected no issues to be returned, but got %#v", issues)
   175  			}
   176  		})
   177  	}
   178  }
   179  
   180  // Tests the writer in the argument to the Changes function
   181  // and generally tests the entire program functionality.
   182  func TestChecker_Check_changesWriter(t *testing.T) {
   183  	tests := map[string]struct {
   184  		subdir  string
   185  		exp     []string // file:linenumber including trailing colon
   186  		revFrom string
   187  		revTo   string
   188  	}{
   189  		"2-untracked":            {exp: []string{"main.go:3:"}},
   190  		"3-untracked-subdir":     {exp: []string{"main.go:3:", "subdir/main.go:3:"}},
   191  		"3-untracked-subdir-cwd": {subdir: "subdir", exp: []string{"main.go:3:"}},
   192  		"4-commit":               {exp: []string{"main.go:3:", "subdir/main.go:3:"}},
   193  		"5-unstaged-no-warning":  {},
   194  		"6-unstaged":             {exp: []string{"main.go:6:"}},
   195  		// From a commit, all changes should be shown
   196  		"7-commit": {exp: []string{"main.go:6:"}, revFrom: "HEAD~1"},
   197  		// From a commit+unstaged, all changes should be shown
   198  		"8-unstaged": {exp: []string{"main.go:6:", "main.go:7:"}, revFrom: "HEAD~1"},
   199  		// From a commit+unstaged+untracked, all changes should be shown
   200  		"9-untracked": {exp: []string{"main.go:6:", "main.go:7:", "main2.go:3:"}, revFrom: "HEAD~1"},
   201  		// From a commit to last commit, all changes should be shown except recent unstaged, untracked
   202  		"10-committed": {exp: []string{"main.go:6:"}, revFrom: "HEAD~1", revTo: "HEAD~0"},
   203  		// static analysis tools with absolute paths should be handled
   204  		"11-abs-path": {exp: []string{"main.go:6:"}, revFrom: "HEAD~1", revTo: "HEAD~0"},
   205  		// Removing a single line shouldn't raise any issues.
   206  		"12-removed-lines": {},
   207  	}
   208  
   209  	for stage, test := range tests {
   210  		t.Run(stage, func(t *testing.T) {
   211  			prevwd, goVetOutput := setup(t, stage, test.subdir)
   212  
   213  			var out bytes.Buffer
   214  
   215  			c := Checker{
   216  				RevisionFrom: test.revFrom,
   217  				RevisionTo:   test.revTo,
   218  			}
   219  			_, err := c.Check(bytes.NewBuffer(goVetOutput), &out)
   220  			if err != nil {
   221  				t.Errorf("%s: unexpected error: %v", stage, err)
   222  			}
   223  
   224  			var lines []string
   225  
   226  			scanner := bufio.NewScanner(&out)
   227  			for scanner.Scan() {
   228  				// Rewrite abs paths to for simpler matching
   229  				line := rewriteAbs(scanner.Text())
   230  				lines = append(lines, strings.TrimPrefix(line, "./"))
   231  			}
   232  
   233  			sort.Slice(lines, func(i, j int) bool {
   234  				return lines[i] <= lines[j]
   235  			})
   236  
   237  			var count int
   238  			for i, line := range lines {
   239  				count++
   240  				if i > len(test.exp)-1 {
   241  					t.Errorf("%s: unexpected line: %q", stage, line)
   242  				} else if !strings.HasPrefix(line, filepath.FromSlash(test.exp[i])) {
   243  					t.Errorf("%s: line %q does not have prefix %q", stage, line, filepath.FromSlash(test.exp[i]))
   244  				}
   245  			}
   246  
   247  			if count != len(test.exp) {
   248  				t.Errorf("%s: got %d, expected %d", stage, count, len(test.exp))
   249  			}
   250  
   251  			teardown(t, prevwd)
   252  		})
   253  	}
   254  }
   255  
   256  func rewriteAbs(line string) string {
   257  	cwd, err := os.Getwd()
   258  	if err != nil {
   259  		panic(err)
   260  	}
   261  	return strings.TrimPrefix(line, cwd+string(filepath.Separator))
   262  }
   263  
   264  func TestGitPatchNonGitDir(t *testing.T) {
   265  	// Change to non-git dir
   266  	err := os.Chdir("/")
   267  	if err != nil {
   268  		t.Fatalf("could not chdir: %v", err)
   269  	}
   270  
   271  	patch, newfiles, err := GitPatch("", "")
   272  	if err != nil {
   273  		t.Errorf("error expected nil, got: %v", err)
   274  	}
   275  	if patch != nil {
   276  		t.Errorf("patch expected nil, got: %v", patch)
   277  	}
   278  	if newfiles != nil {
   279  		t.Errorf("newFiles expected nil, got: %v", newfiles)
   280  	}
   281  }
   282  
   283  func TestLinesChanged(t *testing.T) {
   284  	diff := []byte(`--- a/file.go
   285  +++ b/file.go
   286  @@ -1,1 +1,1 @@
   287   // comment
   288  -func Line() {}
   289  +func NewLine() {}
   290  @@ -20,1 +20,1 @@
   291   // comment
   292  -func Line() {}
   293  +func NewLine() {}
   294   // comment
   295  @@ -3,1 +30,1 @@
   296  -func Line() {}
   297  +func NewLine() {}
   298   // comment`)
   299  
   300  	checker := Checker{
   301  		Patch: bytes.NewReader(diff),
   302  	}
   303  
   304  	have := checker.linesChanged()
   305  
   306  	want := map[string][]pos{
   307  		"file.go": {
   308  			{lineNo: 2, hunkPos: 3},
   309  			{lineNo: 21, hunkPos: 7},
   310  			{lineNo: 30, hunkPos: 11},
   311  		},
   312  	}
   313  
   314  	if !reflect.DeepEqual(have, want) {
   315  		t.Errorf("unexpected pos:\nhave: %#v\nwant: %#v", have, want)
   316  	}
   317  }