refactor cmd: thin main, testable run(io.Writer), AllStrategies in engine
The strategy list moves into the engine (AllStrategies), where the API and frontend will read it too: one source of truth, per the handoff. main shrinks to the standard Go shell pattern: all work happens in run(out io.Writer) error; main only maps the error to stderr and the exit code. Writing to an interface instead of stdout is what lets main_test.go capture output in a bytes.Buffer. errcheck flagged every unchecked Fprintf, so formatting became pure Sprintf string building with one checked write at the end: nicer than discarding four errors with '_, _ ='.
This commit is contained in:
parent
e73db0bdd4
commit
2a5b78cb9a
3 changed files with 63 additions and 20 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
21
cmd/spreadlab/main_test.go
Normal file
21
cmd/spreadlab/main_test.go
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue