diff --git a/cmd/spreadlab/main.go b/cmd/spreadlab/main.go index 1ed00e8..62266ac 100644 --- a/cmd/spreadlab/main.go +++ b/cmd/spreadlab/main.go @@ -5,31 +5,47 @@ package main import ( "fmt" + "io" "os" + "strings" "github.com/JustinZeus/spreadlab/internal/engine" ) +// main stays a thin shell: the work lives in run, which takes its output +// as an io.Writer so tests can capture it. func main() { - config := engine.DefaultConfig() - fmt.Printf("spreadlab: %d students, educate %d, forwarding probability %.2f\n", - config.NumStudents, config.NumEducated, config.ForwardProb) - fmt.Println("(illustrative, not validated)") - fmt.Println() - - strategies := []engine.Strategy{ - engine.StrategyNone, - engine.StrategyRandom, - engine.StrategyMostConnected, - } - for _, strategy := range strategies { - result, err := engine.RunScenario(config, strategy) - if err != nil { - fmt.Fprintln(os.Stderr, "spreadlab:", err) - os.Exit(1) - } - fmt.Printf("%-15s educated=%3d reached=%3d/%d (%2.0f%%) in %d rounds\n", - result.Strategy, len(result.Educated), result.NumReached, - config.NumStudents, result.ReachedPct, result.NumRounds) + if err := run(os.Stdout); err != nil { + fmt.Fprintln(os.Stderr, "spreadlab:", err) + os.Exit(1) } } + +// run executes the three-scenario comparison in the default world and +// writes the table to out. Formatting is pure string building; the single +// write at the end is the only error to handle. +func run(out io.Writer) error { + config := engine.DefaultConfig() + lines := []string{ + fmt.Sprintf("spreadlab: %d students, educate %d, forwarding probability %.2f", + config.NumStudents, config.NumEducated, config.ForwardProb), + "(illustrative, not validated)", + "", + } + for _, strategy := range engine.AllStrategies() { + result, err := engine.RunScenario(config, strategy) + if err != nil { + return err + } + lines = append(lines, formatResult(config, result)) + } + _, err := fmt.Fprintln(out, strings.Join(lines, "\n")) + return err +} + +// formatResult renders one scenario's outcome as a table row. +func formatResult(config engine.Config, result engine.Result) string { + return fmt.Sprintf("%-15s educated=%3d reached=%3d/%d (%2.0f%%) in %d rounds", + result.Strategy, len(result.Educated), result.NumReached, + config.NumStudents, result.ReachedPct, result.NumRounds) +} diff --git a/cmd/spreadlab/main_test.go b/cmd/spreadlab/main_test.go new file mode 100644 index 0000000..e882ba4 --- /dev/null +++ b/cmd/spreadlab/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "bytes" + "strings" + "testing" +) + +// run writes to an io.Writer instead of straight to stdout precisely so +// this test can hand it a buffer and inspect the output. +func TestRunPrintsAllScenarios(t *testing.T) { + var output bytes.Buffer + if err := run(&output); err != nil { + t.Fatal(err) + } + for _, want := range []string{"none", "random", "most-connected", "illustrative"} { + if !strings.Contains(output.String(), want) { + t.Errorf("output is missing %q:\n%s", want, output.String()) + } + } +} diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 2e29bd2..e229cdd 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -11,6 +11,12 @@ const ( StrategyMostConnected Strategy = "most-connected" ) +// AllStrategies lists every strategy in display order. The CLI, and later +// the API and frontend, read this one list; nothing redefines it. +func AllStrategies() []Strategy { + return []Strategy{StrategyNone, StrategyRandom, StrategyMostConnected} +} + // Config fully describes one simulation world. It is the single source of // truth for parameters: API and frontend types will be generated from the // structs in this file, never redefined by hand. Identical configs produce