Refactor exit code logic and remove Verbose flag
This commit is contained in:
parent
c7730bcaa6
commit
909609f21f
14
cmd.go
14
cmd.go
|
|
@ -44,7 +44,7 @@ func testCmd() *cobra.Command {
|
||||||
Short: "Run .shout test files",
|
Short: "Run .shout test files",
|
||||||
Args: cobra.ArbitraryArgs,
|
Args: cobra.ArbitraryArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
timeoutDur, err := parseDuration(timeout)
|
timeoutDur, err := time.ParseDuration(timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +151,6 @@ func testCmd() *cobra.Command {
|
||||||
PathDirs: pathDirs,
|
PathDirs: pathDirs,
|
||||||
EnvVars: envVars,
|
EnvVars: envVars,
|
||||||
Timeout: timeoutDur,
|
Timeout: timeoutDur,
|
||||||
Verbose: verbose,
|
|
||||||
OnCommand: onCommand,
|
OnCommand: onCommand,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -159,16 +158,7 @@ func testCmd() *cobra.Command {
|
||||||
for i := 0; i < len(setupCommands) && i < len(fileResult.Results); i++ {
|
for i := 0; i < len(setupCommands) && i < len(fileResult.Results); i++ {
|
||||||
r := fileResult.Results[i]
|
r := fileResult.Results[i]
|
||||||
sc := setupCommands[i]
|
sc := setupCommands[i]
|
||||||
ok := false
|
if !exitCodeOK(sc.ExitCodeType, sc.ExitCodeValue, r.ExitCode) {
|
||||||
switch sc.ExitCodeType {
|
|
||||||
case ExitCodeNone:
|
|
||||||
ok = r.ExitCode == 0
|
|
||||||
case ExitCodeWildcard:
|
|
||||||
ok = r.ExitCode != 0
|
|
||||||
case ExitCodeExact:
|
|
||||||
ok = r.ExitCode == sc.ExitCodeValue
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
if keep {
|
if keep {
|
||||||
fmt.Fprintln(os.Stderr, fileResult.TmpDir)
|
fmt.Fprintln(os.Stderr, fileResult.TmpDir)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
29
duration.go
29
duration.go
|
|
@ -1,29 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var durationRe = regexp.MustCompile(`^(\d+(?:\.\d+)?)(ms|s|m)$`)
|
|
||||||
|
|
||||||
func parseDuration(s string) (time.Duration, error) {
|
|
||||||
m := durationRe.FindStringSubmatch(s)
|
|
||||||
if m == nil {
|
|
||||||
return 0, fmt.Errorf("invalid duration: %s", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, _ := strconv.ParseFloat(m[1], 64)
|
|
||||||
switch m[2] {
|
|
||||||
case "ms":
|
|
||||||
return time.Duration(value * float64(time.Millisecond)), nil
|
|
||||||
case "s":
|
|
||||||
return time.Duration(value * float64(time.Second)), nil
|
|
||||||
case "m":
|
|
||||||
return time.Duration(value * float64(time.Minute)), nil
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("unknown unit: %s", m[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseDurationMs(t *testing.T) {
|
|
||||||
d, err := parseDuration("500ms")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if d != 500*time.Millisecond {
|
|
||||||
t.Errorf("got %v, want 500ms", d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDurationSeconds(t *testing.T) {
|
|
||||||
d, err := parseDuration("10s")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if d != 10*time.Second {
|
|
||||||
t.Errorf("got %v, want 10s", d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDurationDecimal(t *testing.T) {
|
|
||||||
d, err := parseDuration("1.5s")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if d != 1500*time.Millisecond {
|
|
||||||
t.Errorf("got %v, want 1.5s", d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDurationMinutes(t *testing.T) {
|
|
||||||
d, err := parseDuration("1m")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if d != time.Minute {
|
|
||||||
t.Errorf("got %v, want 1m", d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDurationInvalid(t *testing.T) {
|
|
||||||
_, err := parseDuration("abc")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for invalid duration")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDurationNoUnit(t *testing.T) {
|
|
||||||
_, err := parseDuration("10")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for missing unit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
format.go
19
format.go
|
|
@ -21,16 +21,7 @@ func evaluateFile(path string, results []CommandResult, fileError string) TestRe
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
cmd := r.Command
|
cmd := r.Command
|
||||||
outputMatches := matchOutput(cmd.Expected, r.Actual)
|
outputMatches := matchOutput(cmd.Expected, r.Actual)
|
||||||
|
exitCodeMismatch := !exitCodeOK(cmd.ExitCodeType, cmd.ExitCodeValue, r.ExitCode)
|
||||||
var exitCodeMismatch bool
|
|
||||||
switch cmd.ExitCodeType {
|
|
||||||
case ExitCodeNone:
|
|
||||||
exitCodeMismatch = r.ExitCode != 0
|
|
||||||
case ExitCodeWildcard:
|
|
||||||
exitCodeMismatch = r.ExitCode == 0
|
|
||||||
case ExitCodeExact:
|
|
||||||
exitCodeMismatch = r.ExitCode != cmd.ExitCodeValue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !outputMatches || exitCodeMismatch {
|
if !outputMatches || exitCodeMismatch {
|
||||||
var diffLines []DiffLine
|
var diffLines []DiffLine
|
||||||
|
|
@ -114,7 +105,12 @@ func formatFailure(t TestResult) string {
|
||||||
func formatSummary(results []TestResult, elapsed time.Duration) string {
|
func formatSummary(results []TestResult, elapsed time.Duration) string {
|
||||||
totalCommands := 0
|
totalCommands := 0
|
||||||
failedCommands := 0
|
failedCommands := 0
|
||||||
|
errorCount := 0
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
|
if r.Error != "" {
|
||||||
|
errorCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
totalCommands += r.CommandCount
|
totalCommands += r.CommandCount
|
||||||
failedCommands += len(r.Failures)
|
failedCommands += len(r.Failures)
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +123,9 @@ func formatSummary(results []TestResult, elapsed time.Duration) string {
|
||||||
if failedCommands > 0 {
|
if failedCommands > 0 {
|
||||||
parts = append(parts, red(fmt.Sprintf("%d failed", failedCommands)))
|
parts = append(parts, red(fmt.Sprintf("%d failed", failedCommands)))
|
||||||
}
|
}
|
||||||
|
if errorCount > 0 {
|
||||||
|
parts = append(parts, red(fmt.Sprintf("%d errored", errorCount)))
|
||||||
|
}
|
||||||
|
|
||||||
var timeStr string
|
var timeStr string
|
||||||
if elapsed < time.Second {
|
if elapsed < time.Second {
|
||||||
|
|
|
||||||
74
run.go
74
run.go
|
|
@ -24,7 +24,6 @@ type RunOptions struct {
|
||||||
PathDirs []string
|
PathDirs []string
|
||||||
EnvVars map[string]string
|
EnvVars map[string]string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
Verbose bool
|
|
||||||
OnCommand func(Command)
|
OnCommand func(Command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,28 +38,8 @@ func buildScript(commands []Command, sentinelPrefix string) string {
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSentinelOutput(raw string, commandCount int, sentinelPrefix string) (outputs [][]string, exitCodes []int) {
|
func splitSentinelBlock(s string) []string {
|
||||||
re := regexp.MustCompile(regexp.QuoteMeta(sentinelPrefix) + `(\d+)_(\d+)__`)
|
lines := strings.Split(s, "\n")
|
||||||
|
|
||||||
remaining := raw
|
|
||||||
for i := 0; i < commandCount; i++ {
|
|
||||||
loc := re.FindStringSubmatchIndex(remaining)
|
|
||||||
if loc == nil {
|
|
||||||
lines := strings.Split(remaining, "\n")
|
|
||||||
if len(lines) > 0 && lines[0] == "" {
|
|
||||||
lines = lines[1:]
|
|
||||||
}
|
|
||||||
lines = trimTrailingEmpty(lines)
|
|
||||||
outputs = append(outputs, lines)
|
|
||||||
exitCodes = append(exitCodes, 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
before := remaining[:loc[0]]
|
|
||||||
exitCodeStr := remaining[loc[2]:loc[3]]
|
|
||||||
ec, _ := strconv.Atoi(exitCodeStr)
|
|
||||||
|
|
||||||
lines := strings.Split(before, "\n")
|
|
||||||
if len(lines) > 0 && lines[0] == "" {
|
if len(lines) > 0 && lines[0] == "" {
|
||||||
lines = lines[1:]
|
lines = lines[1:]
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +47,39 @@ func parseSentinelOutput(raw string, commandCount int, sentinelPrefix string) (o
|
||||||
if len(lines) == 1 && lines[0] == "" {
|
if len(lines) == 1 && lines[0] == "" {
|
||||||
lines = nil
|
lines = nil
|
||||||
}
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
outputs = append(outputs, lines)
|
func parseSentinelOutput(raw string, commandCount int, sentinelPrefix string) (outputs [][]string, exitCodes []int) {
|
||||||
|
re := regexp.MustCompile(regexp.QuoteMeta(sentinelPrefix) + `(\d+)_(\d+)__`)
|
||||||
|
|
||||||
|
remaining := raw
|
||||||
|
for i := 0; i < commandCount; i++ {
|
||||||
|
loc := re.FindStringSubmatchIndex(remaining)
|
||||||
|
if loc == nil {
|
||||||
|
// No more sentinels — assign remaining output, mark as failed
|
||||||
|
outputs = append(outputs, splitSentinelBlock(remaining))
|
||||||
|
exitCodes = append(exitCodes, 1)
|
||||||
|
remaining = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCodeStr := remaining[loc[2]:loc[3]]
|
||||||
|
ec, _ := strconv.Atoi(exitCodeStr)
|
||||||
|
cmdIdxStr := remaining[loc[4]:loc[5]]
|
||||||
|
cmdIdx, _ := strconv.Atoi(cmdIdxStr)
|
||||||
|
before := remaining[:loc[0]]
|
||||||
|
|
||||||
|
// If sentinel belongs to a later command, this command's sentinel is missing
|
||||||
|
if cmdIdx > i {
|
||||||
|
outputs = append(outputs, splitSentinelBlock(before))
|
||||||
|
exitCodes = append(exitCodes, 1)
|
||||||
|
// Keep remaining from the sentinel onwards for the next iteration
|
||||||
|
remaining = remaining[loc[0]:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs = append(outputs, splitSentinelBlock(before))
|
||||||
exitCodes = append(exitCodes, ec)
|
exitCodes = append(exitCodes, ec)
|
||||||
|
|
||||||
afterSentinel := remaining[loc[1]:]
|
afterSentinel := remaining[loc[1]:]
|
||||||
|
|
@ -101,11 +111,8 @@ func runFile(file ShoutFile, opts RunOptions) FileResult {
|
||||||
script := buildScript(file.Commands, sentinel)
|
script := buildScript(file.Commands, sentinel)
|
||||||
|
|
||||||
// Build environment
|
// Build environment
|
||||||
var envMap map[string]string
|
envMap := make(map[string]string)
|
||||||
if opts.CleanEnv {
|
if !opts.CleanEnv {
|
||||||
envMap = make(map[string]string)
|
|
||||||
} else {
|
|
||||||
envMap = make(map[string]string)
|
|
||||||
for _, e := range os.Environ() {
|
for _, e := range os.Environ() {
|
||||||
if k, v, ok := strings.Cut(e, "="); ok {
|
if k, v, ok := strings.Cut(e, "="); ok {
|
||||||
envMap[k] = v
|
envMap[k] = v
|
||||||
|
|
@ -161,6 +168,12 @@ func runFile(file ShoutFile, opts RunOptions) FileResult {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if opts.OnCommand != nil {
|
||||||
|
for _, c := range file.Commands {
|
||||||
|
opts.OnCommand(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = io.WriteString(stdin, script)
|
_, _ = io.WriteString(stdin, script)
|
||||||
stdin.Close()
|
stdin.Close()
|
||||||
|
|
||||||
|
|
@ -198,9 +211,6 @@ func runFile(file ShoutFile, opts RunOptions) FileResult {
|
||||||
|
|
||||||
results := make([]CommandResult, len(file.Commands))
|
results := make([]CommandResult, len(file.Commands))
|
||||||
for i, c := range file.Commands {
|
for i, c := range file.Commands {
|
||||||
if opts.Verbose && opts.OnCommand != nil {
|
|
||||||
opts.OnCommand(c)
|
|
||||||
}
|
|
||||||
actual := outputs[i]
|
actual := outputs[i]
|
||||||
if actual == nil {
|
if actual == nil {
|
||||||
actual = []string{}
|
actual = []string{}
|
||||||
|
|
|
||||||
13
types.go
13
types.go
|
|
@ -9,6 +9,19 @@ const (
|
||||||
ExitCodeWildcard // [*] — expect any non-zero
|
ExitCodeWildcard // [*] — expect any non-zero
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func exitCodeOK(ecType ExitCodeType, ecValue int, actual int) bool {
|
||||||
|
switch ecType {
|
||||||
|
case ExitCodeNone:
|
||||||
|
return actual == 0
|
||||||
|
case ExitCodeWildcard:
|
||||||
|
return actual != 0
|
||||||
|
case ExitCodeExact:
|
||||||
|
return actual == ecValue
|
||||||
|
default:
|
||||||
|
return actual == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Line int
|
Line int
|
||||||
Raw string
|
Raw string
|
||||||
|
|
|
||||||
20
update.go
20
update.go
|
|
@ -43,16 +43,6 @@ func rewriteFile(file ShoutFile, results []CommandResult, originalContent string
|
||||||
|
|
||||||
oldExpectedRaw := lines[i+1 : j]
|
oldExpectedRaw := lines[i+1 : j]
|
||||||
|
|
||||||
// Check for exit code marker
|
|
||||||
oldTrimmed := trimTrailingEmpty(oldExpectedRaw)
|
|
||||||
var oldExitMarker string
|
|
||||||
if len(oldTrimmed) > 0 {
|
|
||||||
last := oldTrimmed[len(oldTrimmed)-1]
|
|
||||||
if exitCodeMarkerRe.MatchString(last) {
|
|
||||||
oldExitMarker = last
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count trailing blank lines
|
// Count trailing blank lines
|
||||||
trailingBlanks := 0
|
trailingBlanks := 0
|
||||||
for k := len(oldExpectedRaw) - 1; k >= 0; k-- {
|
for k := len(oldExpectedRaw) - 1; k >= 0; k-- {
|
||||||
|
|
@ -68,12 +58,12 @@ func rewriteFile(file ShoutFile, results []CommandResult, originalContent string
|
||||||
output = append(output, oldExpectedRaw...)
|
output = append(output, oldExpectedRaw...)
|
||||||
} else {
|
} else {
|
||||||
output = append(output, result.Actual...)
|
output = append(output, result.Actual...)
|
||||||
if oldExitMarker != "" {
|
if result.ExitCode != 0 {
|
||||||
output = append(output, oldExitMarker)
|
output = append(output, fmt.Sprintf("[%d]", result.ExitCode))
|
||||||
} else if len(result.Actual) > 0 && exitCodeMarkerRe.MatchString(result.Actual[len(result.Actual)-1]) {
|
} else if len(result.Actual) > 0 && exitCodeMarkerRe.MatchString(result.Actual[len(result.Actual)-1]) {
|
||||||
// Actual output's last line looks like an exit code marker.
|
// Actual output's last line looks like an exit code marker.
|
||||||
// Add an explicit marker to prevent parser from consuming it as one.
|
// Add an explicit [0] to prevent parser from consuming it.
|
||||||
output = append(output, fmt.Sprintf("[%d]", result.ExitCode))
|
output = append(output, "[0]")
|
||||||
}
|
}
|
||||||
for k := 0; k < trailingBlanks; k++ {
|
for k := 0; k < trailingBlanks; k++ {
|
||||||
output = append(output, "")
|
output = append(output, "")
|
||||||
|
|
@ -82,7 +72,7 @@ func rewriteFile(file ShoutFile, results []CommandResult, originalContent string
|
||||||
|
|
||||||
i = j - 1
|
i = j - 1
|
||||||
cmdIdx++
|
cmdIdx++
|
||||||
} else if cmdIdx == 0 {
|
} else if cmdIdx == 0 || cmdIdx >= len(file.Commands) {
|
||||||
output = append(output, line)
|
output = append(output, line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user