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; +}  | 
