From 151d516e68f5d43aa2d0c5ff462752d640b6a614 Mon Sep 17 00:00:00 2001
From: Flu0r1ne <flur01ne@flu0r1ne.net>
Date: Sun, 5 Sep 2021 00:37:23 -0500
Subject: Refactor gtest adapter to fit new pipeline

---
 adapters/gtest/adapter.go    | 187 +++++--------------------------------------
 adapters/gtest/config.go     |  58 +++++++++-----
 adapters/gtest/executable.go | 186 ++++++++++++++++++++++++++++++++++++++++++
 adapters/gtest/templating.go |  14 ++--
 4 files changed, 254 insertions(+), 191 deletions(-)
 create mode 100644 adapters/gtest/executable.go

(limited to 'adapters')

diff --git a/adapters/gtest/adapter.go b/adapters/gtest/adapter.go
index 7033c7f..ec11748 100644
--- a/adapters/gtest/adapter.go
+++ b/adapters/gtest/adapter.go
@@ -1,34 +1,14 @@
 package gtest
 
 import (
-	"context"
-	"errors"
-	"fmt"
-	"io/ioutil"
 	"log"
 	"os"
-	"os/exec"
 	"path"
-	"sync"
-	"time"
 	"golang.flu0r1ne.net/planr"
 )
 
 const GTEST_CMAKE = "CMakeLists.txt"
 
-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())
-
-  return cmakeUnit {
-    tc.Cname,
-    testpath,
-    srclist,
-  };
-}
-
 func safeWd() string{
   wd, err := os.Getwd()
 
@@ -39,104 +19,6 @@ func safeWd() string{
   return wd
 }
 
-type ResultFromId map[string] Result
-
-func (adapter *Adapter) execTests(cnames []string) ResultFromId {
-  buildDir := safeWd()
-
-  lut := make(ResultFromId, 0)
-  for _, exe := range cnames {
-
-      exePath := path.Join(buildDir, exe)
-
-      f, err := ioutil.TempFile(buildDir, "gtest_adapter_*.json")
-
-      if err != nil {
-        log.Fatal(err)
-      }
-
-      ctx, cancel := context.WithTimeout(context.Background(), 9999*time.Millisecond)
-      cmd := exec.CommandContext(ctx, exePath, "--gtest_output=json:" + f.Name())
-
-      defer cancel()
-      defer os.Remove(f.Name())
-
-      out, err := cmd.CombinedOutput()
-      if err != nil {
-        var exiterr *exec.ExitError
-
-        if !errors.As(err, &exiterr) {
-          log.Printf("%v\n", err)
-          os.Exit(exiterr.ExitCode())
-        }
-      }
-
-      results, err := decodeResults(f)
-
-      if err != nil {
-        log.Printf("Could not collect results from %s: %v", exe, err)
-        continue
-      }
-
-      for _, r  := range results {
-        r.testOutput = string(out)
-        lut[exe + "." + r.id] = r
-      }
-  }
-
-  return lut
-}
-
-// An executable may contain more than one test
-// Gather all executables and deduplicate them
-func exes(tcs []planr.TestCase) []string {
-  set := make(map[string] bool, 0)
-  
-  for _, tc := range tcs {
-    // Tests which have encountered a failure
-    // may not have an executable
-    if tc.Result.Status != planr.PASSING {
-      continue
-    }
-
-    if(!set[tc.Cname]) {
-      set[tc.Cname] = true
-    }
-  }
- 
-  exes := make([]string, 0)
-  for k := range set {
-    exes = append(exes, k)
-  }
-  
-  return exes
-}
-
-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) {
-  defer wg.Done()
-
-  cmd := exec.Command("make", tc.Cname)
-  out, err := cmd.CombinedOutput()
-  tc.Result = new(planr.TestResult)
-
-  // Don't treat command failure as anything but a build failure
-  if err != nil{
-    var exiterr *exec.ExitError
-    if errors.As(err, &exiterr) && exiterr.ExitCode() == 0 {
-      log.Fatal(err)
-    }
-
-    tc.Result.Status = planr.COMPILATION_FAILURE
-  }
-
-  tc.Result.DebugOutput = string(out)
-}
-
 type Adapter struct {
   dirs planr.DirConfig
 }
