diff options
| author | Flu0r1ne <flur01ne@flu0r1ne.net> | 2021-08-04 14:32:22 -0500 | 
|---|---|---|
| committer | Flu0r1ne <flur01ne@flu0r1ne.net> | 2021-08-04 14:32:22 -0500 | 
| commit | f90a14d5d723c5d2b87f2eaa19f441dec33bb9b2 (patch) | |
| tree | e0abd76b6ebd9adcc60732d532cb68c512b0c2d1 | |
| parent | a0b020a78eb0b33965c59460fc093c6959216e44 (diff) | |
| download | planr-f90a14d5d723c5d2b87f2eaa19f441dec33bb9b2.tar.xz planr-f90a14d5d723c5d2b87f2eaa19f441dec33bb9b2.zip  | |
Prototyped build pipeline
| -rw-r--r-- | adapters.go | 2 | ||||
| -rw-r--r-- | adapters/gtest_adapter.go | 149 | ||||
| -rw-r--r-- | adapters/template_defs.go | 50 | ||||
| -rw-r--r-- | cmd/sub/build.go | 2 | ||||
| -rw-r--r-- | fs.go | 108 | ||||
| -rw-r--r-- | fs_test.go | 24 | ||||
| -rw-r--r-- | runner.go | 19 | ||||
| -rw-r--r-- | softscript.go | 42 | ||||
| -rw-r--r-- | testcase.go | 10 | 
9 files changed, 357 insertions, 49 deletions
diff --git a/adapters.go b/adapters.go index b9c8d9c..7c7dd0b 100644 --- a/adapters.go +++ b/adapters.go @@ -19,6 +19,8 @@ type Adapter interface {    Build(testCase TestCase)    // Called once after all builds    FinalizeBuild() +  // Called pre-evaluate +  Make()    // Called once per test case after FinalizeBuild    Evaluate(testCase TestCase) TestResult    // Called once after each test has been evaluated diff --git a/adapters/gtest_adapter.go b/adapters/gtest_adapter.go index 4cbaa9c..fd06a48 100644 --- a/adapters/gtest_adapter.go +++ b/adapters/gtest_adapter.go @@ -1,41 +1,85 @@  package adapters  import ( -	"fmt" +	"bytes" +	"log" +	"os" +	"path" +	"strings" +	"text/template" +  	"github.com/BurntSushi/toml"  	"golang.flu0r1ne.net/planr" -        "log"  )  /*    CONFIGURATION  */ -  type GtestDefaults struct { -  Name   *string -  Suite  *string -  File   *string +  Name      *string +  Suite     *string +  Testfile  *string +  Test_root *string +  Srcs      *[]string +  Srcs_root *string  }  func (child *GtestDefaults) Inherit(p interface{}) {    parent := p.(*GtestDefaults) -  if(child.Name == nil) { -    child.Name = parent.Name  -  } +  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.Test_root == nil) { child.Test_root = parent.Test_root } +  if(child.Srcs == nil)      { child.Srcs = parent.Srcs } +  if(child.Srcs_root == nil) { child.Srcs_root = parent.Srcs_root } +} + + +type GtestConfig struct { +  GtestDefaults +} + -  if(child.Suite == nil) { -    child.Suite = parent.Suite +func (cfg GtestConfig) joinTests(path_ string) string { +  if cfg.Test_root == nil { +    return planr.JoinConfigDir("tests", path_)    } +    +  return planr.JoinConfigDir(*cfg.Test_root, path_) +} -  if(child.File == nil) { -    child.File = parent.File +func (cfg GtestConfig) joinSrcs(path_ string) string { +  if cfg.Srcs_root == nil { +    return planr.JoinConfigDir("../src", path_)    } + +  return planr.JoinConfigDir(*cfg.Srcs_root, path_)  } +func (cfg GtestConfig) srcList() string { +  var srcList string -type GtestConfig struct { -  GtestDefaults +  if cfg.Srcs != nil { +    srcs := make([]string, len(*cfg.Srcs)) +    for i, src := range *cfg.Srcs { +      srcs[i] = cfg.joinSrcs(src) +    } +     +    srcList = strings.Join(srcs, "\n  ") +  } + +  return srcList +} + +func (g GtestConfig) EnsureSatisfied(path string) { +  if g.Name == nil { +    log.Fatalf("\"name\" is not defined for unit: %s\n", path) +  } else if g.Suite == nil { +    log.Fatalf("\"suite\" is not defined for unit: %s\n", path) +  } else if g.Testfile == nil { +    log.Fatalf("\"testfile\" is not defined for unit: %s\n", path) +  }  }  func primitiveDecode(primitive toml.Primitive, config interface{}) { @@ -64,34 +108,81 @@ func ParseDefaultConfig(prim toml.Primitive) planr.InheritableConfig {    BUILD PROCESS  */ -type GtestAdapter struct {} +type GtestAdapter struct { +  unitTmpl  *template.Template +  cbuf      bytes.Buffer +} -func (a GtestAdapter) Config() planr.AdapterConfig { +func (a *GtestAdapter) Config() planr.AdapterConfig {    return planr.AdapterConfig {      Name: "gtest",      ParseConfig: ParseConfig,      ParseDefaultConfig: ParseDefaultConfig, -  } + }  } -func (a GtestAdapter) InitializeBuild() { -  fmt.Println("Initializing"); +func (a *GtestAdapter) InitializeBuild() { +  a.unitTmpl  = UnitTemplate() +  a.cbuf = bytes.Buffer{} + +  WriteCMakeBoiler(&a.cbuf)  } -func (a GtestAdapter) Build(tc planr.TestCase) { -  fmt.Printf("Building %v\n", tc); +const GTEST_CMAKE = "CMakeLists.txt" + +func Chdir(dir string) { +  if err := os.Chdir(dir); err != nil { +    log.Fatal(err) +  }  } -func (a GtestAdapter) FinalizeBuild() { -  fmt.Println("Finalizing"); +func (a *GtestAdapter) Build(tc planr.TestCase) { +  cfg := tc.AdapterConfig("gtest").(*GtestConfig) +  cfg.EnsureSatisfied(tc.Path) + +  cname    := tc.Cname +  testfile := cfg.joinTests(*cfg.Testfile) +  srcList  := cfg.srcList() + +  err := a.unitTmpl.Execute(&a.cbuf, struct {Cname, File, Srcs string} { +    cname, testfile, srcList, +  }) + +  if err != nil { +    log.Fatal(err) +  }  } -func (a GtestAdapter) Evaluate(tc planr.TestCase) planr.TestResult { -  fmt.Printf("Evaluating %v\n", tc); +func (a *GtestAdapter) FinalizeBuild() { +  dir :=  a.Config().ConfigDir() +  cmakeFile := path.Join(dir, GTEST_CMAKE) +   +  file, err := os.OpenFile(cmakeFile, os.O_RDWR | os.O_CREATE, 0644) +  defer func () { +    err := file.Close() + +    if err != nil { +      log.Fatal(err) +    } +  }() + +  if err != nil { +    log.Fatalf("Could not open CMakeFile (%s)\n%v", cmakeFile, err) +  } -  return planr.TestResult {} +  file.Write(a.cbuf.Bytes()) + +  Chdir(dir) + +  planr.RunCmd("cmake", "-S", ".", "-B", ".")  } -func (a GtestAdapter) Cleanup() { -  fmt.Printf("Cleaning\n") +func (a *GtestAdapter) Make() { +  planr.RunCmd("make", "-k")  } + +func (a *GtestAdapter) Evaluate(tc planr.TestCase) planr.TestResult { +  return planr.TestResult {} +} + +func (a *GtestAdapter) Cleanup() { } diff --git a/adapters/template_defs.go b/adapters/template_defs.go new file mode 100644 index 0000000..54f2840 --- /dev/null +++ b/adapters/template_defs.go @@ -0,0 +1,50 @@ +package adapters + +import ( +	"io" +	"log" +	"text/template" +) + + +func UnitTemplate() *template.Template { +  tmpl, err := template.New("gtest_unit").Parse(` +add_executable( +  {{.Cname}} +  {{.File}} +  {{.Srcs}} +) + +target_link_libraries( +  {{.Cname}} +  gtest_main +) + +gtest_discover_tests( +  {{.Cname}} +) +`) + +  if err != nil { +    log.Fatalf("Cannot load Gtest Unit Template %v", err) +  } + +  return tmpl +} + +func WriteCMakeBoiler(w io.Writer) { +  w.Write([]byte(` +cmake_minimum_required (VERSION 3.1.0) + +project(PlanRGtestAdapter) + +include(FetchContent) +FetchContent_Declare( +  googletest +  URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip +) + +include(GoogleTest) +FetchContent_MakeAvailable(googletest) +`)) +} diff --git a/cmd/sub/build.go b/cmd/sub/build.go index 84201a2..19220d3 100644 --- a/cmd/sub/build.go +++ b/cmd/sub/build.go @@ -9,7 +9,7 @@ func Build(params []string) {    gtestAdapter := adapters.GtestAdapter {}    r := planr.Runner{} -  r.RegisterAdapter(gtestAdapter) +  r.RegisterAdapter(>estAdapter)    rd := planr.RubricDir() @@ -6,7 +6,7 @@ import (          "io"  	"path"  	"path/filepath" -        // "fmt" +        "strings"  )  /* CONFIG DIRECTORY */ @@ -50,7 +50,7 @@ func directoryExists(path string) bool {  // 1. PlANR_DIRECTORY env if set  // 2. planr  // 3. .planr -func configDir() string { +func ConfigDir() string {    // Return environmental override if set    if dir, isSet := os.LookupEnv("PLANR_DIRECTORY"); isSet { @@ -95,9 +95,22 @@ func configDir() string {    return rubricDir  } +func JoinConfigDir(path_ string, file string) string { +  if path.IsAbs(path_) { +    return path.Join(path_, file)  +  } + +  return path.Join(ConfigDir(), path_, file) +} + + +func RootDir() string { +  return path.Join(ConfigDir(), "..") +} +  // Find rubric directory at PLANR_DIR/rubric  func RubricDir() string { -  rubricDir := path.Join(configDir(), "rubric"); +  rubricDir := path.Join(ConfigDir(), "rubric");    if !directoryExists(rubricDir) {      log.Fatal("Could not find the rubric directory inside of planr")  @@ -106,21 +119,95 @@ func RubricDir() string {    return rubricDir  } +func BuildDir() string { +  buildDir := path.Join(ConfigDir(), "build") + +  if !directoryExists(buildDir) { +    err := os.Mkdir(buildDir, 0755) + +    if err != nil { +      log.Fatalf("Cannot create build directory %v\n", err) +    } +  } + +  return buildDir +} + +func CleanBuildDir() { +  buildDir := path.Join(ConfigDir(), "build") +  if err := os.RemoveAll(buildDir); err != nil { +    log.Fatalf("Cannot clean (removeAll) in build directory %v\n", err) +  } +} + +func (ac AdapterConfig) ConfigDir() string { +  dir := BuildDir() +  dir = path.Join(dir, ac.Name) +  +  if !directoryExists(dir) { +    err := os.Mkdir(dir, 0755) + +    if err != nil { +      log.Fatalf("Cannot create build/%s directory %v\n", ac.Name, err) +    } +  } + +  return dir +} + +func basename(path string) string { +  ext := filepath.Ext(path) +  return path[0:len(path) - len(ext)] +} + +func cname(root string, path string) string { +  rel, err := filepath.Rel(root, path)  +   +  if err != nil { +    log.Fatal(err) +  } +   +  rel = filepath.ToSlash(rel) +  parts := strings.Split(rel, "/") +  n := len(parts) + +  if n == 0 { +    return "" +  } + +  parts[n-1] = basename(parts[n-1]) + +  return strings.Join(parts, ".") +} + +func collectUnits(root string, cfgs []AdapterConfig) []TestCase { +  tcs := make([]TestCase, 0) + +  collectFromDir(root, nil, cfgs, &tcs) + +  for i := range tcs { +    tcs[i].Cname = cname(root, tcs[i].Path) +  } + +  return tcs +} + +const DEFAULTS = "defaults.toml"  // Collects the units from the configuration tree  // TODO: Cleanup -func collectUnits( -  name      string, +func collectFromDir( +  dir       string,    defaults *Defaults,    cfgs      []AdapterConfig,    units    *[]TestCase,  ) { -  fp, err := os.Open(name) +  fp, err := os.Open(dir)    if err != nil {      log.Fatal(err)    }    // Process defaults for this directory if a defaults.toml is found -  defaultsPath := path.Join(name, "defaults.toml") +  defaultsPath := path.Join(dir, DEFAULTS)    if info, err := os.Stat(defaultsPath); err == nil && !info.IsDir() {      d := DecodeDefaults(defaultsPath, cfgs) @@ -143,13 +230,13 @@ func collectUnits(      for _, ent := range dirs { -      child := path.Join(name, ent.Name()) +      child := path.Join(dir, ent.Name())        nm := ent.Name()        if ent.IsDir() { -        collectUnits(child, defaults, cfgs, units) +        collectFromDir(child, defaults, cfgs, units)        } else { -        if nm == "defaults.toml" { +        if nm == DEFAULTS {            continue          } @@ -159,7 +246,6 @@ func collectUnits(          tc := TestCase {            Path: child, -          Cname: nm,            Config: config,          } diff --git a/fs_test.go b/fs_test.go new file mode 100644 index 0000000..4e36469 --- /dev/null +++ b/fs_test.go @@ -0,0 +1,24 @@ +package planr + +import ( +  "testing" +) + +func TestCname(t *testing.T) { +  ROOT := "/home/rubric" + +  v := [] struct { path, cname string } { +    {"/home/rubric/tc1.toml", "tc1" }, +    {"/home/rubric/alpha/tc1.toml", "alpha.tc1" }, +    {"/home/rubric/alpha/beta/tc2.toml", "alpha.beta.tc2"}, +    {"/home/rubric/.a/_b./.abcd.toml", ".a._b...abcd"}, +  } + +  for _, vec := range v { +    got := cname(ROOT, vec.path) + +    if vec.cname != got { +      t.Fatalf("Cname(%s) = %s, wanted %s", vec.path, got, vec.cname) +    } +  } +} @@ -1,5 +1,9 @@  package planr +import ( +  "fmt" +) +  type Runner struct {    adapters    []Adapter  } @@ -18,13 +22,8 @@ func (r Runner) adapterCfgs() []AdapterConfig {    return cgs  } -func (r Runner) collectUnits(root string) []TestCase { -  tcs := make([]TestCase, 10) - -  collectUnits(root, nil, r.adapterCfgs(), &tcs) - -  return tcs -} +// [Initialization] -> [Generation] -> [Finalization] ->  +// [Build] -> [Evaluation] -> [Clean]  func (r Runner) cycle(tcs []TestCase) []TestResult {    results := make([]TestResult, 0) @@ -36,14 +35,18 @@ func (r Runner) cycle(tcs []TestCase) []TestResult {      for _, tc := range tcs {        if tc.ContainsAdapter(aname) { +        fmt.Printf("[R] Building %s\n", tc.Path)          adapter.Build(tc)        }      }      adapter.FinalizeBuild() +    adapter.Make() +      for _, tc := range tcs {        if tc.ContainsAdapter(aname) { +        fmt.Printf("[R] Evaluating %s\n", tc.Path)          results = append(results, adapter.Evaluate(tc))        }      } @@ -55,7 +58,7 @@ func (r Runner) cycle(tcs []TestCase) []TestResult {  }  func (r Runner) Run(root string) [] TestResult { -  tcs := r.collectUnits(root) +  tcs := collectUnits(root, r.adapterCfgs())    trs := r.cycle(tcs) diff --git a/softscript.go b/softscript.go new file mode 100644 index 0000000..9924d54 --- /dev/null +++ b/softscript.go @@ -0,0 +1,42 @@ +package planr + +import ( +        "io" +        "os" +        "os/exec" +        "log" +) + +func RunCmd(name string, args ...string) { +  cmd := exec.Command(name, args...) + +  stderr, err := cmd.StderrPipe() +  if err != nil { +    log.Fatal(err); +  } + +  stdout, err := cmd.StdoutPipe() +  if err != nil { +    log.Fatal(err); +  } +  +  go func() { +    io.Copy(os.Stderr, stderr) +  }() + +  go func() { +    io.Copy(os.Stdout, stdout) +  }() + +  if err := cmd.Start(); err != nil { +	  log.Fatal(err) +  } + +  if err != nil { +    log.Fatal(err) +  } + +  if err := cmd.Wait(); err != nil { +    log.Fatalf("Could not execute cmake command\n%v", err) +  } +} diff --git a/testcase.go b/testcase.go index 02db738..70c188e 100644 --- a/testcase.go +++ b/testcase.go @@ -5,7 +5,13 @@ type TestResult struct {  }  type TestCase struct { +  // absolute path to the test case configuration    Path   string +  // The canonical name is a semantically meaningful name  +  // guaranteed to be unique among the tests +  // Obtained by replacing separators in the relative path of the  +  // configuration with dots. The `toml` extension is also stripped. +  // rubric/alpha/beta/tc1.toml -> alpha.beta.tc1    Cname  string    Config TestCaseConfig  } @@ -19,3 +25,7 @@ func (tc TestCase) ContainsAdapter(name string) bool {    return false;  } + +func (tc TestCase) AdapterConfig(name string) InheritableConfig { +  return tc.Config.adapters_[name] +}  | 
