From 96701f87a114557f1a013229e889b4b726aa7dc1 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Thu, 2 Sep 2021 02:16:20 -0500 Subject: Break searching / error handling for standard directory structure into auxiliary file / struct --- stddirs.go | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 stddirs.go diff --git a/stddirs.go b/stddirs.go new file mode 100644 index 0000000..adefce8 --- /dev/null +++ b/stddirs.go @@ -0,0 +1,177 @@ +package planr + +import ( + "os" + "log" + "path" + "path/filepath" +) + +// Standard Directories: +// +// Planr relies on a standard directory structure to resolve paths to source code +// These directories can be overridden via environmental variables // +// Directories: +// - `.` root directory +// - `src` contains source code +// - `planr` contains test metadata (also called configuration directory) +// - `planr/rubric` contains test case configuration +// - `planr/tests` contains source code for test cases +// - `planr/build` contains all files written during the build process (ephemeral) + +// Env overrides +const ( + ENV_CONFIG_DIR="PLANR_PLANR_DIR" + ENV_SRC_DIR="ENV_SRC_DIR" + ENV_BUILD_DIR="PLANR_BUILD_DIR" +) + +// Try these search directories +var CONFIG_SEARCH_DIRS = [2] string { + "planr", + ".planr", +} + +// Path are relative to the "planr" project config directory +const ( + DEFAULT_PATH_SRC="../src" + DEFAULT_PATH_BUILD="build" + DEFAULT_PATH_RUBRIC="rubric" + DEFAULT_PATH_TESTS="tests" +) + +type dirConfig struct { + src string + config string + build string +} + +func dieDirAbsent(name, path string) { + if !directoryExists(path) { + log.Fatalf("Could not find %s directory tried %s", name, path) + } +} + +func dirFromEnv(name, env string) *string { + if dir, isSet := os.LookupEnv(env); isSet { + + dieDirAbsent(name, dir) + + return &dir + } + + return nil +} + +func (c *dirConfig) SetSrcDir(srcDir string) { + dieDirAbsent("src", srcDir) + c.src = srcDir +} + +func (c *dirConfig) SetConfigDir(configDir string) { + dieDirAbsent("planr (config)", configDir) + c.config = configDir +} + +func (c *dirConfig) SetBuildDir(buildDir string) { + dieDirAbsent("build", buildDir) + c.build = buildDir +} + +func (c *dirConfig) SetConfigDirFromTree(cdir string) { + var configDir string + + found := traverseUp(cdir, func (path string) bool { + + for _, dir := range CONFIG_SEARCH_DIRS { + configDir = filepath.Join(path, dir) + + if directoryExists(configDir) { + return true + } + } + + return false + }); + + if !found { + log.Fatal("Could not find planr directory"); + } + + c.config = configDir +} + +func (c dirConfig) ConfigDir() string { + if c.config != "" { + return c.config + } + + dir := dirFromEnv("config", ENV_CONFIG_DIR) + if dir == nil { + log.Fatal("Could not find directory") + } + + c.config = *dir + return c.config +} + +func (c dirConfig) SrcDir() string { + if c.src != "" { + return c.src + } + + if dir := dirFromEnv("src", ENV_SRC_DIR); dir != nil { + c.src = *dir + return c.src + } + + // set path relative to config + dir := c.ConfigDir() + return path.Join(dir, DEFAULT_PATH_SRC) +} + +func (c dirConfig) BuildDir() string { + if c.src != "" { + return c.src + } + + if dir := dirFromEnv("build", ENV_BUILD_DIR); dir != nil { + c.build = *dir + return c.build + } + + dir := c.ConfigDir() + return path.Join(dir, DEFAULT_PATH_BUILD) +} + +func (c dirConfig) CleanBuildDir() { + build := c.BuildDir() + + if err := os.RemoveAll(build); err != nil { + log.Fatalf("Cannot build directory %v\n", err) + } + + if err := os.Remove(build); err != nil { + log.Fatalf("Could not remove build directory %v\n", err) + } +} + +func (c dirConfig) MkBuildDir() { + build := c.BuildDir() + + if err := os.Mkdir(build, 0755); err != nil { + log.Fatalf("Could not create build directory %v\n", err) + } +} + +func (c dirConfig) RubricDir() string { + rubric := path.Join(c.ConfigDir(), "rubric") + dieDirAbsent("rubric", rubric) + return rubric +} + +func (c dirConfig) TestsDir() string { + tests := path.Join(c.ConfigDir(), "tests") + dieDirAbsent("tests", tests) + return tests +} -- cgit v1.2.3 From d078f6dc10eb265a5d88cd96adf86173d6d3ba2e Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Thu, 2 Sep 2021 03:14:47 -0500 Subject: Make adapters and internals complient with new directory structure --- adapters.go | 24 ++++++++- adapters/gtest/adapter.go | 77 ++++++++++++++++++----------- adapters/gtest/config.go | 21 ++------ adapters/gtest/templating.go | 2 - cmd/planr/sub/build.go | 12 +++-- cmd/planr/sub/evaluate.go | 4 +- config.go | 33 ------------- fs.go | 115 +++---------------------------------------- runner.go | 48 ++++++++++++------ stddirs.go | 53 +++++++++++--------- testcase.go | 19 ++++++- 11 files changed, 170 insertions(+), 238 deletions(-) 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 +} diff --git a/adapters/gtest/adapter.go b/adapters/gtest/adapter.go index f4fde27..9331cf7 100644 --- a/adapters/gtest/adapter.go +++ b/adapters/gtest/adapter.go @@ -11,55 +11,38 @@ import ( "path" "sync" "time" - "golang.flu0r1ne.net/planr" ) const GTEST_CMAKE = "CMakeLists.txt" -func mkUnit(tc *planr.TestCase) cmakeUnit { +func makeUnit(tc *planr.TestCase, dirs planr.DirConfig) cmakeUnit { cfg := tc.AdapterConfig().(*GtestConfig) + testpath := path.Join(dirs.TestsDir(), *cfg.Testfile) + srclist := cfg.srcList(dirs.SrcDir()) + return cmakeUnit { tc.Cname, - cfg.joinTests(*cfg.Testfile), - cfg.srcList(), + testpath, + srclist, }; } +func safeWd() string{ + wd, err := os.Getwd() -type GtestAdapter struct {} - -func (a *GtestAdapter) 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)) + if err != nil { + log.Fatalf("Could not get GtestBuildDir %s %v\n", wd, err) } - genCmake(cmakeFile, units) - - planr.RunCmd("cmake", "-S", ".", "-B", ".") + return wd } type ResultFromId map[string] Result func (adapter *GtestAdapter) execTests(cnames []string) ResultFromId { - buildDir := adapter.Config().Dir() + buildDir := safeWd() lut := make(ResultFromId, 0) for _, exe := range cnames { @@ -154,6 +137,40 @@ func compile(wg * sync.WaitGroup, tc *planr.TestCase) { tc.Result.DebugOutput = string(out) } +type GtestAdapter struct { + dirs planr.DirConfig +} + +func (a *GtestAdapter) Config() planr.AdapterConfig { + return planr.AdapterConfig { + Name: "gtest", + ParseConfig: ParseConfig, + ParseDefaultConfig: ParseDefaultConfig, + } +} + +func (a *GtestAdapter) Init(dirs planr.DirConfig) { + a.dirs = dirs +} + +func (adapter *GtestAdapter) 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.ensureSatisfied(tc.Path) + + units = append(units, makeUnit(tc, adapter.dirs)) + } + + genCmake(cmakeFile, units) + + planr.RunCmd("cmake", "-S", ".", "-B", ".") +} + // ./planr eval 0.93s user 0.16s system 100% cpu 1.089 total func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) { var wg sync.WaitGroup @@ -169,7 +186,7 @@ func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) { for _, tc := range tcs { result, ok := resultById[id(tc)] - // compilation failure + // compilation failure if !ok { fmt.Printf("CAN'T FIND %s: status %d\n", tc.Cname, tc.Result.Status) diff --git a/adapters/gtest/config.go b/adapters/gtest/config.go index cff45fa..4f8735f 100644 --- a/adapters/gtest/config.go +++ b/adapters/gtest/config.go @@ -5,6 +5,7 @@ import ( "golang.flu0r1ne.net/planr" "strings" "github.com/BurntSushi/toml" + "path" ) type GtestDefaults struct { @@ -42,29 +43,13 @@ func (g GtestConfig) ensureSatisfied(path string) { } } -func (cfg GtestConfig) joinTests(path_ string) string { - if cfg.Test_root == nil { - return planr.JoinConfigDir("tests", path_) - } - - return planr.JoinConfigDir(*cfg.Test_root, path_) -} - -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 { +func (cfg GtestConfig) srcList(srcDir string) string { var srcList string if cfg.Srcs != nil { srcs := make([]string, len(*cfg.Srcs)) for i, src := range *cfg.Srcs { - srcs[i] = "\"" + cfg.joinSrcs(src) + "\"" + srcs[i] = "\"" + path.Join(srcDir, src) + "\"" } srcList = strings.Join(srcs, "\n ") diff --git a/adapters/gtest/templating.go b/adapters/gtest/templating.go index c49f170..a9a3b07 100644 --- a/adapters/gtest/templating.go +++ b/adapters/gtest/templating.go @@ -80,5 +80,3 @@ include(GoogleTest) FetchContent_MakeAvailable(googletest) `)) } - - diff --git a/cmd/planr/sub/build.go b/cmd/planr/sub/build.go index 58c3a38..a8b19ec 100644 --- a/cmd/planr/sub/build.go +++ b/cmd/planr/sub/build.go @@ -3,17 +3,21 @@ package sub import ( "golang.flu0r1ne.net/planr" "golang.flu0r1ne.net/planr/adapters/gtest" + "os" ) func Runner() planr.Runner { r := planr.Runner {} + r.RegisterAdapter(>est.GtestAdapter{}) + + if wd, err := os.Getwd(); err != nil { + r.SetConfigDirFromTree(wd) + } + return r } func Build(params []string) { - - rd := planr.RubricDir() - - Runner().Build(rd) + Runner().Build() } diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index 8ce4d81..d183b86 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -5,9 +5,7 @@ import ( ) func Evaluate(params []string) { - rd := planr.RubricDir() - - tcs := Runner().Evaluate(rd) + tcs := Runner().Evaluate() earned := 0.0 total := 0.0 diff --git a/config.go b/config.go index bc0fa6a..887bbb0 100644 --- a/config.go +++ b/config.go @@ -1,11 +1,8 @@ package planr import ( - "log" "github.com/BurntSushi/toml" ) - - /* TODO: Every property defined within the defaults currently has to implement the "inherit" method to conditionally inherit a @@ -38,22 +35,6 @@ type InheritableConfig interface { Inherit(parent interface{}) } -// 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 -} // Program-wide configuration which is recognized // in defaults.toml @@ -82,18 +63,6 @@ type Defaults struct { configs_ *[]AdapterConfig } -// Program-wide testcase config -type TestCaseConfig struct { - Defaults - Title *string - Description *string -} - -func (c TestCaseConfig) ensureSatisfied(name string) { - if (c.Adapter == nil) { - log.Fatalf("Adapter must be provided for testcase %s", name) - } -} // The default configuration must be able in inherit from // other defaults further up the tree @@ -169,8 +138,6 @@ func (defaults *Defaults) decodeAdapters( func DecodeDefaults(path string, adapterCfg []AdapterConfig) (Defaults, error) { defaults := Defaults { } - - if _, err := toml.DecodeFile(path, &defaults); err != nil { return defaults, err } diff --git a/fs.go b/fs.go index 86de16b..ffc5f05 100644 --- a/fs.go +++ b/fs.go @@ -45,115 +45,6 @@ func directoryExists(path string) bool { return info.IsDir() } -// Find the configuration directory -// Uses: -// 1. PlANR_DIRECTORY env if set -// 2. planr -// 3. .planr -func ConfigDir() string { - - // Return environmental override if set - if dir, isSet := os.LookupEnv("PLANR_DIRECTORY"); isSet { - - if !directoryExists(dir) { - log.Fatalf("Cannot find planr directory %s", dir); - } - - return dir; - } - - cwd, err := os.Getwd() - - if err != nil { - log.Fatal(err) - } - - var rubricDir string - - rubric_search_dirs := [2]string{ - "planr", - ".planr", - } - - found := traverseUp(cwd, func (path string) bool { - - for _, dir := range rubric_search_dirs { - rubricDir = filepath.Join(path, dir) - - if directoryExists(rubricDir) { - return true - } - } - - return false - }); - - if !found { - log.Fatal("Could not find planr directory"); - } - - 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"); - - if !directoryExists(rubricDir) { - log.Fatal("Could not find the rubric directory inside of planr") - } - - 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) Dir() 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)] @@ -262,3 +153,9 @@ func collectFromDir( } } } + +func safeCd(newWd string) { + if err := os.Chdir(newWd); err != nil { + log.Fatalf("Could not change into directory %v\n", err) + } +} diff --git a/runner.go b/runner.go index 3bee17a..f2eb8e9 100644 --- a/runner.go +++ b/runner.go @@ -3,10 +3,12 @@ package planr import ( "log" "os" + "path" ) type Runner struct { adapters []Adapter + DirConfig } func (r *Runner) RegisterAdapter(a Adapter) { @@ -43,12 +45,17 @@ func (r Runner) checkConfig(tcs []TestCase) { } } -func cdBuild(adapter Adapter) { - dir := adapter.Config().Dir() +func (r Runner) setupEnv(adapter Adapter) { + nm := adapter.Config().Name + wd := path.Join(r.BuildDir(), nm) - if err := os.Chdir(dir); err != nil { - log.Fatal(err) - } + if !directoryExists(wd) { + if err := os.Mkdir(wd, 0755); err != nil { + log.Fatalf("Could not create adapter config %s %v\n", wd, err) + } + } + + safeCd(wd) } func (r Runner) build(tcs []TestCase) { @@ -58,18 +65,25 @@ func (r Runner) build(tcs []TestCase) { for _, adapter := range r.adapters { nm := adapter.Config().Name - cdBuild(adapter) + r.setupEnv(adapter) adapter.Build(tcTab[nm]) } + + safeCd(r.ConfigDir()) } -func (r Runner) units(root string) []TestCase { - return collectUnits(root, r.adapterCfgs()) +func (r Runner) units() []TestCase { + return collectUnits(r.RubricDir(), r.adapterCfgs()) } -func (r Runner) Build(root string) { - units := r.units(root) +func (r Runner) Build() { + units := r.units() + + if !directoryExists(r.BuildDir()) { + r.MkBuildDir() + } + r.build(units) } @@ -78,16 +92,22 @@ func (r Runner) evaluate(tcs []TestCase) { for _, adapter := range r.adapters { nm := adapter.Config().Name - cdBuild(adapter) - + + r.setupEnv(adapter) adapter.Evaluate(tcTab[nm]) } + + safeCd(r.ConfigDir()) } -func (r Runner) Evaluate(root string) []TestCase { - units := r.units(root) +func (r Runner) Evaluate() []TestCase { + units := r.units() r.evaluate(units) return units } + +func (r Runner) Clean() { + r.CleanBuildDir() +} diff --git a/stddirs.go b/stddirs.go index adefce8..14776d8 100644 --- a/stddirs.go +++ b/stddirs.go @@ -40,10 +40,13 @@ const ( DEFAULT_PATH_TESTS="tests" ) -type dirConfig struct { - src string - config string - build string +type DirConfig struct { + src string + config string + build string + + // Config falls back to the config found in the parent directory if the env variable hasn't been overridden + pdFallback string } func dieDirAbsent(name, path string) { @@ -63,22 +66,22 @@ func dirFromEnv(name, env string) *string { return nil } -func (c *dirConfig) SetSrcDir(srcDir string) { +func (c *DirConfig) SetSrcDir(srcDir string) { dieDirAbsent("src", srcDir) c.src = srcDir } -func (c *dirConfig) SetConfigDir(configDir string) { +func (c *DirConfig) SetConfigDir(configDir string) { dieDirAbsent("planr (config)", configDir) c.config = configDir } -func (c *dirConfig) SetBuildDir(buildDir string) { +func (c *DirConfig) SetBuildDir(buildDir string) { dieDirAbsent("build", buildDir) c.build = buildDir } -func (c *dirConfig) SetConfigDirFromTree(cdir string) { +func (c *DirConfig) SetConfigDirFromTree(cdir string) { var configDir string found := traverseUp(cdir, func (path string) bool { @@ -94,28 +97,32 @@ func (c *dirConfig) SetConfigDirFromTree(cdir string) { return false }); - if !found { - log.Fatal("Could not find planr directory"); - } - c.config = configDir + if found { + c.pdFallback = configDir + } } -func (c dirConfig) ConfigDir() string { +func (c DirConfig) ConfigDir() string { if c.config != "" { return c.config } - dir := dirFromEnv("config", ENV_CONFIG_DIR) - if dir == nil { - log.Fatal("Could not find directory") + + if dir := dirFromEnv("config", ENV_CONFIG_DIR); dir != nil { + c.config = *dir + return c.config + } + + if c.pdFallback == "" { + log.Fatal("Could not find planr directory"); } - c.config = *dir + c.config = c.pdFallback; return c.config } -func (c dirConfig) SrcDir() string { +func (c DirConfig) SrcDir() string { if c.src != "" { return c.src } @@ -130,7 +137,7 @@ func (c dirConfig) SrcDir() string { return path.Join(dir, DEFAULT_PATH_SRC) } -func (c dirConfig) BuildDir() string { +func (c DirConfig) BuildDir() string { if c.src != "" { return c.src } @@ -144,7 +151,7 @@ func (c dirConfig) BuildDir() string { return path.Join(dir, DEFAULT_PATH_BUILD) } -func (c dirConfig) CleanBuildDir() { +func (c DirConfig) CleanBuildDir() { build := c.BuildDir() if err := os.RemoveAll(build); err != nil { @@ -156,7 +163,7 @@ func (c dirConfig) CleanBuildDir() { } } -func (c dirConfig) MkBuildDir() { +func (c DirConfig) MkBuildDir() { build := c.BuildDir() if err := os.Mkdir(build, 0755); err != nil { @@ -164,13 +171,13 @@ func (c dirConfig) MkBuildDir() { } } -func (c dirConfig) RubricDir() string { +func (c DirConfig) RubricDir() string { rubric := path.Join(c.ConfigDir(), "rubric") dieDirAbsent("rubric", rubric) return rubric } -func (c dirConfig) TestsDir() string { +func (c DirConfig) TestsDir() string { tests := path.Join(c.ConfigDir(), "tests") dieDirAbsent("tests", tests) return tests diff --git a/testcase.go b/testcase.go index 8506b84..7e0bf17 100644 --- a/testcase.go +++ b/testcase.go @@ -1,9 +1,13 @@ package planr +import ( + "log" +) + type TestStatus uint const ( - PASSING TestStatus = iota + PASSING TestStatus = iota COMPILATION_FAILURE RUNTIME_FAILURE ) @@ -15,6 +19,19 @@ type TestResult struct { TestOutput string } +// Program-wide testcase config +type TestCaseConfig struct { + Defaults + Title *string + Description *string +} + +func (c TestCaseConfig) ensureSatisfied(name string) { + if (c.Adapter == nil) { + log.Fatalf("Adapter must be provided for testcase %s", name) + } +} + type TestCase struct { // absolute path to the test case configuration Path string -- cgit v1.2.3 From 287d029975b7718109f81b480079f375f7d8700a Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 00:02:31 -0500 Subject: Add clean option and fix issue with dir propegation --- Makefile | 8 +++++++- adapters/gtest/adapter.go | 4 ++-- adapters/gtest/config.go | 4 ---- cmd/planr/main.go | 4 ++++ cmd/planr/sub/build.go | 2 +- cmd/planr/sub/clean.go | 5 +++++ runner.go | 32 +++++++++++++++++++++++--------- stddirs.go | 41 ++++++++++++++++++++--------------------- 8 files changed, 62 insertions(+), 38 deletions(-) create mode 100644 cmd/planr/sub/clean.go diff --git a/Makefile b/Makefile index 424c67d..dd6031f 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,14 @@ endif CMD := planr +ifdef DEBUG + FLAGS=-ldflags=-w +endif + +MAIN_PKG := ./cmd/planr/main.go + $(CMD): - go build -o $(CMD) ./cmd/planr/main.go + go build $(FLAGS) -o $(CMD) $(MAIN_PKG) install: mkdir -p $(DESTDIR)$(PREFIX)$(BINDIR)/ diff --git a/adapters/gtest/adapter.go b/adapters/gtest/adapter.go index 9331cf7..415d823 100644 --- a/adapters/gtest/adapter.go +++ b/adapters/gtest/adapter.go @@ -19,8 +19,8 @@ const GTEST_CMAKE = "CMakeLists.txt" func makeUnit(tc *planr.TestCase, dirs planr.DirConfig) cmakeUnit { cfg := tc.AdapterConfig().(*GtestConfig) - testpath := path.Join(dirs.TestsDir(), *cfg.Testfile) - srclist := cfg.srcList(dirs.SrcDir()) + testpath := path.Join(dirs.Tests(), *cfg.Testfile) + srclist := cfg.srcList(dirs.Src()) return cmakeUnit { tc.Cname, diff --git a/adapters/gtest/config.go b/adapters/gtest/config.go index 4f8735f..4105932 100644 --- a/adapters/gtest/config.go +++ b/adapters/gtest/config.go @@ -12,9 +12,7 @@ type GtestDefaults struct { Name *string Suite *string Testfile *string - Test_root *string Srcs *[]string - Srcs_root *string } func (child *GtestDefaults) Inherit(p interface{}) { @@ -23,9 +21,7 @@ func (child *GtestDefaults) 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.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 } } diff --git a/cmd/planr/main.go b/cmd/planr/main.go index e2a85ed..4b69a36 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -19,6 +19,8 @@ func printUsage(w io.Writer) { fmt.Fprintln(w, " version ") fmt.Fprintln(w, " build ") fmt.Fprintln(w, " evaluate ") + fmt.Fprintln(w, " clean ") + } func dieUsage() { @@ -45,6 +47,8 @@ func main() { sub.Build(subargs) case "evaluate","eval": sub.Evaluate(subargs) + case "clean": + sub.Clean(subargs) case "help", "-h", "-help", "--help": printUsage(os.Stdout) default: diff --git a/cmd/planr/sub/build.go b/cmd/planr/sub/build.go index a8b19ec..142ef1c 100644 --- a/cmd/planr/sub/build.go +++ b/cmd/planr/sub/build.go @@ -11,7 +11,7 @@ func Runner() planr.Runner { r.RegisterAdapter(>est.GtestAdapter{}) - if wd, err := os.Getwd(); err != nil { + if wd, err := os.Getwd(); err == nil { r.SetConfigDirFromTree(wd) } diff --git a/cmd/planr/sub/clean.go b/cmd/planr/sub/clean.go new file mode 100644 index 0000000..d40e967 --- /dev/null +++ b/cmd/planr/sub/clean.go @@ -0,0 +1,5 @@ +package sub + +func Clean(params []string) { + Runner().Clean() +} diff --git a/runner.go b/runner.go index f2eb8e9..0a6243c 100644 --- a/runner.go +++ b/runner.go @@ -8,7 +8,7 @@ import ( type Runner struct { adapters []Adapter - DirConfig + dirs DirConfig } func (r *Runner) RegisterAdapter(a Adapter) { @@ -47,7 +47,7 @@ func (r Runner) checkConfig(tcs []TestCase) { func (r Runner) setupEnv(adapter Adapter) { nm := adapter.Config().Name - wd := path.Join(r.BuildDir(), nm) + wd := path.Join(r.dirs.Build(), nm) if !directoryExists(wd) { if err := os.Mkdir(wd, 0755); err != nil { @@ -70,18 +70,26 @@ func (r Runner) build(tcs []TestCase) { adapter.Build(tcTab[nm]) } - safeCd(r.ConfigDir()) + safeCd(r.dirs.Config()) +} + +func (r Runner) init_adapters() { + for _, adapter := range r.adapters { + adapter.Init(r.dirs) + } } func (r Runner) units() []TestCase { - return collectUnits(r.RubricDir(), r.adapterCfgs()) + return collectUnits(r.dirs.Rubric(), r.adapterCfgs()) } func (r Runner) Build() { - units := r.units() + r.init_adapters() - if !directoryExists(r.BuildDir()) { - r.MkBuildDir() + units := r.units() + + if !directoryExists(r.dirs.Build()) { + r.dirs.MkBuild() } r.build(units) @@ -97,10 +105,12 @@ func (r Runner) evaluate(tcs []TestCase) { adapter.Evaluate(tcTab[nm]) } - safeCd(r.ConfigDir()) + safeCd(r.dirs.Config()) } func (r Runner) Evaluate() []TestCase { + r.init_adapters() + units := r.units() r.evaluate(units) @@ -109,5 +119,9 @@ func (r Runner) Evaluate() []TestCase { } func (r Runner) Clean() { - r.CleanBuildDir() + r.dirs.CleanBuild() +} + +func (r * Runner) SetConfigDirFromTree(childPath string) { + r.dirs.SetConfigFromTree(childPath) } diff --git a/stddirs.go b/stddirs.go index 14776d8..2385529 100644 --- a/stddirs.go +++ b/stddirs.go @@ -66,22 +66,22 @@ func dirFromEnv(name, env string) *string { return nil } -func (c *DirConfig) SetSrcDir(srcDir string) { +func (c *DirConfig) SetSrc(srcDir string) { dieDirAbsent("src", srcDir) c.src = srcDir } -func (c *DirConfig) SetConfigDir(configDir string) { +func (c *DirConfig) SetConfig(configDir string) { dieDirAbsent("planr (config)", configDir) c.config = configDir } -func (c *DirConfig) SetBuildDir(buildDir string) { +func (c *DirConfig) SetBuild(buildDir string) { dieDirAbsent("build", buildDir) c.build = buildDir } -func (c *DirConfig) SetConfigDirFromTree(cdir string) { +func (c *DirConfig) SetConfigFromTree(cdir string) { var configDir string found := traverseUp(cdir, func (path string) bool { @@ -103,12 +103,11 @@ func (c *DirConfig) SetConfigDirFromTree(cdir string) { } } -func (c DirConfig) ConfigDir() string { +func (c DirConfig) Config() string { if c.config != "" { return c.config } - if dir := dirFromEnv("config", ENV_CONFIG_DIR); dir != nil { c.config = *dir return c.config @@ -122,7 +121,7 @@ func (c DirConfig) ConfigDir() string { return c.config } -func (c DirConfig) SrcDir() string { +func (c DirConfig) Src() string { if c.src != "" { return c.src } @@ -133,11 +132,11 @@ func (c DirConfig) SrcDir() string { } // set path relative to config - dir := c.ConfigDir() + dir := c.Config() return path.Join(dir, DEFAULT_PATH_SRC) } -func (c DirConfig) BuildDir() string { +func (c DirConfig) Build() string { if c.src != "" { return c.src } @@ -147,38 +146,38 @@ func (c DirConfig) BuildDir() string { return c.build } - dir := c.ConfigDir() + dir := c.Config() return path.Join(dir, DEFAULT_PATH_BUILD) } -func (c DirConfig) CleanBuildDir() { - build := c.BuildDir() +func (c DirConfig) CleanBuild() { + build := c.Build() if err := os.RemoveAll(build); err != nil { log.Fatalf("Cannot build directory %v\n", err) } - if err := os.Remove(build); err != nil { - log.Fatalf("Could not remove build directory %v\n", err) - } + // if err := os.Remove(build); err != nil { + // log.Fatalf("Could not remove build directory %v\n", err) + // } } -func (c DirConfig) MkBuildDir() { - build := c.BuildDir() +func (c DirConfig) MkBuild() { + build := c.Build() if err := os.Mkdir(build, 0755); err != nil { log.Fatalf("Could not create build directory %v\n", err) } } -func (c DirConfig) RubricDir() string { - rubric := path.Join(c.ConfigDir(), "rubric") +func (c DirConfig) Rubric() string { + rubric := path.Join(c.Config(), "rubric") dieDirAbsent("rubric", rubric) return rubric } -func (c DirConfig) TestsDir() string { - tests := path.Join(c.ConfigDir(), "tests") +func (c DirConfig) Tests() string { + tests := path.Join(c.Config(), "tests") dieDirAbsent("tests", tests) return tests } -- cgit v1.2.3 From 0e6b7378a564bb51b3a4289059e80fe9e3c6545b Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 00:51:47 -0500 Subject: BUG: Fix decoding when no defaults is present --- adapters/gtest/config.go | 11 +++++++++++ fs.go | 4 +++- stddirs.go | 11 ++++------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/adapters/gtest/config.go b/adapters/gtest/config.go index 4105932..457eb64 100644 --- a/adapters/gtest/config.go +++ b/adapters/gtest/config.go @@ -8,11 +8,16 @@ import ( "path" ) +const ( + DEFAULT_TIMEOUT = 1000 +) + type GtestDefaults struct { Name *string Suite *string Testfile *string Srcs *[]string + Timeout *uint } func (child *GtestDefaults) Inherit(p interface{}) { @@ -22,6 +27,7 @@ func (child *GtestDefaults) Inherit(p interface{}) { 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(child.Timeout == nil) { child.Timeout = parent.Timeout } } @@ -37,6 +43,11 @@ func (g GtestConfig) ensureSatisfied(path string) { } else if g.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; + } } func (cfg GtestConfig) srcList(srcDir string) string { diff --git a/fs.go b/fs.go index ffc5f05..575517c 100644 --- a/fs.go +++ b/fs.go @@ -141,7 +141,9 @@ func collectFromDir( log.Fatalf("Error encountered in %s: %v", child, config) } - config.Inherit(*defaults) + if defaults != nil { + config.Inherit(*defaults) + } tc := TestCase { Path: child, diff --git a/stddirs.go b/stddirs.go index 2385529..e6b510d 100644 --- a/stddirs.go +++ b/stddirs.go @@ -21,13 +21,13 @@ import ( // Env overrides const ( - ENV_CONFIG_DIR="PLANR_PLANR_DIR" - ENV_SRC_DIR="ENV_SRC_DIR" + ENV_CONFIG_DIR="PLANR_CONFIG_DIR" + ENV_SRC_DIR="PLANR_SRC_DIR" ENV_BUILD_DIR="PLANR_BUILD_DIR" ) // Try these search directories -var CONFIG_SEARCH_DIRS = [2] string { +var CONFIG_SEARCH_DIRS = [] string { "planr", ".planr", } @@ -88,7 +88,7 @@ func (c *DirConfig) SetConfigFromTree(cdir string) { for _, dir := range CONFIG_SEARCH_DIRS { configDir = filepath.Join(path, dir) - + if directoryExists(configDir) { return true } @@ -157,9 +157,6 @@ func (c DirConfig) CleanBuild() { log.Fatalf("Cannot build directory %v\n", err) } - // if err := os.Remove(build); err != nil { - // log.Fatalf("Could not remove build directory %v\n", err) - // } } func (c DirConfig) MkBuild() { -- cgit v1.2.3 From aff7b69cab2ac1d09b47f28c6fa653febd8595ea Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 18:12:38 -0500 Subject: Ensure stddirs returns an absolute path for correct operation in adapter build directories --- stddirs.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/stddirs.go b/stddirs.go index e6b510d..1eae439 100644 --- a/stddirs.go +++ b/stddirs.go @@ -109,7 +109,7 @@ func (c DirConfig) Config() string { } if dir := dirFromEnv("config", ENV_CONFIG_DIR); dir != nil { - c.config = *dir + c.config = abs(*dir) return c.config } @@ -117,7 +117,7 @@ func (c DirConfig) Config() string { log.Fatal("Could not find planr directory"); } - c.config = c.pdFallback; + c.config = abs(c.pdFallback); return c.config } @@ -136,6 +136,16 @@ func (c DirConfig) Src() string { return path.Join(dir, DEFAULT_PATH_SRC) } +func abs(path string) string { + apath, err := filepath.Abs(path) + + if err != nil { + log.Fatalf("Could not find path %s", path) + } + + return apath +} + func (c DirConfig) Build() string { if c.src != "" { return c.src @@ -168,13 +178,13 @@ func (c DirConfig) MkBuild() { } func (c DirConfig) Rubric() string { - rubric := path.Join(c.Config(), "rubric") + rubric := path.Join(c.Config(), DEFAULT_PATH_RUBRIC) dieDirAbsent("rubric", rubric) return rubric } func (c DirConfig) Tests() string { - tests := path.Join(c.Config(), "tests") + tests := path.Join(c.Config(), DEFAULT_PATH_TESTS) dieDirAbsent("tests", tests) return tests } -- cgit v1.2.3 From 38912d85394604fe9cc238ef6eb9f6e75ceb38f9 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 18:13:49 -0500 Subject: Add config command to retrieve configuration values and clean command to clean build files --- cmd/planr/main.go | 51 ++++++++++++++++++++++++++++++++++++++++------- cmd/planr/sub/build.go | 17 ++-------------- cmd/planr/sub/clean.go | 6 ++++-- cmd/planr/sub/config.go | 28 ++++++++++++++++++++++++++ cmd/planr/sub/evaluate.go | 4 ++-- runner.go | 24 ++++++++++++++++++++++ 6 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 cmd/planr/sub/config.go diff --git a/cmd/planr/main.go b/cmd/planr/main.go index 4b69a36..adcb50e 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -5,8 +5,11 @@ import ( "io" "log" "os" + "flag" + "golang.flu0r1ne.net/planr" "golang.flu0r1ne.net/planr/cmd/planr/sub" + "golang.flu0r1ne.net/planr/adapters/gtest" ) const ( @@ -20,7 +23,7 @@ func printUsage(w io.Writer) { fmt.Fprintln(w, " build ") fmt.Fprintln(w, " evaluate ") fmt.Fprintln(w, " clean ") - + fmt.Fprintln(w, " config ") } func dieUsage() { @@ -28,6 +31,36 @@ func dieUsage() { os.Exit(1) } +func NewRunner() planr.Runner { + r := planr.Runner {} + + r.RegisterAdapter(>est.GtestAdapter{}) + + if wd, err := os.Getwd(); err == nil { + r.SetConfigDirFromTree(wd) + } + + src := flag.String("srcdir", "", "source directory") + config := flag.String("configdir", "", "config directory") + build := flag.String("builddir", "", "build directory") + + flag.Parse() + + if src != nil && *src != "" { + r.SetSrcDir(*src) + } + + if config != nil && *config != "" { + r.SetConfigDir(*config) + } + + if build != nil && *build != "" { + r.SetBuildDir(*build) + } + + return r +} + func main() { log.SetFlags(log.Llongfile | log.Lmsgprefix) @@ -37,18 +70,22 @@ func main() { dieUsage() } - subcommand := os.Args[1] - subargs := os.Args[2:] + runner := NewRunner() + + subcommand := flag.Arg(0) + subargs := flag.Args()[1:] switch subcommand { case "version": fmt.Printf("%s\n", VERSION) case "build": - sub.Build(subargs) - case "evaluate","eval": - sub.Evaluate(subargs) + sub.Build(runner, subargs) + case "evaluate", "eval": + sub.Evaluate(runner, subargs) case "clean": - sub.Clean(subargs) + sub.Clean(runner, subargs) + case "config": + sub.Config(runner, subargs) case "help", "-h", "-help", "--help": printUsage(os.Stdout) default: diff --git a/cmd/planr/sub/build.go b/cmd/planr/sub/build.go index 142ef1c..4a1cda9 100644 --- a/cmd/planr/sub/build.go +++ b/cmd/planr/sub/build.go @@ -2,22 +2,9 @@ package sub import ( "golang.flu0r1ne.net/planr" - "golang.flu0r1ne.net/planr/adapters/gtest" - "os" ) -func Runner() planr.Runner { - r := planr.Runner {} - r.RegisterAdapter(>est.GtestAdapter{}) - - if wd, err := os.Getwd(); err == nil { - r.SetConfigDirFromTree(wd) - } - - return r -} - -func Build(params []string) { - Runner().Build() +func Build(runner planr.Runner, params []string) { + runner.Build() } diff --git a/cmd/planr/sub/clean.go b/cmd/planr/sub/clean.go index d40e967..d658c10 100644 --- a/cmd/planr/sub/clean.go +++ b/cmd/planr/sub/clean.go @@ -1,5 +1,7 @@ package sub -func Clean(params []string) { - Runner().Clean() +import "golang.flu0r1ne.net/planr" + +func Clean(runner planr.Runner, params []string) { + runner.Clean() } diff --git a/cmd/planr/sub/config.go b/cmd/planr/sub/config.go new file mode 100644 index 0000000..ee372c8 --- /dev/null +++ b/cmd/planr/sub/config.go @@ -0,0 +1,28 @@ +package sub + +import ( + "golang.flu0r1ne.net/planr" + "fmt" + "os" +) + + +func Config(runner planr.Runner, params []string) { + if len(params) != 1 { + fmt.Fprintf(os.Stderr, "Usage: planr config \n") + os.Exit(1) + } + + key := params[0] + + switch key { + case "builddir": + fmt.Printf("%s\n", runner.BuildDir()) + case "configdir": + fmt.Printf("%s\n", runner.ConfigDir()) + case "srcdir": + fmt.Printf("%s\n", runner.SrcDir()) + default: + fmt.Fprintf(os.Stderr, "\"%s\" not found in configuration\n", key) + } +} diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index d183b86..79d377e 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -4,8 +4,8 @@ import ( "golang.flu0r1ne.net/planr" ) -func Evaluate(params []string) { - tcs := Runner().Evaluate() +func Evaluate(runner planr.Runner, params []string) { + tcs := runner.Evaluate() earned := 0.0 total := 0.0 diff --git a/runner.go b/runner.go index 0a6243c..9014b8c 100644 --- a/runner.go +++ b/runner.go @@ -125,3 +125,27 @@ func (r Runner) Clean() { func (r * Runner) SetConfigDirFromTree(childPath string) { r.dirs.SetConfigFromTree(childPath) } + +func (r * Runner) SetBuildDir(dir string) { + r.dirs.SetBuild(dir) +} + +func (r * Runner) SetConfigDir(dir string) { + r.dirs.SetConfig(dir) +} + +func (r * Runner) SetSrcDir(dir string) { + r.dirs.SetSrc(dir) +} + +func (r Runner) BuildDir() string { + return r.dirs.Build(); +} + +func (r Runner) ConfigDir() string { + return r.dirs.Config() +} + +func (r Runner) SrcDir() string { + return r.dirs.Src() +} -- cgit v1.2.3 From ff3c1b99f691640a386fd452fa3b3e77769b9138 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 19:53:06 -0500 Subject: Add project-wide configuration file --- config.go | 165 +++++------------------------------------------------- rubric_config.go | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ version.go | 3 + 3 files changed, 184 insertions(+), 150 deletions(-) create mode 100644 rubric_config.go create mode 100644 version.go diff --git a/config.go b/config.go index 887bbb0..d2d32e9 100644 --- a/config.go +++ b/config.go @@ -2,165 +2,30 @@ package planr import ( "github.com/BurntSushi/toml" + "log" + "path" ) -/* - TODO: Every property defined within the defaults currently - has to implement the "inherit" method to conditionally inherit a - property in relation to a parent. (Ostensibly so that test cases - can override default configuration.) This is pedantic because - most properties will end up writing boilerplate amounting to: - parent.inherit() - ... - if config.property == nil { - config.property = config.parent.property - } - - This library provides copying behavior between structs - with common properties using reflection. It seems like - a slight abuse of reflection... But, it could be - retro-fitted to implement this behavior if a "onlyCopyZeroFields" - option was provided. - - > "github.com/jinzhu/copier" -*/ - -// Inheritable configuration can inherit properties defined in a -// defaults file. This happens on a per-directory basis so multiple -// tests can share common configuration. -// -// The parent will always be of the same type as the child and an -// assertion is required to define the proper behavior. -type InheritableConfig interface { - Inherit(parent interface{}) -} - - -// Program-wide configuration which is recognized -// in defaults.toml -type Defaults struct { - Points *float32 - Adapter *string - - /* - The TOML library only parses exported fields. - The Adapters field is an intermediate mapping - individual adapters to their locally defined - configuration. After they individually process - the configuration, it is mapped to the adapters_ - field. - - See: decodeAdapters() - */ - Adapters *map[string] toml.Primitive - adapters_ map[string] InheritableConfig - - /* - The configs_ field is necessary to property - implement the Inherit method using a common - interface. - */ - configs_ *[]AdapterConfig -} - - -// The default configuration must be able in inherit from -// other defaults further up the tree -// -// This provides multiple levels of configurability -func (child *Defaults) Inherit(p interface{}) { - parent := p.(Defaults) - - // Inherit properties which haven't been configured - if child.Points == nil { - child.Points = parent.Points; - } - - if child.Adapter == nil { - child.Adapter = parent.Adapter; - } - - // Call the inherit method as defined by the adapters - // If an adapter is undefined, inherit the parent configuration - // - // _configs represents all adapters (registered to a runner) - for _, adapter := range *child.configs_ { - parent_adapter, parent_exists := parent.adapters_[adapter.Name] - child_adapter, child_exists := child.adapters_[adapter.Name] - - if parent_exists { - if child_exists { - child_adapter.Inherit(parent_adapter) - } else { - child.adapters_[adapter.Name] = parent_adapter - } - } - } +type planrConfig struct { + Version string + Project_title string } -// Parses the intermediate adapters Adapters containing TOML primitives -// according to methods registered with the runner -// Once parsed, they are stored alongside the registered name to determine -// which adapter will receive the configuration -func (defaults *Defaults) decodeAdapters( - adapters []AdapterConfig, - asDefault bool, -) error { - defaults.configs_ = &adapters - defaults.adapters_ = make(map[string]InheritableConfig) - - if defaults.Adapters != nil { - for _, config := range adapters { - primitive, exists := (*defaults.Adapters)[config.Name] - - if exists { - var parsed InheritableConfig - var err error - if asDefault { - parsed, err = config.ParseDefaultConfig(primitive) - } else { - parsed, err = config.ParseConfig(primitive) - } - - if err != nil { - return err - } - - defaults.adapters_[config.Name] = parsed - } - } - } - - return nil -} +const PLANR_CONFIG = "config.toml" -// Decode defaults.toml -func DecodeDefaults(path string, adapterCfg []AdapterConfig) (Defaults, error) { - defaults := Defaults { } +func decodeConfig(configDir string) planrConfig { + cfg := planrConfig { } - if _, err := toml.DecodeFile(path, &defaults); err != nil { - return defaults, err - } + configFile := path.Join(configDir, PLANR_CONFIG) - if err := defaults.decodeAdapters(adapterCfg, true); err != nil { - return defaults, err + if _, err := toml.DecodeFile(configFile, &cfg); err != nil { + // TODO: handle missing config + log.Fatalf("Could not decode global configuration %s: %v", configFile, err) } - - return defaults, nil + return cfg } -// Decode an individual unit -func DecodeConfig(path string, adapterCfg []AdapterConfig) (TestCaseConfig, error) { - config := TestCaseConfig { } - - if _, err := toml.DecodeFile(path, &config); err != nil { - return config, nil - } - - if err := config.decodeAdapters(adapterCfg, false); err != nil { - return config, err - } - - return config, nil +func (cfg planrConfig) isIncompatibleWithVersion() bool { + return cfg.Version > VERSION } diff --git a/rubric_config.go b/rubric_config.go new file mode 100644 index 0000000..887bbb0 --- /dev/null +++ b/rubric_config.go @@ -0,0 +1,166 @@ +package planr + +import ( + "github.com/BurntSushi/toml" +) +/* + TODO: Every property defined within the defaults currently + has to implement the "inherit" method to conditionally inherit a + property in relation to a parent. (Ostensibly so that test cases + can override default configuration.) This is pedantic because + most properties will end up writing boilerplate amounting to: + + parent.inherit() + ... + if config.property == nil { + config.property = config.parent.property + } + + This library provides copying behavior between structs + with common properties using reflection. It seems like + a slight abuse of reflection... But, it could be + retro-fitted to implement this behavior if a "onlyCopyZeroFields" + option was provided. + + > "github.com/jinzhu/copier" +*/ + +// Inheritable configuration can inherit properties defined in a +// defaults file. This happens on a per-directory basis so multiple +// tests can share common configuration. +// +// The parent will always be of the same type as the child and an +// assertion is required to define the proper behavior. +type InheritableConfig interface { + Inherit(parent interface{}) +} + + +// Program-wide configuration which is recognized +// in defaults.toml +type Defaults struct { + Points *float32 + Adapter *string + + /* + The TOML library only parses exported fields. + The Adapters field is an intermediate mapping + individual adapters to their locally defined + configuration. After they individually process + the configuration, it is mapped to the adapters_ + field. + + See: decodeAdapters() + */ + Adapters *map[string] toml.Primitive + adapters_ map[string] InheritableConfig + + /* + The configs_ field is necessary to property + implement the Inherit method using a common + interface. + */ + configs_ *[]AdapterConfig +} + + +// The default configuration must be able in inherit from +// other defaults further up the tree +// +// This provides multiple levels of configurability +func (child *Defaults) Inherit(p interface{}) { + parent := p.(Defaults) + + // Inherit properties which haven't been configured + if child.Points == nil { + child.Points = parent.Points; + } + + if child.Adapter == nil { + child.Adapter = parent.Adapter; + } + + // Call the inherit method as defined by the adapters + // If an adapter is undefined, inherit the parent configuration + // + // _configs represents all adapters (registered to a runner) + for _, adapter := range *child.configs_ { + parent_adapter, parent_exists := parent.adapters_[adapter.Name] + child_adapter, child_exists := child.adapters_[adapter.Name] + + if parent_exists { + if child_exists { + child_adapter.Inherit(parent_adapter) + } else { + child.adapters_[adapter.Name] = parent_adapter + } + } + } +} + +// Parses the intermediate adapters Adapters containing TOML primitives +// according to methods registered with the runner +// Once parsed, they are stored alongside the registered name to determine +// which adapter will receive the configuration +func (defaults *Defaults) decodeAdapters( + adapters []AdapterConfig, + asDefault bool, +) error { + defaults.configs_ = &adapters + defaults.adapters_ = make(map[string]InheritableConfig) + + if defaults.Adapters != nil { + for _, config := range adapters { + primitive, exists := (*defaults.Adapters)[config.Name] + + if exists { + var parsed InheritableConfig + var err error + if asDefault { + parsed, err = config.ParseDefaultConfig(primitive) + } else { + parsed, err = config.ParseConfig(primitive) + } + + if err != nil { + return err + } + + defaults.adapters_[config.Name] = parsed + } + } + } + + return nil +} + +// Decode defaults.toml +func DecodeDefaults(path string, adapterCfg []AdapterConfig) (Defaults, error) { + defaults := Defaults { } + + if _, err := toml.DecodeFile(path, &defaults); err != nil { + return defaults, err + } + + if err := defaults.decodeAdapters(adapterCfg, true); err != nil { + return defaults, err + } + + + return defaults, nil +} + +// Decode an individual unit +func DecodeConfig(path string, adapterCfg []AdapterConfig) (TestCaseConfig, error) { + config := TestCaseConfig { } + + if _, err := toml.DecodeFile(path, &config); err != nil { + return config, nil + } + + if err := config.decodeAdapters(adapterCfg, false); err != nil { + return config, err + } + + return config, nil +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..51a9233 --- /dev/null +++ b/version.go @@ -0,0 +1,3 @@ +package planr + +const VERSION = "0.0.3" -- cgit v1.2.3 From bc26f02dcdd5ce7d3badf8df5065321538c4c872 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 19:56:07 -0500 Subject: Renamed softscript to dirtyscripting to better describe function --- dirtyscripting.go | 42 ++++++++++++++++++++++++++++++++++++++++++ softscript.go | 42 ------------------------------------------ 2 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 dirtyscripting.go delete mode 100644 softscript.go diff --git a/dirtyscripting.go b/dirtyscripting.go new file mode 100644 index 0000000..9924d54 --- /dev/null +++ b/dirtyscripting.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/softscript.go b/softscript.go deleted file mode 100644 index 9924d54..0000000 --- a/softscript.go +++ /dev/null @@ -1,42 +0,0 @@ -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) - } -} -- cgit v1.2.3 From 7671f58c301ffb22bb49a1de6f87b6db6865ff13 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 20:07:51 -0500 Subject: Use version code in PLANR for the CLI --- cmd/planr/main.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cmd/planr/main.go b/cmd/planr/main.go index adcb50e..277ffc8 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -12,10 +12,6 @@ import ( "golang.flu0r1ne.net/planr/adapters/gtest" ) -const ( - VERSION = "0.0.3" -) - func printUsage(w io.Writer) { fmt.Fprintf (w, "usage: %s command args ... \n", os.Args[0]) fmt.Fprintln(w, " help ") @@ -77,7 +73,7 @@ func main() { switch subcommand { case "version": - fmt.Printf("%s\n", VERSION) + fmt.Printf("%s\n", planr.VERSION) case "build": sub.Build(runner, subargs) case "evaluate", "eval": -- cgit v1.2.3 From 8fa0c34eac027c69a0fce8b5a8251e394cd4d888 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 20:41:11 -0500 Subject: Don't export testcase LUT --- runner.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runner.go b/runner.go index 9014b8c..c0c926a 100644 --- a/runner.go +++ b/runner.go @@ -25,10 +25,10 @@ func (r Runner) adapterCfgs() []AdapterConfig { return cgs } -type TcTab map[string] []*TestCase +type tcTab map[string] []*TestCase -func (r Runner) buildTcLUT(tcs []TestCase) TcTab { - m := make(TcTab, 0) +func (r Runner) buildTcLUT(tcs []TestCase) tcTab { + m := make(tcTab, 0) for i := range tcs { tc := &tcs[i] -- cgit v1.2.3 From 2d197f1ce3eac8cecb5a655fcb5343cbe562ab1a Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 21:14:28 -0500 Subject: Add new test runner API --- adapters/gtest/adapter.go | 7 +++++-- cmd/planr/main.go | 21 ++++++++++----------- runner.go | 20 -------------------- runner_builder.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 runner_builder.go diff --git a/adapters/gtest/adapter.go b/adapters/gtest/adapter.go index 415d823..0a955ef 100644 --- a/adapters/gtest/adapter.go +++ b/adapters/gtest/adapter.go @@ -188,8 +188,7 @@ func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) { // 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) @@ -214,3 +213,7 @@ func (adapter *GtestAdapter) Evaluate(tcs []*planr.TestCase) { tc.Result.TestOutput = result.testOutput } } + +func New() *GtestAdapter { + return new(GtestAdapter) +} diff --git a/cmd/planr/main.go b/cmd/planr/main.go index 277ffc8..b2a0975 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -27,15 +27,14 @@ func dieUsage() { os.Exit(1) } -func NewRunner() planr.Runner { - r := planr.Runner {} - - r.RegisterAdapter(>est.GtestAdapter{}) +func getConfiguredRunner() planr.Runner { + r := planr.ConfigureRunner() + r = planr.RegisterAdapter(r, gtest.New()) if wd, err := os.Getwd(); err == nil { - r.SetConfigDirFromTree(wd) + r = planr.SetConfigDirFromTree(r, wd) } - + src := flag.String("srcdir", "", "source directory") config := flag.String("configdir", "", "config directory") build := flag.String("builddir", "", "build directory") @@ -43,18 +42,18 @@ func NewRunner() planr.Runner { flag.Parse() if src != nil && *src != "" { - r.SetSrcDir(*src) + r = planr.SetSrcDir(r, *src) } if config != nil && *config != "" { - r.SetConfigDir(*config) + r = planr.SetConfigDir(r, *config) } if build != nil && *build != "" { - r.SetBuildDir(*build) + r = planr.SetBuildDir(r, *build) } - return r + return r.New() } func main() { @@ -66,7 +65,7 @@ func main() { dieUsage() } - runner := NewRunner() + runner := getConfiguredRunner() subcommand := flag.Arg(0) subargs := flag.Args()[1:] diff --git a/runner.go b/runner.go index c0c926a..1cc509e 100644 --- a/runner.go +++ b/runner.go @@ -11,10 +11,6 @@ type Runner struct { dirs DirConfig } -func (r *Runner) RegisterAdapter(a Adapter) { - r.adapters = append(r.adapters, a) -} - func (r Runner) adapterCfgs() []AdapterConfig { cgs := make([]AdapterConfig, len(r.adapters)) @@ -122,22 +118,6 @@ func (r Runner) Clean() { r.dirs.CleanBuild() } -func (r * Runner) SetConfigDirFromTree(childPath string) { - r.dirs.SetConfigFromTree(childPath) -} - -func (r * Runner) SetBuildDir(dir string) { - r.dirs.SetBuild(dir) -} - -func (r * Runner) SetConfigDir(dir string) { - r.dirs.SetConfig(dir) -} - -func (r * Runner) SetSrcDir(dir string) { - r.dirs.SetSrc(dir) -} - func (r Runner) BuildDir() string { return r.dirs.Build(); } diff --git a/runner_builder.go b/runner_builder.go new file mode 100644 index 0000000..848d09b --- /dev/null +++ b/runner_builder.go @@ -0,0 +1,42 @@ +package planr + +type RunnerBuilder struct { + adapters [] Adapter + dirs DirConfig +} + +func ConfigureRunner() RunnerBuilder { + return RunnerBuilder {} +} + +func RegisterAdapter(b RunnerBuilder, a Adapter) RunnerBuilder { + b.adapters = append(b.adapters, a) + return b +} + +func SetConfigDirFromTree(b RunnerBuilder, childPath string) RunnerBuilder { + b.dirs.SetConfigFromTree(childPath) + return b +} + +func SetConfigDir(b RunnerBuilder, dir string) RunnerBuilder { + b.dirs.SetConfig(dir) + return b +} + +func SetBuildDir(b RunnerBuilder, dir string) RunnerBuilder { + b.dirs.SetBuild(dir) + return b +} + +func SetSrcDir(b RunnerBuilder, dir string) RunnerBuilder { + b.dirs.SetSrc(dir) + return b +} + +func (b RunnerBuilder) New() Runner { + return Runner { + dirs: b.dirs, + adapters: b.adapters, + } +} -- cgit v1.2.3 From 8c803d453201c8b85172d984fc1aee9eb5af2173 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Fri, 3 Sep 2021 21:45:03 -0500 Subject: Forgo scoring zero point assignment --- cmd/planr/sub/cli.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/planr/sub/cli.go b/cmd/planr/sub/cli.go index 0e6a942..d667b88 100644 --- a/cmd/planr/sub/cli.go +++ b/cmd/planr/sub/cli.go @@ -116,7 +116,9 @@ func printResults(passed, tc_total int, earned, points_total float64) { percent := earned / points_total * 100 - pprintLabeled("score", fmt.Sprintf( - "%.2f/%.2f ~= %.1f%%", earned, points_total, percent, - )); + if points_total != 0 { + pprintLabeled("score", fmt.Sprintf( + "%.2f/%.2f ~= %.1f%%", earned, points_total, percent, + )); + } } -- cgit v1.2.3 From b3252d2bd488b5b58cf0e46151ff9db0721c5fc6 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sat, 4 Sep 2021 13:41:52 -0500 Subject: Add runner initalization --- runner.go | 19 ++++++++++--------- runner_builder.go | 5 +---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/runner.go b/runner.go index 1cc509e..d3d4b08 100644 --- a/runner.go +++ b/runner.go @@ -69,19 +69,11 @@ func (r Runner) build(tcs []TestCase) { safeCd(r.dirs.Config()) } -func (r Runner) init_adapters() { - for _, adapter := range r.adapters { - adapter.Init(r.dirs) - } -} - func (r Runner) units() []TestCase { return collectUnits(r.dirs.Rubric(), r.adapterCfgs()) } func (r Runner) Build() { - r.init_adapters() - units := r.units() if !directoryExists(r.dirs.Build()) { @@ -105,7 +97,6 @@ func (r Runner) evaluate(tcs []TestCase) { } func (r Runner) Evaluate() []TestCase { - r.init_adapters() units := r.units() @@ -129,3 +120,13 @@ func (r Runner) ConfigDir() string { func (r Runner) SrcDir() string { return r.dirs.Src() } + +func NewRunner(adapters []Adapter, dirs DirConfig) Runner { + r := Runner{adapters, dirs} + + for _, adapter := range r.adapters { + adapter.Init(dirs) + } + + return r +} diff --git a/runner_builder.go b/runner_builder.go index 848d09b..b369635 100644 --- a/runner_builder.go +++ b/runner_builder.go @@ -35,8 +35,5 @@ func SetSrcDir(b RunnerBuilder, dir string) RunnerBuilder { } func (b RunnerBuilder) New() Runner { - return Runner { - dirs: b.dirs, - adapters: b.adapters, - } + return NewRunner(b.adapters, b.dirs) } -- cgit v1.2.3 From cc7ba659adbc5ad55e1ce67f76952f2b8392c9c9 Mon Sep 17 00:00:00 2001 From: Flu0r1ne 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(-) 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 0a955ef..7033c7f 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 457eb64..3305977 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 b2a0975..4ff57eb 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 4a1cda9..d24bb02 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 79d377e..0366d44 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 151d516e68f5d43aa2d0c5ff462752d640b6a614 Mon Sep 17 00:00:00 2001 From: Flu0r1ne 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 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 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}}" ) `) diff --git a/cmd/planr/sub/cli.go b/cmd/planr/sub/cli.go index d667b88..3c58f4e 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 0366d44..18cccb1 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 From 4cc29e50d59daf445f4152d10f9f41cb1828f702 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 01:00:12 -0500 Subject: Remove pointers and cleanup templating, add version info --- adapters/gtest/config.go | 27 +++++++++++++-------------- adapters/gtest/executable.go | 15 +++++++-------- adapters/gtest/templating.go | 41 ++++++++++++++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/adapters/gtest/config.go b/adapters/gtest/config.go index 04d426c..533f266 100644 --- a/adapters/gtest/config.go +++ b/adapters/gtest/config.go @@ -13,21 +13,21 @@ const ( ) type Defaults struct { - Name *string - Suite *string - Testfile *string + Name string + Suite string + Testfile string Srcs []string - Timeout *uint + Timeout uint } 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.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 == nil) { child.Timeout = parent.Timeout } + if(child.Timeout == 0) { child.Timeout = parent.Timeout } } @@ -36,17 +36,16 @@ type Config struct { } func (c * Config) finalize(path string) { - if c.Name == nil { + if c.Name == "" { log.Fatalf("\"name\" is not defined for unit: %s\n", path) - } else if c.Suite == nil { + } else if c.Suite == "" { log.Fatalf("\"suite\" is not defined for unit: %s\n", path) - } else if c.Testfile == nil { + } else if c.Testfile == "" { log.Fatalf("\"testfile\" is not defined for unit: %s\n", path) } - if c.Timeout == nil { - c.Timeout = new(uint) - *c.Timeout = DEFAULT_TIMEOUT; + if c.Timeout == 0 { + c.Timeout = DEFAULT_TIMEOUT; } } diff --git a/adapters/gtest/executable.go b/adapters/gtest/executable.go index 78b0b56..25c83c1 100644 --- a/adapters/gtest/executable.go +++ b/adapters/gtest/executable.go @@ -27,7 +27,7 @@ func createExecutables(tcs []planr.TestCase) []executable { for _, tc := range tcs { cfg := tc.AdapterConfig().(*Config) - file := *cfg.Testfile + file := cfg.Testfile exe, contained := exes[file] // For set comparison @@ -55,7 +55,7 @@ func createExecutables(tcs []planr.TestCase) []executable { 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.testpath, cfg.Testfile, ) } @@ -119,12 +119,12 @@ func runGtest(exe string, tc planr.TestCase, builddir string) planr.TestResult { log.Fatal(err) } - timeout := time.Duration(*cfg.Timeout) * time.Millisecond + 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 + testFlag := "--gtest_filter=" + cfg.Suite + "." + cfg.Name cmd := exec.CommandContext(ctx, exePath, jsonFlag, testFlag) @@ -151,14 +151,13 @@ func runGtest(exe string, tc planr.TestCase, builddir string) planr.TestResult { 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, + cfg.Name, + cfg.Suite, ) } - // TODO: Cleanup -- ZERO TESTS? if len(results) > 1 { - log.Fatalf("Unexpected number of results") + log.Fatalf("Unexpected number of results, filter should have produced one result") } decodeResult := results[0] diff --git a/adapters/gtest/templating.go b/adapters/gtest/templating.go index 57532fa..41c54c1 100644 --- a/adapters/gtest/templating.go +++ b/adapters/gtest/templating.go @@ -3,8 +3,9 @@ package gtest import ( "io" "log" - "text/template" "os" + "golang.flu0r1ne.net/planr" + "text/template" ) type cmakeUnit struct { @@ -27,7 +28,7 @@ func generateCmakeScript(out string, units []cmakeUnit) { log.Fatalf("Could not open CMakeFile (%s)\n%v", out, err) } - writeBoiler(file) + writeCmakeBoilerplate(file) tmpl := unitTemplate() @@ -43,6 +44,10 @@ func generateCmakeScript(out string, units []cmakeUnit) { func unitTemplate() *template.Template { tmpl, err := template.New("gtest_unit").Parse(` +################################################ + +## {{.ExeNm}} + add_executable( "{{.ExeNm}}" "{{.File}}" @@ -60,14 +65,30 @@ gtest_discover_tests( `) 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) @@ -75,10 +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 } -- cgit v1.2.3 From e120a9f5955551fcf314543c8889e58277aefb20 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 01:29:24 -0500 Subject: Add concurrency to evaluate --- adapters/gtest/adapter.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/adapters/gtest/adapter.go b/adapters/gtest/adapter.go index ec11748..2961f29 100644 --- a/adapters/gtest/adapter.go +++ b/adapters/gtest/adapter.go @@ -59,17 +59,24 @@ func (adapter *Adapter) Evaluate(tcs []planr.TestCase) [] planr.TestResult { exes := createExecutables(tcs) + c := make(chan []planr.TestResult, len(exes)) for i := range exes { - succeed, buildFailures := exes[i].compile(buildDir) + go func(exe *executable) { + succeed, buildFailures := exe.compile(buildDir) - if ! succeed { - results = append(results, buildFailures...) - continue - } + if ! succeed { + c <- buildFailures + return + } - runtimeResults := exes[i].execute(buildDir) + runtimeResults := exe.execute(buildDir) - results = append(results, runtimeResults...) + c <- runtimeResults + }(&exes[i]) + } + + for range exes { + results = append(results, (<-c)...) } return results -- cgit v1.2.3 From 4a6efc74aebcc689a734d05bc8435eb9560340e1 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 02:11:09 -0500 Subject: Add CPU profiling flag --- cmd/planr/main.go | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/cmd/planr/main.go b/cmd/planr/main.go index 4ff57eb..d7f62aa 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -1,15 +1,16 @@ package main import ( + "flag" "fmt" "io" "log" "os" - "flag" + "runtime/pprof" "golang.flu0r1ne.net/planr" - "golang.flu0r1ne.net/planr/cmd/planr/sub" "golang.flu0r1ne.net/planr/adapters/gtest" + "golang.flu0r1ne.net/planr/cmd/planr/sub" ) func printUsage(w io.Writer) { @@ -27,6 +28,11 @@ func dieUsage() { os.Exit(1) } +var src = flag.String("srcdir", "", "source directory") +var config = flag.String("configdir", "", "config directory") +var build = flag.String("builddir", "", "build directory") +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") + func getConfiguredRunner() planr.Runner { r := planr.ConfigureRunner() r = planr.RegisterAdapter(r, gtest.NewAdapter()) @@ -35,21 +41,15 @@ func getConfiguredRunner() planr.Runner { r = planr.SetConfigDirFromTree(r, wd) } - src := flag.String("srcdir", "", "source directory") - config := flag.String("configdir", "", "config directory") - build := flag.String("builddir", "", "build directory") - - flag.Parse() - - if src != nil && *src != "" { + if *src != "" { r = planr.SetSrcDir(r, *src) } - if config != nil && *config != "" { + if *config != "" { r = planr.SetConfigDir(r, *config) } - if build != nil && *build != "" { + if *build != "" { r = planr.SetBuildDir(r, *build) } @@ -57,6 +57,18 @@ func getConfiguredRunner() planr.Runner { } func main() { + flag.Parse() + + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + + if err != nil { + log.Fatal(err) + } + + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } log.SetFlags(log.Llongfile | log.Lmsgprefix) log.SetPrefix("planr: ") -- cgit v1.2.3 From a6f45bfa932b9e8e2393b5643e2bc38e44ba76df Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 02:41:21 -0500 Subject: Add config w/ version information --- cmd/planr/main.go | 6 ++++-- cmd/planr/sub/build.go | 4 ++-- cmd/planr/sub/common.go | 15 +++++++++++++++ cmd/planr/sub/evaluate.go | 4 +++- config.go | 31 ++++++++++++++++++++++--------- fs.go | 4 ++-- rubric_config.go | 4 ++-- 7 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 cmd/planr/sub/common.go diff --git a/cmd/planr/main.go b/cmd/planr/main.go index d7f62aa..ce20811 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -79,6 +79,8 @@ func main() { runner := getConfiguredRunner() + cfg := planr.DecodeConfig(runner.ConfigDir()) + subcommand := flag.Arg(0) subargs := flag.Args()[1:] @@ -86,9 +88,9 @@ func main() { case "version": fmt.Printf("%s\n", planr.VERSION) case "build": - sub.Build(runner, subargs) + sub.Build(runner, subargs, cfg) case "evaluate", "eval": - sub.Evaluate(runner, subargs) + sub.Evaluate(runner, subargs, cfg) case "clean": sub.Clean(runner, subargs) case "config": diff --git a/cmd/planr/sub/build.go b/cmd/planr/sub/build.go index d24bb02..48edd9c 100644 --- a/cmd/planr/sub/build.go +++ b/cmd/planr/sub/build.go @@ -4,8 +4,8 @@ import ( "golang.flu0r1ne.net/planr" ) - -func Build(runner planr.Runner, params []string) { +func Build(runner planr.Runner, params []string, cfg planr.Config) { + dieIncompatibleVersion(cfg) tcs := runner.CollectCases() runner.Build(tcs) } diff --git a/cmd/planr/sub/common.go b/cmd/planr/sub/common.go new file mode 100644 index 0000000..67d07f8 --- /dev/null +++ b/cmd/planr/sub/common.go @@ -0,0 +1,15 @@ +package sub + +import ( + "golang.flu0r1ne.net/planr" + "os" + "fmt" +) + +func dieIncompatibleVersion(cfg planr.Config) { + if cfg.IncompatibleWithVersion() { + fmt.Fprintf(os.Stderr, "This version of PlanR (%v) is incompatible with config version %s\n", planr.VERSION, cfg.Version) + fmt.Fprintf(os.Stderr, "Please upgrade to version %s or greater\n", cfg.Version) + os.Exit(1) + } +} diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index 18cccb1..c2549be 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -4,7 +4,9 @@ import ( "golang.flu0r1ne.net/planr" ) -func Evaluate(runner planr.Runner, params []string) { +func Evaluate(runner planr.Runner, params []string, cfg planr.Config) { + dieIncompatibleVersion(cfg) + tcs := runner.CollectCases() trs := runner.Evaluate(tcs) diff --git a/config.go b/config.go index d2d32e9..88495e3 100644 --- a/config.go +++ b/config.go @@ -4,19 +4,19 @@ import ( "github.com/BurntSushi/toml" "log" "path" + "strings" ) -type planrConfig struct { - Version string - Project_title string +type Config struct { + Version string } -const PLANR_CONFIG = "config.toml" +const PLANR_CONFIG_FILE = "config.toml" -func decodeConfig(configDir string) planrConfig { - cfg := planrConfig { } +func DecodeConfig(configDir string) Config { + cfg := Config { } - configFile := path.Join(configDir, PLANR_CONFIG) + configFile := path.Join(configDir, PLANR_CONFIG_FILE) if _, err := toml.DecodeFile(configFile, &cfg); err != nil { // TODO: handle missing config @@ -26,6 +26,19 @@ func decodeConfig(configDir string) planrConfig { return cfg } -func (cfg planrConfig) isIncompatibleWithVersion() bool { - return cfg.Version > VERSION +func (cfg Config) IncompatibleWithVersion() bool { + if strings.Count(cfg.Version, ".") != 2 { + log.Fatalf("Version %s is not semantic", cfg.Version) + } + + cfgbits := strings.SplitN(cfg.Version, ".", 2) + bits := strings.SplitN(VERSION, ".", 2) + + // major version change + if cfgbits[0] != bits[0] { + return true + } + + // Config newer, possible feature additions + return cfgbits[1] > bits[1] } diff --git a/fs.go b/fs.go index a42ff2c..04a3522 100644 --- a/fs.go +++ b/fs.go @@ -99,7 +99,7 @@ func collectFromDir( // Process defaults for this directory if a defaults.toml is found defaultsPath := path.Join(dir, DEFAULTS) if info, err := os.Stat(defaultsPath); err == nil && !info.IsDir() { - d, err := DecodeDefaults(defaultsPath, cfgs) + d, err := DecodeRubricDefaults(defaultsPath, cfgs) if err != nil { log.Fatalf("Error encounter in %s: %v\n", defaultsPath, err); @@ -135,7 +135,7 @@ func collectFromDir( } // Decode a unit - config, err := DecodeConfig(child, cfgs) + config, err := DecodeRubricConfig(child, cfgs) if err != nil { log.Fatalf("Error encountered in %s: %v", child, config) diff --git a/rubric_config.go b/rubric_config.go index 887bbb0..322e58a 100644 --- a/rubric_config.go +++ b/rubric_config.go @@ -135,7 +135,7 @@ func (defaults *Defaults) decodeAdapters( } // Decode defaults.toml -func DecodeDefaults(path string, adapterCfg []AdapterConfig) (Defaults, error) { +func DecodeRubricDefaults(path string, adapterCfg []AdapterConfig) (Defaults, error) { defaults := Defaults { } if _, err := toml.DecodeFile(path, &defaults); err != nil { @@ -151,7 +151,7 @@ func DecodeDefaults(path string, adapterCfg []AdapterConfig) (Defaults, error) { } // Decode an individual unit -func DecodeConfig(path string, adapterCfg []AdapterConfig) (TestCaseConfig, error) { +func DecodeRubricConfig(path string, adapterCfg []AdapterConfig) (TestCaseConfig, error) { config := TestCaseConfig { } if _, err := toml.DecodeFile(path, &config); err != nil { -- cgit v1.2.3 From 62e85f40923b3485ba60766052de7150acf039c5 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 04:30:33 -0500 Subject: Add bash adapter --- adapters/bash/adapter.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ adapters/bash/config.go | 64 ++++++++++++++++++++++++++++++++ cmd/planr/main.go | 11 +++--- 3 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 adapters/bash/adapter.go create mode 100644 adapters/bash/config.go diff --git a/adapters/bash/adapter.go b/adapters/bash/adapter.go new file mode 100644 index 0000000..b98523e --- /dev/null +++ b/adapters/bash/adapter.go @@ -0,0 +1,96 @@ +package bash + +import ( + "context" + "errors" + "log" + "os" + "os/exec" + "path" + "strings" + "time" + "fmt" + + "golang.flu0r1ne.net/planr" +) + +type Adapter struct { + dirs planr.DirConfig +} + +func (a *Adapter) Config() planr.AdapterConfig { + return planr.AdapterConfig { + Name: "bash", + ParseConfig: ParseConfig, + ParseDefaultConfig: ParseDefaultConfig, + } +} + +func safeWd() string{ + wd, err := os.Getwd() + + if err != nil { + log.Fatalf("Could not get GtestBuildDir %s %v\n", wd, err) + } + + return wd +} + +func (a *Adapter) Init(dirs planr.DirConfig) { + a.dirs = dirs +} + +func (adapter Adapter) Build(tcs []planr.TestCase) { } + +func executeScriptedTest(testdir string, tc planr.TestCase) planr.TestResult { + cfg := tc.AdapterConfig().(*Config) + + timeout := time.Duration(cfg.Timeout) * time.Millisecond + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + + defer cancel() + + path := path.Join(testdir, cfg.Testfile) + + result := planr.TestResult {} + result.Tc = tc + + cmd := exec.CommandContext(ctx, "bash", path) + + if out, err := cmd.CombinedOutput(); err != nil { + result.Status = planr.RUNTIME_FAILURE + result.TestOutput = string(out) + + var exiterr *exec.ExitError + if !errors.As(err, &exiterr) { + log.Fatalf("Test script %s failed with unknown error %v\n", path, err) + } else { + if strings.Contains(exiterr.String(), "killed") { + result.TestOutput += fmt.Sprintf("TEST TERMINATED (Timeout=%d)\n", cfg.Timeout) + } + } + + return result + } + + result.Status = planr.PASSING + + + return result +} + +func (adapter Adapter) Evaluate(tcs []planr.TestCase) [] planr.TestResult { + finalizeConfigs(tcs) + + trs := make([]planr.TestResult, len(tcs)) + for i, tc := range tcs { + trs[i] = executeScriptedTest(adapter.dirs.Tests(), tc) + } + + return trs +} + +func NewAdapter() *Adapter { + return new(Adapter) +} diff --git a/adapters/bash/config.go b/adapters/bash/config.go new file mode 100644 index 0000000..aaa405a --- /dev/null +++ b/adapters/bash/config.go @@ -0,0 +1,64 @@ +package bash + +import ( + "log" + "golang.flu0r1ne.net/planr" + "github.com/BurntSushi/toml" +) + +const ( + DEFAULT_TIMEOUT=1000 +) + +type Defaults struct { + Testfile string + Timeout uint +} + +func (child *Defaults) Inherit(p interface{}) { + parent := p.(*Defaults) + + if(child.Timeout == 0) { child.Timeout = parent.Timeout } +} + +type Config struct { + Defaults +} + +func (c *Config) finalize(path string) { + if c.Testfile == "" { + log.Fatalf("\"Testfile\" is not defined for unit %s\n", path) + } + + if c.Timeout == 0 { + c.Timeout = DEFAULT_TIMEOUT; + } +} + +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) { + config := Config {} + + if err := toml.PrimitiveDecode(prim, &config); err != nil { + return nil, err + } + + return &config, nil +} + +func ParseDefaultConfig(prim toml.Primitive) (planr.InheritableConfig, error) { + config := Defaults{} + + if err := toml.PrimitiveDecode(prim, &config); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/cmd/planr/main.go b/cmd/planr/main.go index ce20811..d250bbd 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -1,7 +1,6 @@ package main -import ( - "flag" +import ( "flag" "fmt" "io" "log" @@ -10,6 +9,7 @@ import ( "golang.flu0r1ne.net/planr" "golang.flu0r1ne.net/planr/adapters/gtest" + "golang.flu0r1ne.net/planr/adapters/bash" "golang.flu0r1ne.net/planr/cmd/planr/sub" ) @@ -28,14 +28,15 @@ func dieUsage() { os.Exit(1) } -var src = flag.String("srcdir", "", "source directory") -var config = flag.String("configdir", "", "config directory") -var build = flag.String("builddir", "", "build directory") +var src = flag.String("srcdir", "", "source directory") +var config = flag.String("configdir", "", "config directory") +var build = flag.String("builddir", "", "build directory") var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") func getConfiguredRunner() planr.Runner { r := planr.ConfigureRunner() r = planr.RegisterAdapter(r, gtest.NewAdapter()) + r = planr.RegisterAdapter(r, bash.NewAdapter()) if wd, err := os.Getwd(); err == nil { r = planr.SetConfigDirFromTree(r, wd) -- cgit v1.2.3 From d186e730a73b5b3009edbaab7617617d35dc2cff Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 04:35:21 -0500 Subject: Thread bash adapter --- adapters/bash/adapter.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/adapters/bash/adapter.go b/adapters/bash/adapter.go index b98523e..65a6c80 100644 --- a/adapters/bash/adapter.go +++ b/adapters/bash/adapter.go @@ -83,9 +83,16 @@ func executeScriptedTest(testdir string, tc planr.TestCase) planr.TestResult { func (adapter Adapter) Evaluate(tcs []planr.TestCase) [] planr.TestResult { finalizeConfigs(tcs) - trs := make([]planr.TestResult, len(tcs)) - for i, tc := range tcs { - trs[i] = executeScriptedTest(adapter.dirs.Tests(), tc) + trs := make([]planr.TestResult, 0) + c := make(chan planr.TestResult, 0) + for i := range tcs { + go func(i int) { + c <- executeScriptedTest(adapter.dirs.Tests(), tcs[i]) + }(i) + } + + for range tcs { + trs = append(trs, <-c) } return trs -- cgit v1.2.3 From 2ccad1c9bc42093fb341faed26ea41c1463fcfae Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 04:40:39 -0500 Subject: Concurrent Eval Loop --- runner.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/runner.go b/runner.go index 2d66dc6..9c1e385 100644 --- a/runner.go +++ b/runner.go @@ -111,15 +111,22 @@ func (r Runner) Build(tcs []TestCase) { func (r Runner) Evaluate(tcs []TestCase) []TestResult { testSets := r.groupByAdapter(tcs) results := make([]TestResult, 0) - + + c := make(chan []TestResult) for _, pair := range testSets { - adapter := pair.adapter - cases := pair.tcs + go func (pair adapterTestSet) { + adapter := pair.adapter + cases := pair.tcs - r.setupEnv(adapter) - resultSet := adapter.Evaluate(cases) + r.setupEnv(adapter) + resultSet := adapter.Evaluate(cases) + + c <- resultSet + }(pair) + } - results = append(results, resultSet...) + for range testSets { + results = append(results, (<-c)...) } safeCd(r.dirs.Config()) -- cgit v1.2.3 From 7f3a568983470016bf99baafb4db47c8223c0494 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 13:38:27 -0500 Subject: Preserve original ordering --- fs.go | 1 + runner.go | 3 +++ testcase.go | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/fs.go b/fs.go index 04a3522..b76f182 100644 --- a/fs.go +++ b/fs.go @@ -77,6 +77,7 @@ func collectUnits(root string, cfgs []AdapterConfig) []TestCase { for i := range tcs { tcs[i].Cname = Cname(root, tcs[i].Path) + tcs[i].readIdx = i } return tcs diff --git a/runner.go b/runner.go index 9c1e385..15f08ae 100644 --- a/runner.go +++ b/runner.go @@ -4,6 +4,7 @@ import ( "log" "os" "path" + "sort" ) type Runner struct { @@ -128,6 +129,8 @@ func (r Runner) Evaluate(tcs []TestCase) []TestResult { for range testSets { results = append(results, (<-c)...) } + + sort.Sort(ByReadIdx(results)) safeCd(r.dirs.Config()) diff --git a/testcase.go b/testcase.go index 19f1e58..d1db292 100644 --- a/testcase.go +++ b/testcase.go @@ -45,8 +45,17 @@ type TestCase struct { Cname string Config TestCaseConfig + + // Reorder according to original read order after concurrent operation + readIdx int } func (tc TestCase) AdapterConfig() InheritableConfig { return tc.Config.adapters_[*tc.Config.Adapter] } + +type ByReadIdx []TestResult + +func (a ByReadIdx) Len() int { return len(a) } +func (a ByReadIdx) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByReadIdx) Less(i, j int) bool { return a[i].Tc.readIdx < a[j].Tc.readIdx } -- cgit v1.2.3 From 54276b73eacfab6a5d6b899bd638f6cfe9499b80 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 13:39:34 -0500 Subject: Refactor with scoring object --- cmd/planr/sub/cli.go | 10 +++++----- cmd/planr/sub/evaluate.go | 26 ++++---------------------- scoring.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 27 deletions(-) create mode 100644 scoring.go diff --git a/cmd/planr/sub/cli.go b/cmd/planr/sub/cli.go index 3c58f4e..e6f2256 100644 --- a/cmd/planr/sub/cli.go +++ b/cmd/planr/sub/cli.go @@ -107,16 +107,16 @@ func tcPprint(tr planr.TestResult) { fmt.Println() } -func printResults(passed, tc_total int, earned, points_total float64) { +func printScoring(score planr.Scoring) { col_title.Println("Final Results:") - pprintLabeled("passed", fmt.Sprintf("%d/%d", passed, tc_total)); + pprintLabeled("passed", fmt.Sprintf("%d/%d", score.Passed, score.Total)); - percent := earned / points_total * 100 + percent := score.EarnedPoints / score.TotalPoints * 100 - if points_total != 0 { + if score.TotalPoints != 0 { pprintLabeled("score", fmt.Sprintf( - "%.2f/%.2f ~= %.1f%%", earned, points_total, percent, + "%.2f/%.2f ~= %.1f%%", score.EarnedPoints, score.TotalPoints, percent, )); } } diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index c2549be..81c9989 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -4,36 +4,18 @@ import ( "golang.flu0r1ne.net/planr" ) + func Evaluate(runner planr.Runner, params []string, cfg planr.Config) { dieIncompatibleVersion(cfg) tcs := runner.CollectCases() trs := runner.Evaluate(tcs) - earned := 0.0 - total := 0.0 - passed := 0 - for _, tr := range trs { - cfg := tr.Tc.Config - - if cfg.Points != nil { - points := float64(*cfg.Points) - - total += points - - if tr.Status == planr.PASSING { - earned += points - passed++ - } - } + score := planr.Score(trs) + for _, tr := range trs { tcPprint(tr) } - printResults( - passed, - len(tcs), - earned, - total, - ); + printScoring(score) } diff --git a/scoring.go b/scoring.go new file mode 100644 index 0000000..675058a --- /dev/null +++ b/scoring.go @@ -0,0 +1,31 @@ +package planr + +type Scoring struct { + EarnedPoints float64 + TotalPoints float64 + Passed int + Total int +} + +func Score(trs []TestResult) Scoring { + score := Scoring {} + + for _, tr := range trs { + cfg := tr.Tc.Config + points := 0.0 + + if cfg.Points != nil { + points = float64(*cfg.Points) + } + + score.TotalPoints += points + if tr.Status == PASSING { + score.EarnedPoints += points + score.Passed++ + } + + score.Total += 1 + } + + return score +} -- cgit v1.2.3 From 6876ea1e5cc3319b0d23d829a14f83b9a55663d2 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 13:52:39 -0500 Subject: Add flag for JSON eval output --- cmd/planr/main.go | 1 + cmd/planr/sub/evaluate.go | 52 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/cmd/planr/main.go b/cmd/planr/main.go index d250bbd..b073115 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -19,6 +19,7 @@ func printUsage(w io.Writer) { fmt.Fprintln(w, " version ") fmt.Fprintln(w, " build ") fmt.Fprintln(w, " evaluate ") + fmt.Fprintln(w, " evaluate -json ") fmt.Fprintln(w, " clean ") fmt.Fprintln(w, " config ") } diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index 81c9989..95f88d0 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -1,21 +1,57 @@ package sub import ( - "golang.flu0r1ne.net/planr" + "encoding/json" + "fmt" + "log" + "flag" + + "golang.flu0r1ne.net/planr" ) +type gradingResults struct { + TestResults []planr.TestResult + Score planr.Scoring +} + +func prettyPrint(results gradingResults) { + for _, tr := range results.TestResults { + tcPprint(tr) + } + + printScoring(results.Score) +} + +func jsonPrint(results gradingResults) { + res, err := json.Marshal(results) + + if err != nil { + log.Fatalf("Error printing JSON: %v\n", err) + } + + fmt.Println(string(res)) +} func Evaluate(runner planr.Runner, params []string, cfg planr.Config) { + f := flag.NewFlagSet("evaluate", flag.ExitOnError) + + jsonOutput := f.Bool("json", false, "print json output") + + f.Parse(params) + dieIncompatibleVersion(cfg) tcs := runner.CollectCases() trs := runner.Evaluate(tcs) - - score := planr.Score(trs) - - for _, tr := range trs { - tcPprint(tr) + + results := gradingResults { + TestResults: trs, + Score: planr.Score(trs), + } + + if *jsonOutput { + jsonPrint(results) + } else { + prettyPrint(results) } - - printScoring(score) } -- cgit v1.2.3 From 0f8c8df3e9d4ed05475b0fa3f1d26253e3de719d Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 14:01:17 -0500 Subject: Clarify config cmd in version --- cmd/planr/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/planr/main.go b/cmd/planr/main.go index b073115..d179b76 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -21,7 +21,7 @@ func printUsage(w io.Writer) { fmt.Fprintln(w, " evaluate ") fmt.Fprintln(w, " evaluate -json ") fmt.Fprintln(w, " clean ") - fmt.Fprintln(w, " config ") + fmt.Fprintln(w, " config ") } func dieUsage() { -- cgit v1.2.3 From b9b8d706efd0d9c1848d50e7f8afa1a74ce90037 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 14:18:31 -0500 Subject: Add mechanism to conditionally run some tests - FS approch should be taken upon revision --- cmd/planr/main.go | 1 + cmd/planr/sub/evaluate.go | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/cmd/planr/main.go b/cmd/planr/main.go index d179b76..93f1cbf 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -19,6 +19,7 @@ func printUsage(w io.Writer) { fmt.Fprintln(w, " version ") fmt.Fprintln(w, " build ") fmt.Fprintln(w, " evaluate ") + fmt.Fprintln(w, " evaluate ... ") fmt.Fprintln(w, " evaluate -json ") fmt.Fprintln(w, " clean ") fmt.Fprintln(w, " config ") diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index 95f88d0..21481a1 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -14,12 +14,14 @@ type gradingResults struct { Score planr.Scoring } -func prettyPrint(results gradingResults) { +func prettyPrint(results gradingResults, summarize bool) { for _, tr := range results.TestResults { tcPprint(tr) } - printScoring(results.Score) + if summarize { + printScoring(results.Score) + } } func jsonPrint(results gradingResults) { @@ -37,12 +39,34 @@ func Evaluate(runner planr.Runner, params []string, cfg planr.Config) { jsonOutput := f.Bool("json", false, "print json output") - f.Parse(params) - dieIncompatibleVersion(cfg) + f.Parse(params) + tcs := runner.CollectCases() - trs := runner.Evaluate(tcs) + + // Filter those tests which patch IDs in params + filteredTcs := make([]planr.TestCase, 0) + summarizeScore := false + if f.NArg() > 0 { + ids := f.Args() + + membershipFun := make(map[string] bool, 0) + for _, id := range ids { + membershipFun[id] = true + } + + for i := range tcs { + if membershipFun[tcs[i].Cname] { + filteredTcs = append(filteredTcs, tcs[i]) + } + } + } else { + summarizeScore = true + filteredTcs = tcs + } + + trs := runner.Evaluate(filteredTcs) results := gradingResults { TestResults: trs, @@ -52,6 +76,6 @@ func Evaluate(runner planr.Runner, params []string, cfg planr.Config) { if *jsonOutput { jsonPrint(results) } else { - prettyPrint(results) + prettyPrint(results, summarizeScore) } } -- cgit v1.2.3 From 465969af0d6a3e69e56de2048eb811e68d6f27a1 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 14:44:41 -0500 Subject: Add changelog and bump version --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ version.go | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f48554b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +## [ v0.1.0 ] + +### Added +- Changelog +- Clean subcommand to clean build folder +- Test selection via `Evaluate` command +- Env overrides for standard directory structure + + `PLANR_CONFIG_DIR` - E.g. `planr` directory + + `PLANR_SRC_DIR` + + `PLANR_BUILD_DIR` +- Flags overrides for standard directory structure + + `-configdir` + + `-builddir` + + `-srcdir` +- `bash` adapter +- `config` command to get directory configuration settings +- "Primary" configuration file in "planr/config.toml" for rubric version information +- `json` output for the `evaluate` command + +### Changed +- Better executable handling in the `gtest` adapter + + Generates commends in `CMakeLists.txt` for debugging +- Better concurrency in test pipeline +- Much refactoring + +### Fixed +- BUG: Fix decoding when no defaults file are present + +## [ v0.0.3 ] + +### Fixed +- BUG: Paths to source or test files containing spaces would fail to compile + +## [ v0.0.2 ] + +### Added +- Improve CLI output by fencing traces +- Add colorized compilation traces +- Internal refactoring + +## [ v0.0.1 ] + +### Added +- Basic build pipeline +- Gtest Test Adapter +- Configuration decoder +- Basic CLI + + `build` command + + `clean` command + + `help` command diff --git a/version.go b/version.go index 51a9233..52c7c5d 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package planr -const VERSION = "0.0.3" +const VERSION = "0.1.0" -- cgit v1.2.3 From 91666d1f2e766bc7c3073ed4cd731637c6885e36 Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 18:57:59 -0500 Subject: Do not throw fatal error when missing config, this behavior will be enabled in future releases --- cmd/planr/sub/build.go | 2 +- cmd/planr/sub/common.go | 4 ++-- cmd/planr/sub/evaluate.go | 2 +- config.go | 15 +++++++++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cmd/planr/sub/build.go b/cmd/planr/sub/build.go index 48edd9c..e93e0cb 100644 --- a/cmd/planr/sub/build.go +++ b/cmd/planr/sub/build.go @@ -4,7 +4,7 @@ import ( "golang.flu0r1ne.net/planr" ) -func Build(runner planr.Runner, params []string, cfg planr.Config) { +func Build(runner planr.Runner, params []string, cfg * planr.Config) { dieIncompatibleVersion(cfg) tcs := runner.CollectCases() runner.Build(tcs) diff --git a/cmd/planr/sub/common.go b/cmd/planr/sub/common.go index 67d07f8..9126f3c 100644 --- a/cmd/planr/sub/common.go +++ b/cmd/planr/sub/common.go @@ -6,8 +6,8 @@ import ( "fmt" ) -func dieIncompatibleVersion(cfg planr.Config) { - if cfg.IncompatibleWithVersion() { +func dieIncompatibleVersion(cfg *planr.Config) { + if cfg != nil && cfg.IncompatibleWithVersion() { fmt.Fprintf(os.Stderr, "This version of PlanR (%v) is incompatible with config version %s\n", planr.VERSION, cfg.Version) fmt.Fprintf(os.Stderr, "Please upgrade to version %s or greater\n", cfg.Version) os.Exit(1) diff --git a/cmd/planr/sub/evaluate.go b/cmd/planr/sub/evaluate.go index 21481a1..30d30d2 100644 --- a/cmd/planr/sub/evaluate.go +++ b/cmd/planr/sub/evaluate.go @@ -34,7 +34,7 @@ func jsonPrint(results gradingResults) { fmt.Println(string(res)) } -func Evaluate(runner planr.Runner, params []string, cfg planr.Config) { +func Evaluate(runner planr.Runner, params []string, cfg *planr.Config) { f := flag.NewFlagSet("evaluate", flag.ExitOnError) jsonOutput := f.Bool("json", false, "print json output") diff --git a/config.go b/config.go index 88495e3..d7cd3e4 100644 --- a/config.go +++ b/config.go @@ -13,14 +13,21 @@ type Config struct { const PLANR_CONFIG_FILE = "config.toml" -func DecodeConfig(configDir string) Config { - cfg := Config { } +// TODO: REMOVE +const STRICTLY_REQUIRE_CONFIG = false + +func DecodeConfig(configDir string) *Config { + cfg := new(Config) configFile := path.Join(configDir, PLANR_CONFIG_FILE) - if _, err := toml.DecodeFile(configFile, &cfg); err != nil { + if _, err := toml.DecodeFile(configFile, cfg); err != nil { + cfg = nil + // TODO: handle missing config - log.Fatalf("Could not decode global configuration %s: %v", configFile, err) + if STRICTLY_REQUIRE_CONFIG { + log.Fatalf("Could not decode global configuration %s: %v", configFile, err) + } } return cfg -- cgit v1.2.3 From fd66fb134967067ed7e1c3182325f646b73c730b Mon Sep 17 00:00:00 2001 From: Flu0r1ne Date: Sun, 5 Sep 2021 20:17:00 -0500 Subject: Fix bug where standard dirs are not set due to abs positioning --- cmd/planr/main.go | 2 +- stddirs.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/planr/main.go b/cmd/planr/main.go index 93f1cbf..2243203 100644 --- a/cmd/planr/main.go +++ b/cmd/planr/main.go @@ -76,7 +76,7 @@ func main() { log.SetFlags(log.Llongfile | log.Lmsgprefix) log.SetPrefix("planr: ") - if len(os.Args) < 2 { + if flag.NArg() < 1 { dieUsage() } diff --git a/stddirs.go b/stddirs.go index 1eae439..581af87 100644 --- a/stddirs.go +++ b/stddirs.go @@ -68,17 +68,17 @@ func dirFromEnv(name, env string) *string { func (c *DirConfig) SetSrc(srcDir string) { dieDirAbsent("src", srcDir) - c.src = srcDir + c.src = abs(srcDir) } func (c *DirConfig) SetConfig(configDir string) { dieDirAbsent("planr (config)", configDir) - c.config = configDir + c.config = abs(configDir) } func (c *DirConfig) SetBuild(buildDir string) { dieDirAbsent("build", buildDir) - c.build = buildDir + c.build = abs(buildDir) } func (c *DirConfig) SetConfigFromTree(cdir string) { @@ -147,8 +147,8 @@ func abs(path string) string { } func (c DirConfig) Build() string { - if c.src != "" { - return c.src + if c.build != "" { + return c.build } if dir := dirFromEnv("build", ENV_BUILD_DIR); dir != nil { -- cgit v1.2.3