diff --git a/cmd/bindown/cli.go b/cmd/bindown/cli.go index 463df7f..cf4ca72 100644 --- a/cmd/bindown/cli.go +++ b/cmd/bindown/cli.go @@ -93,10 +93,38 @@ func loadConfigFile(ctx *runContext, noDefaultDirs bool) (*bindown.Config, error return configFile, nil } +// fileWriter covers terminal.FileWriter. Needed for survey +type fileWriter interface { + io.Writer + Fd() uintptr +} + +type SimpleFileWriter struct { + io.Writer +} + +func (s SimpleFileWriter) Fd() uintptr { + return 0 +} + +// fileReader covers terminal.FileReader. Needed for survey +type fileReader interface { + io.Reader + Fd() uintptr +} + +type SimpleFileReader struct { + io.Reader +} + +func (s SimpleFileReader) Fd() uintptr { + return 0 +} + type runContext struct { parent context.Context - stdin io.Reader - stdout io.Writer + stdin fileReader + stdout fileWriter rootCmd *rootCmd } @@ -123,8 +151,8 @@ func (r *runContext) Value(key any) any { } type runOpts struct { - stdin io.Reader - stdout io.Writer + stdin fileReader + stdout fileWriter stderr io.Writer cmdName string exitHandler func(int) @@ -171,7 +199,7 @@ func Run(ctx context.Context, args []string, opts *runOpts) { kongCtx, err := parser.Parse(args) parser.FatalIfErrorf(err) if root.Quiet { - runCtx.stdout = io.Discard + runCtx.stdout = SimpleFileWriter{io.Discard} kongCtx.Stdout = io.Discard } err = kongCtx.Run() diff --git a/cmd/bindown/dependency.go b/cmd/bindown/dependency.go index 056756c..4b193dd 100644 --- a/cmd/bindown/dependency.go +++ b/cmd/bindown/dependency.go @@ -1,12 +1,12 @@ package main import ( - "bufio" "encoding/json" "fmt" "regexp" "strings" + "github.com/AlecAivazis/survey/v2" "github.com/willabides/bindown/v4/internal/bindown" "github.com/willabides/bindown/v4/internal/builddep" ) @@ -157,10 +157,10 @@ func (c *dependencyRemoveCmd) Run(ctx *runContext) error { type dependencyAddCmd struct { Name string `kong:"arg"` - Template string `kong:"arg,predictor=template"` + Template string `kong:"arg,optional,predictor=template"` TemplateSource string `kong:"name=source,help='template source',predictor=templateSource"` Vars map[string]string `kong:"name=var"` - SkipRequiredVars bool `kong:"name=skipvars,help='do not prompt for required vars'"` + SkipRequiredVars bool `kong:"name=skipvars,help='do not prompt for required vars. implies --skipchecksums'"` SkipChecksums bool `kong:"name=skipchecksums,help='do not add checksums for this dependency'"` } @@ -170,19 +170,21 @@ func (c *dependencyAddCmd) Run(ctx *runContext) error { return err } tmpl := c.Template + if tmpl == "" { + tmpl = c.Name + } tmplSrc := c.TemplateSource if tmplSrc == "" { - tmplParts := strings.SplitN(tmpl, "#", 2) - if len(tmplParts) == 2 { - tmpl = tmplParts[1] - tmplSrc = tmplParts[0] + ts, t, ok := strings.Cut(tmpl, "#") + if ok { + tmplSrc, tmpl = ts, t } } if c.Vars == nil { c.Vars = map[string]string{} } - err = config.AddDependencyFromTemplate(ctx, tmpl, &bindown.AddDependencyFromTemplateOpts{ + dep, varVals, err := config.AddDependencyFromTemplate(ctx, tmpl, &bindown.AddDependencyFromTemplateOpts{ DependencyName: c.Name, TemplateSource: tmplSrc, Vars: c.Vars, @@ -190,26 +192,33 @@ func (c *dependencyAddCmd) Run(ctx *runContext) error { if err != nil { return err } - missingVars, err := config.MissingDependencyVars(c.Name) + // This shouldn't be possible, but just in case + if dep.Template == nil || config.Templates == nil || config.Templates[*dep.Template] == nil { + return fmt.Errorf("template not found: %q", tmpl) + } + if varVals == nil { + varVals = map[string][]string{} + } + tmplCfg := config.Templates[*dep.Template] + // Don't need to output the list of systems + systems := tmplCfg.Systems + tmplCfg.Systems = nil + fmt.Fprintf(ctx.stdout, "Adding dependency %q from template ", c.Name) + err = bindown.EncodeYaml(ctx.stdout, map[string]bindown.Dependency{ + *dep.Template: *tmplCfg, + }) if err != nil { return err } - hasMissingVars := len(missingVars) > 0 - if hasMissingVars && !c.SkipRequiredVars { - hasMissingVars = false - scanner := bufio.NewScanner(ctx.stdin) - for _, missingVar := range missingVars { - fmt.Fprintf(ctx.stdout, "Please enter a value for required variable %q:\t", missingVar) - scanner.Scan() - err = scanner.Err() - if err != nil { - return err - } - val := scanner.Text() - config.Dependencies[c.Name].Vars[missingVar] = val - } + tmplCfg.Systems = systems + + err = c.promptForVars(ctx, config, dep, varVals) + if err != nil { + return err } - if !hasMissingVars && !c.SkipChecksums { + + skipChecksums := c.SkipChecksums || c.SkipRequiredVars + if !skipChecksums { err = config.AddChecksums([]string{c.Name}, nil) if err != nil { return err @@ -218,6 +227,70 @@ func (c *dependencyAddCmd) Run(ctx *runContext) error { return config.WriteFile(ctx.rootCmd.JSONConfig) } +func (c *dependencyAddCmd) promptForVars(ctx *runContext, config *bindown.Config, dep *bindown.Dependency, varVals map[string][]string) error { + if c.SkipRequiredVars { + return nil + } + missingVars, err := config.MissingDependencyVars(c.Name) + if err != nil { + return err + } + if len(missingVars) == 0 { + return nil + } + for _, d := range config.Dependencies { + if d.Template == nil || dep.Template == nil || *d.Template != *dep.Template { + continue + } + for k, v := range d.Vars { + varVals[k] = append([]string{v}, varVals[k]...) + } + } + + questions := make([]*survey.Question, 0, len(missingVars)) + for _, missingVar := range missingVars { + prompt := survey.Input{ + Message: missingVar, + // Help: `The thing you need to do is...`, + } + knownVals := varVals[missingVar] + bindown.SortBySemverOrString(knownVals) + knownVals = bindown.Unique(knownVals, knownVals[:0]) + if len(knownVals) > 0 { + prompt.Default = knownVals[0] + prompt.Suggest = func(toComplete string) []string { + suggestions := make([]string, 0, len(knownVals)) + for _, v := range knownVals { + if strings.HasPrefix(v, toComplete) { + suggestions = append(suggestions, v) + } + } + return suggestions + } + } + questions = append(questions, &survey.Question{ + Name: missingVar, + Prompt: &prompt, + }) + } + answers := map[string]any{} + err = survey.Ask(questions, &answers, survey.WithStdio(ctx.stdin, ctx.stdout, nil), survey.WithShowCursor(true)) + if err != nil { + return err + } + for k, v := range answers { + s, ok := v.(string) + if !ok { + return fmt.Errorf("expected string for %q, got %T", k, v) + } + config.Dependencies[c.Name].Vars[k] = s + } + + fmt.Fprintf(ctx.stdout, "Adding dependency %q from template\n", c.Name) + + return nil +} + type dependencyAddByUrlsCmd struct { Name string `kong:"arg,help='dependency name'"` Version string `kong:"arg,help='dependency version'"` diff --git a/cmd/bindown/dependency_test.go b/cmd/bindown/dependency_test.go index db4dcdf..282f468 100644 --- a/cmd/bindown/dependency_test.go +++ b/cmd/bindown/dependency_test.go @@ -5,9 +5,9 @@ import ( "os" "path/filepath" "runtime" - "strings" "testing" + "github.com/Netflix/go-expect" "github.com/stretchr/testify/require" "github.com/willabides/bindown/v4/internal/testutil" ) @@ -362,7 +362,9 @@ url_checksums: foo-darwin-amd64-1.2.3: deadbeef `) result := runner.run("dependency", "add", "dep1", "tmpl", "--var=version=1.2.3") - result.assertState(resultState{}) + result.assertState(resultState{ + stdout: `Adding dependency "dep1" from template tmpl`, + }) cfg := runner.getConfigFile() wantDep := mustConfigFromYAML(t, ` dependencies: @@ -417,7 +419,9 @@ templates: `), 0o600) require.NoError(t, err) result := runner.run("dependency", "add", "dep1", "tmpl", "--var=version=1.2.3", "--source=origin") - result.assertState(resultState{}) + result.assertState(resultState{ + stdout: `Adding dependency "dep1" from template origin#tmpl`, + }) wantDep := mustConfigFromYAML(t, ` dependencies: dep1: @@ -448,7 +452,9 @@ templates: `), 0o600) require.NoError(t, err) result := runner.run("dependency", "add", "dep1", "origin#tmpl", "--var=version=1.2.3") - result.assertState(resultState{}) + result.assertState(resultState{ + stdout: `Adding dependency "dep1" from template origin#tmpl`, + }) wantDep := mustConfigFromYAML(t, ` dependencies: dep1: @@ -463,6 +469,12 @@ dependencies: runner := newCmdRunner(t) runner.writeConfigYaml(` systems: ["linux/amd64", "darwin/amd64"] +dependencies: + dep2: + template: tmpl + vars: + version: "1.2.3" + foo: bar templates: tmpl: url: foo-{{ .os }}-{{ .arch }}-{{ .version }} @@ -472,10 +484,19 @@ url_checksums: foo-darwin-amd64-1.2.3: deadbeef foo-windows-amd64-1.2.3: deadbeef `) - runner.stdin = strings.NewReader("1.2.3\nbar") - result := runner.run("dependency", "add", "dep1", "tmpl") + + ex := func(console *expect.Console) { + _, err := console.ExpectString("(bar) ") + require.NoError(t, err) + _, err = console.SendLine("") + require.NoError(t, err) + _, err = console.ExpectString("template") + require.NoError(t, err) + require.NoError(t, console.Close()) + } + result := runner.runExpect(ex, "dependency", "add", "dep1", "tmpl", "--var=version=1.2.3") result.assertState(resultState{ - stdout: `Please enter a value for required variable "version": Please enter a value for required variable "foo":`, + stdout: `.*`, }) cfg := runner.getConfigFile() wantDep := mustConfigFromYAML(t, ` @@ -506,11 +527,14 @@ systems: ["darwin/amd64", "darwin/arm64", "linux/amd64", "windows/amd64"] template_sources: origin: %q `, srcPath)) - wantStdout := `Please enter a value for required variable "version": Please enter a value for required variable "addr": ` - runner.stdin = strings.NewReader(fmt.Sprintf("%s\n%s", "1.2.3", server.URL)) - result := runner.run("dependency", "add", "foo", "tmpl1", "--source", "origin") - require.Equal(t, 0, result.exitVal) - require.Equal(t, wantStdout, result.stdOut.String()) + result := runner.run("dependency", "add", "foo", "tmpl1", + "--source", "origin", + "--var", "version=1.2.3", + "--var", "addr="+server.URL, + ) + result.assertState(resultState{ + stdout: `Adding dependency "foo" from template origin#tmpl`, + }) gotCfg := runner.getConfigFile() wantDep := mustConfigFromYAML(t, fmt.Sprintf(` dependencies: diff --git a/cmd/bindown/testutil_test.go b/cmd/bindown/testutil_test.go index 1dbe0c0..e0119e8 100644 --- a/cmd/bindown/testutil_test.go +++ b/cmd/bindown/testutil_test.go @@ -12,9 +12,11 @@ import ( "strings" "testing" + "github.com/Netflix/go-expect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/willabides/bindown/v4/internal/bindown" + "github.com/willabides/bindown/v4/internal/expecttest" "gopkg.in/yaml.v3" ) @@ -58,8 +60,8 @@ func (c *cmdRunner) run(commandLine ...string) *runCmdResult { ctx, commandLine, &runOpts{ - stdin: c.stdin, - stdout: &result.stdOut, + stdin: SimpleFileReader{c.stdin}, + stdout: SimpleFileWriter{&result.stdOut}, stderr: &result.stdErr, cmdName: "cmd", exitHandler: func(i int) { @@ -71,6 +73,42 @@ func (c *cmdRunner) run(commandLine ...string) *runCmdResult { return &result } +func (c *cmdRunner) runExpect(expectFunc func(*expect.Console), commandLine ...string) *runCmdResult { + t := c.t + t.Helper() + ctx := context.Background() + + if c.configFile != "" { + commandLine = append(commandLine, "--configfile", c.configFile) + } + if c.cache != "" { + commandLine = append(commandLine, "--cache", c.cache) + } + + result := runCmdResult{t: t} + + testFunc := func(console *expect.Console) { + Run( + ctx, + commandLine, + &runOpts{ + stdin: console.Tty(), + stdout: console.Tty(), + stderr: &result.stdErr, + cmdName: "cmd", + exitHandler: func(i int) { + result.exited = true + result.exitVal = i + }, + }, + ) + } + + expecttest.Run(t, expectFunc, testFunc, expecttest.WithConsoleOpt(expect.WithStdout(&result.stdOut))) + + return &result +} + func mustConfigFromYAML(t *testing.T, yml string) *bindown.Config { t.Helper() got, err := bindown.ConfigFromYAML(context.Background(), []byte(yml)) diff --git a/go.mod b/go.mod index 5aebffd..f111a40 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,12 @@ go 1.20 require ( github.com/AlecAivazis/survey/v2 v2.3.6 github.com/Masterminds/semver/v3 v3.1.1 + github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/alecthomas/kong v0.7.1 + github.com/creack/pty v1.1.18 github.com/google/go-cmp v0.5.9 github.com/google/go-github/v52 v52.0.0 + github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec github.com/invopop/jsonschema v0.7.0 github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v4 v4.0.0-alpha.8 diff --git a/go.sum b/go.sum index 294da68..1eb7354 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,9 @@ github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEM github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/bindown/config.go b/internal/bindown/config.go index 48a88bc..baaee9b 100644 --- a/internal/bindown/config.go +++ b/internal/bindown/config.go @@ -454,10 +454,10 @@ type AddDependencyFromTemplateOpts struct { Vars map[string]string } -// AddDependencyFromTemplate adds a dependency to the config -func (c *Config) AddDependencyFromTemplate(ctx context.Context, templateName string, opts *AddDependencyFromTemplateOpts) error { +// AddDependencyFromTemplate adds a dependency to the config. Returns a map of known values for template vars +func (c *Config) AddDependencyFromTemplate(ctx context.Context, templateName string, opts *AddDependencyFromTemplateOpts) (*Dependency, map[string][]string, error) { if opts == nil { - opts = new(AddDependencyFromTemplateOpts) + opts = &AddDependencyFromTemplateOpts{} } dependencyName := opts.DependencyName if dependencyName == "" { @@ -467,29 +467,32 @@ func (c *Config) AddDependencyFromTemplate(ctx context.Context, templateName str c.Dependencies = map[string]*Dependency{} } if c.Dependencies[dependencyName] != nil { - return fmt.Errorf("dependency named %q already exists", dependencyName) + return nil, nil, fmt.Errorf("dependency named %q already exists", dependencyName) } - templateName, err := c.addOrGetTemplate(ctx, templateName, opts.TemplateSource) + templateName, varVals, err := c.addOrGetTemplate(ctx, templateName, opts.TemplateSource) if err != nil { - return err + return nil, nil, err + } + dep := &Dependency{ + Overrideable: Overrideable{ + Vars: opts.Vars, + }, + Template: &templateName, } - dep := new(Dependency) - dep.Vars = opts.Vars - dep.Template = &templateName c.Dependencies[dependencyName] = dep - return nil + return dep, varVals, nil } -func (c *Config) addOrGetTemplate(ctx context.Context, name, src string) (string, error) { - destName := name +func (c *Config) addOrGetTemplate(ctx context.Context, name, src string) (destName string, varVals map[string][]string, _ error) { + destName = name if src != "" { destName = fmt.Sprintf("%s#%s", src, name) } if _, ok := c.Templates[destName]; ok { - return destName, nil + return destName, nil, nil } if src == "" { - return "", fmt.Errorf("no template named %q", name) + return "", nil, fmt.Errorf("no template named %q", name) } tmplSrc := src tmplSrcs := c.TemplateSources @@ -499,11 +502,12 @@ func (c *Config) addOrGetTemplate(ctx context.Context, name, src string) (string if _, ok := tmplSrcs[tmplSrc]; ok { tmplSrc = tmplSrcs[tmplSrc] } - err := c.addTemplateFromSource(ctx, tmplSrc, name, destName) + var err error + varVals, err = c.addTemplateFromSource(ctx, tmplSrc, name, destName) if err != nil { - return "", err + return "", nil, err } - return destName, nil + return destName, varVals, nil } // CopyTemplateFromSource copies a template from source @@ -515,24 +519,34 @@ func (c *Config) CopyTemplateFromSource(ctx context.Context, src, srcTemplate, d if tmplSrc == "" { return fmt.Errorf("no template source named %q", src) } - return c.addTemplateFromSource(ctx, tmplSrc, srcTemplate, destName) + _, err := c.addTemplateFromSource(ctx, tmplSrc, srcTemplate, destName) + return err } // addTemplateFromSource copies a template from another config file -func (c *Config) addTemplateFromSource(ctx context.Context, src, srcTemplate, destName string) error { +func (c *Config) addTemplateFromSource(ctx context.Context, src, srcTemplate, destName string) (map[string][]string, error) { srcCfg, err := NewConfig(ctx, src, true) if err != nil { - return err + return nil, err } tmpl := srcCfg.Templates[srcTemplate] if tmpl == nil { - return fmt.Errorf("source has no template named %q", srcTemplate) + return nil, fmt.Errorf("source has no template named %q", srcTemplate) + } + varVals := map[string][]string{} + for _, dep := range srcCfg.Dependencies { + if dep.Template == nil || *dep.Template != srcTemplate { + continue + } + for k, v := range dep.Vars { + varVals[k] = append(varVals[k], v) + } } if c.Templates == nil { c.Templates = map[string]*Dependency{} } c.Templates[destName] = tmpl - return nil + return varVals, nil } func (c *Config) templatesList() []string { diff --git a/internal/bindown/config_test.go b/internal/bindown/config_test.go index f2eb4bc..e4336f3 100644 --- a/internal/bindown/config_test.go +++ b/internal/bindown/config_test.go @@ -152,26 +152,27 @@ func TestConfig_addTemplateFromSource(t *testing.T) { ctx := context.Background() t.Run("file", func(t *testing.T) { t.Run("success", func(t *testing.T) { - cfg := new(Config) + cfg := &Config{} src := filepath.Join("testdata", "configs", "ex1.yaml") srcCfg, err := NewConfig(ctx, src, true) require.NoError(t, err) - err = cfg.addTemplateFromSource(ctx, src, "goreleaser", "mygoreleaser") + varVals, err := cfg.addTemplateFromSource(ctx, src, "goreleaser", "mygoreleaser") require.NoError(t, err) require.Equal(t, srcCfg.Templates["goreleaser"], cfg.Templates["mygoreleaser"]) + require.Equal(t, map[string][]string{"version": {"0.120.7"}}, varVals) }) t.Run("missing template", func(t *testing.T) { - cfg := new(Config) + cfg := &Config{} src := filepath.Join("testdata", "configs", "ex1.yaml") - err := cfg.addTemplateFromSource(ctx, src, "fake", "myfake") + _, err := cfg.addTemplateFromSource(ctx, src, "fake", "myfake") require.EqualError(t, err, `source has no template named "fake"`) }) t.Run("missing file", func(t *testing.T) { - cfg := new(Config) + cfg := &Config{} src := filepath.Join("testdata", "configs", "thisdoesnotexist.yaml") - err := cfg.addTemplateFromSource(ctx, src, "fake", "myfake") + _, err := cfg.addTemplateFromSource(ctx, src, "fake", "myfake") require.Error(t, err) require.True(t, os.IsNotExist(err)) }) @@ -180,13 +181,14 @@ func TestConfig_addTemplateFromSource(t *testing.T) { t.Run("http", func(t *testing.T) { srcFile := filepath.Join("testdata", "configs", "ex1.yaml") ts := testutil.ServeFile(t, srcFile, "/ex1.yaml", "") - cfg := new(Config) + cfg := &Config{} src := ts.URL + "/ex1.yaml" srcCfg, err := NewConfig(ctx, srcFile, true) require.NoError(t, err) - err = cfg.addTemplateFromSource(ctx, src, "goreleaser", "mygoreleaser") + varVals, err := cfg.addTemplateFromSource(ctx, src, "goreleaser", "mygoreleaser") require.NoError(t, err) require.Equal(t, srcCfg.Templates["goreleaser"], cfg.Templates["mygoreleaser"]) + require.Equal(t, map[string][]string{"version": {"0.120.7"}}, varVals) }) } diff --git a/internal/bindown/util.go b/internal/bindown/util.go index cd844d3..d8f15ca 100644 --- a/internal/bindown/util.go +++ b/internal/bindown/util.go @@ -14,7 +14,9 @@ import ( "strings" "text/template" + "github.com/Masterminds/semver/v3" "golang.org/x/exp/maps" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) @@ -188,3 +190,41 @@ func EncodeYaml(w io.Writer, v any) (errOut error) { encoder.SetIndent(2) return encoder.Encode(v) } + +func SortBySemverOrString(vals []string) { + semvers := make(map[string]*semver.Version) + for _, val := range vals { + ver, err := semver.NewVersion(val) + if err == nil { + semvers[val] = ver + } + } + + // descending order only if all values are semvers + if len(vals) == len(semvers) { + slices.SortFunc(vals, func(a, b string) bool { + return semvers[a].GreaterThan(semvers[b]) + }) + return + } + + slices.SortFunc(vals, func(a, b string) bool { + aVer, bVer := semvers[a], semvers[b] + if aVer == nil || bVer == nil { + return a < b + } + return aVer.LessThan(bVer) + }) +} + +// Unique appends unique values from vals to buf and returns buf +func Unique[V comparable](vals, buf []V) []V { + seen := make(map[V]bool) + for _, val := range vals { + if !seen[val] { + seen[val] = true + buf = append(buf, val) + } + } + return buf +} diff --git a/internal/build-bootstrapper/build_test.go b/internal/build-bootstrapper/build_test.go index 361cdc1..a768709 100644 --- a/internal/build-bootstrapper/build_test.go +++ b/internal/build-bootstrapper/build_test.go @@ -11,6 +11,9 @@ import ( ) func TestBuild(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } // This doesn't work on windows, and for now I don't care because it is only used in CI if runtime.GOOS == "windows" { t.Skip("skipping on windows") diff --git a/internal/builddep/builddep_test.go b/internal/builddep/builddep_test.go index b5af8ba..1a2adc8 100644 --- a/internal/builddep/builddep_test.go +++ b/internal/builddep/builddep_test.go @@ -45,7 +45,6 @@ systems: require.NoError(t, err) got, err := yaml.Marshal(&cfg) require.NoError(t, err) - fmt.Println(string(got)) want := ` systems: diff --git a/internal/expecttest/expecttest.go b/internal/expecttest/expecttest.go new file mode 100644 index 0000000..1ac1983 --- /dev/null +++ b/internal/expecttest/expecttest.go @@ -0,0 +1,28 @@ +package expecttest + +import ( + "testing" + + "github.com/Netflix/go-expect" +) + +type option struct { + consoleOpts []expect.ConsoleOpt +} + +type Option func(*option) + +func WithConsoleOpt(opt ...expect.ConsoleOpt) Option { + return func(o *option) { + o.consoleOpts = append(o.consoleOpts, opt...) + } +} + +func Run( + t testing.TB, + expectFunc func(*expect.Console), + testFunc func(*expect.Console), + opts ...Option, +) bool { + return run(t, expectFunc, testFunc, opts...) +} diff --git a/internal/expecttest/expecttest_posix.go b/internal/expecttest/expecttest_posix.go new file mode 100644 index 0000000..f267693 --- /dev/null +++ b/internal/expecttest/expecttest_posix.go @@ -0,0 +1,49 @@ +//go:build !windows + +package expecttest + +import ( + "sync" + "testing" + + "github.com/Netflix/go-expect" + pseudotty "github.com/creack/pty" + "github.com/hinshun/vt10x" + "github.com/stretchr/testify/assert" +) + +func run( + t testing.TB, + expectFunc func(*expect.Console), + testFunc func(*expect.Console), + opts ...Option, +) bool { + t.Helper() + o := option{} + for _, opt := range opts { + opt(&o) + } + pty, tty, err := pseudotty.Open() + if !assert.NoError(t, err) { + return false + } + consoleOpts := append([]expect.ConsoleOpt{ + expect.WithStdout(vt10x.New(vt10x.WithWriter(tty))), + expect.WithStdin(pty), + expect.WithCloser(pty, tty), + }, o.consoleOpts...) + console, err := expect.NewConsole(consoleOpts...) + if !assert.NoError(t, err) { + return false + } + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + expectFunc(console) + }() + testFunc(console) + wg.Wait() + err = console.Close() + return assert.NoError(t, err) +} diff --git a/internal/expecttest/expecttest_windows.go b/internal/expecttest/expecttest_windows.go new file mode 100644 index 0000000..350a989 --- /dev/null +++ b/internal/expecttest/expecttest_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package expecttest + +import ( + "testing" + + "github.com/Netflix/go-expect" +) + +func run( + t testing.TB, + expectFunc func(*expect.Console), + testFunc func(*expect.Console), + opts ...Option, +) bool { + t.Skip("expecttest is not supported on Windows") + return false +}