From f90a14d5d723c5d2b87f2eaa19f441dec33bb9b2 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Wed, 4 Aug 2021 14:32:22 -0500 Subject: Prototyped build pipeline --- adapters.go | 2 + adapters/gtest_adapter.go | 149 +++++++++++++++++++++++++++++++++++++--------- adapters/template_defs.go | 50 ++++++++++++++++ cmd/sub/build.go | 2 +- fs.go | 108 +++++++++++++++++++++++++++++---- fs_test.go | 24 ++++++++ runner.go | 19 +++--- softscript.go | 42 +++++++++++++ testcase.go | 10 ++++ 9 files changed, 357 insertions(+), 49 deletions(-) create mode 100644 adapters/template_defs.go create mode 100644 fs_test.go create mode 100644 softscript.go 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() diff --git a/fs.go b/fs.go index 905d6d1..978c291 100644 --- a/fs.go +++ b/fs.go @@ -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) + } + } +} diff --git a/runner.go b/runner.go index 5c66573..1470fbd 100644 --- a/runner.go +++ b/runner.go @@ -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] +} -- cgit v1.2.3