golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/gerritbot/internal/rules/rules_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package rules
     6  
     7  import (
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/google/go-cmp/cmp"
    12  )
    13  
    14  func TestFormatRuleResults(t *testing.T) {
    15  	tests := []struct {
    16  		title string
    17  		repo  string // if empty string, treated as "go" repo
    18  		body  string
    19  		want  string
    20  	}{
    21  		{
    22  			title: `fmt: improve some things`, // We consider this a good commit message title.
    23  			body:  goodCommitBody,
    24  			want:  ``,
    25  		},
    26  		{
    27  			title: `a bad commit message title.`,
    28  			body:  goodCommitBody,
    29  			want: `Possible problems detected:
    30    1. The commit title should start with the primary affected package name followed by a colon, like "net/http: improve [...]".
    31    2. The first word in the commit title after the package should be a lowercase English word (usually a verb).
    32    3. The commit title should not end with a period.
    33  
    34  The commit title and commit message body come from the GitHub PR title and description, and must be edited in the GitHub web interface (not via git). For instructions, see [here](https://go.dev/wiki/GerritBot/#how-does-gerritbot-determine-the-final-commit-message). For guidelines on commit messages for the Go project, see [here](https://go.dev/doc/contribute#commit_messages).
    35  `,
    36  		},
    37  		{
    38  			title: `A bad vscode-go commit title`, // This verifies we complain about a "component" rather than "package".
    39  			repo:  "vscode-go",
    40  			body:  "This includes a bad bug format for vscode-go repo.\nFixes #1234",
    41  			want: `Possible problems detected:
    42    1. The commit title should start with the primary affected component name followed by a colon, like "src/goInstallTools: improve [...]".
    43    2. The first word in the commit title after the component should be a lowercase English word (usually a verb).
    44    3. Do you have the right bug reference format? For the vscode-go repo, the format is usually 'Fixes golang/vscode-go#1234' or 'Updates golang/vscode-go#1234' at the end of the commit message.
    45  
    46  The commit title and commit message body come from the GitHub PR title and description, and must be edited in the GitHub web interface (not via git). For instructions, see [here](https://go.dev/wiki/GerritBot/#how-does-gerritbot-determine-the-final-commit-message). For guidelines on commit messages for the Go project, see [here](https://go.dev/doc/contribute#commit_messages).
    47  `,
    48  		},
    49  		{
    50  			title: goodCommitTitle,
    51  			body:  "This commit body is missing a bug reference.",
    52  			want: `Possible problems detected:
    53    1. You usually need to reference a bug number for all but trivial or cosmetic fixes. For this repo, the format is usually 'Fixes #12345' or 'Updates #12345' at the end of the commit message. Should you have a bug reference?
    54  
    55  The commit title and commit message body come from the GitHub PR title and description, and must be edited in the GitHub web interface (not via git). For instructions, see [here](https://go.dev/wiki/GerritBot/#how-does-gerritbot-determine-the-final-commit-message). For guidelines on commit messages for the Go project, see [here](https://go.dev/doc/contribute#commit_messages).
    56  `,
    57  		},
    58  	}
    59  	for _, tt := range tests {
    60  		t.Run("title "+tt.title, func(t *testing.T) {
    61  			commit := commitMessage(tt.title, tt.body, goodCommitFooters)
    62  			repo := "go"
    63  			if tt.repo != "" {
    64  				repo = tt.repo
    65  			}
    66  			change, err := ParseCommitMessage(repo, commit)
    67  			if err != nil {
    68  				t.Fatalf("ParseCommitMessage failed: %v", err)
    69  			}
    70  			results := Check(change)
    71  			got := FormatResults(results)
    72  			t.Log("FormatResults:\n" + got)
    73  			if diff := cmp.Diff(tt.want, got); diff != "" {
    74  				t.Errorf("checkRules() mismatch (-want +got):\n%s", diff)
    75  			}
    76  		})
    77  	}
    78  }
    79  
    80  func TestTitleRules(t *testing.T) {
    81  	tests := []struct {
    82  		title string
    83  		repo  string // if empty string, treated as "go" repo
    84  		body  string
    85  		want  []string
    86  	}{
    87  		// We consider these good titles.
    88  		{
    89  			title: "fmt: good",
    90  			want:  nil,
    91  		},
    92  		{
    93  			title: "go/types, types2: good",
    94  			want:  nil,
    95  		},
    96  		{
    97  			title: "go/types, types2, types3: good",
    98  			want:  nil,
    99  		},
   100  		{
   101  			title: "fmt: improve & make: Good",
   102  			want:  nil,
   103  		},
   104  		{
   105  			title: "README: fix something",
   106  			want:  nil,
   107  		},
   108  		{
   109  			title: "_content/doc/go1.21: fix something",
   110  			repo:  "website",
   111  			want:  nil,
   112  		},
   113  		{
   114  			title: "Fix a proposal", // We are lenient with proposal repo titles.
   115  			repo:  "proposal",
   116  			want:  nil,
   117  		},
   118  
   119  		// We consider these bad titles.
   120  		{
   121  			title: "bad.",
   122  			want: []string{
   123  				"title: no package found",
   124  				"title: no lowercase word after a first colon",
   125  				"title: ends with period",
   126  			},
   127  		},
   128  		{
   129  			title: "bad",
   130  			want: []string{
   131  				"title: no package found",
   132  				"title: no lowercase word after a first colon",
   133  			},
   134  		},
   135  		{
   136  			title: "fmt: bad.",
   137  			want: []string{
   138  				"title: ends with period",
   139  			},
   140  		},
   141  		{
   142  			title: "Fmt: bad",
   143  			want: []string{
   144  				"title: no package found",
   145  			},
   146  		},
   147  		{
   148  			title: "fmt: Bad",
   149  			want: []string{
   150  				"title: no lowercase word after a first colon",
   151  			},
   152  		},
   153  		{
   154  			title: "fmt:  bad",
   155  			want: []string{
   156  				"title: no colon then single space after package",
   157  			},
   158  		},
   159  		{
   160  			title: "fmt:  Bad",
   161  			want: []string{
   162  				"title: no colon then single space after package",
   163  				"title: no lowercase word after a first colon",
   164  			},
   165  		},
   166  		{
   167  			title: "fmt:bad",
   168  			want: []string{
   169  				"title: no colon then single space after package",
   170  			},
   171  		},
   172  		{
   173  			title: "fmt : bad",
   174  			want: []string{
   175  				"title: no package found",
   176  			},
   177  		},
   178  		{
   179  			title: ": bad",
   180  			want: []string{
   181  				"title: no package found",
   182  			},
   183  		},
   184  		{
   185  			title: " : bad",
   186  			want: []string{
   187  				"title: no package found",
   188  			},
   189  		},
   190  		{
   191  			title: "go/types types2: bad",
   192  			want: []string{
   193  				"title: no package found",
   194  			},
   195  		},
   196  		{
   197  			title: "a sentence, with a comma and colon: bad",
   198  			want: []string{
   199  				"title: no package found",
   200  			},
   201  		},
   202  		{
   203  			title: "a sentence with a colon: and a wrongly placed package fmt: bad",
   204  			want: []string{
   205  				"title: no package found",
   206  			},
   207  		},
   208  		{
   209  			title: "",
   210  			want: []string{
   211  				"title: no package found",
   212  				"title: no lowercase word after a first colon",
   213  			},
   214  		},
   215  
   216  		// We allow these titles (in interests of simplicity or leniency).
   217  		// TODO: are some of these considered an alternative good style?
   218  		{
   219  			title: "go/types,types2: we allow",
   220  			want:  nil,
   221  		},
   222  		{
   223  			title: "cmd/{compile,link}: we allow",
   224  			want:  nil,
   225  		},
   226  		{
   227  			title: "cmd/{compile, link}: we allow",
   228  			want:  nil,
   229  		},
   230  	}
   231  	for _, tt := range tests {
   232  		t.Run("title "+tt.title, func(t *testing.T) {
   233  			commit := commitMessage(tt.title, goodCommitBody, goodCommitFooters)
   234  			repo := "go"
   235  			if tt.repo != "" {
   236  				repo = tt.repo
   237  			}
   238  			change, err := ParseCommitMessage(repo, commit)
   239  			if err != nil {
   240  				t.Fatalf("ParseCommitMessage failed: %v", err)
   241  			}
   242  			results := Check(change)
   243  
   244  			var got []string
   245  			for _, r := range results {
   246  				got = append(got, r.Name)
   247  			}
   248  
   249  			if diff := cmp.Diff(tt.want, got); diff != "" {
   250  				t.Errorf("checkRules() mismatch (-want +got):\n%s", diff)
   251  			}
   252  		})
   253  	}
   254  }
   255  
   256  func TestBodyRules(t *testing.T) {
   257  	tests := []struct {
   258  		name  string
   259  		title string // if empty string, we use goodCommitTitle
   260  		repo  string // if empty string, treated as "go" repo
   261  		body  string
   262  		want  []string
   263  	}{
   264  		// We consider these good bodies.
   265  		{
   266  			name: "good",
   267  			body: goodCommitBody,
   268  			want: nil,
   269  		},
   270  		{
   271  			name: "good bug format for go repo",
   272  			repo: "go",
   273  			body: "This is This is body text.\n\nFixes #1234",
   274  			want: nil,
   275  		},
   276  		{
   277  			name: "good bug format for tools repo",
   278  			repo: "tools",
   279  			body: "This is This is body text.\n\nFixes golang/go#1234",
   280  			want: nil,
   281  		},
   282  		{
   283  			name: "good bug format for vscode-go",
   284  			repo: "vscode-go",
   285  			body: "This is This is body text.\n\nFixes golang/vscode-go#1234",
   286  			want: nil,
   287  		},
   288  		{
   289  			name: "good bug format for unknown repo",
   290  			repo: "some-future-repo",
   291  			body: "This is This is body text.\n\nFixes golang/go#1234",
   292  			want: nil,
   293  		},
   294  		{
   295  			name: "allowed long lines for benchstat output",
   296  			body: "Encode/format=json-48   1.718µ ± 1%   1.423µ ± 1%  -17.20% (p=0.000 n=10)" +
   297  				strings.Repeat("Hello. ", 100) + "\n" + goodCommitBody,
   298  			want: nil,
   299  		},
   300  		{
   301  			name:  "a trival fix",
   302  			title: "fmt: fix spelling mistakes",
   303  			body:  "",
   304  			want:  nil, // we don't flag short body or missing bug reference because "spelling" is in title
   305  		},
   306  
   307  		// Now we consider some bad bodies.
   308  		// First, some basic mistakes.
   309  		{
   310  			name: "too short",
   311  			body: "Short body",
   312  			want: []string{
   313  				"body: short",
   314  				"body: no bug reference candidate found",
   315  			},
   316  		},
   317  		{
   318  			name: "missing body",
   319  			body: "",
   320  			want: []string{
   321  				"body: short",
   322  				"body: no bug reference candidate found",
   323  			},
   324  		},
   325  		{
   326  			name: "not word wrapped",
   327  			body: strings.Repeat("Hello. ", 100) + "\n" + goodCommitBody,
   328  			want: []string{
   329  				"body: long lines",
   330  			},
   331  		},
   332  		{
   333  			name: "not a sentence",
   334  			body: "This is missing a period",
   335  			want: []string{
   336  				"body: no sentence candidates found",
   337  				"body: no bug reference candidate found",
   338  			},
   339  		},
   340  		{
   341  			name: "Signed-off-by",
   342  			body: "Signed-off-by: bad\n\n" + goodCommitBody,
   343  			want: []string{
   344  				"body: contains Signed-off-by",
   345  			},
   346  		},
   347  		{
   348  			name: "PR instructions",
   349  			body: "Delete these instructions once you have read and applied them.\n",
   350  			want: []string{
   351  				"body: still contains PR instructions",
   352  				"body: no bug reference candidate found",
   353  			},
   354  		},
   355  
   356  		// Next, mistakes in the repo-specific format or location of bug references.
   357  		{
   358  			name: "bad bug format for go repo",
   359  			repo: "go",
   360  			body: "This is body text.\n\nFixes golang/go#1234",
   361  			want: []string{
   362  				"body: bug format looks incorrect",
   363  			},
   364  		},
   365  		{
   366  			name: "bad bug format for tools repo",
   367  			repo: "tools",
   368  			body: "This is body text.\n\nFixes #1234",
   369  			want: []string{
   370  				"body: bug format looks incorrect",
   371  			},
   372  		},
   373  		{
   374  			name: "bad bug format for vscode-go",
   375  			repo: "vscode-go",
   376  			body: "This is body text.\n\nFixes #1234",
   377  			want: []string{
   378  				"body: bug format looks incorrect",
   379  			},
   380  		},
   381  		{
   382  			name: "bad bug format for unknown repo",
   383  			repo: "some-future-repo",
   384  			body: "This is body text.\n\nFixes #1234",
   385  			want: []string{
   386  				"body: bug format looks incorrect",
   387  			},
   388  		},
   389  		{
   390  			name: "bad bug location",
   391  			body: "This is body text.\nFixes #1234\nAnd a final line we should not have.",
   392  			want: []string{
   393  				"body: no bug reference candidate at end",
   394  			},
   395  		},
   396  
   397  		// We next have some good bodies that are markdown-ish,
   398  		// but we allow them.
   399  		{
   400  			name:  "allowed markdown: mention regex in title",
   401  			title: "regexp: fix something",
   402  			body:  "Example `.*`.\n" + goodCommitBody,
   403  			want:  nil,
   404  		},
   405  		{
   406  			name: "allowed markdown: mention regex in body",
   407  			body: "A regex `.*`.\n" + goodCommitBody,
   408  			want: nil,
   409  		},
   410  		{
   411  			name: "allowed markdown: mention markdown",
   412  			body: "A markdown bug `and this is ok`.\n" + goodCommitBody,
   413  			want: nil,
   414  		},
   415  		{
   416  			name: "allowed markdown: might be go code",
   417  			body: " s := `raw`\n" + goodCommitBody,
   418  			want: nil,
   419  		},
   420  
   421  		// Examples of using markdown that we flag.
   422  		{
   423  			name: "markdown backticks",
   424  			body: "A variable `foo`.\n" + goodCommitBody,
   425  			want: []string{
   426  				"body: might use markdown",
   427  			},
   428  		},
   429  		{
   430  			name: "markdown block quote",
   431  			body: "Some code:\n```\nx := y\n```\n" + goodCommitBody,
   432  			want: []string{
   433  				"body: might use markdown",
   434  			},
   435  		},
   436  		{
   437  			name: "markdown link",
   438  			body: "[click here](https://example.com)\n" + goodCommitBody,
   439  			want: []string{
   440  				"body: might use markdown",
   441  			},
   442  		},
   443  	}
   444  	for _, tt := range tests {
   445  		t.Run(tt.name, func(t *testing.T) {
   446  			title := goodCommitTitle
   447  			if tt.title != "" {
   448  				title = tt.title
   449  			}
   450  			commit := commitMessage(title, tt.body, goodCommitFooters)
   451  			repo := "go"
   452  			if tt.repo != "" {
   453  				repo = tt.repo
   454  			}
   455  			change, err := ParseCommitMessage(repo, commit)
   456  			if err != nil {
   457  				t.Fatalf("ParseCommitMessage failed: %v", err)
   458  			}
   459  			results := Check(change)
   460  
   461  			var got []string
   462  			for _, r := range results {
   463  				got = append(got, r.Name)
   464  			}
   465  
   466  			if diff := cmp.Diff(tt.want, got); diff != "" {
   467  				t.Errorf("checkRules() mismatch (-want +got):\n%s", diff)
   468  			}
   469  		})
   470  	}
   471  }
   472  
   473  func TestParseCommitMessage(t *testing.T) {
   474  	tests := []struct {
   475  		name    string
   476  		repo    string
   477  		text    string
   478  		want    Change
   479  		wantErr bool
   480  	}{
   481  		// Bad examples.
   482  		{
   483  			name:    "not enough lines",
   484  			text:    "title",
   485  			want:    Change{},
   486  			wantErr: true,
   487  		},
   488  		{
   489  			name:    "second line not blank",
   490  			text:    "title\nbad line\nBody 1",
   491  			want:    Change{},
   492  			wantErr: true,
   493  		},
   494  		{
   495  			name:    "no footer",
   496  			text:    "title\n\nBody 1\n",
   497  			want:    Change{},
   498  			wantErr: true,
   499  		},
   500  		{
   501  			name:    "no footer and no body",
   502  			text:    "title\n\n\n",
   503  			want:    Change{},
   504  			wantErr: true,
   505  		},
   506  
   507  		// Good examples.
   508  		{
   509  			name: "good",
   510  			text: "title\n\nBody 1\n\nFooter: 1\n",
   511  			want: Change{
   512  				Title: "title",
   513  				Body:  "Body 1",
   514  			},
   515  			wantErr: false,
   516  		},
   517  		{
   518  			name: "good with two body lines",
   519  			text: "title\n\nBody 1\nBody 2\n\nFooter: 1\n",
   520  			want: Change{
   521  				Title: "title",
   522  				Body:  "Body 1\nBody 2",
   523  			},
   524  			wantErr: false,
   525  		},
   526  		{
   527  			name: "good with empty body",
   528  			text: "title\n\nFooter: 1\n",
   529  			want: Change{
   530  				Title: "title",
   531  				Body:  "",
   532  			},
   533  			wantErr: false,
   534  		},
   535  		{
   536  			name: "good with extra blank lines after footer",
   537  			text: "title\n\nBody 1\n\nFooter: 1\n\n\n",
   538  			want: Change{
   539  				Title: "title",
   540  				Body:  "Body 1",
   541  			},
   542  			wantErr: false,
   543  		},
   544  		{
   545  			name: "good with body line that looks like footer",
   546  			text: "title\n\nBody 1\nLink: example.com\n\nFooter: 1\n\n\n",
   547  			want: Change{
   548  				Title: "title",
   549  				Body:  "Body 1\nLink: example.com",
   550  			},
   551  			wantErr: false,
   552  		},
   553  		{
   554  			name: "allowed cherry pick in footer", // Example from CL 346093.
   555  			text: "title\n\nBody 1\n\nFooter: 1\n(cherry picked from commit ebd07b13caf35114b32e7d6783b27902af4829ce)\n",
   556  			want: Change{
   557  				Title: "title",
   558  				Body:  "Body 1",
   559  			},
   560  			wantErr: false,
   561  		},
   562  	}
   563  	for _, tt := range tests {
   564  		t.Run(tt.name, func(t *testing.T) {
   565  			got, err := ParseCommitMessage(tt.repo, tt.text)
   566  			if (err != nil) != tt.wantErr {
   567  				t.Errorf("ParseCommitMessage() error = %v, wantErr %v", err, tt.wantErr)
   568  				return
   569  			}
   570  			if diff := cmp.Diff(tt.want, got); diff != "" {
   571  				t.Errorf("checkRules() mismatch (-want +got):\n%s", diff)
   572  			}
   573  		})
   574  	}
   575  }
   576  
   577  // commitMessage helps us create valid commit messages while testing.
   578  func commitMessage(title, body, footers string) string {
   579  	return title + "\n\n" + body + "\n\n" + footers
   580  }
   581  
   582  // Some auxiliary testing variables available for use when creating commit messages.
   583  var (
   584  	goodCommitTitle   = "pkg: a title that does not trigger any rules"
   585  	goodCommitBody    = "A commit message body that does not trigger any rules.\n\nFixes #1234"
   586  	goodCommitFooters = `Change-Id: I1d8d10b142358983194ef2c389de4d9862d4ce97
   587  GitHub-Last-Rev: 6d27e1471ee5dac0323a10b46e6e64e647068ecf
   588  GitHub-Pull-Request: golang/build#69`
   589  )