diff options
Diffstat (limited to 'adapters/gtest')
-rw-r--r-- | adapters/gtest/adapter.go | 202 | ||||
-rw-r--r-- | adapters/gtest/config.go | 97 | ||||
-rw-r--r-- | adapters/gtest/executable.go | 185 | ||||
-rw-r--r-- | adapters/gtest/templating.go | 57 |
4 files changed, 325 insertions, 216 deletions
diff --git a/adapters/gtest/adapter.go b/adapters/gtest/adapter.go index f4fde27..2961f29 100644 --- a/adapters/gtest/adapter.go +++ b/adapters/gtest/adapter.go @@ -1,199 +1,87 @@ 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 mkUnit(tc *planr.TestCase) cmakeUnit { - cfg := tc.AdapterConfig().(*GtestConfig) +func safeWd() string{ + wd, err := os.Getwd() - return cmakeUnit { - tc.Cname, - cfg.joinTests(*cfg.Testfile), - cfg.srcList(), - }; -} + if err != nil { + log.Fatalf("Could not get GtestBuildDir %s %v\n", wd, err) + } + return wd +} -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, ParseDefaultConfig: ParseDefaultConfig, - } -} - -func (adapter *GtestAdapter) Build(tcs []*planr.TestCase) { - buildDir := adapter.Config().Dir() - cmakeFile := path.Join(buildDir, GTEST_CMAKE) - - units := make([]cmakeUnit, 0) - for _, tc := range tcs { - - cfg := tc.AdapterConfig().(*GtestConfig) - cfg.ensureSatisfied(tc.Path) - - units = append(units, mkUnit(tc)) } - - genCmake(cmakeFile, units) - - planr.RunCmd("cmake", "-S", ".", "-B", ".") } -type ResultFromId map[string] Result - -func (adapter *GtestAdapter) execTests(cnames []string) ResultFromId { - buildDir := adapter.Config().Dir() - - 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()) +func (a *Adapter) Init(dirs planr.DirConfig) { + a.dirs = dirs +} - out, err := cmd.CombinedOutput() - if err != nil { - var exiterr *exec.ExitError +func (adapter Adapter) Build(tcs []planr.TestCase) { + buildDir := safeWd() - if !errors.As(err, &exiterr) { - log.Printf("%v\n", err) - os.Exit(exiterr.ExitCode()) - } - } + finalizeConfigs(tcs) - results, err := decodeResults(f) + exes := createExecutables(tcs) - if err != nil { - log.Printf("Could not collect results from %s: %v", exe, err) - continue - } + cmakeFile := path.Join(buildDir, GTEST_CMAKE) + cmakeUnits := cmakeUnits(exes, adapter.dirs) - for _, r := range results { - r.testOutput = string(out) - lut[exe + "." + r.id] = r - } - } + generateCmakeScript(cmakeFile, cmakeUnits) - return lut + planr.RunCmd("cmake", "-S", ".", "-B", ".") } -// 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) +func (adapter *Adapter) Evaluate(tcs []planr.TestCase) [] planr.TestResult { + buildDir := safeWd() + + finalizeConfigs(tcs) - 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) - } + results := make([]planr.TestResult, 0) - return exes -} - -func id(tc *planr.TestCase) string { - cfg := tc.AdapterConfig().(*GtestConfig) - return tc.Cname + "." + *cfg.Suite + "." + *cfg.Name -} + exes := createExecutables(tcs) -func compile(wg * sync.WaitGroup, tc *planr.TestCase) { - defer wg.Done() + c := make(chan []planr.TestResult, len(exes)) + for i := range exes { + go func(exe *executable) { + succeed, buildFailures := exe.compile(buildDir) - cmd := exec.Command("make", tc.Cname) - out, err := cmd.CombinedOutput() - tc.Result = new(planr.TestResult) + if ! succeed { + c <- buildFailures + return + } - // 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) - } + runtimeResults := exe.execute(buildDir) - tc.Result.Status = planr.COMPILATION_FAILURE + c <- runtimeResults + }(&exes[i]) } - tc.Result.DebugOutput = string(out) -} - -// ./planr eval 0.93s user 0.16s system 100% cpu 1.089 total -func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) { - var wg sync.WaitGroup - for _, tc := range tcs { - wg.Add(1) - go compile(&wg, tc) + for range exes { + results = append(results, (<-c)...) } - wg.Wait() - - files := exes(tcs) - resultById := adapter.execTests(files) - - for _, tc := range tcs { - result, ok := resultById[id(tc)] - - // compilation failure - if !ok { - fmt.Printf("CAN'T FIND %s: status %d\n", tc.Cname, tc.Result.Status) - - if tc.Result.Status == planr.PASSING { - cfg := tc.AdapterConfig().(*GtestConfig) - - 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, - ) - - tc.Result.Status = planr.COMPILATION_FAILURE - tc.Result.DebugOutput += fmt.Sprintf("planr: Did not find testcase %s in any test executable\n", id(tc)) - } - - continue - } - if !result.pass { - tc.Result.Status = planr.RUNTIME_FAILURE - } + return results +} - tc.Result.TestOutput = result.testOutput - } +func NewAdapter() *Adapter { + return new(Adapter) } diff --git a/adapters/gtest/config.go b/adapters/gtest/config.go index cff45fa..533f266 100644 --- a/adapters/gtest/config.go +++ b/adapters/gtest/config.go @@ -2,79 +2,88 @@ package gtest import ( "log" - "golang.flu0r1ne.net/planr" "strings" + "path" + "golang.flu0r1ne.net/planr" "github.com/BurntSushi/toml" ) -type GtestDefaults struct { - Name *string - Suite *string - Testfile *string - Test_root *string - Srcs *[]string - Srcs_root *string +const ( + DEFAULT_TIMEOUT = 1000 +) + +type Defaults struct { + Name string + Suite string + Testfile string + Srcs []string + 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 } - 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 } + if(child.Name == "") { child.Name = parent.Name } + if(child.Suite == "") { child.Suite = parent.Suite } + if(child.Testfile == "") { child.Testfile = parent.Testfile } + if(len(child.Srcs) == 0) { child.Srcs = parent.Srcs } + if(child.Timeout == 0) { child.Timeout = parent.Timeout } } -type GtestConfig struct { - GtestDefaults +type Config struct { + Defaults } -func (g GtestConfig) ensureSatisfied(path string) { - if g.Name == nil { +func (c * Config) finalize(path string) { + if c.Name == "" { log.Fatalf("\"name\" is not defined for unit: %s\n", path) - } else if g.Suite == nil { + } else if c.Suite == "" { log.Fatalf("\"suite\" is not defined for unit: %s\n", path) - } else if g.Testfile == nil { + } else if c.Testfile == "" { log.Fatalf("\"testfile\" is not defined for unit: %s\n", path) } + + if c.Timeout == 0 { + c.Timeout = DEFAULT_TIMEOUT; + } } -func (cfg GtestConfig) joinTests(path_ string) string { - if cfg.Test_root == nil { - return planr.JoinConfigDir("tests", path_) +func srcList(srcdir string, srcs []string) string { + builder := strings.Builder {} + + for _, src := range srcs { + builder.WriteString("\"") + builder.WriteString(path.Join(srcdir, src)) + builder.WriteString("\"\n ") } - - return planr.JoinConfigDir(*cfg.Test_root, path_) + + return builder.String() } -func (cfg GtestConfig) joinSrcs(path_ string) string { - if cfg.Srcs_root == nil { - return planr.JoinConfigDir("../src", path_) +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) + + units[i] = cmakeUnit { exe.exeNm, testpath, srclist } } - return planr.JoinConfigDir(*cfg.Srcs_root, path_) + return units } -func (cfg GtestConfig) srcList() string { - var srcList string +func finalizeConfigs(tcs []planr.TestCase) { + for i := range tcs { + cfg := tcs[i].AdapterConfig().(*Config) - 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 ") + cfg.finalize(tcs[i].Path) } - - return srcList } func ParseConfig(prim toml.Primitive) (planr.InheritableConfig, error) { - config := GtestConfig{} + config := Config{} if err := toml.PrimitiveDecode(prim, &config); err != nil { return nil, err @@ -84,7 +93,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/adapters/gtest/executable.go b/adapters/gtest/executable.go new file mode 100644 index 0000000..25c83c1 --- /dev/null +++ b/adapters/gtest/executable.go @@ -0,0 +1,185 @@ +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, + ) + } + + if len(results) > 1 { + log.Fatalf("Unexpected number of results, filter should have produced one result") + } + + 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 c49f170..41c54c1 100644 --- a/adapters/gtest/templating.go +++ b/adapters/gtest/templating.go @@ -3,17 +3,18 @@ package gtest import ( "io" "log" - "text/template" "os" + "golang.flu0r1ne.net/planr" + "text/template" ) 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() @@ -27,45 +28,67 @@ func genCmake(out string, units []cmakeUnit) { log.Fatalf("Could not open CMakeFile (%s)\n%v", out, err) } - writeBoiler(file) + writeCmakeBoilerplate(file) tmpl := unitTemplate() 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(` + +################################################ + +## {{.ExeNm}} + add_executable( - "{{.Cname}}" + "{{.ExeNm}}" "{{.File}}" {{.Srcs}} ) target_link_libraries( - "{{.Cname}}" + "{{.ExeNm}}" gtest_main ) gtest_discover_tests( - "{{.Cname}}" + "{{.ExeNm}}" ) `) if err != nil { - log.Fatalf("Cannot load Gtest Unit Template %v", err) + log.Fatalf("Cannot load Gtest unit template %v", err) } return tmpl } -func writeBoiler(w io.Writer) { - w.Write([]byte(` +const GOOGLE_TEST_URL = "https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip" + +func writeCmakeBoilerplate(w io.Writer) { + tmpl := boilderTemplate() + + tmpl.Execute(w, struct { + Url string + Version string + }{ + Url: GOOGLE_TEST_URL, + Version: planr.VERSION, + }) +} + +func boilderTemplate() *template.Template { + tmpl, err := template.New("gtest_boilerplate").Parse(` +# AUTOMATICALLY GENERATED BY PLANR VERSION {{.Version}} + cmake_minimum_required (VERSION 3.1.0) project(PlanRGtestAdapter) @@ -73,12 +96,16 @@ project(PlanRGtestAdapter) include(FetchContent) FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip + URL {{.Url}} ) include(GoogleTest) FetchContent_MakeAvailable(googletest) -`)) -} - +`) + + if err != nil { + log.Fatalf("Cannot load Gtest Cmake boilerplate") + } + return tmpl +} |