Merge pull request #79 from github/raffo/delete-codespaces
Add code and command to delete unused codespaces
This commit is contained in:
commit
92d0abd6ab
8 changed files with 810 additions and 183 deletions
|
|
@ -19,11 +19,14 @@ import (
|
|||
var errNoCodespaces = errors.New("you have no codespaces")
|
||||
|
||||
func chooseCodespace(ctx context.Context, apiClient *api.API, user *api.User) (*api.Codespace, error) {
|
||||
codespaces, err := apiClient.ListCodespaces(ctx, user)
|
||||
codespaces, err := apiClient.ListCodespaces(ctx, user.Login)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting codespaces: %w", err)
|
||||
}
|
||||
return chooseCodespaceFromList(ctx, codespaces)
|
||||
}
|
||||
|
||||
func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (*api.Codespace, error) {
|
||||
if len(codespaces) == 0 {
|
||||
return nil, errNoCodespaces
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,216 +6,179 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/github/ghcs/cmd/ghcs/output"
|
||||
"github.com/github/ghcs/internal/api"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func newDeleteCmd() *cobra.Command {
|
||||
var (
|
||||
codespace string
|
||||
allCodespaces bool
|
||||
repo string
|
||||
force bool
|
||||
)
|
||||
type deleteOptions struct {
|
||||
deleteAll bool
|
||||
skipConfirm bool
|
||||
codespaceName string
|
||||
repoFilter string
|
||||
keepDays uint16
|
||||
|
||||
isInteractive bool
|
||||
now func() time.Time
|
||||
apiClient apiClient
|
||||
prompter prompter
|
||||
}
|
||||
|
||||
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_prompter.go . prompter
|
||||
type prompter interface {
|
||||
Confirm(message string) (bool, error)
|
||||
}
|
||||
|
||||
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
|
||||
type apiClient interface {
|
||||
GetUser(ctx context.Context) (*api.User, error)
|
||||
GetCodespaceToken(ctx context.Context, user, name string) (string, error)
|
||||
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
|
||||
ListCodespaces(ctx context.Context, user string) ([]*api.Codespace, error)
|
||||
DeleteCodespace(ctx context.Context, user, name string) error
|
||||
}
|
||||
|
||||
func newDeleteCmd() *cobra.Command {
|
||||
opts := deleteOptions{
|
||||
isInteractive: hasTTY,
|
||||
now: time.Now,
|
||||
apiClient: api.New(os.Getenv("GITHUB_TOKEN")),
|
||||
prompter: &surveyPrompter{},
|
||||
}
|
||||
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, false)
|
||||
deleteCmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a codespace",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("delete: unexpected positional arguments")
|
||||
}
|
||||
switch {
|
||||
case allCodespaces && repo != "":
|
||||
if opts.deleteAll && opts.repoFilter != "" {
|
||||
return errors.New("both --all and --repo is not supported")
|
||||
case allCodespaces:
|
||||
return deleteAll(log, force)
|
||||
case repo != "":
|
||||
return deleteByRepo(log, repo, force)
|
||||
default:
|
||||
return delete_(log, codespace, force)
|
||||
}
|
||||
log := output.NewLogger(cmd.OutOrStdout(), cmd.ErrOrStderr(), !opts.isInteractive)
|
||||
return delete(context.Background(), log, opts)
|
||||
},
|
||||
}
|
||||
|
||||
deleteCmd.Flags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace")
|
||||
deleteCmd.Flags().BoolVar(&allCodespaces, "all", false, "Delete all codespaces")
|
||||
deleteCmd.Flags().StringVarP(&repo, "repo", "r", "", "Delete all codespaces for a repository")
|
||||
deleteCmd.Flags().BoolVarP(&force, "force", "f", false, "Delete codespaces with unsaved changes without confirmation")
|
||||
deleteCmd.Flags().StringVarP(&opts.codespaceName, "codespace", "c", "", "Name of the codespace")
|
||||
deleteCmd.Flags().BoolVar(&opts.deleteAll, "all", false, "Delete all codespaces")
|
||||
deleteCmd.Flags().StringVarP(&opts.repoFilter, "repo", "r", "", "Delete codespaces for a `repository`")
|
||||
deleteCmd.Flags().BoolVarP(&opts.skipConfirm, "force", "f", false, "Skip confirmation for codespaces that contain unsaved changes")
|
||||
deleteCmd.Flags().Uint16Var(&opts.keepDays, "days", 0, "Delete codespaces older than `N` days")
|
||||
|
||||
return deleteCmd
|
||||
}
|
||||
|
||||
func delete_(log *output.Logger, codespaceName string, force bool) error {
|
||||
apiClient := api.New(GithubToken)
|
||||
ctx := context.Background()
|
||||
|
||||
user, err := apiClient.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting user: %w", err)
|
||||
}
|
||||
|
||||
codespace, token, err := getOrChooseCodespace(ctx, apiClient, user, codespaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get or choose codespace: %w", err)
|
||||
}
|
||||
|
||||
confirmed, err := confirmDeletion(codespace, force)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deletion could not be confirmed: %w", err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := apiClient.DeleteCodespace(ctx, user, token, codespace.Name); err != nil {
|
||||
return fmt.Errorf("error deleting codespace: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Codespace deleted.")
|
||||
|
||||
return list(&listOptions{})
|
||||
type logger interface {
|
||||
Errorf(format string, v ...interface{}) (int, error)
|
||||
}
|
||||
|
||||
func deleteAll(log *output.Logger, force bool) error {
|
||||
apiClient := api.New(GithubToken)
|
||||
ctx := context.Background()
|
||||
|
||||
user, err := apiClient.GetUser(ctx)
|
||||
func delete(ctx context.Context, log logger, opts deleteOptions) error {
|
||||
user, err := opts.apiClient.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting user: %w", err)
|
||||
}
|
||||
|
||||
codespaces, err := apiClient.ListCodespaces(ctx, user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespaces: %w", err)
|
||||
}
|
||||
|
||||
for _, c := range codespaces {
|
||||
confirmed, err := confirmDeletion(c, force)
|
||||
var codespaces []*api.Codespace
|
||||
nameFilter := opts.codespaceName
|
||||
if nameFilter == "" {
|
||||
codespaces, err = opts.apiClient.ListCodespaces(ctx, user.Login)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deletion could not be confirmed: %w", err)
|
||||
return fmt.Errorf("error getting codespaces: %w", err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
continue
|
||||
}
|
||||
|
||||
token, err := apiClient.GetCodespaceToken(ctx, user.Login, c.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespace token: %w", err)
|
||||
}
|
||||
|
||||
if err := apiClient.DeleteCodespace(ctx, user, token, c.Name); err != nil {
|
||||
return fmt.Errorf("error deleting codespace: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Codespace deleted: %s\n", c.Name)
|
||||
}
|
||||
|
||||
return list(&listOptions{})
|
||||
}
|
||||
|
||||
func deleteByRepo(log *output.Logger, repo string, force bool) error {
|
||||
apiClient := api.New(GithubToken)
|
||||
ctx := context.Background()
|
||||
|
||||
user, err := apiClient.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting user: %w", err)
|
||||
}
|
||||
|
||||
codespaces, err := apiClient.ListCodespaces(ctx, user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespaces: %w", err)
|
||||
}
|
||||
|
||||
delete := func(name string) error {
|
||||
token, err := apiClient.GetCodespaceToken(ctx, user.Login, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespace token: %w", err)
|
||||
}
|
||||
|
||||
if err := apiClient.DeleteCodespace(ctx, user, token, name); err != nil {
|
||||
return fmt.Errorf("error deleting codespace: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform deletions in parallel, for performance,
|
||||
// and to ensure all are attempted even if any one fails.
|
||||
var (
|
||||
found bool
|
||||
mu sync.Mutex // guards errs, logger
|
||||
errs []error
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
for _, c := range codespaces {
|
||||
if !strings.EqualFold(c.RepositoryNWO, repo) {
|
||||
continue
|
||||
}
|
||||
|
||||
confirmed, err := confirmDeletion(c, force)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
errs = append(errs, fmt.Errorf("deletion could not be confirmed: %w", err))
|
||||
mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
continue
|
||||
}
|
||||
|
||||
found = true
|
||||
c := c
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := delete(c.Name)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !opts.deleteAll && opts.repoFilter == "" {
|
||||
c, err := chooseCodespaceFromList(ctx, codespaces)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
log.Printf("Codespace deleted: %s\n", c.Name)
|
||||
return fmt.Errorf("error choosing codespace: %w", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("no codespace was found for repository: %s", repo)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Return first error, plus count of others.
|
||||
if errs != nil {
|
||||
err := errs[0]
|
||||
if others := len(errs) - 1; others > 0 {
|
||||
err = fmt.Errorf("%w (+%d more)", err, others)
|
||||
nameFilter = c.Name
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
// TODO: this token is discarded and then re-requested later in DeleteCodespace
|
||||
token, err := opts.apiClient.GetCodespaceToken(ctx, user.Login, nameFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespace token: %w", err)
|
||||
}
|
||||
|
||||
codespace, err := opts.apiClient.GetCodespace(ctx, token, user.Login, nameFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching codespace information: %w", err)
|
||||
}
|
||||
|
||||
codespaces = []*api.Codespace{codespace}
|
||||
}
|
||||
|
||||
codespacesToDelete := make([]*api.Codespace, 0, len(codespaces))
|
||||
lastUpdatedCutoffTime := opts.now().AddDate(0, 0, -int(opts.keepDays))
|
||||
for _, c := range codespaces {
|
||||
if nameFilter != "" && c.Name != nameFilter {
|
||||
continue
|
||||
}
|
||||
if opts.repoFilter != "" && !strings.EqualFold(c.RepositoryNWO, opts.repoFilter) {
|
||||
continue
|
||||
}
|
||||
if opts.keepDays > 0 {
|
||||
t, err := time.Parse(time.RFC3339, c.LastUsedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing last_used_at timestamp %q: %w", c.LastUsedAt, err)
|
||||
}
|
||||
if t.After(lastUpdatedCutoffTime) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !opts.skipConfirm {
|
||||
confirmed, err := confirmDeletion(opts.prompter, c, opts.isInteractive)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
if !confirmed {
|
||||
continue
|
||||
}
|
||||
}
|
||||
codespacesToDelete = append(codespacesToDelete, c)
|
||||
}
|
||||
|
||||
if len(codespacesToDelete) == 0 {
|
||||
return errors.New("no codespaces to delete")
|
||||
}
|
||||
|
||||
g := errgroup.Group{}
|
||||
for _, c := range codespacesToDelete {
|
||||
codespaceName := c.Name
|
||||
g.Go(func() error {
|
||||
if err := opts.apiClient.DeleteCodespace(ctx, user.Login, codespaceName); err != nil {
|
||||
_, _ = log.Errorf("error deleting codespace %q: %v\n", codespaceName, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return errors.New("some codespaces failed to delete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func confirmDeletion(codespace *api.Codespace, force bool) (bool, error) {
|
||||
func confirmDeletion(p prompter, codespace *api.Codespace, isInteractive bool) (bool, error) {
|
||||
gs := codespace.Environment.GitStatus
|
||||
hasUnsavedChanges := gs.HasUncommitedChanges || gs.HasUnpushedChanges
|
||||
if force || !hasUnsavedChanges {
|
||||
if !hasUnsavedChanges {
|
||||
return true, nil
|
||||
}
|
||||
if !hasTTY {
|
||||
if !isInteractive {
|
||||
return false, fmt.Errorf("codespace %s has unsaved changes (use --force to override)", codespace.Name)
|
||||
}
|
||||
return p.Confirm(fmt.Sprintf("Codespace %s has unsaved changes. OK to delete?", codespace.Name))
|
||||
}
|
||||
|
||||
type surveyPrompter struct{}
|
||||
|
||||
func (p *surveyPrompter) Confirm(message string) (bool, error) {
|
||||
var confirmed struct {
|
||||
Confirmed bool
|
||||
}
|
||||
|
|
@ -223,7 +186,7 @@ func confirmDeletion(codespace *api.Codespace, force bool) (bool, error) {
|
|||
{
|
||||
Name: "confirmed",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: fmt.Sprintf("Codespace %s has unsaved changes. OK to delete?", codespace.Name),
|
||||
Message: message,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
239
cmd/ghcs/delete_test.go
Normal file
239
cmd/ghcs/delete_test.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
package ghcs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/github/ghcs/cmd/ghcs/output"
|
||||
"github.com/github/ghcs/internal/api"
|
||||
)
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
user := &api.User{Login: "hubot"}
|
||||
now, _ := time.Parse(time.RFC3339, "2021-09-22T00:00:00Z")
|
||||
daysAgo := func(n int) string {
|
||||
return now.Add(time.Hour * -time.Duration(24*n)).Format(time.RFC3339)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts deleteOptions
|
||||
codespaces []*api.Codespace
|
||||
confirms map[string]bool
|
||||
deleteErr error
|
||||
wantErr bool
|
||||
wantDeleted []string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "by name",
|
||||
opts: deleteOptions{
|
||||
codespaceName: "hubot-robawt-abc",
|
||||
},
|
||||
codespaces: []*api.Codespace{
|
||||
{
|
||||
Name: "hubot-robawt-abc",
|
||||
},
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc"},
|
||||
},
|
||||
{
|
||||
name: "by repo",
|
||||
opts: deleteOptions{
|
||||
repoFilter: "monalisa/spoon-knife",
|
||||
},
|
||||
codespaces: []*api.Codespace{
|
||||
{
|
||||
Name: "monalisa-spoonknife-123",
|
||||
RepositoryNWO: "monalisa/Spoon-Knife",
|
||||
},
|
||||
{
|
||||
Name: "hubot-robawt-abc",
|
||||
RepositoryNWO: "hubot/ROBAWT",
|
||||
},
|
||||
{
|
||||
Name: "monalisa-spoonknife-c4f3",
|
||||
RepositoryNWO: "monalisa/Spoon-Knife",
|
||||
},
|
||||
},
|
||||
wantDeleted: []string{"monalisa-spoonknife-123", "monalisa-spoonknife-c4f3"},
|
||||
},
|
||||
{
|
||||
name: "unused",
|
||||
opts: deleteOptions{
|
||||
deleteAll: true,
|
||||
keepDays: 3,
|
||||
},
|
||||
codespaces: []*api.Codespace{
|
||||
{
|
||||
Name: "monalisa-spoonknife-123",
|
||||
LastUsedAt: daysAgo(1),
|
||||
},
|
||||
{
|
||||
Name: "hubot-robawt-abc",
|
||||
LastUsedAt: daysAgo(4),
|
||||
},
|
||||
{
|
||||
Name: "monalisa-spoonknife-c4f3",
|
||||
LastUsedAt: daysAgo(10),
|
||||
},
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
|
||||
},
|
||||
{
|
||||
name: "deletion failed",
|
||||
opts: deleteOptions{
|
||||
deleteAll: true,
|
||||
},
|
||||
codespaces: []*api.Codespace{
|
||||
{
|
||||
Name: "monalisa-spoonknife-123",
|
||||
},
|
||||
{
|
||||
Name: "hubot-robawt-abc",
|
||||
},
|
||||
},
|
||||
deleteErr: errors.New("aborted by test"),
|
||||
wantErr: true,
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-123"},
|
||||
wantStderr: "error deleting codespace \"hubot-robawt-abc\": aborted by test\nerror deleting codespace \"monalisa-spoonknife-123\": aborted by test\n",
|
||||
},
|
||||
{
|
||||
name: "with confirm",
|
||||
opts: deleteOptions{
|
||||
isInteractive: true,
|
||||
deleteAll: true,
|
||||
skipConfirm: false,
|
||||
},
|
||||
codespaces: []*api.Codespace{
|
||||
{
|
||||
Name: "monalisa-spoonknife-123",
|
||||
Environment: api.CodespaceEnvironment{
|
||||
GitStatus: api.CodespaceEnvironmentGitStatus{
|
||||
HasUnpushedChanges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "hubot-robawt-abc",
|
||||
Environment: api.CodespaceEnvironment{
|
||||
GitStatus: api.CodespaceEnvironmentGitStatus{
|
||||
HasUncommitedChanges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "monalisa-spoonknife-c4f3",
|
||||
Environment: api.CodespaceEnvironment{
|
||||
GitStatus: api.CodespaceEnvironmentGitStatus{
|
||||
HasUnpushedChanges: false,
|
||||
HasUncommitedChanges: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
confirms: map[string]bool{
|
||||
"Codespace monalisa-spoonknife-123 has unsaved changes. OK to delete?": false,
|
||||
"Codespace hubot-robawt-abc has unsaved changes. OK to delete?": true,
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
apiMock := &apiClientMock{
|
||||
GetUserFunc: func(_ context.Context) (*api.User, error) {
|
||||
return user, nil
|
||||
},
|
||||
DeleteCodespaceFunc: func(_ context.Context, userLogin, name string) error {
|
||||
if userLogin != user.Login {
|
||||
return fmt.Errorf("unexpected user %q", userLogin)
|
||||
}
|
||||
if tt.deleteErr != nil {
|
||||
return tt.deleteErr
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if tt.opts.codespaceName == "" {
|
||||
apiMock.ListCodespacesFunc = func(_ context.Context, userLogin string) ([]*api.Codespace, error) {
|
||||
if userLogin != user.Login {
|
||||
return nil, fmt.Errorf("unexpected user %q", userLogin)
|
||||
}
|
||||
return tt.codespaces, nil
|
||||
}
|
||||
} else {
|
||||
apiMock.GetCodespaceTokenFunc = func(_ context.Context, userLogin, name string) (string, error) {
|
||||
if userLogin != user.Login {
|
||||
return "", fmt.Errorf("unexpected user %q", userLogin)
|
||||
}
|
||||
return "CS_TOKEN", nil
|
||||
}
|
||||
apiMock.GetCodespaceFunc = func(_ context.Context, token, userLogin, name string) (*api.Codespace, error) {
|
||||
if userLogin != user.Login {
|
||||
return nil, fmt.Errorf("unexpected user %q", userLogin)
|
||||
}
|
||||
if token != "CS_TOKEN" {
|
||||
return nil, fmt.Errorf("unexpected token %q", token)
|
||||
}
|
||||
return tt.codespaces[0], nil
|
||||
}
|
||||
}
|
||||
opts := tt.opts
|
||||
opts.apiClient = apiMock
|
||||
opts.now = func() time.Time { return now }
|
||||
opts.prompter = &prompterMock{
|
||||
ConfirmFunc: func(msg string) (bool, error) {
|
||||
res, found := tt.confirms[msg]
|
||||
if !found {
|
||||
return false, fmt.Errorf("unexpected prompt %q", msg)
|
||||
}
|
||||
return res, nil
|
||||
},
|
||||
}
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
log := output.NewLogger(stdout, stderr, false)
|
||||
err := delete(context.Background(), log, opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if n := len(apiMock.GetUserCalls()); n != 1 {
|
||||
t.Errorf("GetUser invoked %d times, expected %d", n, 1)
|
||||
}
|
||||
var gotDeleted []string
|
||||
for _, delArgs := range apiMock.DeleteCodespaceCalls() {
|
||||
gotDeleted = append(gotDeleted, delArgs.Name)
|
||||
}
|
||||
sort.Strings(gotDeleted)
|
||||
if !sliceEquals(gotDeleted, tt.wantDeleted) {
|
||||
t.Errorf("deleted %q, want %q", gotDeleted, tt.wantDeleted)
|
||||
}
|
||||
if out := stdout.String(); out != tt.wantStdout {
|
||||
t.Errorf("stdout = %q, want %q", out, tt.wantStdout)
|
||||
}
|
||||
if out := stderr.String(); out != tt.wantStderr {
|
||||
t.Errorf("stderr = %q, want %q", out, tt.wantStderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sliceEquals(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ func list(opts *listOptions) error {
|
|||
return fmt.Errorf("error getting user: %w", err)
|
||||
}
|
||||
|
||||
codespaces, err := apiClient.ListCodespaces(ctx, user)
|
||||
codespaces, err := apiClient.ListCodespaces(ctx, user.Login)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting codespaces: %w", err)
|
||||
}
|
||||
|
|
|
|||
292
cmd/ghcs/mock_api.go
Normal file
292
cmd/ghcs/mock_api.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package ghcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/github/ghcs/internal/api"
|
||||
)
|
||||
|
||||
// apiClientMock is a mock implementation of apiClient.
|
||||
//
|
||||
// func TestSomethingThatUsesapiClient(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked apiClient
|
||||
// mockedapiClient := &apiClientMock{
|
||||
// DeleteCodespaceFunc: func(ctx context.Context, user string, name string) error {
|
||||
// panic("mock out the DeleteCodespace method")
|
||||
// },
|
||||
// GetCodespaceFunc: func(ctx context.Context, token string, user string, name string) (*api.Codespace, error) {
|
||||
// panic("mock out the GetCodespace method")
|
||||
// },
|
||||
// GetCodespaceTokenFunc: func(ctx context.Context, user string, name string) (string, error) {
|
||||
// panic("mock out the GetCodespaceToken method")
|
||||
// },
|
||||
// GetUserFunc: func(ctx context.Context) (*api.User, error) {
|
||||
// panic("mock out the GetUser method")
|
||||
// },
|
||||
// ListCodespacesFunc: func(ctx context.Context, user string) ([]*api.Codespace, error) {
|
||||
// panic("mock out the ListCodespaces method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedapiClient in code that requires apiClient
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type apiClientMock struct {
|
||||
// DeleteCodespaceFunc mocks the DeleteCodespace method.
|
||||
DeleteCodespaceFunc func(ctx context.Context, user string, name string) error
|
||||
|
||||
// GetCodespaceFunc mocks the GetCodespace method.
|
||||
GetCodespaceFunc func(ctx context.Context, token string, user string, name string) (*api.Codespace, error)
|
||||
|
||||
// GetCodespaceTokenFunc mocks the GetCodespaceToken method.
|
||||
GetCodespaceTokenFunc func(ctx context.Context, user string, name string) (string, error)
|
||||
|
||||
// GetUserFunc mocks the GetUser method.
|
||||
GetUserFunc func(ctx context.Context) (*api.User, error)
|
||||
|
||||
// ListCodespacesFunc mocks the ListCodespaces method.
|
||||
ListCodespacesFunc func(ctx context.Context, user string) ([]*api.Codespace, error)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// DeleteCodespace holds details about calls to the DeleteCodespace method.
|
||||
DeleteCodespace []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// User is the user argument value.
|
||||
User string
|
||||
// Name is the name argument value.
|
||||
Name string
|
||||
}
|
||||
// GetCodespace holds details about calls to the GetCodespace method.
|
||||
GetCodespace []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Token is the token argument value.
|
||||
Token string
|
||||
// User is the user argument value.
|
||||
User string
|
||||
// Name is the name argument value.
|
||||
Name string
|
||||
}
|
||||
// GetCodespaceToken holds details about calls to the GetCodespaceToken method.
|
||||
GetCodespaceToken []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// User is the user argument value.
|
||||
User string
|
||||
// Name is the name argument value.
|
||||
Name string
|
||||
}
|
||||
// GetUser holds details about calls to the GetUser method.
|
||||
GetUser []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
}
|
||||
// ListCodespaces holds details about calls to the ListCodespaces method.
|
||||
ListCodespaces []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// User is the user argument value.
|
||||
User string
|
||||
}
|
||||
}
|
||||
lockDeleteCodespace sync.RWMutex
|
||||
lockGetCodespace sync.RWMutex
|
||||
lockGetCodespaceToken sync.RWMutex
|
||||
lockGetUser sync.RWMutex
|
||||
lockListCodespaces sync.RWMutex
|
||||
}
|
||||
|
||||
// DeleteCodespace calls DeleteCodespaceFunc.
|
||||
func (mock *apiClientMock) DeleteCodespace(ctx context.Context, user string, name string) error {
|
||||
if mock.DeleteCodespaceFunc == nil {
|
||||
panic("apiClientMock.DeleteCodespaceFunc: method is nil but apiClient.DeleteCodespace was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
Name string
|
||||
}{
|
||||
Ctx: ctx,
|
||||
User: user,
|
||||
Name: name,
|
||||
}
|
||||
mock.lockDeleteCodespace.Lock()
|
||||
mock.calls.DeleteCodespace = append(mock.calls.DeleteCodespace, callInfo)
|
||||
mock.lockDeleteCodespace.Unlock()
|
||||
return mock.DeleteCodespaceFunc(ctx, user, name)
|
||||
}
|
||||
|
||||
// DeleteCodespaceCalls gets all the calls that were made to DeleteCodespace.
|
||||
// Check the length with:
|
||||
// len(mockedapiClient.DeleteCodespaceCalls())
|
||||
func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
Name string
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
Name string
|
||||
}
|
||||
mock.lockDeleteCodespace.RLock()
|
||||
calls = mock.calls.DeleteCodespace
|
||||
mock.lockDeleteCodespace.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetCodespace calls GetCodespaceFunc.
|
||||
func (mock *apiClientMock) GetCodespace(ctx context.Context, token string, user string, name string) (*api.Codespace, error) {
|
||||
if mock.GetCodespaceFunc == nil {
|
||||
panic("apiClientMock.GetCodespaceFunc: method is nil but apiClient.GetCodespace was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Token string
|
||||
User string
|
||||
Name string
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Token: token,
|
||||
User: user,
|
||||
Name: name,
|
||||
}
|
||||
mock.lockGetCodespace.Lock()
|
||||
mock.calls.GetCodespace = append(mock.calls.GetCodespace, callInfo)
|
||||
mock.lockGetCodespace.Unlock()
|
||||
return mock.GetCodespaceFunc(ctx, token, user, name)
|
||||
}
|
||||
|
||||
// GetCodespaceCalls gets all the calls that were made to GetCodespace.
|
||||
// Check the length with:
|
||||
// len(mockedapiClient.GetCodespaceCalls())
|
||||
func (mock *apiClientMock) GetCodespaceCalls() []struct {
|
||||
Ctx context.Context
|
||||
Token string
|
||||
User string
|
||||
Name string
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Token string
|
||||
User string
|
||||
Name string
|
||||
}
|
||||
mock.lockGetCodespace.RLock()
|
||||
calls = mock.calls.GetCodespace
|
||||
mock.lockGetCodespace.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetCodespaceToken calls GetCodespaceTokenFunc.
|
||||
func (mock *apiClientMock) GetCodespaceToken(ctx context.Context, user string, name string) (string, error) {
|
||||
if mock.GetCodespaceTokenFunc == nil {
|
||||
panic("apiClientMock.GetCodespaceTokenFunc: method is nil but apiClient.GetCodespaceToken was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
Name string
|
||||
}{
|
||||
Ctx: ctx,
|
||||
User: user,
|
||||
Name: name,
|
||||
}
|
||||
mock.lockGetCodespaceToken.Lock()
|
||||
mock.calls.GetCodespaceToken = append(mock.calls.GetCodespaceToken, callInfo)
|
||||
mock.lockGetCodespaceToken.Unlock()
|
||||
return mock.GetCodespaceTokenFunc(ctx, user, name)
|
||||
}
|
||||
|
||||
// GetCodespaceTokenCalls gets all the calls that were made to GetCodespaceToken.
|
||||
// Check the length with:
|
||||
// len(mockedapiClient.GetCodespaceTokenCalls())
|
||||
func (mock *apiClientMock) GetCodespaceTokenCalls() []struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
Name string
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
Name string
|
||||
}
|
||||
mock.lockGetCodespaceToken.RLock()
|
||||
calls = mock.calls.GetCodespaceToken
|
||||
mock.lockGetCodespaceToken.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetUser calls GetUserFunc.
|
||||
func (mock *apiClientMock) GetUser(ctx context.Context) (*api.User, error) {
|
||||
if mock.GetUserFunc == nil {
|
||||
panic("apiClientMock.GetUserFunc: method is nil but apiClient.GetUser was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
}{
|
||||
Ctx: ctx,
|
||||
}
|
||||
mock.lockGetUser.Lock()
|
||||
mock.calls.GetUser = append(mock.calls.GetUser, callInfo)
|
||||
mock.lockGetUser.Unlock()
|
||||
return mock.GetUserFunc(ctx)
|
||||
}
|
||||
|
||||
// GetUserCalls gets all the calls that were made to GetUser.
|
||||
// Check the length with:
|
||||
// len(mockedapiClient.GetUserCalls())
|
||||
func (mock *apiClientMock) GetUserCalls() []struct {
|
||||
Ctx context.Context
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
mock.lockGetUser.RLock()
|
||||
calls = mock.calls.GetUser
|
||||
mock.lockGetUser.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ListCodespaces calls ListCodespacesFunc.
|
||||
func (mock *apiClientMock) ListCodespaces(ctx context.Context, user string) ([]*api.Codespace, error) {
|
||||
if mock.ListCodespacesFunc == nil {
|
||||
panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
}{
|
||||
Ctx: ctx,
|
||||
User: user,
|
||||
}
|
||||
mock.lockListCodespaces.Lock()
|
||||
mock.calls.ListCodespaces = append(mock.calls.ListCodespaces, callInfo)
|
||||
mock.lockListCodespaces.Unlock()
|
||||
return mock.ListCodespacesFunc(ctx, user)
|
||||
}
|
||||
|
||||
// ListCodespacesCalls gets all the calls that were made to ListCodespaces.
|
||||
// Check the length with:
|
||||
// len(mockedapiClient.ListCodespacesCalls())
|
||||
func (mock *apiClientMock) ListCodespacesCalls() []struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
User string
|
||||
}
|
||||
mock.lockListCodespaces.RLock()
|
||||
calls = mock.calls.ListCodespaces
|
||||
mock.lockListCodespaces.RUnlock()
|
||||
return calls
|
||||
}
|
||||
69
cmd/ghcs/mock_prompter.go
Normal file
69
cmd/ghcs/mock_prompter.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package ghcs
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// prompterMock is a mock implementation of prompter.
|
||||
//
|
||||
// func TestSomethingThatUsesprompter(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked prompter
|
||||
// mockedprompter := &prompterMock{
|
||||
// ConfirmFunc: func(message string) (bool, error) {
|
||||
// panic("mock out the Confirm method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedprompter in code that requires prompter
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type prompterMock struct {
|
||||
// ConfirmFunc mocks the Confirm method.
|
||||
ConfirmFunc func(message string) (bool, error)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// Confirm holds details about calls to the Confirm method.
|
||||
Confirm []struct {
|
||||
// Message is the message argument value.
|
||||
Message string
|
||||
}
|
||||
}
|
||||
lockConfirm sync.RWMutex
|
||||
}
|
||||
|
||||
// Confirm calls ConfirmFunc.
|
||||
func (mock *prompterMock) Confirm(message string) (bool, error) {
|
||||
if mock.ConfirmFunc == nil {
|
||||
panic("prompterMock.ConfirmFunc: method is nil but prompter.Confirm was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Message string
|
||||
}{
|
||||
Message: message,
|
||||
}
|
||||
mock.lockConfirm.Lock()
|
||||
mock.calls.Confirm = append(mock.calls.Confirm, callInfo)
|
||||
mock.lockConfirm.Unlock()
|
||||
return mock.ConfirmFunc(message)
|
||||
}
|
||||
|
||||
// ConfirmCalls gets all the calls that were made to Confirm.
|
||||
// Check the length with:
|
||||
// len(mockedprompter.ConfirmCalls())
|
||||
func (mock *prompterMock) ConfirmCalls() []struct {
|
||||
Message string
|
||||
} {
|
||||
var calls []struct {
|
||||
Message string
|
||||
}
|
||||
mock.lockConfirm.RLock()
|
||||
calls = mock.calls.Confirm
|
||||
mock.lockConfirm.RUnlock()
|
||||
return calls
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue