From 2af7117891cdb67758219a7075f39c05dc02f3f5 Mon Sep 17 00:00:00 2001 From: Furkan Sahin Date: Thu, 2 Sep 2021 03:14:47 -0500 Subject: Make adapters and internals complient with new directory structure --- adapters.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'adapters.go') diff --git a/adapters.go b/adapters.go index 8419c8b..f4e53ce 100644 --- a/adapters.go +++ b/adapters.go @@ -1,16 +1,38 @@ package planr +import ( + "github.com/BurntSushi/toml" +) + // Test adapters must implement all life cycle hooks // This allows common config, code generation, etc // Test cases matching adapter configurations will be // fed into the adapter interface type Adapter interface { - // Config() AdapterConfig + Init(dirs DirConfig) + // Called once to preform expensive code generation Build(testCase []*TestCase) // Called every time source changes Evaluate(testCase []*TestCase) } + +// A parser function takes a blob of TOML and decodes it into +// configuration relevant to an adapter +type TomlParser func (toml.Primitive) (InheritableConfig, error) + +// The name under which an adapter registers corresponds +// to a table under the super-table adapters. All corresponding +// TOML will be passed to the ParseConfig method or ParseDefaultConfig +// for parsing. The ParseConfig file parses options in test case files. +// The ParseDefaultConfig is parsed by `defaults.toml` files and can +// be used to establish default configuration that will be inherited +// by all units in a common directory (collection) +type AdapterConfig struct { + Name string + ParseConfig TomlParser + ParseDefaultConfig TomlParser +} -- cgit v1.2.3 From 80c205a1d6d25f5a80853a76afcd1f527a894f56 Mon Sep 17 00:00:00 2001 From: Furkan Sahin 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(-) (limited to 'adapters.go') 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 2cc5603..c727805 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 ba7efb1..8057d94 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 413787b..527d277 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 bddeb4b..caf7bde 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 f8ec509..32e19a6 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 From d4f9c927e0efad402e1dbfded1a850fb9e0030e6 Mon Sep 17 00:00:00 2001 From: Furkan Sahin Date: Sun, 5 Sep 2021 00:37:23 -0500 Subject: Refactor gtest adapter to fit new pipeline --- adapters.go | 2 +- adapters/gtest/adapter.go | 187 +++++-------------------------------------- adapters/gtest/config.go | 58 +++++++++----- adapters/gtest/executable.go | 186 ++++++++++++++++++++++++++++++++++++++++++ adapters/gtest/templating.go | 14 ++-- cmd/planr/sub/cli.go | 46 +++++------ cmd/planr/sub/evaluate.go | 10 +-- fs.go | 4 +- runner.go | 4 +- testcase.go | 7 +- 10 files changed, 289 insertions(+), 229 deletions(-) create mode 100644 adapters/gtest/executable.go (limited to 'adapters.go') diff --git a/adapters.go b/adapters.go index b7c4b27..f6c48cb 100644 --- a/adapters.go +++ b/adapters.go @@ -17,7 +17,7 @@ type Adapter interface { Build(testCase []TestCase) // Called every time source changes - Evaluate(testCase []TestCase) []TestCase + Evaluate(testCase []TestCase) []TestResult } // 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 c727805..dfb035c 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.furkistan.com/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 8057d94..4cd2030 100644 --- a/adapters/gtest/config.go +++ b/adapters/gtest/config.go @@ -2,10 +2,10 @@ package gtest import ( "log" - "golang.furkistan.com/planr" "strings" - "github.com/BurntSushi/toml" "path" + "golang.furkistan.com/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..b8f79ec --- /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.furkistan.com/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}}" ) `) diff --git a/cmd/planr/sub/cli.go b/cmd/planr/sub/cli.go index 64318c2..8dc837e 100644 --- a/cmd/planr/sub/cli.go +++ b/cmd/planr/sub/cli.go @@ -14,25 +14,23 @@ var ( col_label = color.New(color.FgCyan) ); -func tcTitle(tc planr.TestCase) string { - title := tc.Cname +func tcTitle(tr planr.TestResult) string { + title := tr.Tc.Cname - if tc.Config.Title != nil { - title = *tc.Config.Title + if tr.Tc.Config.Title != nil { + title = *tr.Tc.Config.Title } return title } -func tcStatus(tc planr.TestCase) string { +func tcStatus(tc planr.TestResult) string { status := "SILENT" - if tc.Result != nil { - if tc.Result.Status == planr.PASSING { - status = "PASS" - } else { - status = "FAIL" - } + if tc.Status == planr.PASSING { + status = "PASS" + } else { + status = "FAIL" } return status @@ -59,9 +57,9 @@ func pprintFenced(title, value string) { fmt.Println(fence) } -func tcStatusLine(tc planr.TestCase) { - title := tcTitle(tc) - status := tcStatus(tc) +func tcStatusLine(tr planr.TestResult) { + title := tcTitle(tr) + status := tcStatus(tr) if status == "PASS" { col_pass.Printf("[%s] ", status); @@ -72,8 +70,10 @@ func tcStatusLine(tc planr.TestCase) { col_title.Println(title); } -func tcPprint(tc planr.TestCase) { - tcStatusLine(tc) +func tcPprint(tr planr.TestResult) { + tcStatusLine(tr) + + tc := tr.Tc pprintLabeled("id", tc.Cname) @@ -86,22 +86,20 @@ func tcPprint(tc planr.TestCase) { pprintLabeled("description", *tc.Config.Description) } - res := tc.Result - - if res.Status == planr.COMPILATION_FAILURE { + if tr.Status == planr.COMPILATION_FAILURE { - if res.DebugOutput != "" { + if tr.DebugOutput != "" { fmt.Println() - pprintFenced("compilation output", tc.Result.DebugOutput); + pprintFenced("compilation output", tr.DebugOutput); } else { fmt.Println("WARN: No debug output provided") } - } else if res.Status == planr.RUNTIME_FAILURE { + } else if tr.Status == planr.RUNTIME_FAILURE { - if tc.Result.TestOutput != "" { + if tr.TestOutput != "" { fmt.Println() - pprintFenced("test output", tc.Result.TestOutput); + pprintFenced("test output", tr.TestOutput); } } diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index 32e19a6..fe864ad 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -6,26 +6,26 @@ import ( func Evaluate(runner planr.Runner, params []string) { tcs := runner.CollectCases() - tcs = runner.Evaluate(tcs) + trs := runner.Evaluate(tcs) earned := 0.0 total := 0.0 passed := 0 - for _, tc := range tcs { - cfg := tc.Config + for _, tr := range trs { + cfg := tr.Tc.Config if cfg.Points != nil { points := float64(*cfg.Points) total += points - if tc.Result.Status == planr.PASSING { + if tr.Status == planr.PASSING { earned += points passed++ } } - tcPprint(tc) + tcPprint(tr) } printResults( diff --git a/fs.go b/fs.go index 575517c..a42ff2c 100644 --- a/fs.go +++ b/fs.go @@ -50,7 +50,7 @@ func basename(path string) string { return path[0:len(path) - len(ext)] } -func cname(root string, path string) string { +func Cname(root string, path string) string { rel, err := filepath.Rel(root, path) if err != nil { @@ -76,7 +76,7 @@ func collectUnits(root string, cfgs []AdapterConfig) []TestCase { collectFromDir(root, nil, cfgs, &tcs) for i := range tcs { - tcs[i].Cname = cname(root, tcs[i].Path) + tcs[i].Cname = Cname(root, tcs[i].Path) } return tcs diff --git a/runner.go b/runner.go index f613d44..2d66dc6 100644 --- a/runner.go +++ b/runner.go @@ -108,9 +108,9 @@ func (r Runner) Build(tcs []TestCase) { safeCd(r.dirs.Config()) } -func (r Runner) Evaluate(tcs []TestCase) []TestCase { +func (r Runner) Evaluate(tcs []TestCase) []TestResult { testSets := r.groupByAdapter(tcs) - results := make([]TestCase, 0) + results := make([]TestResult, 0) for _, pair := range testSets { adapter := pair.adapter diff --git a/testcase.go b/testcase.go index 7e0bf17..19f1e58 100644 --- a/testcase.go +++ b/testcase.go @@ -7,7 +7,8 @@ import ( type TestStatus uint const ( - PASSING TestStatus = iota + NOT_RUN TestStatus = iota + PASSING COMPILATION_FAILURE RUNTIME_FAILURE ) @@ -17,6 +18,7 @@ type TestResult struct { Status TestStatus DebugOutput string TestOutput string + Tc TestCase } // Program-wide testcase config @@ -43,9 +45,6 @@ type TestCase struct { Cname string Config TestCaseConfig - - Result *TestResult - } func (tc TestCase) AdapterConfig() InheritableConfig { -- cgit v1.2.3