blob: 23560d9a8d52178181485b05e26afd67b5a9b412 [file]
// Copyright 2026 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
package pw_ghish
import (
"reflect"
"strings"
"testing"
)
func TestFindGitHubIssueRefs(t *testing.T) {
tests := []struct {
name string
text string
want []GitHubIssueRef
}{
{
// The canonical failure: an agent with `gh` muscle memory writes
// GitHub's closing keyword into the commit body. On Gerrit this is
// inert prose -- the bug is never linked and never closed.
name: "closing keyword",
text: "pw_foo: Rework retry\n\nRework the retry loop.\n\nFixes #456\n",
want: []GitHubIssueRef{{Keyword: "Fixes", Number: "456", Match: "Fixes #456"}},
},
{
name: "every GitHub closing keyword is recognized",
text: "s: t\n\nclose #1\ncloses #2\nclosed #3\nfix #4\nfixes #5\nfixed #6\n" +
"resolve #7\nresolves #8\nresolved #9\n",
want: []GitHubIssueRef{
{Keyword: "close", Number: "1", Match: "close #1"},
{Keyword: "closes", Number: "2", Match: "closes #2"},
{Keyword: "closed", Number: "3", Match: "closed #3"},
{Keyword: "fix", Number: "4", Match: "fix #4"},
{Keyword: "fixes", Number: "5", Match: "fixes #5"},
{Keyword: "fixed", Number: "6", Match: "fixed #6"},
{Keyword: "resolve", Number: "7", Match: "resolve #7"},
{Keyword: "resolves", Number: "8", Match: "resolves #8"},
{Keyword: "resolved", Number: "9", Match: "resolved #9"},
},
},
{
// Someone reaching for a Gerrit trailer but supplying a GitHub
// number. Still wrong, still worth catching.
name: "trailer-shaped line with a GitHub number",
text: "s: t\n\nFixed: #456\n",
want: []GitHubIssueRef{{Keyword: "Fixed", Number: "456", Match: "Fixed: #456"}},
},
{
// The whole point of the feature: a correct Gerrit trailer must
// never be flagged.
name: "correct Gerrit trailers are not flagged",
text: "s: t\n\nBody.\n\nBug: b/12345\nFixed: b/67890\n",
want: nil,
},
{
// Deliberately NOT flagged. A bare "#456" is far more likely to be
// ordinary prose than an issue link, and a false refusal blocks a
// user who did nothing wrong.
name: "bare issue number is not flagged",
text: "s: t\n\nSee #456 for context, and item #3 of the design doc.\n",
want: nil,
},
{
// Indented text is a code sample, matching how trailers are
// detected elsewhere. Refusing to edit a commit because its
// example output mentions "Fixes #1" would be absurd.
name: "indented code sample is not flagged",
text: "s: t\n\nExample GitHub workflow:\n\n Fixes #456\n",
want: nil,
},
{
name: "word boundaries are respected",
text: "s: t\n\nThe prefixes #456 and suffixes #789 are unrelated.\n",
want: nil,
},
{
name: "empty text",
text: "",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FindGitHubIssueRefs(tt.text)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FindGitHubIssueRefs()\n got: %#v\nwant: %#v", got, tt.want)
}
})
}
}
// TestGitHubIssueSyntaxError checks the diagnostic against the project's four
// pillars: what happened, why it matters, what to type instead, and how to
// find the missing information.
func TestGitHubIssueSyntaxError(t *testing.T) {
refs := FindGitHubIssueRefs("s: t\n\nFixes #456\n")
if len(refs) != 1 {
t.Fatalf("setup: expected 1 ref, got %d", len(refs))
}
err := GitHubIssueSyntaxError(refs, "--body")
if err == nil {
t.Fatal("Expected an error, got nil")
}
msg := err.Error()
for _, want := range []string{
"Fixes #456", // pillar 1: quote what was found
"--body", // pillar 1: say where
"Gerrit", // pillar 2: why it does not work
"Fixed: b/", // pillar 3: the trailer to use instead
"Bug: b/", // pillar 3: the non-closing variant
"--fixed", // pillar 3: the flag that writes it for you
"--bug",
} {
if !strings.Contains(msg, want) {
t.Errorf("Error message is missing %q:\n%s", want, msg)
}
}
// Pillar 2, the part that justifies refusing rather than translating: the
// number spaces are different, so b/456 would be an unrelated bug.
if !strings.Contains(msg, "b/456") {
t.Errorf("Error should explain that b/456 would be a different bug:\n%s", msg)
}
}