Introduce testscript acceptance tests generally, and for the PR command specifically by williammartin · Pull Request #9745 · cli/cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d7465bd
Initial testscript introduction
williammartin Oct 8, 2024
5ed237c
Add pr view test script
williammartin Oct 8, 2024
3717162
Acceptance test PR checkout
williammartin Oct 8, 2024
464a69a
Use stdout2env in PR acceptance tests
williammartin Oct 8, 2024
99f7e04
Support targeting other hosts in acceptance tests
williammartin Oct 8, 2024
eb9eeb1
Use txtar extension for testscripts
williammartin Oct 11, 2024
5745eb1
Add initial acceptance test README
williammartin Oct 11, 2024
502daff
Refactor acceptance test environment handling
williammartin Oct 11, 2024
320c5ab
Note syntax highlighting support for txtar files
williammartin Oct 11, 2024
99a9b35
Acceptance test PR merge and rebase
williammartin Oct 11, 2024
ee5709a
Acceptance test PR comment
williammartin Oct 11, 2024
64f5b3c
Add Debug and Authoring sections to Acceptance README
williammartin Oct 11, 2024
dc7c66c
Add Writing Tests section to Acceptance README
williammartin Oct 11, 2024
9d569b3
Isolate acceptance env vars
williammartin Oct 11, 2024
f9b2499
Add codecoverage to Acceptance README
williammartin Oct 11, 2024
fd66555
Error if acceptance tests are targeting github or cli orgs
williammartin Oct 11, 2024
846a39d
Add go to test instructions in Acceptance README
williammartin Oct 11, 2024
0d7ec44
Validate required env vars not-empty for Acceptance tests
williammartin Oct 11, 2024
503659f
Add host recommendation to Acceptance test docs
williammartin Oct 14, 2024
fbc72fd
Suggest using legacy PAT for acceptance tests
williammartin Oct 14, 2024
4d986aa
Acceptance test PR creation with metadata
williammartin Oct 14, 2024
bfa5b6a
Support skipping Acceptance test cleanup
williammartin Oct 14, 2024
1f94cf9
Acceptance test PR list
williammartin Oct 14, 2024
c2c88b2
Fix GH_HOST / GH_ACCEPTANCE_HOST misuse
williammartin Oct 14, 2024
f3589b2
Add VSCode extension links to Acceptance README
williammartin Oct 14, 2024
b095d6b
Add link to testscript pkg documentation
williammartin Oct 14, 2024
f7b279d
Correct testscript description in Acceptance readme
williammartin Oct 14, 2024
5e02326
Document sharedCmds func in acceptance tests
williammartin Oct 14, 2024
2a0be61
Ensure pr create with metadata has assignment
williammartin Oct 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions acceptance/README.md
171 changes: 171 additions & 0 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//go:build acceptance

package acceptance_test

import (
"fmt"
"os"
"path"
"strings"
"testing"

"math/rand"

"github.com/cli/cli/v2/internal/ghcmd"
"github.com/rogpeppe/go-internal/testscript"
)

func ghMain() int {
return int(ghcmd.Main())
}

func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"gh": ghMain,
}))
}

