github.com/OnkarRuikar/reviewdog@v0.0.0-20230802094019-bc1001e3b2e5/service/github/github_test.go (about)

     1  package github
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/url"
     9  	"os"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/google/go-github/v53/github"
    14  	"github.com/kylelemons/godebug/pretty"
    15  	"golang.org/x/oauth2"
    16  
    17  	"github.com/reviewdog/reviewdog"
    18  	"github.com/reviewdog/reviewdog/filter"
    19  	"github.com/reviewdog/reviewdog/proto/rdf"
    20  	"github.com/reviewdog/reviewdog/service/commentutil"
    21  )
    22  
    23  const notokenSkipTestMes = "skipping test (requires actual Personal access tokens. export REVIEWDOG_TEST_GITHUB_API_TOKEN=<GitHub Personal Access Token>)"
    24  
    25  func setupGitHubClient() *github.Client {
    26  	token := os.Getenv("REVIEWDOG_TEST_GITHUB_API_TOKEN")
    27  	if token == "" {
    28  		return nil
    29  	}
    30  	ts := oauth2.StaticTokenSource(
    31  		&oauth2.Token{AccessToken: token},
    32  	)
    33  	tc := oauth2.NewClient(context.TODO(), ts)
    34  	return github.NewClient(tc)
    35  }
    36  
    37  func setupEnvs() (cleanup func()) {
    38  	var cleanEnvs = []string{
    39  		"GITHUB_ACTIONS",
    40  	}
    41  	saveEnvs := make(map[string]string)
    42  	for _, key := range cleanEnvs {
    43  		saveEnvs[key] = os.Getenv(key)
    44  		os.Unsetenv(key)
    45  	}
    46  	return func() {
    47  		for key, value := range saveEnvs {
    48  			os.Setenv(key, value)
    49  		}
    50  	}
    51  }
    52  
    53  func moveToRootDir() {
    54  	os.Chdir("../..")
    55  }
    56  
    57  func TestGitHubPullRequest_Post(t *testing.T) {
    58  	t.Skip("skipping test which post comments actually")
    59  	client := setupGitHubClient()
    60  	if client == nil {
    61  		t.Skip(notokenSkipTestMes)
    62  	}
    63  
    64  	// https://github.com/reviewdog/reviewdog/pull/2
    65  	owner := "haya14busa"
    66  	repo := "reviewdog"
    67  	pr := 2
    68  	sha := "cce89afa9ac5519a7f5b1734db2e3aa776b138a7"
    69  
    70  	g, err := NewGitHubPullRequest(client, owner, repo, pr, sha)
    71  	if err != nil {
    72  		t.Fatal(err)
    73  	}
    74  	comment := &reviewdog.Comment{
    75  		Result: &filter.FilteredDiagnostic{
    76  			Diagnostic: &rdf.Diagnostic{
    77  				Location: &rdf.Location{
    78  					Path: "watchdogs.go",
    79  				},
    80  				Message: "[reviewdog] test",
    81  			},
    82  			InDiffContext: true,
    83  		},
    84  	}
    85  	// https://github.com/reviewdog/reviewdog/pull/2/files#diff-ed1d019a10f54464cfaeaf6a736b7d27L20
    86  	if err := g.Post(context.Background(), comment); err != nil {
    87  		t.Error(err)
    88  	}
    89  	if err := g.Flush(context.Background()); err != nil {
    90  		t.Error(err)
    91  	}
    92  }
    93  
    94  func TestGitHubPullRequest_Diff(t *testing.T) {
    95  	if testing.Short() {
    96  		t.Skip("skipping test which contains actual API requests in short mode")
    97  	}
    98  	client := setupGitHubClient()
    99  	if client == nil {
   100  		t.Skip(notokenSkipTestMes)
   101  	}
   102  
   103  	want := `diff --git a/diff.go b/diff.go
   104  index b380b67..6abc0f1 100644
   105  --- a/diff.go
   106  +++ b/diff.go
   107  @@ -4,6 +4,9 @@ import (
   108   	"os/exec"
   109   )
   110   
   111  +func TestNewExportedFunc() {
   112  +}
   113  +
   114   var _ DiffService = &DiffString{}
   115   
   116   type DiffString struct {
   117  diff --git a/reviewdog.go b/reviewdog.go
   118  index 61450f3..f63f149 100644
   119  --- a/reviewdog.go
   120  +++ b/reviewdog.go
   121  @@ -10,18 +10,18 @@ import (
   122   	"github.com/reviewdog/reviewdog/diff"
   123   )
   124   
   125  +var TestExportedVarWithoutComment = 1
   126  +
   127  +func NewReviewdog(p Parser, c CommentService, d DiffService) *Reviewdog {
   128  +	return &Reviewdog{p: p, c: c, d: d}
   129  +}
   130  +
   131   type Reviewdog struct {
   132   	p Parser
   133   	c CommentService
   134   	d DiffService
   135   }
   136   
   137  -func NewReviewdog(p Parser, c CommentService, d DiffService) *Reviewdog {
   138  -	return &Reviewdog{p: p, c: c, d: d}
   139  -}
   140  -
   141  -// CheckResult represents a checked result of static analysis tools.
   142  -// :h error-file-format
   143   type CheckResult struct {
   144   	Path    string   // file path
   145   	Lnum    int      // line number
   146  `
   147  
   148  	// https://github.com/reviewdog/reviewdog/pull/2
   149  	owner := "haya14busa"
   150  	repo := "reviewdog"
   151  	pr := 2
   152  	g, err := NewGitHubPullRequest(client, owner, repo, pr, "")
   153  	if err != nil {
   154  		t.Fatal(err)
   155  	}
   156  	b, err := g.Diff(context.Background())
   157  	if err != nil {
   158  		t.Fatal(err)
   159  	}
   160  	if got := string(b); got != want {
   161  		t.Errorf("got:\n%v\nwant:\n%v", got, want)
   162  	}
   163  }
   164  
   165  func TestGitHubPullRequest_comment(t *testing.T) {
   166  	if testing.Short() {
   167  		t.Skip("skipping test which contains actual API requests in short mode")
   168  	}
   169  	client := setupGitHubClient()
   170  	if client == nil {
   171  		t.Skip(notokenSkipTestMes)
   172  	}
   173  	// https://github.com/reviewdog/reviewdog/pull/2
   174  	owner := "haya14busa"
   175  	repo := "reviewdog"
   176  	pr := 2
   177  	g, err := NewGitHubPullRequest(client, owner, repo, pr, "")
   178  	if err != nil {
   179  		t.Fatal(err)
   180  	}
   181  	comments, err := g.comment(context.Background())
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  	for _, c := range comments {
   186  		t.Log("---")
   187  		t.Log(*c.Body)
   188  		t.Log(*c.Path)
   189  		if c.Position != nil {
   190  			t.Log(*c.Position)
   191  		}
   192  		t.Log(*c.CommitID)
   193  	}
   194  }
   195  
   196  func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
   197  	cwd, _ := os.Getwd()
   198  	defer os.Chdir(cwd)
   199  	moveToRootDir()
   200  	defer setupEnvs()()
   201  
   202  	listCommentsAPICalled := 0
   203  	postCommentsAPICalled := 0
   204  	mux := http.NewServeMux()
   205  	mux.HandleFunc("/repos/o/r/pulls/14/comments", func(w http.ResponseWriter, r *http.Request) {
   206  		listCommentsAPICalled++
   207  		if r.Method != http.MethodGet {
   208  			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
   209  		}
   210  		switch r.URL.Query().Get("page") {
   211  		default:
   212  			cs := []*github.PullRequestComment{
   213  				{
   214  					Path: github.String("reviewdog.go"),
   215  					Line: github.Int(2),
   216  					Body: github.String(commentutil.BodyPrefix + "already commented"),
   217  				},
   218  			}
   219  			w.Header().Add("Link", `<https://api.github.com/repos/o/r/pulls/14/comments?page=2>; rel="next"`)
   220  			if err := json.NewEncoder(w).Encode(cs); err != nil {
   221  				t.Fatal(err)
   222  			}
   223  		case "2":
   224  			cs := []*github.PullRequestComment{
   225  				{
   226  					Path: github.String("reviewdog.go"),
   227  					Line: github.Int(15),
   228  					Body: github.String(commentutil.BodyPrefix + "already commented 2"),
   229  				},
   230  				{
   231  					Path:      github.String("reviewdog.go"),
   232  					StartLine: github.Int(15),
   233  					Line:      github.Int(16),
   234  					Body:      github.String(commentutil.BodyPrefix + "multiline existing comment"),
   235  				},
   236  				{
   237  					Path:      github.String("reviewdog.go"),
   238  					StartLine: github.Int(15),
   239  					Line:      github.Int(17),
   240  					Body:      github.String(commentutil.BodyPrefix + "multiline existing comment (line-break)"),
   241  				},
   242  			}
   243  			if err := json.NewEncoder(w).Encode(cs); err != nil {
   244  				t.Fatal(err)
   245  			}
   246  		}
   247  	})
   248  	mux.HandleFunc("/repos/o/r/pulls/14/reviews", func(w http.ResponseWriter, r *http.Request) {
   249  		postCommentsAPICalled++
   250  		if r.Method != http.MethodPost {
   251  			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
   252  		}
   253  		var req github.PullRequestReviewRequest
   254  		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
   255  			t.Error(err)
   256  		}
   257  		if *req.Event != "COMMENT" {
   258  			t.Errorf("PullRequestReviewRequest.Event = %v, want COMMENT", *req.Event)
   259  		}
   260  		if req.Body != nil && *req.Body != "" {
   261  			t.Errorf("PullRequestReviewRequest.Body = %v, want empty", *req.Body)
   262  		}
   263  		if *req.CommitID != "sha" {
   264  			t.Errorf("PullRequestReviewRequest.Body = %v, want empty", *req.Body)
   265  		}
   266  		want := []*github.DraftReviewComment{
   267  			{
   268  				Path: github.String("reviewdog.go"),
   269  				Side: github.String("RIGHT"),
   270  				Line: github.Int(15),
   271  				Body: github.String(commentutil.BodyPrefix + "new comment"),
   272  			},
   273  			{
   274  				Path:      github.String("reviewdog.go"),
   275  				Side:      github.String("RIGHT"),
   276  				StartSide: github.String("RIGHT"),
   277  				StartLine: github.Int(15),
   278  				Line:      github.Int(16),
   279  				Body:      github.String(commentutil.BodyPrefix + "multiline new comment"),
   280  			},
   281  			{
   282  				Path:      github.String("reviewdog.go"),
   283  				Side:      github.String("RIGHT"),
   284  				StartSide: github.String("RIGHT"),
   285  				StartLine: github.Int(15),
   286  				Line:      github.Int(16),
   287  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   288  					"multiline suggestion comment",
   289  					"```suggestion",
   290  					"line1",
   291  					"line2",
   292  					"line3",
   293  					"```",
   294  				}, "\n") + "\n"),
   295  			},
   296  			{
   297  				Path: github.String("reviewdog.go"),
   298  				Side: github.String("RIGHT"),
   299  				Line: github.Int(15),
   300  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   301  					"singleline suggestion comment",
   302  					"```suggestion",
   303  					"line1",
   304  					"line2",
   305  					"```",
   306  				}, "\n") + "\n"),
   307  			},
   308  			{
   309  				Path:      github.String("reviewdog.go"),
   310  				Side:      github.String("RIGHT"),
   311  				StartSide: github.String("RIGHT"),
   312  				StartLine: github.Int(15),
   313  				Line:      github.Int(16),
   314  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   315  					"invalid lines suggestion comment",
   316  					invalidSuggestionPre + "GitHub comment range and suggestion line range must be same. L15-L16 v.s. L16-L17" + invalidSuggestionPost,
   317  				}, "\n") + "\n"),
   318  			},
   319  			{
   320  				Path:      github.String("reviewdog.go"),
   321  				Side:      github.String("RIGHT"),
   322  				StartSide: github.String("RIGHT"),
   323  				StartLine: github.Int(14),
   324  				Line:      github.Int(16),
   325  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   326  					"Use suggestion range as GitHub comment range if the suggestion is in diff context",
   327  					"```suggestion",
   328  					"line1",
   329  					"line2",
   330  					"line3",
   331  					"```",
   332  				}, "\n") + "\n"),
   333  			},
   334  			{
   335  				Path:      github.String("reviewdog.go"),
   336  				Side:      github.String("RIGHT"),
   337  				StartSide: github.String("RIGHT"),
   338  				StartLine: github.Int(14),
   339  				Line:      github.Int(16),
   340  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   341  					"Partially invalid suggestions",
   342  					"```suggestion",
   343  					"line1",
   344  					"line2",
   345  					"line3",
   346  					"```",
   347  					invalidSuggestionPre + "GitHub comment range and suggestion line range must be same. L14-L16 v.s. L14-L14" + invalidSuggestionPost,
   348  				}, "\n") + "\n"),
   349  			},
   350  			{
   351  				Path:      github.String("reviewdog.go"),
   352  				Side:      github.String("RIGHT"),
   353  				StartSide: github.String("RIGHT"),
   354  				StartLine: github.Int(15),
   355  				Line:      github.Int(16),
   356  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   357  					"non-line based suggestion comment (no source lines)",
   358  					invalidSuggestionPre + "source lines are not available" + invalidSuggestionPost,
   359  				}, "\n") + "\n"),
   360  			},
   361  			{
   362  				Path: github.String("reviewdog.go"),
   363  				Side: github.String("RIGHT"),
   364  				Line: github.Int(15),
   365  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   366  					"range suggestion (single line)",
   367  					"```suggestion",
   368  					"haya14busa",
   369  					"```",
   370  				}, "\n") + "\n"),
   371  			},
   372  			{
   373  				Path:      github.String("reviewdog.go"),
   374  				Side:      github.String("RIGHT"),
   375  				StartSide: github.String("RIGHT"),
   376  				StartLine: github.Int(15),
   377  				Line:      github.Int(16),
   378  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   379  					"range suggestion (multi-line)",
   380  					"```suggestion",
   381  					"haya14busa (multi-line)",
   382  					"```",
   383  				}, "\n") + "\n"),
   384  			},
   385  			{
   386  				Path:      github.String("reviewdog.go"),
   387  				Side:      github.String("RIGHT"),
   388  				StartSide: github.String("RIGHT"),
   389  				StartLine: github.Int(15),
   390  				Line:      github.Int(17),
   391  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   392  					"range suggestion (line-break, remove)",
   393  					"```suggestion",
   394  					"line 15 (content at line 15)",
   395  					"```",
   396  				}, "\n") + "\n"),
   397  			},
   398  			{
   399  				Path: github.String("reviewdog.go"),
   400  				Side: github.String("RIGHT"),
   401  				Line: github.Int(15),
   402  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   403  					"range suggestion (insert)",
   404  					"```suggestion",
   405  					"haya14busa",
   406  					"```",
   407  				}, "\n") + "\n"),
   408  			},
   409  			{
   410  				Path: github.String("reviewdog.go"),
   411  				Side: github.String("RIGHT"),
   412  				Line: github.Int(15),
   413  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   414  					"multiple suggestions",
   415  					"```suggestion",
   416  					"haya1busa",
   417  					"```",
   418  					"```suggestion",
   419  					"haya4busa",
   420  					"```",
   421  					"```suggestion",
   422  					"haya14busa",
   423  					"```",
   424  				}, "\n") + "\n"),
   425  			},
   426  			{
   427  				Path: github.String("reviewdog.go"),
   428  				Side: github.String("RIGHT"),
   429  				Line: github.Int(15),
   430  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   431  					"range suggestion with start only location",
   432  					"```suggestion",
   433  					"haya14busa",
   434  					"```",
   435  				}, "\n") + "\n"),
   436  			},
   437  			{
   438  				Path:      github.String("reviewdog.go"),
   439  				Side:      github.String("RIGHT"),
   440  				StartSide: github.String("RIGHT"),
   441  				StartLine: github.Int(15),
   442  				Line:      github.Int(16),
   443  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   444  					"multiline suggestion comment including a code fence block",
   445  					"````suggestion",
   446  					"```",
   447  					"some code",
   448  					"```",
   449  					"````",
   450  				}, "\n") + "\n"),
   451  			},
   452  			{
   453  				Path: github.String("reviewdog.go"),
   454  				Side: github.String("RIGHT"),
   455  				Line: github.Int(15),
   456  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   457  					"singleline suggestion comment including a code fence block",
   458  					"````suggestion",
   459  					"```",
   460  					"some code",
   461  					"```",
   462  					"````",
   463  				}, "\n") + "\n"),
   464  			},
   465  			{
   466  				Path:      github.String("reviewdog.go"),
   467  				Side:      github.String("RIGHT"),
   468  				StartSide: github.String("RIGHT"),
   469  				StartLine: github.Int(15),
   470  				Line:      github.Int(16),
   471  				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
   472  					"multiline suggestion comment including an empty code fence block",
   473  					"``````suggestion",
   474  					"```",
   475  					"`````",
   476  					"``````",
   477  				}, "\n") + "\n"),
   478  			},
   479  		}
   480  		if diff := pretty.Compare(want, req.Comments); diff != "" {
   481  			t.Errorf("req.Comments diff: (-got +want)\n%s", diff)
   482  		}
   483  	})
   484  	ts := httptest.NewServer(mux)
   485  	defer ts.Close()
   486  
   487  	cli := github.NewClient(nil)
   488  	cli.BaseURL, _ = url.Parse(ts.URL + "/")
   489  	g, err := NewGitHubPullRequest(cli, "o", "r", 14, "sha")
   490  	if err != nil {
   491  		t.Fatal(err)
   492  	}
   493  	comments := []*reviewdog.Comment{
   494  		{
   495  			Result: &filter.FilteredDiagnostic{
   496  				Diagnostic: &rdf.Diagnostic{
   497  					Location: &rdf.Location{
   498  						Path: "reviewdog.go",
   499  						Range: &rdf.Range{
   500  							Start: &rdf.Position{
   501  								Line: 2,
   502  							},
   503  						},
   504  					},
   505  					Message: "already commented",
   506  				},
   507  				InDiffContext: true,
   508  			},
   509  		},
   510  		{
   511  			Result: &filter.FilteredDiagnostic{
   512  				Diagnostic: &rdf.Diagnostic{
   513  					Location: &rdf.Location{
   514  						Path: "reviewdog.go",
   515  						Range: &rdf.Range{
   516  							Start: &rdf.Position{
   517  								Line: 15,
   518  							},
   519  						},
   520  					},
   521  					Message: "already commented 2",
   522  				},
   523  				InDiffContext: true,
   524  			},
   525  		},
   526  		{
   527  			Result: &filter.FilteredDiagnostic{
   528  				Diagnostic: &rdf.Diagnostic{
   529  					Location: &rdf.Location{
   530  						Path: "reviewdog.go",
   531  						Range: &rdf.Range{
   532  							Start: &rdf.Position{
   533  								Line: 15,
   534  							},
   535  						},
   536  					},
   537  					Message: "new comment",
   538  				},
   539  				InDiffContext: true,
   540  			},
   541  		},
   542  		{
   543  			Result: &filter.FilteredDiagnostic{
   544  				Diagnostic: &rdf.Diagnostic{
   545  					Location: &rdf.Location{
   546  						Path: "reviewdog.go",
   547  						Range: &rdf.Range{
   548  							Start: &rdf.Position{
   549  								Line: 15,
   550  							},
   551  							End: &rdf.Position{
   552  								Line: 16,
   553  							},
   554  						},
   555  					},
   556  					Message: "multiline existing comment",
   557  				},
   558  				InDiffContext: true,
   559  			},
   560  		},
   561  		{
   562  			Result: &filter.FilteredDiagnostic{
   563  				Diagnostic: &rdf.Diagnostic{
   564  					Location: &rdf.Location{
   565  						Path: "reviewdog.go",
   566  						Range: &rdf.Range{
   567  							Start: &rdf.Position{
   568  								Line:   15,
   569  								Column: 1,
   570  							},
   571  							End: &rdf.Position{
   572  								Line:   17,
   573  								Column: 1,
   574  							},
   575  						},
   576  					},
   577  					Message: "multiline existing comment (line-break)",
   578  				},
   579  				InDiffContext: true,
   580  			},
   581  		},
   582  		{
   583  			Result: &filter.FilteredDiagnostic{
   584  				Diagnostic: &rdf.Diagnostic{
   585  					Location: &rdf.Location{
   586  						Path: "reviewdog.go",
   587  						Range: &rdf.Range{
   588  							Start: &rdf.Position{
   589  								Line: 15,
   590  							},
   591  							End: &rdf.Position{
   592  								Line: 16,
   593  							},
   594  						},
   595  					},
   596  					Message: "multiline new comment",
   597  				},
   598  				InDiffContext: true,
   599  			},
   600  		},
   601  		{
   602  			Result: &filter.FilteredDiagnostic{
   603  				Diagnostic: &rdf.Diagnostic{
   604  					Location: &rdf.Location{
   605  						Path: "reviewdog.go",
   606  						// No Line
   607  					},
   608  					Message: "should not be reported via GitHub Review API",
   609  				},
   610  			},
   611  		},
   612  		{
   613  			Result: &filter.FilteredDiagnostic{
   614  				Diagnostic: &rdf.Diagnostic{
   615  					Location: &rdf.Location{
   616  						Path: "reviewdog.go",
   617  						Range: &rdf.Range{
   618  							Start: &rdf.Position{
   619  								Line: 15,
   620  							},
   621  							End: &rdf.Position{
   622  								Line: 16,
   623  							},
   624  						},
   625  					},
   626  					Suggestions: []*rdf.Suggestion{
   627  						{
   628  							Range: &rdf.Range{
   629  								Start: &rdf.Position{
   630  									Line: 15,
   631  								},
   632  								End: &rdf.Position{
   633  									Line: 16,
   634  								},
   635  							},
   636  							Text: "line1\nline2\nline3",
   637  						},
   638  					},
   639  					Message: "multiline suggestion comment",
   640  				},
   641  				InDiffContext: true,
   642  			},
   643  		},
   644  		{
   645  			Result: &filter.FilteredDiagnostic{
   646  				Diagnostic: &rdf.Diagnostic{
   647  					Location: &rdf.Location{
   648  						Path: "reviewdog.go",
   649  						Range: &rdf.Range{
   650  							Start: &rdf.Position{
   651  								Line: 15,
   652  							},
   653  						},
   654  					},
   655  					Suggestions: []*rdf.Suggestion{
   656  						{
   657  							Range: &rdf.Range{
   658  								Start: &rdf.Position{
   659  									Line: 15,
   660  								},
   661  							},
   662  							Text: "line1\nline2",
   663  						},
   664  					},
   665  					Message: "singleline suggestion comment",
   666  				},
   667  				InDiffContext: true,
   668  			},
   669  		},
   670  		{
   671  			Result: &filter.FilteredDiagnostic{
   672  				Diagnostic: &rdf.Diagnostic{
   673  					Location: &rdf.Location{
   674  						Path: "reviewdog.go",
   675  						Range: &rdf.Range{
   676  							Start: &rdf.Position{
   677  								Line: 15,
   678  							},
   679  							End: &rdf.Position{
   680  								Line: 16,
   681  							},
   682  						},
   683  					},
   684  					Suggestions: []*rdf.Suggestion{
   685  						{
   686  							Range: &rdf.Range{
   687  								Start: &rdf.Position{
   688  									Line: 16,
   689  								},
   690  								End: &rdf.Position{
   691  									Line: 17,
   692  								},
   693  							},
   694  							Text: "line1\nline2\nline3",
   695  						},
   696  					},
   697  					Message: "invalid lines suggestion comment",
   698  				},
   699  				InDiffContext:                true,
   700  				FirstSuggestionInDiffContext: false,
   701  			},
   702  		},
   703  		{
   704  			Result: &filter.FilteredDiagnostic{
   705  				SourceLines: map[int]string{
   706  					14: "line 14 before",
   707  					15: "line 15 before",
   708  					16: "line 16 before",
   709  				},
   710  				Diagnostic: &rdf.Diagnostic{
   711  					Location: &rdf.Location{
   712  						Path: "reviewdog.go",
   713  						Range: &rdf.Range{
   714  							Start: &rdf.Position{
   715  								Line: 15,
   716  							},
   717  						},
   718  					},
   719  					Suggestions: []*rdf.Suggestion{
   720  						{
   721  							Range: &rdf.Range{
   722  								Start: &rdf.Position{
   723  									Line: 14,
   724  								},
   725  								End: &rdf.Position{
   726  									Line: 16,
   727  								},
   728  							},
   729  							Text: "line1\nline2\nline3",
   730  						},
   731  					},
   732  					Message: "Use suggestion range as GitHub comment range if the suggestion is in diff context",
   733  				},
   734  				InDiffContext:                true,
   735  				FirstSuggestionInDiffContext: true,
   736  			},
   737  		},
   738  		{
   739  			Result: &filter.FilteredDiagnostic{
   740  				SourceLines: map[int]string{
   741  					14: "line 14 before",
   742  					15: "line 15 before",
   743  					16: "line 16 before",
   744  				},
   745  				Diagnostic: &rdf.Diagnostic{
   746  					Location: &rdf.Location{
   747  						Path: "reviewdog.go",
   748  						Range: &rdf.Range{
   749  							Start: &rdf.Position{
   750  								Line: 15,
   751  							},
   752  						},
   753  					},
   754  					Suggestions: []*rdf.Suggestion{
   755  						{
   756  							Range: &rdf.Range{
   757  								Start: &rdf.Position{
   758  									Line: 14,
   759  								},
   760  								End: &rdf.Position{
   761  									Line: 16,
   762  								},
   763  							},
   764  							Text: "line1\nline2\nline3",
   765  						},
   766  						{
   767  							Range: &rdf.Range{
   768  								Start: &rdf.Position{
   769  									Line: 14,
   770  								},
   771  								End: &rdf.Position{
   772  									Line: 14,
   773  								},
   774  							},
   775  							Text: "line1\nline2",
   776  						},
   777  					},
   778  					Message: "Partially invalid suggestions",
   779  				},
   780  				InDiffContext:                true,
   781  				FirstSuggestionInDiffContext: true,
   782  			},
   783  		},
   784  		{
   785  			Result: &filter.FilteredDiagnostic{
   786  				Diagnostic: &rdf.Diagnostic{
   787  					Location: &rdf.Location{
   788  						Path: "reviewdog.go",
   789  						Range: &rdf.Range{
   790  							Start: &rdf.Position{
   791  								Line: 15,
   792  							},
   793  							End: &rdf.Position{
   794  								Line: 16,
   795  							},
   796  						},
   797  					},
   798  					Suggestions: []*rdf.Suggestion{
   799  						{
   800  							Range: &rdf.Range{
   801  								Start: &rdf.Position{
   802  									Line:   15,
   803  									Column: 5,
   804  								},
   805  								End: &rdf.Position{
   806  									Line:   16,
   807  									Column: 7,
   808  								},
   809  							},
   810  							Text: "replacement",
   811  						},
   812  					},
   813  					Message: "non-line based suggestion comment (no source lines)",
   814  				},
   815  				InDiffContext: true,
   816  			},
   817  		},
   818  		{
   819  			Result: &filter.FilteredDiagnostic{
   820  				SourceLines: map[int]string{15: "haya15busa"},
   821  				Diagnostic: &rdf.Diagnostic{
   822  					Location: &rdf.Location{
   823  						Path: "reviewdog.go",
   824  						Range: &rdf.Range{
   825  							Start: &rdf.Position{Line: 15, Column: 5},
   826  							End:   &rdf.Position{Line: 15, Column: 7},
   827  						},
   828  					},
   829  					Suggestions: []*rdf.Suggestion{
   830  						{
   831  							Range: &rdf.Range{
   832  								Start: &rdf.Position{Line: 15, Column: 5},
   833  								End:   &rdf.Position{Line: 15, Column: 7},
   834  							},
   835  							Text: "14",
   836  						},
   837  					},
   838  					Message: "range suggestion (single line)",
   839  				},
   840  				InDiffContext: true,
   841  			},
   842  		},
   843  		{
   844  			Result: &filter.FilteredDiagnostic{
   845  				SourceLines: map[int]string{
   846  					15: "haya???",
   847  					16: "???busa (multi-line)",
   848  				},
   849  				Diagnostic: &rdf.Diagnostic{
   850  					Location: &rdf.Location{
   851  						Path: "reviewdog.go",
   852  						Range: &rdf.Range{
   853  							Start: &rdf.Position{Line: 15, Column: 5},
   854  							End:   &rdf.Position{Line: 16, Column: 4},
   855  						},
   856  					},
   857  					Suggestions: []*rdf.Suggestion{
   858  						{
   859  							Range: &rdf.Range{
   860  								Start: &rdf.Position{Line: 15, Column: 5},
   861  								End:   &rdf.Position{Line: 16, Column: 4},
   862  							},
   863  							Text: "14",
   864  						},
   865  					},
   866  					Message: "range suggestion (multi-line)",
   867  				},
   868  				InDiffContext: true,
   869  			},
   870  		},
   871  		{
   872  			Result: &filter.FilteredDiagnostic{
   873  				SourceLines: map[int]string{
   874  					15: "line 15 xxx",
   875  					16: "line 16",
   876  					17: "(content at line 15)",
   877  				},
   878  				Diagnostic: &rdf.Diagnostic{
   879  					Location: &rdf.Location{
   880  						Path: "reviewdog.go",
   881  						Range: &rdf.Range{
   882  							Start: &rdf.Position{Line: 15, Column: 9},
   883  							End:   &rdf.Position{Line: 17, Column: 1},
   884  						},
   885  					},
   886  					Suggestions: []*rdf.Suggestion{
   887  						{
   888  							Range: &rdf.Range{
   889  								Start: &rdf.Position{Line: 15, Column: 9},
   890  								End:   &rdf.Position{Line: 17, Column: 1},
   891  							},
   892  							Text: "",
   893  						},
   894  					},
   895  					Message: "range suggestion (line-break, remove)",
   896  				},
   897  				InDiffContext: true,
   898  			},
   899  		},
   900  		{
   901  			Result: &filter.FilteredDiagnostic{
   902  				SourceLines: map[int]string{
   903  					15: "hayabusa",
   904  				},
   905  				Diagnostic: &rdf.Diagnostic{
   906  					Location: &rdf.Location{
   907  						Path: "reviewdog.go",
   908  						Range: &rdf.Range{
   909  							Start: &rdf.Position{Line: 15, Column: 5},
   910  							End:   &rdf.Position{Line: 15, Column: 5},
   911  						},
   912  					},
   913  					Suggestions: []*rdf.Suggestion{
   914  						{
   915  							Range: &rdf.Range{
   916  								Start: &rdf.Position{Line: 15, Column: 5},
   917  								End:   &rdf.Position{Line: 15, Column: 5},
   918  							},
   919  							Text: "14",
   920  						},
   921  					},
   922  					Message: "range suggestion (insert)",
   923  				},
   924  				InDiffContext: true,
   925  			},
   926  		},
   927  		{
   928  			Result: &filter.FilteredDiagnostic{
   929  				SourceLines: map[int]string{15: "haya??busa"},
   930  				Diagnostic: &rdf.Diagnostic{
   931  					Location: &rdf.Location{
   932  						Path: "reviewdog.go",
   933  						Range: &rdf.Range{
   934  							Start: &rdf.Position{Line: 15, Column: 5},
   935  							End:   &rdf.Position{Line: 15, Column: 7},
   936  						},
   937  					},
   938  					Suggestions: []*rdf.Suggestion{
   939  						{
   940  							Range: &rdf.Range{
   941  								Start: &rdf.Position{Line: 15, Column: 5},
   942  								End:   &rdf.Position{Line: 15, Column: 7},
   943  							},
   944  							Text: "1",
   945  						},
   946  						{
   947  							Range: &rdf.Range{
   948  								Start: &rdf.Position{Line: 15, Column: 5},
   949  								End:   &rdf.Position{Line: 15, Column: 7},
   950  							},
   951  							Text: "4",
   952  						},
   953  						{
   954  							Range: &rdf.Range{
   955  								Start: &rdf.Position{Line: 15, Column: 5},
   956  								End:   &rdf.Position{Line: 15, Column: 7},
   957  							},
   958  							Text: "14",
   959  						},
   960  					},
   961  					Message: "multiple suggestions",
   962  				},
   963  				InDiffContext: true,
   964  			},
   965  		},
   966  		{
   967  			Result: &filter.FilteredDiagnostic{
   968  				SourceLines: map[int]string{15: "haya15busa"},
   969  				Diagnostic: &rdf.Diagnostic{
   970  					Location: &rdf.Location{
   971  						Path: "reviewdog.go",
   972  						Range: &rdf.Range{
   973  							Start: &rdf.Position{Line: 15, Column: 5},
   974  						},
   975  					},
   976  					Suggestions: []*rdf.Suggestion{
   977  						{
   978  							Range: &rdf.Range{
   979  								Start: &rdf.Position{Line: 15, Column: 5},
   980  								End:   &rdf.Position{Line: 15, Column: 7},
   981  							},
   982  							Text: "14",
   983  						},
   984  					},
   985  					Message: "range suggestion with start only location",
   986  				},
   987  				InDiffContext: true,
   988  			},
   989  		},
   990  		{
   991  			Result: &filter.FilteredDiagnostic{
   992  				Diagnostic: &rdf.Diagnostic{
   993  					Location: &rdf.Location{
   994  						Path: "reviewdog.go",
   995  						Range: &rdf.Range{
   996  							Start: &rdf.Position{
   997  								Line: 15,
   998  							},
   999  							End: &rdf.Position{
  1000  								Line: 16,
  1001  							},
  1002  						},
  1003  					},
  1004  					Suggestions: []*rdf.Suggestion{
  1005  						{
  1006  							Range: &rdf.Range{
  1007  								Start: &rdf.Position{
  1008  									Line: 15,
  1009  								},
  1010  								End: &rdf.Position{
  1011  									Line: 16,
  1012  								},
  1013  							},
  1014  							Text: "```\nsome code\n```",
  1015  						},
  1016  					},
  1017  					Message: "multiline suggestion comment including a code fence block",
  1018  				},
  1019  				InDiffContext: true,
  1020  			},
  1021  		},
  1022  		{
  1023  			Result: &filter.FilteredDiagnostic{
  1024  				Diagnostic: &rdf.Diagnostic{
  1025  					Location: &rdf.Location{
  1026  						Path: "reviewdog.go",
  1027  						Range: &rdf.Range{
  1028  							Start: &rdf.Position{
  1029  								Line: 15,
  1030  							},
  1031  						},
  1032  					},
  1033  					Suggestions: []*rdf.Suggestion{
  1034  						{
  1035  							Range: &rdf.Range{
  1036  								Start: &rdf.Position{
  1037  									Line: 15,
  1038  								},
  1039  							},
  1040  							Text: "```\nsome code\n```",
  1041  						},
  1042  					},
  1043  					Message: "singleline suggestion comment including a code fence block",
  1044  				},
  1045  				InDiffContext: true,
  1046  			},
  1047  		},
  1048  		{
  1049  			Result: &filter.FilteredDiagnostic{
  1050  				Diagnostic: &rdf.Diagnostic{
  1051  					Location: &rdf.Location{
  1052  						Path: "reviewdog.go",
  1053  						Range: &rdf.Range{
  1054  							Start: &rdf.Position{
  1055  								Line: 15,
  1056  							},
  1057  							End: &rdf.Position{
  1058  								Line: 16,
  1059  							},
  1060  						},
  1061  					},
  1062  					Suggestions: []*rdf.Suggestion{
  1063  						{
  1064  							Range: &rdf.Range{
  1065  								Start: &rdf.Position{
  1066  									Line: 15,
  1067  								},
  1068  								End: &rdf.Position{
  1069  									Line: 16,
  1070  								},
  1071  							},
  1072  							Text: "```\n`````",
  1073  						},
  1074  					},
  1075  					Message: "multiline suggestion comment including an empty code fence block",
  1076  				},
  1077  				InDiffContext: true,
  1078  			},
  1079  		},
  1080  	}
  1081  	for _, c := range comments {
  1082  		if err := g.Post(context.Background(), c); err != nil {
  1083  			t.Error(err)
  1084  		}
  1085  	}
  1086  	if err := g.Flush(context.Background()); err != nil {
  1087  		t.Error(err)
  1088  	}
  1089  	if listCommentsAPICalled != 2 {
  1090  		t.Errorf("GitHub List PullRequest comments API called %v times, want 2 times", listCommentsAPICalled)
  1091  	}
  1092  	if postCommentsAPICalled != 1 {
  1093  		t.Errorf("GitHub post PullRequest comments API called %v times, want 1 times", postCommentsAPICalled)
  1094  	}
  1095  }
  1096  
  1097  func TestGitHubPullRequest_Post_toomany(t *testing.T) {
  1098  	cwd, _ := os.Getwd()
  1099  	defer os.Chdir(cwd)
  1100  	moveToRootDir()
  1101  	defer setupEnvs()()
  1102  
  1103  	listCommentsAPICalled := 0
  1104  	postCommentsAPICalled := 0
  1105  
  1106  	mux := http.NewServeMux()
  1107  	mux.HandleFunc("/repos/o/r/pulls/14/comments", func(w http.ResponseWriter, r *http.Request) {
  1108  		listCommentsAPICalled++
  1109  		if err := json.NewEncoder(w).Encode([]*github.PullRequestComment{}); err != nil {
  1110  			t.Fatal(err)
  1111  		}
  1112  	})
  1113  	mux.HandleFunc("/repos/o/r/pulls/14/reviews", func(w http.ResponseWriter, r *http.Request) {
  1114  		postCommentsAPICalled++
  1115  		var req github.PullRequestReviewRequest
  1116  		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  1117  			t.Error(err)
  1118  		}
  1119  		if req.GetBody() == "" {
  1120  			t.Errorf("PullRequestReviewRequest.Body is empty but want some summary text")
  1121  		}
  1122  	})
  1123  	ts := httptest.NewServer(mux)
  1124  	defer ts.Close()
  1125  
  1126  	cli := github.NewClient(nil)
  1127  	cli.BaseURL, _ = url.Parse(ts.URL + "/")
  1128  	g, err := NewGitHubPullRequest(cli, "o", "r", 14, "sha")
  1129  	if err != nil {
  1130  		t.Fatal(err)
  1131  	}
  1132  	var comments []*reviewdog.Comment
  1133  	for i := 0; i < 100; i++ {
  1134  		comments = append(comments, &reviewdog.Comment{
  1135  			Result: &filter.FilteredDiagnostic{
  1136  				Diagnostic: &rdf.Diagnostic{
  1137  					Location: &rdf.Location{
  1138  						Path: "reviewdog.go",
  1139  						Range: &rdf.Range{Start: &rdf.Position{
  1140  							Line: int32(i),
  1141  						}},
  1142  					},
  1143  					Message: "comment",
  1144  				},
  1145  				InDiffContext: true,
  1146  			},
  1147  			ToolName: "tool",
  1148  		})
  1149  	}
  1150  	for _, c := range comments {
  1151  		if err := g.Post(context.Background(), c); err != nil {
  1152  			t.Error(err)
  1153  		}
  1154  	}
  1155  	if err := g.Flush(context.Background()); err != nil {
  1156  		t.Error(err)
  1157  	}
  1158  	if want := 1; listCommentsAPICalled != want {
  1159  		t.Errorf("GitHub List PullRequest comments API called %v times, want %d times", listCommentsAPICalled, want)
  1160  	}
  1161  	if want := 1; postCommentsAPICalled != want {
  1162  		t.Errorf("GitHub post PullRequest comments API called %v times, want %d times", postCommentsAPICalled, want)
  1163  	}
  1164  }
  1165  
  1166  func TestGitHubPullRequest_workdir(t *testing.T) {
  1167  	cwd, _ := os.Getwd()
  1168  	defer os.Chdir(cwd)
  1169  	moveToRootDir()
  1170  	defer setupEnvs()()
  1171  
  1172  	g, err := NewGitHubPullRequest(nil, "", "", 0, "")
  1173  	if err != nil {
  1174  		t.Fatal(err)
  1175  	}
  1176  	if g.wd != "" {
  1177  		t.Fatalf("g.wd = %q, want empty", g.wd)
  1178  	}
  1179  	ctx := context.Background()
  1180  	want := "a/b/c"
  1181  	g.Post(ctx, &reviewdog.Comment{Result: &filter.FilteredDiagnostic{
  1182  		Diagnostic: &rdf.Diagnostic{Location: &rdf.Location{Path: want}}}})
  1183  	if got := g.postComments[0].Result.Diagnostic.GetLocation().GetPath(); got != want {
  1184  		t.Errorf("wd=%q path=%q, want %q", g.wd, got, want)
  1185  	}
  1186  
  1187  	subDir := "cmd/"
  1188  	if err := os.Chdir(subDir); err != nil {
  1189  		t.Fatal(err)
  1190  	}
  1191  	g, _ = NewGitHubPullRequest(nil, "", "", 0, "")
  1192  	if g.wd != subDir {
  1193  		t.Fatalf("gitRelWorkdir() = %q, want %q", g.wd, subDir)
  1194  	}
  1195  	path := "a/b/c"
  1196  	wantPath := "cmd/" + path
  1197  	g.Post(ctx, &reviewdog.Comment{Result: &filter.FilteredDiagnostic{
  1198  		Diagnostic: &rdf.Diagnostic{Location: &rdf.Location{Path: want}}}})
  1199  	if got := g.postComments[0].Result.Diagnostic.GetLocation().GetPath(); got != wantPath {
  1200  		t.Errorf("wd=%q path=%q, want %q", g.wd, got, wantPath)
  1201  	}
  1202  }
  1203  
  1204  func TestGitHubPullRequest_Diff_fake(t *testing.T) {
  1205  	apiCalled := 0
  1206  	mux := http.NewServeMux()
  1207  	mux.HandleFunc("/repos/o/r/pulls/14", func(w http.ResponseWriter, r *http.Request) {
  1208  		apiCalled++
  1209  		if r.Method != http.MethodGet {
  1210  			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
  1211  		}
  1212  		if accept := r.Header.Get("Accept"); !strings.Contains(accept, "diff") {
  1213  			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
  1214  		}
  1215  		w.Write([]byte("Pull Request diff"))
  1216  	})
  1217  	ts := httptest.NewServer(mux)
  1218  	defer ts.Close()
  1219  
  1220  	cli := github.NewClient(nil)
  1221  	cli.BaseURL, _ = url.Parse(ts.URL + "/")
  1222  	g, err := NewGitHubPullRequest(cli, "o", "r", 14, "sha")
  1223  	if err != nil {
  1224  		t.Fatal(err)
  1225  	}
  1226  	if _, err := g.Diff(context.Background()); err != nil {
  1227  		t.Fatal(err)
  1228  	}
  1229  	if apiCalled != 1 {
  1230  		t.Errorf("GitHub API should be called once; called %v times", apiCalled)
  1231  	}
  1232  }