Add a core reporting example
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Invocation reporting implementation plan
|
||||
|
||||
Status: Stages 1–15 complete. Stage 16 is next.
|
||||
Status: Stages 1–16 complete. Stage 17 is next.
|
||||
|
||||
## Objective and authority
|
||||
|
||||
@@ -610,7 +610,7 @@ module-wide `go test ./...` reaches and passes that package but cannot compile
|
||||
|
||||
## Stage 16: Maintain a core reporting consumer
|
||||
|
||||
Status: pending.
|
||||
Status: complete.
|
||||
|
||||
Primary files: [examples/core/main.go](../../examples/core/main.go) and its
|
||||
existing example tests.
|
||||
@@ -632,6 +632,14 @@ Validation: keep the example runnable offline and cover representative modes
|
||||
and failure paths with its owning suite. Use package tests for detailed cause
|
||||
identity and delivery states rather than expanding the example into a framework.
|
||||
|
||||
Completion: migrated the core example command to the invocation reporter and
|
||||
added a deterministic reporting command with result, warning, info, progress,
|
||||
debug, recovery, failure, and partial-output scenarios. Its tests cover ordinary,
|
||||
progress, verbose, debug, and quiet output, plus cause identity and terminal
|
||||
explanations. `GOTOOLCHAIN=local go test -count=1 ./examples/core` and the debug
|
||||
demo pass; the partial-output demo prints its completed section and terminal
|
||||
explanation, then exits with status 1 as expected.
|
||||
|
||||
## Stage 17: Migrate maintained integration consumers
|
||||
|
||||
Status: pending.
|
||||
|
||||
@@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
@@ -14,6 +16,16 @@ type greetInput struct {
|
||||
Tags commandkit.Supplied[[]string]
|
||||
}
|
||||
|
||||
type reportInput struct {
|
||||
Scenario string
|
||||
}
|
||||
|
||||
var (
|
||||
errRecoveredFixture = errors.New("a local read failed and was recovered")
|
||||
errWarningFixture = errors.New("a local warning has a cause")
|
||||
errPartialFixture = errors.New("the local report could not be completed")
|
||||
)
|
||||
|
||||
func newApp() (*commandkit.App, error) {
|
||||
countDefault := 1
|
||||
tagDefaults := []string{"demo"}
|
||||
@@ -29,6 +41,7 @@ func newApp() (*commandkit.App, error) {
|
||||
}),
|
||||
},
|
||||
Run: func(invocation commandkit.Invocation, input greetInput) commandkit.Outcome {
|
||||
reporter := invocation.Reporter()
|
||||
count := fmt.Sprintf("default %d (not supplied)", input.Count.Value)
|
||||
if input.Count.Set {
|
||||
count = fmt.Sprintf("explicit %d", input.Count.Value)
|
||||
@@ -37,15 +50,39 @@ func newApp() (*commandkit.App, error) {
|
||||
if input.Tags.Set {
|
||||
tags = fmt.Sprintf("explicit %v", input.Tags.Value)
|
||||
}
|
||||
if _, err := fmt.Fprintf(invocation.Streams.Out, "Hello; count=%s; tags=%s; entered=%q; canonical=%q\n", count, tags, invocation.EnteredPath(), invocation.CanonicalPath()); err != nil {
|
||||
return commandkit.Failure(err)
|
||||
}
|
||||
_ = reporter.RenderOutput(func(writer io.Writer) error {
|
||||
_, err := fmt.Fprintf(writer, "Hello; count=%s; tags=%s; entered=%q; canonical=%q\n", count, tags, invocation.EnteredPath(), invocation.CanonicalPath())
|
||||
return err
|
||||
})
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
report := commandkit.NewCommand(commandkit.CommandSpec[reportInput]{
|
||||
Name: "report",
|
||||
Summary: "Demonstrate invocation reporting with local scenarios.",
|
||||
NewInput: func() reportInput {
|
||||
return reportInput{Scenario: "demo"}
|
||||
},
|
||||
Arguments: []commandkit.Argument[reportInput]{
|
||||
commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "scenario", Usage: "demo, recover, warn, warn-error, fail, or partial."}, false, func(value string) (string, error) { return value, nil },
|
||||
func(input *reportInput, value commandkit.Supplied[string]) { input.Scenario = value.Value }),
|
||||
},
|
||||
CheckInput: func(input reportInput) error {
|
||||
switch input.Scenario {
|
||||
case "demo", "recover", "warn", "warn-error", "fail", "partial":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown reporting scenario %q", input.Scenario)
|
||||
}
|
||||
},
|
||||
Run: func(invocation commandkit.Invocation, input reportInput) commandkit.Outcome {
|
||||
return runReportingExample(invocation, input.Scenario)
|
||||
},
|
||||
})
|
||||
workspace := commandkit.Group("workspace", "Workspace commands.", greet)
|
||||
declarations := []commandkit.Declaration{
|
||||
workspace,
|
||||
report,
|
||||
commandkit.Alias("hello", "workspace greet"),
|
||||
commandkit.Alias("ws", "workspace"),
|
||||
commandkit.FlagAliasFor("workspace greet", "--times", "--count"),
|
||||
@@ -54,6 +91,54 @@ func newApp() (*commandkit.App, error) {
|
||||
return commandkit.New(commandkit.Identity{Name: "core-example", Version: "1.0"}, declarations...)
|
||||
}
|
||||
|
||||
func runReportingExample(invocation commandkit.Invocation, scenario string) commandkit.Outcome {
|
||||
reporter := invocation.Reporter()
|
||||
deferred := reporter.AtEnd()
|
||||
switch scenario {
|
||||
case "demo":
|
||||
_ = reporter.Progress("Loaded one local fixture.")
|
||||
_ = deferred.Warn("Optional fixture warning", func(writer io.Writer) error {
|
||||
_, err := io.WriteString(writer, "The sample contains an optional note.\n")
|
||||
return err
|
||||
})
|
||||
_ = deferred.Info("Using deterministic local data.")
|
||||
_ = deferred.Debug("Selected the built-in reporting example.")
|
||||
_ = deferred.Info("Summary", writeExampleSummary)
|
||||
_ = reporter.RenderOutput(writeExampleReport)
|
||||
return commandkit.Success()
|
||||
case "recover":
|
||||
return commandkit.Success().WithCause(errRecoveredFixture)
|
||||
case "warn":
|
||||
_ = deferred.Warn("The local operation completed with a warning.")
|
||||
return commandkit.Success()
|
||||
case "warn-error":
|
||||
_ = deferred.Warn("The local operation completed with a warning.")
|
||||
return commandkit.Success().WithCause(errWarningFixture)
|
||||
case "fail":
|
||||
return commandkit.Failure("The selected local operation could not be completed.")
|
||||
case "partial":
|
||||
_ = reporter.RenderOutput(writePartialExampleReport)
|
||||
return commandkit.Failure("The local report stopped after partial output.").WithCause(errPartialFixture)
|
||||
default:
|
||||
return commandkit.Usage(fmt.Sprintf("unknown reporting scenario %q", scenario))
|
||||
}
|
||||
}
|
||||
|
||||
func writeExampleReport(writer io.Writer) error {
|
||||
_, err := fmt.Fprintln(writer, "Prepared a local report from one fixture.")
|
||||
return err
|
||||
}
|
||||
|
||||
func writePartialExampleReport(writer io.Writer) error {
|
||||
_, err := io.WriteString(writer, "Partial local report: first section complete.\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func writeExampleSummary(writer io.Writer) error {
|
||||
_, err := io.WriteString(writer, "Processed one local fixture.\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
app, err := newApp()
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -61,3 +62,138 @@ func TestGlobalHelpAliasDoesNotRunCoreExampleCommand(t *testing.T) {
|
||||
t.Fatalf("group shortcut help outcome/output = (%#v, %q)", outcome, output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportingExampleModes(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
wantOutput []string
|
||||
wantDiag []string
|
||||
omitDiag []string
|
||||
}{
|
||||
{
|
||||
name: "ordinary result",
|
||||
args: []string{"report", "demo"},
|
||||
wantOutput: []string{"Prepared a local report from one fixture."},
|
||||
omitDiag: []string{"Optional fixture warning", "Using deterministic local data.", "Loaded one local fixture.", "DEBUG:"},
|
||||
},
|
||||
{
|
||||
name: "progress selected",
|
||||
args: []string{"report", "demo", "--progress"},
|
||||
wantOutput: []string{"Prepared a local report from one fixture."},
|
||||
wantDiag: []string{"Loaded one local fixture."},
|
||||
omitDiag: []string{"WARN:", "Using deterministic local data.", "DEBUG:"},
|
||||
},
|
||||
{
|
||||
name: "verbose messages",
|
||||
args: []string{"report", "demo", "--verbose"},
|
||||
wantOutput: []string{"Prepared a local report from one fixture."},
|
||||
wantDiag: []string{"WARN: Optional fixture warning", "The sample contains an optional note.", "Using deterministic local data.", "Summary", "Processed one local fixture."},
|
||||
omitDiag: []string{"Loaded one local fixture.", "DEBUG:"},
|
||||
},
|
||||
{
|
||||
name: "debug messages",
|
||||
args: []string{"report", "demo", "--debug"},
|
||||
wantOutput: []string{"Prepared a local report from one fixture."},
|
||||
wantDiag: []string{"WARN: Optional fixture warning", "Using deterministic local data.", "Loaded one local fixture.", "DEBUG: Selected the built-in reporting example.", "Processed one local fixture."},
|
||||
},
|
||||
{
|
||||
name: "quiet suppresses result and messages",
|
||||
args: []string{"report", "demo", "--quiet"},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
outcome, output, diagnostics := executeExample(t, test.args...)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("Execute(%q) = %#v", test.args, outcome)
|
||||
}
|
||||
for _, fragment := range test.wantOutput {
|
||||
if !strings.Contains(output, fragment) {
|
||||
t.Errorf("output %q does not contain %q", output, fragment)
|
||||
}
|
||||
}
|
||||
for _, fragment := range test.wantDiag {
|
||||
if !strings.Contains(diagnostics, fragment) {
|
||||
t.Errorf("diagnostics %q does not contain %q", diagnostics, fragment)
|
||||
}
|
||||
}
|
||||
for _, fragment := range test.omitDiag {
|
||||
if strings.Contains(diagnostics, fragment) {
|
||||
t.Errorf("diagnostics %q unexpectedly contain %q", diagnostics, fragment)
|
||||
}
|
||||
}
|
||||
if test.name == "quiet suppresses result and messages" && (output != "" || diagnostics != "") {
|
||||
t.Errorf("quiet output/diagnostics = (%q, %q), want both empty", output, diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportingExampleScenarios(t *testing.T) {
|
||||
t.Run("silent recovery retains cause", func(t *testing.T) {
|
||||
outcome, output, diagnostics := executeExample(t, "report", "recover")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !errors.Is(outcome.Cause, errRecoveredFixture) {
|
||||
t.Fatalf("outcome = %#v, want successful recovery with original cause", outcome)
|
||||
}
|
||||
if output != "" || diagnostics != "" {
|
||||
t.Fatalf("recovery output/diagnostics = (%q, %q), want silent recovery", output, diagnostics)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("warning without cause", func(t *testing.T) {
|
||||
outcome, _, diagnostics := executeExample(t, "report", "warn", "--verbose")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Cause != nil {
|
||||
t.Fatalf("outcome = %#v, want success without cause", outcome)
|
||||
}
|
||||
if !strings.Contains(diagnostics, "WARN: The local operation completed with a warning.") {
|
||||
t.Fatalf("diagnostics = %q, want explicit warning", diagnostics)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("warning with independent cause", func(t *testing.T) {
|
||||
outcome, _, diagnostics := executeExample(t, "report", "warn-error", "--verbose")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !errors.Is(outcome.Cause, errWarningFixture) {
|
||||
t.Fatalf("outcome = %#v, want successful warning with original cause", outcome)
|
||||
}
|
||||
if !strings.Contains(diagnostics, "WARN: The local operation completed with a warning.") || strings.Contains(diagnostics, errWarningFixture.Error()) {
|
||||
t.Fatalf("diagnostics = %q, want warning text without cause text", diagnostics)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("terminal explanation without cause", func(t *testing.T) {
|
||||
outcome, _, diagnostics := executeExample(t, "report", "fail", "--quiet")
|
||||
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status == 0 || outcome.Cause != nil {
|
||||
t.Fatalf("outcome = %#v, want terminal failure without cause", outcome)
|
||||
}
|
||||
if outcome.Explanation.Message != "The selected local operation could not be completed." || !strings.Contains(diagnostics, outcome.Explanation.Message) {
|
||||
t.Fatalf("explanation/diagnostics = (%q, %q)", outcome.Explanation.Message, diagnostics)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("partial output keeps cause independent", func(t *testing.T) {
|
||||
outcome, output, diagnostics := executeExample(t, "report", "partial")
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, errPartialFixture) {
|
||||
t.Fatalf("outcome = %#v, want terminal failure with original cause", outcome)
|
||||
}
|
||||
if !strings.Contains(output, "Partial local report: first section complete.") {
|
||||
t.Fatalf("output = %q, want completed first section", output)
|
||||
}
|
||||
if outcome.Explanation.Message != "The local report stopped after partial output." || !strings.Contains(diagnostics, outcome.Explanation.Message) {
|
||||
t.Fatalf("explanation/diagnostics = (%q, %q)", outcome.Explanation.Message, diagnostics)
|
||||
}
|
||||
if strings.Contains(diagnostics, errPartialFixture.Error()) {
|
||||
t.Errorf("diagnostics %q unexpectedly contain cause text", diagnostics)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func executeExample(t *testing.T, args ...string) (commandkit.Outcome, string, string) {
|
||||
t.Helper()
|
||||
app, err := newApp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output, diagnostics bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: &diagnostics})
|
||||
return outcome, output.String(), diagnostics.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user