From cc7ba659adbc5ad55e1ce67f76952f2b8392c9c9 Mon Sep 17 00:00:00 2001
From: Flu0r1ne <flur01ne@flu0r1ne.net>
Date: Sat, 4 Sep 2021 15:38:30 -0500
Subject: Refactor build/eval pipeline to use clearer IO model and adapter
 segmentation methods

---
 adapters.go               |   4 +-
 adapters/gtest/adapter.go |  44 ++++++++++----------
 adapters/gtest/config.go  |  18 ++++-----
 cmd/planr/main.go         |   2 +-
 cmd/planr/sub/build.go    |   3 +-
 cmd/planr/sub/evaluate.go |   3 +-
 runner.go                 | 100 ++++++++++++++++++++++++++++------------------
 runner_builder.go         |   9 +++--
 8 files changed, 107 insertions(+), 76 deletions(-)

diff --git a/adapters.go b/adapters.go
index f4e53ce..b7c4b27 100644
--- a/adapters.go
+++ b/adapters.go
@@ -14,10 +14,10 @@ type Adapter interface {
   Init(dirs DirConfig)
 
   // Called once to preform expensive code generation
-  Build(testCase []*TestCase)
+  Build(testCase []TestCase)
 
   // Called every time source changes
-  Evaluate(testCase []*TestCase)
+  Evaluate(testCase []TestCase) []TestCase
 }
 
 // A parser function takes a blob of TOML and decodes it into