func TestPullRequests(t *testing.T) {
var tsEnv testScriptEnv
if err := tsEnv.fromEnv(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

testscript.Run(t, testScriptParamsFor(tsEnv, "pr"))
}
Comment thread
jtmcg marked this conversation as resolved.

func testScriptParamsFor(tsEnv testScriptEnv, dir string) testscript.Params {
return testscript.Params{
Dir: path.Join("testdata", dir),
Files: []string{},
Setup: sharedSetup(tsEnv),
Cmds: sharedCmds(tsEnv),
RequireExplicitExec: true,
RequireUniqueNames: true,
TestWork: tsEnv.preserveWorkDir,
}
}

func sharedSetup(tsEnv testScriptEnv) func(ts *testscript.Env) error {
return func(ts *testscript.Env) error {
scriptName, ok := extractScriptName(ts.Vars)
if !ok {
ts.T().Fatal("script name not found")
}
ts.Setenv("SCRIPT_NAME", scriptName)
ts.Setenv("HOME", ts.Cd)
ts.Setenv("GH_CONFIG_DIR", ts.Cd)

ts.Setenv("GH_HOST", tsEnv.host)
ts.Setenv("ORG", tsEnv.org)
ts.Setenv("GH_TOKEN", tsEnv.token)

ts.Setenv("RANDOM_STRING", randomString(10))
return nil
}
}

// sharedCmds defines a collection of custom testscript commands for our use.
func sharedCmds(tsEnv testScriptEnv) map[string]func(ts *testscript.TestScript, neg bool, args []string) {
Comment thread
williammartin marked this conversation as resolved.
return map[string]func(ts *testscript.TestScript, neg bool, args []string){
"defer": func(ts *testscript.TestScript, neg bool, args []string) {
if neg {
ts.Fatalf("unsupported: ! defer")
}

if tsEnv.skipDefer {
return
}

ts.Defer(func() {
ts.Check(ts.Exec(args[0], args[1:]...))
})
},
Comment thread
williammartin marked this conversation as resolved.
"stdout2env": func(ts *testscript.TestScript, neg bool, args []string) {
if neg {
ts.Fatalf("unsupported: ! stdout2env")
}
if len(args) != 1 {
ts.Fatalf("usage: stdout2env name")
}

ts.Setenv(args[0], strings.TrimRight(ts.ReadFile("stdout"), "\n"))
},
}
}

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func randomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

func extractScriptName(vars []string) (string, bool) {
for _, kv := range vars {
if strings.HasPrefix(kv, "WORK=") {
v := strings.Split(kv, "=")[1]
return strings.CutPrefix(path.Base(v), "script-")
}
}
return "", false
}

type missingEnvError struct {
missingEnvs []string
}

func (e missingEnvError) Error() string {
return fmt.Sprintf("environment variables %s must be set and non-empty", strings.Join(e.missingEnvs, ", "))
}

type testScriptEnv struct {
host string
org string
token string

skipDefer bool
preserveWorkDir bool
}

func (e *testScriptEnv) fromEnv() error {
envMap := map[string]string{}

requiredEnvVars := []string{
"GH_ACCEPTANCE_HOST",
"GH_ACCEPTANCE_ORG",
"GH_ACCEPTANCE_TOKEN",
}

var missingEnvs []string
for _, key := range requiredEnvVars {
val, ok := os.LookupEnv(key)
if val == "" || !ok {
missingEnvs = append(missingEnvs, key)
continue
}

envMap[key] = val
}

if len(missingEnvs) > 0 {
return missingEnvError{missingEnvs: missingEnvs}
}

if envMap["GH_ACCEPTANCE_ORG"] == "github" || envMap["GH_ACCEPTANCE_ORG"] == "cli" {
return fmt.Errorf("GH_ACCEPTANCE_ORG cannot be 'github' or 'cli'")
}

e.host = envMap["GH_ACCEPTANCE_HOST"]
e.org = envMap["GH_ACCEPTANCE_ORG"]
e.token = envMap["GH_ACCEPTANCE_TOKEN"]

e.preserveWorkDir = os.Getenv("GH_ACCEPTANCE_PRESERVE_WORK_DIR") == "true"
e.skipDefer = os.Getenv("GH_ACCEPTANCE_SKIP_DEFER") == "true"

return nil
}
30 changes: 30 additions & 0 deletions acceptance/testdata/pr/pr-checkout.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Use gh as a credential helper
exec gh auth setup-git

# Create a repository with a file so it has a default branch
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private

# Defer repo cleanup
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING

# Clone the repo
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING

# Prepare a branch to PR
cd $SCRIPT_NAME-$RANDOM_STRING
exec git checkout -b feature-branch
exec git commit --allow-empty -m 'Empty Commit'
exec git push -u origin feature-branch

# Create the PR
exec gh pr create --title 'Feature Title' --body 'Feature Body'
stdout2env PR_URL

# Remove the local branch
exec git checkout main
exec git branch -D feature-branch
stdout 'Deleted branch feature-branch'

# Checkout the PR
exec gh pr checkout $PR_URL
stderr 'Switched to a new branch ''feature-branch'''
28 changes: 28 additions & 0 deletions acceptance/testdata/pr/pr-comment.txtar
Loading