aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--adapters.go26
-rw-r--r--adapters/gtest_adapter.go97
-rw-r--r--cmd/main.go50
-rw-r--r--cmd/sub/build.go17
-rw-r--r--cmd/sub/evaluate.go9
-rw-r--r--config.go172
-rw-r--r--fs.go170
-rw-r--r--go.mod8
-rw-r--r--go.sum4
-rw-r--r--runner.go63
-rw-r--r--testcase.go21
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
+}
diff --git a/fs.go b/fs.go
new file mode 100644
index 0000000..905d6d1
--- /dev/null
+++ b/fs.go
@@ -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)
+ }
+ }
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..cb19fee
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..fca8c3e
--- /dev/null
+++ b/go.sum
@@ -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;
+}