241 lines
9.4 KiB
Go
241 lines
9.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCommandHelpSpellingsWriteUsageToStdout(t *testing.T) {
|
|
tests := [][]string{nil, {"help"}, {"--help"}, {"-h"}}
|
|
for _, args := range tests {
|
|
name := "no arguments"
|
|
if len(args) > 0 {
|
|
name = args[0]
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
code := RunWithOptions(args, &stdout, &stderr, commandContractOptions(t))
|
|
if code != 0 || !strings.Contains(stdout.String(), "Usage:") || stderr.Len() != 0 {
|
|
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandSyntaxErrorsUseStderrAndExitTwo(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{name: "unknown command", args: []string{"unknown"}, want: "unknown command"},
|
|
{name: "missing config subcommand", args: []string{"config"}, want: "config requires a subcommand"},
|
|
{name: "unknown pipelines subcommand", args: []string{"pipelines", "unknown"}, want: "unknown pipelines subcommand"},
|
|
{name: "malformed run flag", args: []string{"run", "demo", "--chunk_cache", "invalid"}, want: "not supported"},
|
|
{name: "unknown flag", args: []string{"config", "validate", "--unknown"}, want: "flag provided but not defined"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
code := RunWithOptions(tt.args, &stdout, &stderr, commandContractOptions(t))
|
|
if code != 2 || !strings.Contains(stderr.String(), tt.want) || stdout.Len() != 0 {
|
|
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigDiscoveryPrefersExplicitPathThenEnvironment(t *testing.T) {
|
|
explicit := writeCommandConfig(t, "explicit", "alpha")
|
|
environment := writeCommandConfig(t, "environment", "beta")
|
|
lookup := func(name string) (string, bool) {
|
|
if name == "NOTARIUS_CONFIG" {
|
|
return environment, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := RunWithOptions([]string{"pipelines", "list", "--config", explicit}, &stdout, &stderr, commandContractOptionsWithLookup(t, lookup))
|
|
if code != 0 || stdout.String() != "alpha\nexplicit\n" || stderr.Len() != 0 {
|
|
t.Fatalf("explicit config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"pipelines", "list"}, &stdout, &stderr, commandContractOptionsWithLookup(t, lookup))
|
|
if code != 0 || stdout.String() != "beta\nenvironment\n" || stderr.Len() != 0 {
|
|
t.Fatalf("environment config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigDiscoveryUsesCompiledDefaultOnlyWhenAvailable(t *testing.T) {
|
|
info, statErr := os.Stat(defaultConfigPath)
|
|
if statErr != nil && !os.IsNotExist(statErr) {
|
|
t.Fatalf("stat compiled default config: %v", statErr)
|
|
}
|
|
if statErr == nil && !info.Mode().IsRegular() {
|
|
t.Skipf("compiled default config has unexpected host state: %s", info.Mode())
|
|
}
|
|
|
|
path, err := discoverConfigPath("", commandContractOptions(t))
|
|
if statErr == nil {
|
|
if err != nil || path != defaultConfigPath {
|
|
t.Fatalf("discoverConfigPath() = %q, %v; want compiled default", path, err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "config file not found") {
|
|
t.Fatalf("discoverConfigPath() error = %v, want documented not-found context", err)
|
|
}
|
|
}
|
|
|
|
func TestConfigLoadingFailuresReturnOneWithPathContext(t *testing.T) {
|
|
missing := filepath.Join(t.TempDir(), "missing.yml")
|
|
var stdout, stderr bytes.Buffer
|
|
code := RunWithOptions([]string{"config", "validate", "--config", missing}, &stdout, &stderr, commandContractOptions(t))
|
|
if code != 1 || !strings.Contains(stderr.String(), missing) || stdout.Len() != 0 {
|
|
t.Fatalf("missing config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
malformed := filepath.Join(t.TempDir(), "malformed.yml")
|
|
if err := os.WriteFile(malformed, []byte("version: [\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"config", "validate", "--config", malformed}, &stdout, &stderr, commandContractOptions(t))
|
|
if code != 1 || !strings.Contains(stderr.String(), malformed) || !strings.Contains(stderr.String(), "parse config file") || stdout.Len() != 0 {
|
|
t.Fatalf("malformed config: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigValidateResolvesPipelineAndChecksSelection(t *testing.T) {
|
|
configPath := writeResolvableCommandConfig(t)
|
|
options := commandContractOptions(t)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "demo", "--only", "spells"}, &stdout, &stderr, options)
|
|
if code != 0 || !strings.Contains(stdout.String(), "valid for pipeline \"demo\"") || stderr.Len() != 0 {
|
|
t.Fatalf("valid resolution: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "missing"}, &stdout, &stderr, options)
|
|
if code != 1 || !strings.Contains(stderr.String(), "pipeline \"missing\"") {
|
|
t.Fatalf("unknown pipeline: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "demo", "--only", "missing"}, &stdout, &stderr, options)
|
|
if code != 1 || !strings.Contains(stderr.String(), "lane \"missing\"") {
|
|
t.Fatalf("unknown lane: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--only", "spells"}, &stdout, &stderr, options)
|
|
if code != 2 || !strings.Contains(stderr.String(), "--only requires --pipeline") {
|
|
t.Fatalf("missing pipeline for only: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"config", "validate", "--config", configPath, "--pipeline", "demo", "--only", "spells,,other"}, &stdout, &stderr, options)
|
|
if code != 2 || !strings.Contains(stderr.String(), "--only must contain") {
|
|
t.Fatalf("malformed only: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestPipelinesListSortsNormalizedIDsInTextAndJSON(t *testing.T) {
|
|
configPath := writeCommandConfig(t, " zeta ", "alpha")
|
|
options := commandContractOptions(t)
|
|
var stdout, stderr bytes.Buffer
|
|
code := RunWithOptions([]string{"pipelines", "list", "--config", configPath}, &stdout, &stderr, options)
|
|
if code != 0 || stdout.String() != "alpha\nzeta\n" || stderr.Len() != 0 {
|
|
t.Fatalf("text list: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"pipelines", "list", "--config", configPath, "--json"}, &stdout, &stderr, options)
|
|
var payload struct {
|
|
Pipelines []string `json:"pipelines"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
|
t.Fatalf("JSON list = %q: %v", stdout.String(), err)
|
|
}
|
|
if code != 0 || len(payload.Pipelines) != 2 || payload.Pipelines[0] != "alpha" || payload.Pipelines[1] != "zeta" || stderr.Len() != 0 {
|
|
t.Fatalf("JSON list: code=%d payload=%#v stderr=%q", code, payload, stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRemovedStructuralFlagsAndRuntimeFailuresKeepExitClasses(t *testing.T) {
|
|
configPath := writeResolvableCommandConfig(t)
|
|
options := commandContractOptions(t)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := RunWithOptions([]string{"run", "demo", "--input", "missing-input", "--config", configPath, "--diagnostics-dir", t.TempDir()}, &stdout, &stderr, options)
|
|
if code != 2 || !strings.Contains(stderr.String(), "flag provided but not defined") {
|
|
t.Fatalf("removed flag: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = RunWithOptions([]string{"run", "missing", "--input", "missing-input", "--config", configPath, "--chunk_cache", "bypass"}, &stdout, &stderr, options)
|
|
if code != 1 || !strings.Contains(stderr.String(), "pipeline \"missing\"") || stdout.Len() != 0 {
|
|
t.Fatalf("valid-runtime failure: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func commandContractOptions(t *testing.T) Options {
|
|
return commandContractOptionsWithLookup(t, emptyLookup)
|
|
}
|
|
|
|
func commandContractOptionsWithLookup(t *testing.T, lookup func(string) (string, bool)) Options {
|
|
t.Helper()
|
|
components, err := newProductionComponents()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return Options{
|
|
Catalog: catalogFromRegistries(components.registries),
|
|
Registries: components.registries,
|
|
LookupEnv: lookup,
|
|
}
|
|
}
|
|
|
|
func writeCommandConfig(t *testing.T, firstID, secondID string) string {
|
|
t.Helper()
|
|
content := fmt.Sprintf("version: 4\npipelines:\n %q:\n input: seriatim\n %q:\n input: seriatim\n", firstID, secondID)
|
|
return writeCommandConfigContent(t, content)
|
|
}
|
|
|
|
func writeResolvableCommandConfig(t *testing.T) string {
|
|
t.Helper()
|
|
return writeCommandConfigContent(t, `version: 4
|
|
pipelines:
|
|
demo:
|
|
input: seriatim
|
|
artifacts:
|
|
spells:
|
|
extract: dnd/spells
|
|
`)
|
|
}
|
|
|
|
func writeCommandConfigContent(t *testing.T, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "config.yml")
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|