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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/JustinZeus/spreadlab/internal/engine"
|
"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() {
|
func main() {
|
||||||
config := engine.DefaultConfig()
|
if err := run(os.Stdout); err != nil {
|
||||||
fmt.Printf("spreadlab: %d students, educate %d, forwarding probability %.2f\n",
|
fmt.Fprintln(os.Stderr, "spreadlab:", err)
|
||||||
config.NumStudents, config.NumEducated, config.ForwardProb)
|
os.Exit(1)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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"
|
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
|
// Config fully describes one simulation world. It is the single source of
|
||||||
// truth for parameters: API and frontend types will be generated from the
|
// truth for parameters: API and frontend types will be generated from the
|
||||||
// structs in this file, never redefined by hand. Identical configs produce
|
// structs in this file, never redefined by hand. Identical configs produce
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue