test(01-04): cover tool, output, watch loop and workspace modules
- Non-TTY widgets/prompts, flag parsing, secret non-leak and malicious IDs - Real hello workspace rebuild latency line, debounce and ignored bin/tmp - scripts/check-phase1.sh runs vet/test/race across root, hello, base, greeter, optional
This commit is contained in:
@@ -2,6 +2,7 @@ package bonfire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -137,6 +138,112 @@ func TestKernelAndNamespacedCommands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagAndArgumentParsing(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
var gotArgs []string
|
||||
var gotTarget, gotMode, gotUnset string
|
||||
var gotTargetOK, gotModeOK, gotUnsetOK bool
|
||||
root, err := NewRootIO("app", []Command{{
|
||||
Name: "demo:run",
|
||||
Description: "Run a demo",
|
||||
Args: []Arg{
|
||||
{Name: "target", Description: "who", Required: true},
|
||||
{Name: "extra", Description: "optional"},
|
||||
},
|
||||
Flags: []Flag{
|
||||
{Name: "mode", Shorthand: "m", Default: "slow", Description: "speed"},
|
||||
{Name: "unused", Default: "", Description: "empty default"},
|
||||
},
|
||||
Run: func(ctx context.Context, in Input, out Output) error {
|
||||
gotArgs = append([]string(nil), in.Args()...)
|
||||
gotTarget, gotTargetOK = in.Argument("target")
|
||||
gotMode, gotModeOK = in.Flag("mode")
|
||||
gotUnset, gotUnsetOK = in.Flag("unused")
|
||||
out.Println("ran " + gotTarget)
|
||||
return nil
|
||||
},
|
||||
}}, strings.NewReader(""), &buf, &buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root.SetArgs([]string{"demo:run", "-m", "fast", "world"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "ran world") {
|
||||
t.Fatalf("captured output = %q", buf.String())
|
||||
}
|
||||
if strings.Join(gotArgs, ",") != "world" {
|
||||
t.Fatalf("Args = %v", gotArgs)
|
||||
}
|
||||
if !gotTargetOK || gotTarget != "world" {
|
||||
t.Fatalf("Argument(target) = (%q, %v)", gotTarget, gotTargetOK)
|
||||
}
|
||||
if !gotModeOK || gotMode != "fast" {
|
||||
t.Fatalf("Flag(mode) = (%q, %v)", gotMode, gotModeOK)
|
||||
}
|
||||
if gotUnsetOK || gotUnset != "" {
|
||||
t.Fatalf("unset flag = (%q, %v)", gotUnset, gotUnsetOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectedOutputCapture(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
out := NewOutput(strings.NewReader(""), &stdout, &stderr)
|
||||
out.Info("heads up")
|
||||
out.Success("done")
|
||||
out.Warning("careful")
|
||||
out.Error("boom")
|
||||
out.Printf("plain %s\n", "line")
|
||||
if !strings.Contains(stdout.String(), "heads up") || !strings.Contains(stdout.String(), "done") || !strings.Contains(stdout.String(), "careful") {
|
||||
t.Fatalf("stdout = %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "plain line") {
|
||||
t.Fatalf("Printf missing from stdout: %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "boom") {
|
||||
t.Fatalf("Error should go to stderr, got %q", stderr.String())
|
||||
}
|
||||
if strings.Contains(stdout.String(), "boom") {
|
||||
t.Fatalf("Error leaked to stdout: %q", stdout.String())
|
||||
}
|
||||
if strings.Contains(stdout.String()+stderr.String(), "\x1b[") {
|
||||
t.Fatalf("non-tty messages have ansi: stdout=%q stderr=%q", stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonTTYSpinnerError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
out := NewOutput(strings.NewReader(""), &buf, &buf)
|
||||
err := out.Spinner("building", func() error { return errors.New("compile failed") })
|
||||
if err == nil || err.Error() != "compile failed" {
|
||||
t.Fatalf("spinner err = %v", err)
|
||||
}
|
||||
if got := buf.String(); got != "[...] building\n" {
|
||||
t.Fatalf("spinner = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonTTYProgressSkipsIntraDecile(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
out := NewOutput(strings.NewReader(""), &buf, &buf)
|
||||
if err := out.Progress(20, func(p Progress) error {
|
||||
p.Advance(1) // 5%
|
||||
p.Advance(1) // 10%
|
||||
p.Advance(1) // 15%
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := buf.String()
|
||||
if !strings.Contains(got, "[2/20] 10%") {
|
||||
t.Fatalf("missing 10%% step in %q", got)
|
||||
}
|
||||
if strings.Contains(got, "[1/20]") || strings.Contains(got, "[3/20]") {
|
||||
t.Fatalf("intra-decile progress leaked: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpUsesSharedAdapter(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
root, err := NewRoot("hello", []Command{{
|
||||
|
||||
@@ -2,6 +2,8 @@ package bonfire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -73,6 +75,58 @@ func TestSecretReadsPlainStdinLine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChoiceEOFUsesDefault(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
out := NewOutput(strings.NewReader(""), &buf, &buf)
|
||||
got, err := out.Choice("pick", []string{"a", "b", "c"}, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "c" {
|
||||
t.Fatalf("choice eof = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChoiceEmptyOptionsError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
out := NewOutput(strings.NewReader("1\n"), &buf, &buf)
|
||||
_, err := out.Choice("pick", nil, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected empty choice error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretNotLeakedOnCommandError(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
root, err := NewRootIO("app", []Command{{
|
||||
Name: "auth:login",
|
||||
Description: "login",
|
||||
Run: func(ctx context.Context, in Input, out Output) error {
|
||||
secret, err := out.Secret("token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if secret == "" {
|
||||
return errors.New("empty token")
|
||||
}
|
||||
out.Error("login failed")
|
||||
return errors.New("login failed")
|
||||
},
|
||||
}}, strings.NewReader("s3cret\n"), &stdout, &stderr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root.SetArgs([]string{"auth:login"})
|
||||
runErr := root.Execute()
|
||||
if runErr == nil {
|
||||
t.Fatal("expected command error")
|
||||
}
|
||||
combined := stdout.String() + stderr.String() + runErr.Error()
|
||||
if strings.Contains(combined, "s3cret") {
|
||||
t.Fatalf("secret leaked into streams: stdout=%q stderr=%q err=%v", stdout.String(), stderr.String(), runErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClosedStdinDoesNotHang(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
out := NewOutput(io.NopCloser(strings.NewReader("")), &buf, &buf)
|
||||
|
||||
Reference in New Issue
Block a user