mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add release candidate (RC) support to release tooling (#23600)
This adds full RC release support to the release scripts and GitHub Actions workflow. Previously, the tooling only supported stable and mainline releases with strict vMAJOR.MINOR.PATCH semver tags. Changes: - scripts/releaser/version.go: Add Pre field to version struct for prerelease suffixes (e.g. "rc.0"), update regex, parsing, String(), comparison methods, and add IsRC()/rcNumber() helpers. - scripts/releaser/release.go: Detect RC branches (release/X.Y-rc.N), suggest RC version numbers, auto-set "rc" channel (skipping stable/mainline prompt), add RC advisory to release notes, skip docs update for RC releases. - .github/workflows/release.yaml: Add "rc" channel option, fix branch derivation for RC tags (v2.32.0-rc.0 -> release/2.32-rc.0 instead of broken release/2.32.0-rc), skip homebrew/winget/package publishing for RC releases. - scripts/release/publish.sh: Add --rc flag, pass --prerelease to gh release create for RC releases. - scripts/releaser/version_test.go: Add comprehensive unit tests for version parsing, string formatting, IsRC, rcNumber, GreaterThan, and Equal with RC versions. <!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. -->
This commit is contained in:
@@ -34,11 +34,12 @@ if [[ "${CI:-}" == "" ]]; then
|
||||
fi
|
||||
|
||||
stable=0
|
||||
rc=0
|
||||
version=""
|
||||
release_notes_file=""
|
||||
dry_run=0
|
||||
|
||||
args="$(getopt -o "" -l stable,version:,release-notes-file:,dry-run -- "$@")"
|
||||
args="$(getopt -o "" -l stable,rc,version:,release-notes-file:,dry-run -- "$@")"
|
||||
eval set -- "$args"
|
||||
while true; do
|
||||
case "$1" in
|
||||
@@ -46,6 +47,10 @@ while true; do
|
||||
stable=1
|
||||
shift
|
||||
;;
|
||||
--rc)
|
||||
rc=1
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
version="$2"
|
||||
shift 2
|
||||
@@ -68,6 +73,10 @@ while true; do
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$stable" == 1 ]] && [[ "$rc" == 1 ]]; then
|
||||
error "Cannot specify both --stable and --rc"
|
||||
fi
|
||||
|
||||
# Check dependencies
|
||||
dependencies gh
|
||||
|
||||
@@ -162,6 +171,11 @@ if [[ "$stable" == 1 ]]; then
|
||||
latest=true
|
||||
fi
|
||||
|
||||
prerelease_flag=()
|
||||
if [[ "$rc" == 1 ]]; then
|
||||
prerelease_flag=(--prerelease)
|
||||
fi
|
||||
|
||||
target_commitish=main # This is the default.
|
||||
# Skip during dry-runs
|
||||
if [[ "$dry_run" == 0 ]]; then
|
||||
@@ -176,6 +190,7 @@ fi
|
||||
true |
|
||||
maybedryrun "$dry_run" gh release create \
|
||||
--latest="$latest" \
|
||||
"${prerelease_flag[@]}" \
|
||||
--title "$new_tag" \
|
||||
--target "$target_commitish" \
|
||||
--notes-file "$release_notes_file" \
|
||||
|
||||
+78
-34
@@ -30,9 +30,11 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
}
|
||||
|
||||
var latestMainline *version
|
||||
if len(allTags) > 0 {
|
||||
v := allTags[0]
|
||||
latestMainline = &v
|
||||
for _, t := range allTags {
|
||||
if t.Pre == "" {
|
||||
latestMainline = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
stableMinor := -1
|
||||
@@ -41,7 +43,7 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
stableMinor = latestMainline.Minor - 1
|
||||
// Find highest tag in the stable minor series.
|
||||
for _, t := range allTags {
|
||||
if t.Major == latestMainline.Major && t.Minor == stableMinor {
|
||||
if t.Major == latestMainline.Major && t.Minor == stableMinor && t.Pre == "" {
|
||||
latestStableStr = t.String()
|
||||
break
|
||||
}
|
||||
@@ -66,15 +68,17 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
return xerrors.Errorf("detecting branch: %w", err)
|
||||
}
|
||||
|
||||
branchRe := regexp.MustCompile(`^release/(\d+)\.(\d+)$`)
|
||||
// Match standard release branches (release/2.32) and RC
|
||||
// branches (release/2.32-rc.0).
|
||||
branchRe := regexp.MustCompile(`^release/(\d+)\.(\d+)(?:-rc\.(\d+))?$`)
|
||||
m := branchRe.FindStringSubmatch(currentBranch)
|
||||
if m == nil {
|
||||
warnf(w, "Current branch %q is not a release branch (release/X.Y).", currentBranch)
|
||||
warnf(w, "Current branch %q is not a release branch (release/X.Y or release/X.Y-rc.N).", currentBranch)
|
||||
branchInput, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Enter the release branch to use (e.g. release/2.21)",
|
||||
Text: "Enter the release branch to use (e.g. release/2.21 or release/2.21-rc.0)",
|
||||
Validate: func(s string) error {
|
||||
if !branchRe.MatchString(s) {
|
||||
return xerrors.New("must be in format release/X.Y (e.g. release/2.21)")
|
||||
return xerrors.New("must be in format release/X.Y or release/X.Y-rc.N (e.g. release/2.21 or release/2.21-rc.0)")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -87,6 +91,10 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
}
|
||||
branchMajor, _ := strconv.Atoi(m[1])
|
||||
branchMinor, _ := strconv.Atoi(m[2])
|
||||
branchRC := -1 // -1 means not an RC branch.
|
||||
if m[3] != "" {
|
||||
branchRC, _ = strconv.Atoi(m[3])
|
||||
}
|
||||
successf(w, "Using release branch: %s", currentBranch)
|
||||
|
||||
// --- Fetch & sync check ---
|
||||
@@ -134,9 +142,27 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
if prevVersion == nil {
|
||||
infof(w, "No previous release tag found on this branch.")
|
||||
suggested = version{Major: branchMajor, Minor: branchMinor, Patch: 0}
|
||||
if branchRC >= 0 {
|
||||
suggested.Pre = fmt.Sprintf("rc.%d", branchRC)
|
||||
}
|
||||
} else {
|
||||
infof(w, "Previous release tag: %s", prevVersion.String())
|
||||
suggested = version{Major: prevVersion.Major, Minor: prevVersion.Minor, Patch: prevVersion.Patch + 1}
|
||||
if branchRC >= 0 {
|
||||
// On an RC branch, suggest the next RC for
|
||||
// the same base version.
|
||||
nextRC := 0
|
||||
if prevVersion.IsRC() {
|
||||
nextRC = prevVersion.rcNumber() + 1
|
||||
}
|
||||
suggested = version{
|
||||
Major: prevVersion.Major,
|
||||
Minor: prevVersion.Minor,
|
||||
Patch: prevVersion.Patch,
|
||||
Pre: fmt.Sprintf("rc.%d", nextRC),
|
||||
}
|
||||
} else {
|
||||
suggested = version{Major: prevVersion.Major, Minor: prevVersion.Minor, Patch: prevVersion.Patch + 1}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(w)
|
||||
@@ -147,7 +173,7 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
Default: suggested.String(),
|
||||
Validate: func(s string) error {
|
||||
if _, ok := parseVersion(s); !ok {
|
||||
return xerrors.New("must be in format vMAJOR.MINOR.PATCH (e.g. v2.31.1)")
|
||||
return xerrors.New("must be in format vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rc.N (e.g. v2.31.1 or v2.31.0-rc.0)")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -303,29 +329,36 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
// --- Channel selection ---
|
||||
// This is done before release notes generation because the
|
||||
// notes format differs between mainline and stable channels.
|
||||
channelDefault := cliui.ConfirmNo
|
||||
channelHint := ""
|
||||
if newVersion.Minor == stableMinor {
|
||||
channelDefault = cliui.ConfirmYes
|
||||
channelHint = " (this looks like a stable release)"
|
||||
}
|
||||
|
||||
// RC releases are always on the "rc" channel and skip the
|
||||
// stable/mainline prompt.
|
||||
channel := "mainline"
|
||||
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: fmt.Sprintf("Mark this as the latest stable release on GitHub?%s", channelHint),
|
||||
Default: channelDefault,
|
||||
IsConfirm: true,
|
||||
})
|
||||
if err == nil {
|
||||
channel = "stable"
|
||||
} else if !errors.Is(err, cliui.ErrCanceled) {
|
||||
return err
|
||||
}
|
||||
|
||||
if channel == "stable" {
|
||||
infof(w, "Channel: stable (will be marked as GitHub Latest).")
|
||||
if newVersion.IsRC() {
|
||||
channel = "rc"
|
||||
infof(w, "Channel: rc (release candidate, will be marked as prerelease on GitHub).")
|
||||
} else {
|
||||
infof(w, "Channel: mainline (will be marked as prerelease).")
|
||||
channelDefault := cliui.ConfirmNo
|
||||
channelHint := ""
|
||||
if newVersion.Minor == stableMinor {
|
||||
channelDefault = cliui.ConfirmYes
|
||||
channelHint = " (this looks like a stable release)"
|
||||
}
|
||||
|
||||
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: fmt.Sprintf("Mark this as the latest stable release on GitHub?%s", channelHint),
|
||||
Default: channelDefault,
|
||||
IsConfirm: true,
|
||||
})
|
||||
if err == nil {
|
||||
channel = "stable"
|
||||
} else if !errors.Is(err, cliui.ErrCanceled) {
|
||||
return err
|
||||
}
|
||||
|
||||
if channel == "stable" {
|
||||
infof(w, "Channel: stable (will be marked as GitHub Latest).")
|
||||
} else {
|
||||
infof(w, "Channel: mainline (will be marked as prerelease).")
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
@@ -408,12 +441,17 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
// scripts/release/generate_release_notes.sh.
|
||||
var notes strings.Builder
|
||||
|
||||
// Stable since header or mainline blurb.
|
||||
// Stable since header, mainline blurb, or RC advisory.
|
||||
if channel == "stable" {
|
||||
fmt.Fprintf(¬es, "> ## Stable (since %s)\n\n", time.Now().Format("January 02, 2006"))
|
||||
}
|
||||
fmt.Fprintln(¬es, "## Changelog")
|
||||
if channel == "mainline" {
|
||||
switch channel {
|
||||
case "rc":
|
||||
fmt.Fprintln(¬es)
|
||||
fmt.Fprintln(¬es, "> [!NOTE]")
|
||||
fmt.Fprintln(¬es, "> This is a **release candidate** (RC) for testing purposes. It is not recommended for production use. Please report any issues you encounter. Learn more about our [Release Schedule](https://coder.com/docs/install/releases).")
|
||||
case "mainline":
|
||||
fmt.Fprintln(¬es)
|
||||
fmt.Fprintln(¬es, "> [!NOTE]")
|
||||
fmt.Fprintln(¬es, "> This is a mainline Coder release. We advise enterprise customers without a staging environment to install our [latest stable release](https://github.com/coder/coder/releases/latest) while we refine this version. Learn more about our [Release Schedule](https://coder.com/docs/install/releases).")
|
||||
@@ -576,7 +614,13 @@ func runRelease(ctx context.Context, inv *serpent.Invocation, executor ReleaseEx
|
||||
successf(w, "Release workflow triggered!")
|
||||
|
||||
// --- Update release docs ---
|
||||
promptAndUpdateDocs(inv, newVersion, channel, dryRun)
|
||||
// RC releases skip docs updates (calendar, helm versions, etc.)
|
||||
// since they are not production releases.
|
||||
if newVersion.IsRC() {
|
||||
infof(w, "Skipping docs update for release candidate.")
|
||||
} else {
|
||||
promptAndUpdateDocs(inv, newVersion, channel, dryRun)
|
||||
}
|
||||
|
||||
fmt.Fprintln(w)
|
||||
successf(w, "Done! 🎉")
|
||||
|
||||
@@ -7,14 +7,16 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// version holds a parsed semver version.
|
||||
// version holds a parsed semver version with optional prerelease
|
||||
// suffix (e.g. "rc.0").
|
||||
type version struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
Pre string // e.g. "rc.0", "" for stable releases.
|
||||
}
|
||||
|
||||
var semverRe = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`)
|
||||
var semverRe = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)(-(.+))?$`)
|
||||
|
||||
func parseVersion(s string) (version, bool) {
|
||||
m := semverRe.FindStringSubmatch(s)
|
||||
@@ -24,13 +26,35 @@ func parseVersion(s string) (version, bool) {
|
||||
maj, _ := strconv.Atoi(m[1])
|
||||
mnr, _ := strconv.Atoi(m[2])
|
||||
pat, _ := strconv.Atoi(m[3])
|
||||
return version{Major: maj, Minor: mnr, Patch: pat}, true
|
||||
return version{Major: maj, Minor: mnr, Patch: pat, Pre: m[5]}, true
|
||||
}
|
||||
|
||||
func (v version) String() string {
|
||||
if v.Pre != "" {
|
||||
return fmt.Sprintf("v%d.%d.%d-%s", v.Major, v.Minor, v.Patch, v.Pre)
|
||||
}
|
||||
return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
}
|
||||
|
||||
// IsRC returns true when the version has a prerelease suffix starting
|
||||
// with "rc." (e.g. "rc.0", "rc.1").
|
||||
func (v version) IsRC() bool {
|
||||
return strings.HasPrefix(v.Pre, "rc.")
|
||||
}
|
||||
|
||||
// rcNumber returns the numeric RC identifier (e.g. 0 for "rc.0").
|
||||
// It returns -1 when the version is not an RC.
|
||||
func (v version) rcNumber() int {
|
||||
if !v.IsRC() {
|
||||
return -1
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(v.Pre, "rc."))
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (v version) GreaterThan(b version) bool {
|
||||
if v.Major != b.Major {
|
||||
return v.Major > b.Major
|
||||
@@ -38,11 +62,27 @@ func (v version) GreaterThan(b version) bool {
|
||||
if v.Minor != b.Minor {
|
||||
return v.Minor > b.Minor
|
||||
}
|
||||
return v.Patch > b.Patch
|
||||
if v.Patch != b.Patch {
|
||||
return v.Patch > b.Patch
|
||||
}
|
||||
// A release without prerelease suffix is greater than one
|
||||
// with a prerelease suffix (v2.32.0 > v2.32.0-rc.0).
|
||||
if v.Pre == "" && b.Pre != "" {
|
||||
return true
|
||||
}
|
||||
if v.Pre != "" && b.Pre == "" {
|
||||
return false
|
||||
}
|
||||
// Both have prerelease: compare numerically for RC versions.
|
||||
if v.IsRC() && b.IsRC() {
|
||||
return v.rcNumber() > b.rcNumber()
|
||||
}
|
||||
// Fallback for non-RC prerelease strings.
|
||||
return v.Pre > b.Pre
|
||||
}
|
||||
|
||||
func (v version) Equal(b version) bool {
|
||||
return v.Major == b.Major && v.Minor == b.Minor && v.Patch == b.Patch
|
||||
return v.Major == b.Major && v.Minor == b.Minor && v.Patch == b.Patch && v.Pre == b.Pre
|
||||
}
|
||||
|
||||
// allSemverTags returns all semver tags sorted descending.
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
ok bool
|
||||
want version
|
||||
}{
|
||||
{"v2.32.0", true, version{2, 32, 0, ""}},
|
||||
{"v1.0.0", true, version{1, 0, 0, ""}},
|
||||
{"v2.32.0-rc.0", true, version{2, 32, 0, "rc.0"}},
|
||||
{"v2.32.0-rc.1", true, version{2, 32, 0, "rc.1"}},
|
||||
{"v2.32.1-beta.3", true, version{2, 32, 1, "beta.3"}},
|
||||
{"2.32.0", false, version{}},
|
||||
{"v2.32", false, version{}},
|
||||
{"vx.y.z", false, version{}},
|
||||
{"", false, version{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, ok := parseVersion(tt.input)
|
||||
if ok != tt.ok {
|
||||
t.Fatalf("parseVersion(%q) ok = %v, want %v", tt.input, ok, tt.ok)
|
||||
}
|
||||
if ok && got != tt.want {
|
||||
t.Fatalf("parseVersion(%q) = %+v, want %+v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
v version
|
||||
want string
|
||||
}{
|
||||
{version{2, 32, 0, ""}, "v2.32.0"},
|
||||
{version{2, 32, 0, "rc.0"}, "v2.32.0-rc.0"},
|
||||
{version{1, 0, 0, "beta.1"}, "v1.0.0-beta.1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tt.v.String(); got != tt.want {
|
||||
t.Fatalf("String() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionIsRC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
v version
|
||||
want bool
|
||||
}{
|
||||
{version{2, 32, 0, "rc.0"}, true},
|
||||
{version{2, 32, 0, "rc.1"}, true},
|
||||
{version{2, 32, 0, ""}, false},
|
||||
{version{2, 32, 0, "beta.1"}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.v.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tt.v.IsRC(); got != tt.want {
|
||||
t.Fatalf("IsRC() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionRCNumber(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
v version
|
||||
want int
|
||||
}{
|
||||
{version{2, 32, 0, "rc.0"}, 0},
|
||||
{version{2, 32, 0, "rc.5"}, 5},
|
||||
{version{2, 32, 0, ""}, -1},
|
||||
{version{2, 32, 0, "beta.1"}, -1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.v.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tt.v.rcNumber(); got != tt.want {
|
||||
t.Fatalf("rcNumber() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionGreaterThan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
a, b version
|
||||
want bool
|
||||
}{
|
||||
// Standard comparisons.
|
||||
{version{2, 32, 1, ""}, version{2, 32, 0, ""}, true},
|
||||
{version{2, 32, 0, ""}, version{2, 32, 1, ""}, false},
|
||||
{version{2, 33, 0, ""}, version{2, 32, 0, ""}, true},
|
||||
{version{3, 0, 0, ""}, version{2, 99, 99, ""}, true},
|
||||
|
||||
// Release > RC with same base version.
|
||||
{version{2, 32, 0, ""}, version{2, 32, 0, "rc.0"}, true},
|
||||
{version{2, 32, 0, "rc.0"}, version{2, 32, 0, ""}, false},
|
||||
|
||||
// RC ordering.
|
||||
{version{2, 32, 0, "rc.1"}, version{2, 32, 0, "rc.0"}, true},
|
||||
{version{2, 32, 0, "rc.0"}, version{2, 32, 0, "rc.1"}, false},
|
||||
{version{2, 32, 0, "rc.10"}, version{2, 32, 0, "rc.9"}, true},
|
||||
{version{2, 32, 0, "rc.9"}, version{2, 32, 0, "rc.10"}, false},
|
||||
|
||||
// Equal.
|
||||
{version{2, 32, 0, ""}, version{2, 32, 0, ""}, false},
|
||||
{version{2, 32, 0, "rc.0"}, version{2, 32, 0, "rc.0"}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.a.String()+"_gt_"+tt.b.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tt.a.GreaterThan(tt.b); got != tt.want {
|
||||
t.Fatalf("%s.GreaterThan(%s) = %v, want %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionEqual(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
a, b version
|
||||
want bool
|
||||
}{
|
||||
{version{2, 32, 0, ""}, version{2, 32, 0, ""}, true},
|
||||
{version{2, 32, 0, "rc.0"}, version{2, 32, 0, "rc.0"}, true},
|
||||
{version{2, 32, 0, ""}, version{2, 32, 0, "rc.0"}, false},
|
||||
{version{2, 32, 0, "rc.0"}, version{2, 32, 0, "rc.1"}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.a.String()+"_eq_"+tt.b.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tt.a.Equal(tt.b); got != tt.want {
|
||||
t.Fatalf("%s.Equal(%s) = %v, want %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user