diff --git a/README.md b/README.md index b970217..ccb1d64 100644 --- a/README.md +++ b/README.md @@ -102,18 +102,26 @@ morphic compile openapi.yaml -o api.ir.json # ...or to a file ``` usage: - morphic compile [-o out.json] [--fail-on error|warning] [--skip-validate] + morphic [flags] + morphic compile [flags] ``` +`morphic`, `morphic help`, and `morphic` with a help flag (`-h`, `--help` or `-help`) print the +command list. `morphic help compile` and `morphic compile --help` print a command's flags. Help +always prints to stdout and exits `0`. + +The flags below are `compile`'s: + | Flag | Meaning | |---|---| | `-o ` | Write IR JSON to `` instead of stdout. | | `--fail-on error\|warning` | Exit non-zero when a diagnostic at or above this severity is emitted (default `error`). | | `--skip-validate` | Skip the referential-integrity `validate` pass. | +| `--explain ` | Report what compiling produced at this source coordinate instead of writing the document. | Diagnostics print one per line as ` #: `. Exit codes: -`0` clean, `1` a diagnostic reached the `--fail-on` threshold (or the spec could not be lowered), -`2` a usage or I/O error. +`0` clean (and for any help request), `1` a diagnostic reached the `--fail-on` threshold (or the +spec could not be lowered), `2` a usage or I/O error. ### Library diff --git a/cmd/morphic/help_test.go b/cmd/morphic/help_test.go index f421ac9..5fda7ea 100644 --- a/cmd/morphic/help_test.go +++ b/cmd/morphic/help_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/dexpace/morphic/internal/testspec" + "github.com/dexpace/morphic/ir/irtest" ) func TestRun_HelpForms(t *testing.T) { @@ -131,3 +132,63 @@ func TestRootHelp_ListsEveryCommand(t *testing.T) { assert.Contains(t, got, c.summary) } } + +// compareHelpGolden compares got against the golden file at path, or rewrites +// it when -update is passed to go test. +// +// The -update flag comes from irtest rather than being declared here. A second +// flag.Bool("update", ...) in this package would not merely shadow the first: +// both register on the same command-line FlagSet, so the binary panics at init +// the moment anything pulls irtest into these tests. +func compareHelpGolden(t *testing.T, path, got string) { + t.Helper() + + if irtest.Update() { + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(got), 0o644)) + return + } + + want, err := os.ReadFile(path) + require.NoError(t, err, "read golden %s (run with -update to create)", path) + assert.Empty(t, cmp.Diff(string(want), got), "golden mismatch for %s", path) +} + +// TestHelp_MatchesGolden pins every text the CLI renders. The two help texts +// are the ones a user asks for; the two misuse texts are the ones a user gets +// by accident, and nothing else holds their wording — writeCommandUsage and the +// root help that misuse prints to stderr could otherwise drift in silence. +// +// The wantStderr column doubles as the stream-discipline assertion: help must +// never reach stderr, misuse must never reach stdout, and whichever stream is +// not under test must be empty. +func TestHelp_MatchesGolden(t *testing.T) { + // Not parallel: the -update path writes files. + tests := []struct { + name string + args []string + golden string + wantCode int + wantStderr bool + }{ + {"root", nil, "root-help.txt", 0, false}, + {"compile", []string{"help", "compile"}, "compile-help.txt", 0, false}, + {"compile misuse", []string{"compile"}, "compile-usage.txt", 2, true}, + {"unknown command", []string{"bogus"}, "unknown-command.txt", 2, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + require.Equal(t, tt.wantCode, run(tt.args, &stdout, &stderr)) + + got, quiet := stdout.String(), stderr.String() + if tt.wantStderr { + got, quiet = quiet, got + } + require.Empty(t, quiet, "nothing may reach the stream this text does not use") + + compareHelpGolden(t, filepath.Join("testdata", tt.golden), got) + }) + } +} diff --git a/cmd/morphic/testdata/compile-help.txt b/cmd/morphic/testdata/compile-help.txt new file mode 100644 index 0000000..115d6b1 --- /dev/null +++ b/cmd/morphic/testdata/compile-help.txt @@ -0,0 +1,19 @@ +usage: + morphic compile [flags] + +Lower an API spec (OpenAPI 3.x) into Morphic IR JSON on stdout, and write +diagnostics to stderr. + +--explain reports what compiling produced at one source coordinate — the +type node interned there, the coordinates interned beneath it, and the +diagnostics stamped at it — instead of writing the document. + +flags: + -explain string + report what compiling produced at this source pointer instead of writing IR JSON + -fail-on string + fail (exit 1) on diagnostics at or above this severity: error|warning (default "error") + -o string + write IR JSON to this file instead of stdout + -skip-validate + skip the referential-integrity validate pass diff --git a/cmd/morphic/testdata/compile-usage.txt b/cmd/morphic/testdata/compile-usage.txt new file mode 100644 index 0000000..5c00ad9 --- /dev/null +++ b/cmd/morphic/testdata/compile-usage.txt @@ -0,0 +1,4 @@ +morphic: compile requires exactly one spec file +usage: + morphic compile [flags] +run "morphic help compile" for details. diff --git a/cmd/morphic/testdata/root-help.txt b/cmd/morphic/testdata/root-help.txt new file mode 100644 index 0000000..d753569 --- /dev/null +++ b/cmd/morphic/testdata/root-help.txt @@ -0,0 +1,9 @@ +usage: + morphic [flags] + +morphic lowers an API spec into Morphic IR. + +commands: + compile lower an API spec (OpenAPI 3.x) into Morphic IR JSON + +run "morphic help " for command details. diff --git a/cmd/morphic/testdata/unknown-command.txt b/cmd/morphic/testdata/unknown-command.txt new file mode 100644 index 0000000..8210934 --- /dev/null +++ b/cmd/morphic/testdata/unknown-command.txt @@ -0,0 +1,10 @@ +morphic: unknown command "bogus" +usage: + morphic [flags] + +morphic lowers an API spec into Morphic IR. + +commands: + compile lower an API spec (OpenAPI 3.x) into Morphic IR JSON + +run "morphic help " for command details.