diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | adapters.go | 26 | ||||
-rw-r--r-- | adapters/gtest_adapter.go | 97 | ||||
-rw-r--r-- | cmd/main.go | 50 | ||||
-rw-r--r-- | cmd/sub/build.go | 17 | ||||
-rw-r--r-- | cmd/sub/evaluate.go | 9 | ||||
-rw-r--r-- | config.go | 172 | ||||
-rw-r--r-- | fs.go | 170 | ||||
-rw-r--r-- | go.mod | 8 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | runner.go | 63 | ||||
-rw-r--r-- | testcase.go | 21 |
12 files changed, 641 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e0032f2 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +planr: + go build -o planr ./cmd/main.go + +.PHONY: planr diff --git a/adapters.go b/adapters.go new file mode 100644 index 0000000..b9c8d9c --- /dev/null +++ b/adapters.go @@ -0,0 +1,26 @@ +package planr + +// 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 { + + /* CONFIGURATION HOOKS */ + + Config() AdapterConfig + + /* BUILD CYCLE */ + + // Called once at the beginning of the build process + InitializeBuild() + // Called once with every registered test case + // Can access configuration directly + Build(testCase TestCase) + // Called once after all builds + FinalizeBuild() + // Called once per test case after FinalizeBuild + Evaluate(testCase TestCase) TestResult + // Called once after each test has been evaluated + Cleanup() +} diff --git a/adapters/gtest_adapter.go b/adapters/gtest_adapter.go new file mode 100644 index 0000000..4cbaa9c --- /dev/null +++ b/adapters/gtest_adapter.go @@ -0,0 +1,97 @@ +package adapters + +import ( + "fmt" + "github.com/BurntSushi/toml" + "golang.flu0r1ne.net/planr" + "log" +) + +/* + CONFIGURATION +*/ + +type GtestDefaults struct { + Name *string + Suite *string + File *string +} + +func (child *GtestDefaults) Inherit(p interface{}) { + parent := p.(*GtestDefaults) + + if(child.Name == nil) { + child.Name = parent.Name + } + + if(child.Suite == nil) { + child.Suite = parent.Suite + } + + if(child.File == nil) { + child.File = parent.File + } +} + + +type GtestConfig struct { + GtestDefaults +} + +func primitiveDecode(primitive toml.Primitive, config interface{}) { + if err := toml.PrimitiveDecode(primitive, config); err != nil { + log.Fatal(err) + } +} + +func ParseConfig(prim toml.Primitive) planr.InheritableConfig { + config := GtestConfig{} + + primitiveDecode(prim, &config) + + return &config +} + +func ParseDefaultConfig(prim toml.Primitive) planr.InheritableConfig { + config := GtestDefaults{} + + primitiveDecode(prim, &config) + + return &config +} + +/* + BUILD PROCESS +*/ + +type GtestAdapter struct {} + +func (a GtestAdapter) Config() planr.AdapterConfig { + return planr.AdapterConfig { + Name: "gtest", + ParseConfig: ParseConfig, + ParseDefaultConfig: ParseDefaultConfig, + } +} + +func (a GtestAdapter) InitializeBuild() { + fmt.Println("Initializing"); +} + +func (a GtestAdapter) Build(tc planr.TestCase) { + fmt.Printf("Building %v\n", tc); +} + +func (a GtestAdapter) FinalizeBuild() { + fmt.Println("Finalizing"); +} + +func (a GtestAdapter) Evaluate(tc planr.TestCase) planr.TestResult { + fmt.Printf("Evaluating %v\n", tc); + + return planr.TestResult {} +} + +func (a GtestAdapter) Cleanup() { + fmt.Printf("Cleaning\n") +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..9cacf98 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "io" + "fmt" + "golang.flu0r1ne.net/planr/cmd/sub" +) + +const ( + VERSION = "0.0.1" +) + +func printUsage(w io.Writer) { + fmt.Fprintf (w, "usage: %s command args ... \n", os.Args[0]) + fmt.Fprintln(w, " help ") + fmt.Fprintln(w, " version ") + fmt.Fprintln(w, " build ") + fmt.Fprintln(w, " evaluate ") +} + +func dieUsage() { + printUsage(os.Stderr) + os.Exit(1) +} + +func main() { + + if len(os.Args) < 2 { + dieUsage() + } + + subcommand := os.Args[1] + subargs := os.Args[2:] + + switch subcommand { + case "version": + fmt.Printf("%s\n", VERSION) + case "build": + sub.Build(subargs) + case "evaluate": + sub.Evaluate(subargs) + case "help", "-h", "-help", "--help": + printUsage(os.Stdout) + default: + fmt.Fprintf(os.Stderr, "unrecognized command %s\n", subcommand) + dieUsage() + } + +} diff --git a/cmd/sub/build.go b/cmd/sub/build.go new file mode 100644 index 0000000..84201a2 --- /dev/null +++ b/cmd/sub/build.go @@ -0,0 +1,17 @@ +package sub + +import ( + "golang.flu0r1ne.net/planr" + "golang.flu0r1ne.net/planr/adapters" +) + +func Build(params []string) { + gtestAdapter := adapters.GtestAdapter {} + + r := planr.Runner{} + r.RegisterAdapter(gtestAdapter) + + rd := planr.RubricDir() + + r.Run(rd) +} diff --git a/cmd/sub/evaluate.go b/cmd/sub/evaluate.go new file mode 100644 index 0000000..35dd48d --- /dev/null +++ b/cmd/sub/evaluate.go @@ -0,0 +1,9 @@ +package sub + +import ( + "fmt" +) + +func Evaluate(params []string) { + fmt.Print(params) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..6dd264b --- /dev/null +++ b/config.go @@ -0,0 +1,172 @@ +package planr + +import ( + // "fmt" + "log" + "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{}) +} + +// A parser function takes a blob of TOML and decodes it into +// configuration relevant to an adapter +type TomlParser func (toml.Primitive) InheritableConfig + +// 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 +type Defaults struct { + Points *float32 + + /* + 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 +} + +// Program-wide testcase config +type TestCaseConfig struct { + Defaults + Title *string + Description *string +} + +// 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; + } + + // 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) { + 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 + if asDefault { + parsed = config.ParseDefaultConfig(primitive) + } else { + parsed = config.ParseConfig(primitive) + } + + defaults.adapters_[config.Name] = parsed + } + } + } +} + +// Decode defaults.toml +func DecodeDefaults(path string, adapterCfg []AdapterConfig) Defaults { + defaults := Defaults { } + + if _, err := toml.DecodeFile(path, &defaults); err != nil { + log.Fatal(err) + } + + defaults.decodeAdapters(adapterCfg, true) + + return defaults +} + +// Decode an individual unit +func DecodeConfig(path string, adapterCfg []AdapterConfig) TestCaseConfig { + config := TestCaseConfig { } + + if _, err := toml.DecodeFile(path, &config); err != nil { + log.Fatal(err) + } + + config.decodeAdapters(adapterCfg, false) + + return config +} @@ -0,0 +1,170 @@ +package planr + +import ( + "log" + "os" + "io" + "path" + "path/filepath" + // "fmt" +) + +/* CONFIG DIRECTORY */ + +// Consumes path +// Returns true if traversal should halt +type traversalFunc func(path string) bool; + +// Traverse up until the root is reached +// Calls traverseFunc each iteration +// Returns true if prematurely stopped +func traverseUp(reference string, shouldStop traversalFunc) bool { + cursor := reference + + for !shouldStop(cursor) { + if filepath.ToSlash(cursor) == "/" { + return false + } + cursor = filepath.Join(cursor, "..") + } + + return true +} + +func directoryExists(path string) bool { + info, err := os.Stat(path) + + if err != nil { + if !os.IsNotExist(err) { + log.Fatal(err) + } + + return false; + } + + 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 +} + +// 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 +} + +// Collects the units from the configuration tree +// TODO: Cleanup +func collectUnits( + name string, + defaults *Defaults, + cfgs []AdapterConfig, + units *[]TestCase, +) { + fp, err := os.Open(name) + if err != nil { + log.Fatal(err) + } + + // Process defaults for this directory if a defaults.toml is found + defaultsPath := path.Join(name, "defaults.toml") + if info, err := os.Stat(defaultsPath); err == nil && !info.IsDir() { + d := DecodeDefaults(defaultsPath, cfgs) + + // inherit the properties not defined in this defaults + if defaults != nil { + d.Inherit(defaults) + } + + defaults = &d + } + + // Read the entries in this directory + for { + dirs, err := fp.ReadDir(100) + if err == io.EOF { + break + } else if err != nil { + log.Fatal(err) + } + + + for _, ent := range dirs { + child := path.Join(name, ent.Name()) + nm := ent.Name() + + if ent.IsDir() { + collectUnits(child, defaults, cfgs, units) + } else { + if nm == "defaults.toml" { + continue + } + + // Decode a unit + config := DecodeConfig(child, cfgs) + config.Inherit(*defaults) + + tc := TestCase { + Path: child, + Cname: nm, + Config: config, + } + + *units = append(*units, tc) + } + } + } +} @@ -0,0 +1,8 @@ +module golang.flu0r1ne.net/planr + +go 1.16 + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/jinzhu/copier v0.3.2 // indirect +) @@ -0,0 +1,4 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= +github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= diff --git a/runner.go b/runner.go new file mode 100644 index 0000000..5c66573 --- /dev/null +++ b/runner.go @@ -0,0 +1,63 @@ +package planr + +type Runner struct { + adapters []Adapter +} + +func (r *Runner) RegisterAdapter(a Adapter) { + r.adapters = append(r.adapters, a) +} + +func (r Runner) adapterCfgs() []AdapterConfig { + cgs := make([]AdapterConfig, len(r.adapters)) + + for _, adapter := range r.adapters { + cgs = append(cgs, adapter.Config()) + } + + return cgs +} + +func (r Runner) collectUnits(root string) []TestCase { + tcs := make([]TestCase, 10) + + collectUnits(root, nil, r.adapterCfgs(), &tcs) + + return tcs +} + +func (r Runner) cycle(tcs []TestCase) []TestResult { + results := make([]TestResult, 0) + + for _, adapter := range r.adapters { + aname := adapter.Config().Name + + adapter.InitializeBuild() + + for _, tc := range tcs { + if tc.ContainsAdapter(aname) { + adapter.Build(tc) + } + } + + adapter.FinalizeBuild() + + for _, tc := range tcs { + if tc.ContainsAdapter(aname) { + results = append(results, adapter.Evaluate(tc)) + } + } + + adapter.Cleanup() + } + + return results +} + +func (r Runner) Run(root string) [] TestResult { + tcs := r.collectUnits(root) + + trs := r.cycle(tcs) + + return trs +} diff --git a/testcase.go b/testcase.go new file mode 100644 index 0000000..02db738 --- /dev/null +++ b/testcase.go @@ -0,0 +1,21 @@ +package planr + +type TestResult struct { + Pass bool +} + +type TestCase struct { + Path string + Cname string + Config TestCaseConfig +} + +func (tc TestCase) ContainsAdapter(name string) bool { + for adapter := range tc.Config.adapters_ { + if adapter == name { + return true; + } + } + + return false; +} |