diff --git a/adapters/gtest/adapter.go b/adapters/gtest/adapter.go
index 0a955ef..7033c7f 100644
--- a/adapters/gtest/adapter.go
+++ b/adapters/gtest/adapter.go
@@ -16,8 +16,8 @@ import (
 
 const GTEST_CMAKE = "CMakeLists.txt"
 
-func makeUnit(tc *planr.TestCase, dirs planr.DirConfig) cmakeUnit {
-  cfg := tc.AdapterConfig().(*GtestConfig)
+func makeUnit(tc planr.TestCase, dirs planr.DirConfig) cmakeUnit {
+  cfg := tc.AdapterConfig().(*Config)
 
   testpath := path.Join(dirs.Tests(), *cfg.Testfile)
   srclist := cfg.srcList(dirs.Src())
@@ -41,7 +41,7 @@ func safeWd() string{
 
 type ResultFromId map[string] Result
 
-func (adapter *GtestAdapter) execTests(cnames []string) ResultFromId {
+func (adapter *Adapter) execTests(cnames []string) ResultFromId {
   buildDir := safeWd()
 
   lut := make(ResultFromId, 0)
@@ -89,7 +89,7 @@ func (adapter *GtestAdapter) execTests(cnames []string) ResultFromId {
 
 // An executable may contain more than one test
 // Gather all executables and deduplicate them
-func exes(tcs []*planr.TestCase) []string {
+func exes(tcs []planr.TestCase) []string {
   set := make(map[string] bool, 0)
   
   for _, tc := range tcs {
@@ -112,12 +112,12 @@ func exes(tcs []*planr.TestCase) []string {
   return exes
 }
 
-func id(tc *planr.TestCase) string {
-  cfg := tc.AdapterConfig().(*GtestConfig)
+func id(tc planr.TestCase) string {
+  cfg := tc.AdapterConfig().(*Config)
   return tc.Cname + "." + *cfg.Suite + "." + *cfg.Name
 }
 
-func compile(wg * sync.WaitGroup, tc *planr.TestCase) {
+func compile(wg * sync.WaitGroup, tc * planr.TestCase) {
   defer wg.Done()
 
   cmd := exec.Command("make", tc.Cname)
@@ -137,11 +137,11 @@ func compile(wg * sync.WaitGroup, tc *planr.TestCase) {
   tc.Result.DebugOutput = string(out)
 }
 
-type GtestAdapter struct {
+type Adapter struct {
   dirs planr.DirConfig
 }
 
-func (a *GtestAdapter) Config() planr.AdapterConfig {
+func (a *Adapter) Config() planr.AdapterConfig {
   return planr.AdapterConfig {
     Name: "gtest",
     ParseConfig: ParseConfig,
@@ -149,18 +149,18 @@ func (a *GtestAdapter) Config() planr.AdapterConfig {
   }
 }
 
-func (a *GtestAdapter) Init(dirs planr.DirConfig) {
+func (a *Adapter) Init(dirs planr.DirConfig) {
   a.dirs = dirs
 }
 
-func (adapter *GtestAdapter) Build(tcs []*planr.TestCase) {
+func (adapter Adapter) Build(tcs []planr.TestCase) {
   buildDir := safeWd()
   cmakeFile := path.Join(buildDir, GTEST_CMAKE)
 
   units := make([]cmakeUnit, 0)
   for _, tc := range tcs {
     
-    cfg := tc.AdapterConfig().(*GtestConfig)
+    cfg := tc.AdapterConfig().(*Config)
     cfg.ensureSatisfied(tc.Path)
 
     units = append(units, makeUnit(tc, adapter.dirs))
@@ -172,9 +172,10 @@ func (adapter *GtestAdapter) Build(tcs []*planr.TestCase) {
 }
 
 // ./planr eval  0.93s user 0.16s system 100% cpu 1.089 total
-func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) {
+func (adapter *Adapter) Evaluate(tcs []planr.TestCase) [] planr.TestCase {
   var wg sync.WaitGroup
-  for _, tc := range tcs {
+  for i := range tcs {
+    tc := &tcs[i]
     wg.Add(1)
     go compile(&wg, tc)
   }
@@ -183,14 +184,15 @@ func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) {
   files := exes(tcs)
   resultById := adapter.execTests(files)
 
-  for _, tc := range tcs {
-    result, ok := resultById[id(tc)]
+  for i := range tcs {
+    tc := &tcs[i]
+    result, ok := resultById[id(*tc)]
 
     // compilation failure 
     if !ok {
 
       if tc.Result.Status == planr.PASSING {
-        cfg := tc.AdapterConfig().(*GtestConfig)
+        cfg := tc.AdapterConfig().(*Config)
 
         log.Printf(
           "Could not find testcase %s with name=\"%s\" and suite=\"%s\". Does such a test exist in the test source?",
@@ -200,7 +202,7 @@ func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) {
         )
 
         tc.Result.Status = planr.COMPILATION_FAILURE
-        tc.Result.DebugOutput += fmt.Sprintf("planr: Did not find testcase %s in any test executable\n", id(tc))
+        tc.Result.DebugOutput += fmt.Sprintf("planr: Did not find testcase %s in any test executable\n", id(*tc))
       }
 
       continue
@@ -212,8 +214,10 @@ func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) {
 
     tc.Result.TestOutput = result.testOutput
   }
+
+  return tcs
 }
 
-func New() *GtestAdapter {
-  return new(GtestAdapter)
+func NewAdapter() *Adapter {
+  return new(Adapter)
 }
diff --git a/adapters/gtest/config.go b/adapters/gtest/config.go
index 457eb64..3305977 100644
--- a/adapters/gtest/config.go
+++ b/adapters/gtest/config.go
@@ -12,7 +12,7 @@ const (
   DEFAULT_TIMEOUT = 1000
 )
 
-type GtestDefaults struct {
+type Defaults struct {
   Name          *string
   Suite         *string
   Testfile      *string
@@ -20,8 +20,8 @@ type GtestDefaults struct {
   Timeout       *uint
 }
 
-func (child *GtestDefaults) Inherit(p interface{}) {
-  parent := p.(*GtestDefaults)
+func (child *Defaults) Inherit(p interface{}) {
+  parent := p.(*Defaults)
 
   if(child.Name == nil)          { child.Name = parent.Name }
   if(child.Suite == nil)         { child.Suite = parent.Suite }
@@ -31,11 +31,11 @@ func (child *GtestDefaults) Inherit(p interface{}) {
 }
 
 
-type GtestConfig struct {
-  GtestDefaults
+type Config struct {
+  Defaults
 }
 
-func (g GtestConfig) ensureSatisfied(path string) {
+func (g Config) ensureSatisfied(path string) {
   if g.Name == nil {
     log.Fatalf("\"name\" is not defined for unit: %s\n", path)
   } else if g.Suite == nil {
@@ -50,7 +50,7 @@ func (g GtestConfig) ensureSatisfied(path string) {
   }
 }
 
-func (cfg GtestConfig) srcList(srcDir string) string {
+func (cfg Config) srcList(srcDir string) string {
   var srcList string
 
   if cfg.Srcs != nil {
@@ -66,7 +66,7 @@ func (cfg GtestConfig) srcList(srcDir string) string {
 }
 
 func ParseConfig(prim toml.Primitive) (planr.InheritableConfig, error) {
-    config := GtestConfig{}
+    config := Config{}
   
     if err := toml.PrimitiveDecode(prim, &config); err != nil {
       return nil, err
@@ -76,7 +76,7 @@ func ParseConfig(prim toml.Primitive) (planr.InheritableConfig, error) {
 }
 
 func ParseDefaultConfig(prim toml.Primitive) (planr.InheritableConfig, error) {
-    config := GtestDefaults{}
+    config := Defaults{}
 
     if err := toml.PrimitiveDecode(prim, &config); err != nil {
       return nil, err
diff --git a/cmd/planr/main.go b/cmd/planr/main.go
index b2a0975..4ff57eb 100644
--- a/cmd/planr/main.go
+++ b/cmd/planr/main.go
@@ -29,7 +29,7 @@ func dieUsage() {
 
 func getConfiguredRunner() planr.Runner {
   r := planr.ConfigureRunner()
-  r  = planr.RegisterAdapter(r, gtest.New())
+  r  = planr.RegisterAdapter(r, gtest.NewAdapter())
 
   if wd, err := os.Getwd(); err == nil {
     r = planr.SetConfigDirFromTree(r, wd) 
diff --git a/cmd/planr/sub/build.go b/cmd/planr/sub/build.go
index 4a1cda9..d24bb02 100644
--- a/cmd/planr/sub/build.go
+++ b/cmd/planr/sub/build.go
@@ -6,5 +6,6 @@ import (
 
 
 func Build(runner planr.Runner, params []string) {
-  runner.Build()
+  tcs := runner.CollectCases()
+  runner.Build(tcs)
 }
diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go
index 79d377e..0366d44 100644
--- a/cmd/planr/sub/evaluate.go
+++ b/cmd/planr/sub/evaluate.go
@@ -5,7 +5,8 @@ import (
 )
 
 func Evaluate(runner planr.Runner, params []string) {
-  tcs := runner.Evaluate()
+  tcs := runner.CollectCases()
+  tcs = runner.Evaluate(tcs)
 
   earned := 0.0
   total  := 0.0
diff --git a/runner.go b/runner.go
index d3d4b08..f613d44 100644
--- a/runner.go
+++ b/runner.go
@@ -7,8 +7,8 @@ import (
 )
 
 type Runner struct {
-  adapters    []Adapter
-  dirs        DirConfig
+  adapters      map[string] Adapter
+  dirs          DirConfig
 }
 
 func (r Runner) adapterCfgs() []AdapterConfig {
@@ -21,20 +21,7 @@ func (r Runner) adapterCfgs() []AdapterConfig {
   return cgs
 }
 
-type tcTab map[string] []*TestCase
-
-func (r Runner) buildTcLUT(tcs []TestCase) tcTab {
-  m := make(tcTab, 0)
-
-  for i := range tcs {
-    tc := &tcs[i]
-    nm := *tc.Config.Adapter
-    m[nm] = append(m[nm], tc)
-  }
-
-  return m 
-}
-
+// TODO: Move into configuration parsing
 func (r Runner) checkConfig(tcs []TestCase) {
   for _, tc := range tcs {
     tc.Config.ensureSatisfied(tc.Path)
@@ -54,55 +41,90 @@ func (r Runner) setupEnv(adapter Adapter) {
   safeCd(wd)
 }
 
-func (r Runner) build(tcs []TestCase) {
+type adapterTestSet struct {
+  adapter Adapter
+  tcs     []TestCase
+}
+
+func (r Runner) groupByAdapter(tcs []TestCase) []adapterTestSet {
   r.checkConfig(tcs)
+  
+  pairs := make(map[string] adapterTestSet, 0)
 
-  tcTab := r.buildTcLUT(tcs)
+  for _, tc := range tcs {
+    // TODO: Make non-pointer
+    adptNm := *tc.Config.Adapter
+   
+    // See if adapter if contained in map
+    adapter, contained := r.adapters[adptNm]
+
+    if !contained {
+      log.Fatalf("Cannot find adapter \"%s\" for testcase \"%s\"", adptNm, tc.Cname)
+    }
 
-  for _, adapter := range r.adapters {
-    nm := adapter.Config().Name
-    r.setupEnv(adapter)
+    pair, exists :=  pairs[adptNm]
 
-    adapter.Build(tcTab[nm])
+    if !exists {
+      pair.adapter = adapter 
+    }
+    
+    pair.tcs = append(pair.tcs, tc)
+
+    pairs[adptNm] = pair
   }
 
-  safeCd(r.dirs.Config())
+
+  // Convert to slice
+  set := make([]adapterTestSet, 0)
+
+  for _, pair := range pairs {
+    set = append(set, pair)
+  }
+ 
+  return set
 }
 
-func (r Runner) units() []TestCase {
+func (r Runner) CollectCases() []TestCase {
   return collectUnits(r.dirs.Rubric(), r.adapterCfgs())
 }
 
-func (r Runner) Build() {
-  units := r.units()
+func (r Runner) Build(tcs []TestCase) {
 
   if !directoryExists(r.dirs.Build()) {
     r.dirs.MkBuild()
   }
 
-  r.build(units)
-}
+  testSets := r.groupByAdapter(tcs)
 
-func (r Runner) evaluate(tcs []TestCase) {
-  tcTab := r.buildTcLUT(tcs)
-  
-  for _, adapter := range r.adapters {
-    nm := adapter.Config().Name
+  for _, pair := range testSets {
+    adapter := pair.adapter
+    cases   := pair.tcs
     
     r.setupEnv(adapter)
-    adapter.Evaluate(tcTab[nm])
+
+    adapter.Build(cases)
   }
 
   safeCd(r.dirs.Config())
 }
 
-func (r Runner) Evaluate() []TestCase {
+func (r Runner) Evaluate(tcs []TestCase) []TestCase {
+  testSets := r.groupByAdapter(tcs)
+  results := make([]TestCase, 0)
 
-  units := r.units()
+  for _, pair := range testSets {
+    adapter := pair.adapter
+    cases   := pair.tcs
+
+    r.setupEnv(adapter)
+    resultSet := adapter.Evaluate(cases)
 
-  r.evaluate(units)
+    results = append(results, resultSet...)
+  }
+  
+  safeCd(r.dirs.Config())
 
-  return units
+  return results
 }
 
 func (r Runner) Clean() {
@@ -121,7 +143,7 @@ func (r Runner) SrcDir() string {
   return r.dirs.Src()
 }
 
-func NewRunner(adapters []Adapter, dirs DirConfig) Runner {
+func NewRunner(adapters map[string]Adapter, dirs DirConfig) Runner {
   r := Runner{adapters, dirs}
   
   for _, adapter := range r.adapters {
diff --git a/runner_builder.go b/runner_builder.go
index b369635..b3c07d9 100644
--- a/runner_builder.go
+++ b/runner_builder.go
@@ -1,16 +1,19 @@
 package planr
 
 type RunnerBuilder struct {
-  adapters [] Adapter
+  adapters    map[string] Adapter
   dirs        DirConfig
 }
 
 func ConfigureRunner() RunnerBuilder {
-  return RunnerBuilder {}
+  builder := RunnerBuilder{}
+  builder.adapters = make(map[string] Adapter, 0)
+  return builder
 }
 
 func RegisterAdapter(b RunnerBuilder, a Adapter) RunnerBuilder {
-  b.adapters = append(b.adapters, a)
+  nm := a.Config().Name
+  b.adapters[nm] = a
   return b
 }
 
-- 
cgit v1.2.3