@@ -155,67 +37,42 @@ func (a *Adapter) Init(dirs planr.DirConfig) {
 
 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().(*Config)
-    cfg.ensureSatisfied(tc.Path)
+  finalizeConfigs(tcs)
 
-    units = append(units, makeUnit(tc, adapter.dirs))
-  }
+  exes := createExecutables(tcs)
 
-  genCmake(cmakeFile, units)
+  cmakeFile := path.Join(buildDir, GTEST_CMAKE)
+  cmakeUnits := cmakeUnits(exes, adapter.dirs)
+
+  generateCmakeScript(cmakeFile, cmakeUnits)
 
   planr.RunCmd("cmake", "-S", ".", "-B", ".")
 }
 
-// ./planr eval  0.93s user 0.16s system 100% cpu 1.089 total
-func (adapter *Adapter) Evaluate(tcs []planr.TestCase) [] planr.TestCase {
-  var wg sync.WaitGroup
-  for i := range tcs {
-    tc := &tcs[i]
-    wg.Add(1)
-    go compile(&wg, tc)
-  }
-  wg.Wait()
-
-  files := exes(tcs)
-  resultById := adapter.execTests(files)
-
-  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().(*Config)
+func (adapter *Adapter) Evaluate(tcs []planr.TestCase) [] planr.TestResult {
+  buildDir := safeWd()
 
-        log.Printf(
-          "Could not find testcase %s with name=\"%s\" and suite=\"%s\". Does such a test exist in the test source?",
-          tc.Cname,
-          *cfg.Name,
-          *cfg.Suite,
-        )
+  finalizeConfigs(tcs)
+  
+  results := make([]planr.TestResult, 0)
+  
+  exes := createExecutables(tcs)
 
-        tc.Result.Status = planr.COMPILATION_FAILURE
-        tc.Result.DebugOutput += fmt.Sprintf("planr: Did not find testcase %s in any test executable\n", id(*tc))
-      }
+  for i := range exes {
+    succeed, buildFailures := exes[i].compile(buildDir)
 
+    if ! succeed {
+      results = append(results, buildFailures...)
       continue
     }
- 
-    if !result.pass {
-      tc.Result.Status = planr.RUNTIME_FAILURE
-    }
 
-    tc.Result.TestOutput = result.testOutput
-  }
+    runtimeResults := exes[i].execute(buildDir)
 
-  return tcs
+    results = append(results, runtimeResults...)
+  }
+ 
+  return results
 }
 
 func NewAdapter() *Adapter {
diff --git a/adapters/gtest/config.go b/adapters/gtest/config.go
index 3305977..04d426c 100644
--- a/adapters/gtest/config.go
+++ b/adapters/gtest/config.go
@@ -2,10 +2,10 @@ package gtest
 
 import (
   "log"
-  "golang.flu0r1ne.net/planr"
   "strings"
-  "github.com/BurntSushi/toml"
   "path"
+  "golang.flu0r1ne.net/planr"
+  "github.com/BurntSushi/toml"
 )
 
 const (
@@ -16,7 +16,7 @@ type Defaults struct {
   Name          *string
   Suite         *string
   Testfile      *string
-  Srcs          *[]string
+  Srcs          []string
   Timeout       *uint
 }
 
@@ -26,7 +26,7 @@ func (child *Defaults) Inherit(p interface{}) {
   if(child.Name == nil)          { child.Name = parent.Name }
   if(child.Suite == nil)         { child.Suite = parent.Suite }
   if(child.Testfile == nil)      { child.Testfile = parent.Testfile }
-  if(child.Srcs == nil)          { child.Srcs = parent.Srcs }
+  if(len(child.Srcs) == 0)       { child.Srcs = parent.Srcs }
   if(child.Timeout == nil)       { child.Timeout = parent.Timeout }
 }
 
@@ -35,34 +35,52 @@ type Config struct {
   Defaults
 }
 
-func (g Config) ensureSatisfied(path string) {
-  if g.Name == nil {
+func (c * Config) finalize(path string) {
+  if c.Name == nil {
     log.Fatalf("\"name\" is not defined for unit: %s\n", path)
-  } else if g.Suite == nil {
+  } else if c.Suite == nil {
     log.Fatalf("\"suite\" is not defined for unit: %s\n", path)
-  } else if g.Testfile == nil {
+  } else if c.Testfile == nil {
     log.Fatalf("\"testfile\" is not defined for unit: %s\n", path)
   }
 
-  if g.Timeout == nil {
-    g.Timeout = new(uint)
-    *g.Timeout = DEFAULT_TIMEOUT;
+  if c.Timeout == nil {
+    c.Timeout = new(uint)
+    *c.Timeout = DEFAULT_TIMEOUT;
   }
 }
 
-func (cfg Config) srcList(srcDir string) string {
-  var srcList string
+func srcList(srcdir string, srcs []string) string {
+  builder := strings.Builder {}
 
-  if cfg.Srcs != nil {
-    srcs := make([]string, len(*cfg.Srcs))
-    for i, src := range *cfg.Srcs {
-      srcs[i] = "\"" + path.Join(srcDir, src) + "\""
-    }
+  for _, src := range srcs {
+    builder.WriteString("\"")
+    builder.WriteString(path.Join(srcdir, src))
+    builder.WriteString("\"\n  ")
+  }
+  
+  return builder.String()
+}
+
+func cmakeUnits(e []executable, dirs planr.DirConfig) []cmakeUnit {
+  
+  units := make([]cmakeUnit, len(e))
+  for i, exe := range e {
+    testpath := path.Join(dirs.Tests(), exe.testpath)
+    srclist  := srcList(dirs.Src(), exe.srcs)
     
-    srcList = strings.Join(srcs, "\n  ")
+    units[i] = cmakeUnit { exe.exeNm, testpath, srclist }
   }
 
-  return srcList
+  return units
+}
+
+func finalizeConfigs(tcs []planr.TestCase) {
+  for i := range tcs {
+    cfg := tcs[i].AdapterConfig().(*Config)
+
+    cfg.finalize(tcs[i].Path)
+  }
 }
 
 func ParseConfig(prim toml.Primitive) (planr.InheritableConfig, error) {
diff --git a/adapters/gtest/executable.go b/adapters/gtest/executable.go
new file mode 100644
index 0000000..78b0b56
--- /dev/null
+++ b/adapters/gtest/executable.go
@@ -0,0 +1,186 @@
+package gtest
+
+import (
+        "os"
+	"errors"
+        "time"
+	"io/ioutil"
+	"log"
+	"os/exec"
+	"path"
+	"reflect"
+	"sort"
+        "context"
+
+	"golang.flu0r1ne.net/planr"
+)
+
+type executable struct {
+  exeNm  string
+  testpath string
+  srcs     []string
+  tcs      []planr.TestCase
+}
+
+func createExecutables(tcs []planr.TestCase) []executable {
+  exes := make(map[string] executable, 0)
+
+  for _, tc := range tcs {
+    cfg := tc.AdapterConfig().(*Config)
+    file := *cfg.Testfile
+    exe, contained := exes[file]
+
+    // For set comparison
+    sort.Strings(cfg.Srcs)
+
+    if !contained {
+      exeTcs := make([]planr.TestCase, 1)
+      exeTcs[0] = tc
+
+
+      exe := executable {
+        planr.Cname("", file),
+        file,
+        cfg.Srcs,
+        exeTcs,
+      }
+
+      exes[file] = exe
+      
+      continue
+    }
+
+    // We could create two different executables for each source list
+    // But, that would be confusing so we're going to disallow it
+    if !reflect.DeepEqual(exe.srcs, cfg.Srcs) {
+      log.Fatalf(
+        "Two test case definitions %s and %s have different lists of sources", 
+        exe.testpath, *cfg.Testfile,
+      )
+    }
+
+    exe.tcs = append(exe.tcs, tc)
+    
+    exes[file] = exe
+  }
+
+  exesList := make([]executable, 0)
+
+  for _, exe := range exes {
+    exesList = append(exesList, exe)
+  }
+
+  return exesList
+}
+
+func (exe executable) compile(builddir string) (succeeded bool, buildFailures []planr.TestResult) {
+  cmd := exec.Command("make", "-C", builddir, exe.exeNm)
+  out, err := cmd.CombinedOutput()
+  buildFailures = make([]planr.TestResult, 0)
+
+  outputLog := string(out)
+
+  if err != nil{
+    var exiterr *exec.ExitError
+    if errors.As(err, &exiterr) && exiterr.ExitCode() == 0 {
+      log.Fatalf("Unrecoverable build failure: %v", err)
+    }
+
+    for i := range exe.tcs {
+      res := planr.TestResult {}
+      res.Tc = exe.tcs[i]
+      res.DebugOutput = outputLog
+      res.Status = planr.COMPILATION_FAILURE
+
+      buildFailures = append(buildFailures, res)
+    }
+
+    succeeded = false
+
+    return
+  }
+
+  succeeded = true
+  return
+}
+
+const TMPFILENAME = "gtest_adapter_*.json"
+
+func runGtest(exe string, tc planr.TestCase, builddir string) planr.TestResult {
+  result := planr.TestResult {}
+  result.Tc = tc
+
+  exePath := path.Join(builddir, exe)
+  cfg := tc.AdapterConfig().(*Config)
+
+  f, err := ioutil.TempFile(builddir, TMPFILENAME)
+      
+  if err != nil {
+    log.Fatal(err)
+  }
+
+  timeout := time.Duration(*cfg.Timeout) * time.Millisecond
+
+  ctx, cancel := context.WithTimeout(context.Background(), timeout)
+
+  jsonFlag := "--gtest_output=json:" + f.Name()
+  testFlag := "--gtest_filter=" + *cfg.Suite + "." + *cfg.Name
+
+  cmd := exec.CommandContext(ctx, exePath, jsonFlag, testFlag)
+
+  defer cancel()
+  defer os.Remove(f.Name())
+
+  out, err := cmd.CombinedOutput()
+  if err != nil {
+    var exiterr *exec.ExitError
+
+    if !errors.As(err, &exiterr) {
+      log.Printf("%v\n", err)
+      os.Exit(exiterr.ExitCode())
+    }
+  }
+
+  results, err := decodeResults(f)
+
+  if err != nil {
+    log.Fatalf("Could not collect results from %s: %v", exe, err)
+  }
+
+  if len(results) < 1 {
+    log.Fatalf(
+      "Could not find testcase %s with name=\"%s\" and suite=\"%s\". Does such a test exist in the test source?",
+      tc.Cname,
+      *cfg.Name,
+      *cfg.Suite,
+    )
+  }
+
+  // TODO: Cleanup -- ZERO TESTS?
+  if len(results) > 1 {
+    log.Fatalf("Unexpected number of results")
+  }
+
+  decodeResult := results[0]
+  
+  result.TestOutput = string(out)
+
+  if decodeResult.pass {
+    result.Status = planr.PASSING
+  } else {
+    result.Status = planr.RUNTIME_FAILURE
+  }
+
+  return result
+}
+
+func (exe executable) execute(builddir string) []planr.TestResult {
+  results := make([]planr.TestResult, len(exe.tcs))
+
+  for i := range exe.tcs {
+    results[i] = runGtest(exe.exeNm, exe.tcs[i], builddir)
+  }
+
+  return results
+}
+
diff --git a/adapters/gtest/templating.go b/adapters/gtest/templating.go
index a9a3b07..57532fa 100644
--- a/adapters/gtest/templating.go
+++ b/adapters/gtest/templating.go
@@ -8,12 +8,12 @@ import (
 )
 
 type cmakeUnit struct {
-  Cname string
+  ExeNm string
   File  string
   Srcs  string
 };
 
-func genCmake(out string, units []cmakeUnit) {
+func generateCmakeScript(out string, units []cmakeUnit) {
   file, err := os.OpenFile(out, os.O_RDWR | os.O_CREATE, 0644)
   defer func () {
     err := file.Close()
@@ -33,27 +33,29 @@ func genCmake(out string, units []cmakeUnit) {
 
   for _, unit := range units {
     if err := tmpl.Execute(file, unit); err != nil {
-      log.Fatalf("Failed to generate unit %s: %v", unit.Cname, err);
+      log.Fatalf("Failed to generate unit %s: %v", unit.ExeNm, err);
     }
   }
 }
 
 
+// TODO: Add comments
 func unitTemplate() *template.Template {
   tmpl, err := template.New("gtest_unit").Parse(`
+
 add_executable(
-  "{{.Cname}}"
+  "{{.ExeNm}}"
   "{{.File}}"
   {{.Srcs}}
 )
 
 target_link_libraries(
-  "{{.Cname}}"
+  "{{.ExeNm}}"
   gtest_main
 )
 
 gtest_discover_tests(
-  "{{.Cname}}"
+  "{{.ExeNm}}"
 )
 `)
 
-- 
cgit v1.2.3