Skip to content

Commit

Permalink
Add support for assertions (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
TekWizely committed Mar 15, 2020
1 parent 91ae25d commit 0da0b32
Show file tree
Hide file tree
Showing 12 changed files with 636 additions and 81 deletions.
86 changes: 85 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ In run, the entire script is executed within a single sub-shell.
- [Forgetting To Define An Exported Variable](#forgetting-to-define-an-exported-variable)
- [Referencing Other Variables](#referencing-other-variables)
- [Shell Substitution](#shell-substitution)
- [Assertions](#assertions)
- [Conditional Assignment](#conditional-assignment)
- [Invoking Other Commands & Runfiles](#invoking-other-commands--runfiles)
- [.RUN & .RUNFILE Attributes](#run--runfile-attributes)
Expand Down Expand Up @@ -677,11 +678,94 @@ hello:
echo "${MESSAGE}"
```

#### Assertions

Assertions let you check against expected conditions, exiting with an error message when checks fail.

Assertions have the following syntax:

```
ASSERT <condition> [ "<error message>" | '<error message>' ]
```

*Note:* The error message is optional and will default to `"Assertion failed"` if not provided

##### Condition

The following condition patterns are supported:

* `[ ... ]`
* `[[ ... ]]`
* `( ... )`
* `(( ... ))`

*Note:* Run does not interpret the condition. The condition text will be executed, unmodified (including surrounding braces/parens/etc), by the configured shell. Run will inspect the exit status of the check and pass/fail the assertion accordingly.

##### Assertion Example

Here's an example that uses both global and command-level assertions:

_Runfile_
```
##
# Not subject to any assertions
world:
echo Hello, World
# Assertion applies to ALL following commands
ASSERT [ -n "${HELLO}" ] "Variable HELLO not defined"
##
# Subject to HELLO assertion, even though it doesn't use it
newman:
echo Hello, Newman
##
# Subject to HELLO assertion, and adds another
# ASSERT [ -n "${NAME}" ] 'Variable NAME not defined'
name:
echo ${HELLO}, ${NAME}
```

_example with no vars_
```
$ run world
Hello, World
$ run newman
run: Variable HELLO not defined
$ run name
run: Variable HELLO not defined
```

_example with HELLO_
```
$ HELLO=Hello run newman
Hello, Newman
$ HELLO=Hello run name
run: Variable NAME not defined
```

_example with HELLO and NAME_
```
$ HELLO=Hello NAME=Everybody run name
Hello, Everybody
```

*Note:* Assertions only apply to commands and are only checked when a command is invoked. Any globally-defined assertions will apply to ALL commands defined after the assertion.

#### Conditional Assignment

You can conditionally assign a variable, which only assigns a value if one does not already exist.


_Runfile_
```
EXPORT NAME ?= "world"
Expand Down
119 changes: 118 additions & 1 deletion internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ type ScopeExportList struct {
Names []string
}

// NewScopeExportList1 is a convience method for wrapping a single export.
// NewScopeExportList1 is a convenience method for wrapping a single export.
//
func NewScopeExportList1(name string) *ScopeExportList {
return &ScopeExportList{[]string{name}}
Expand All @@ -129,6 +129,96 @@ func (a *ScopeExportList) Apply(s *runfile.Scope) {
}
}

// ScopeAssert asserts the test, exiting with message on failure.
//
type ScopeAssert struct {
Line int
Test ScopeValueNode
Message ScopeValueNode
}

// Apply applies the node to the scope.
//
func (a *ScopeAssert) Apply(s *runfile.Scope) {
assert := &runfile.Assert{}
assert.Line = a.Line
assert.Test = a.Test.Apply(s)
assert.Message = strings.TrimSpace(a.Message.Apply(s))
s.AddAssert(assert)
}

// ScopeBracketString wraps a bracketed string.
//
type ScopeBracketString struct {
Value ScopeValueNode
}

// NewScopeBracketString is a convenience method.
//
func NewScopeBracketString(value ScopeValueNode) ScopeValueNode {
return &ScopeBracketString{Value: value}
}

// Apply applies the node to the scope.
//
func (a *ScopeBracketString) Apply(s *runfile.Scope) string {
return "[ " + a.Value.Apply(s) + " ]"
}

// ScopeDBracketString wraps a double-bracketed string.
//
type ScopeDBracketString struct {
Value ScopeValueNode
}

// NewScopeDBracketString is a convenience method.
//
func NewScopeDBracketString(value ScopeValueNode) ScopeValueNode {
return &ScopeDBracketString{Value: value}
}

// Apply applies the node to the scope.
//
func (a *ScopeDBracketString) Apply(s *runfile.Scope) string {
return "[[ " + a.Value.Apply(s) + " ]]"
}

// ScopeParenString wraps a paren-string.
//
type ScopeParenString struct {
Value ScopeValueNode
}

// NewScopeParenString is a convenience method.
//
func NewScopeParenString(value ScopeValueNode) ScopeValueNode {
return &ScopeParenString{Value: value}
}

// Apply applies the node to the scope.
//
func (a *ScopeParenString) Apply(s *runfile.Scope) string {
return "( " + a.Value.Apply(s) + " )"
}

// ScopeDParenString wraps a double-paren string.
//
type ScopeDParenString struct {
Value ScopeValueNode
}

// NewScopeDParenString is a convenience method.
//
func NewScopeDParenString(value ScopeValueNode) ScopeValueNode {
return &ScopeDParenString{Value: value}
}

// Apply applies the node to the scope.
//
func (a *ScopeDParenString) Apply(s *runfile.Scope) string {
return "(( " + a.Value.Apply(s) + " ))"
}

// Cmd wraps a parsed command.
//
type Cmd struct {
Expand Down Expand Up @@ -194,6 +284,14 @@ func (a *Cmd) Apply(r *runfile.Runfile) {
for _, opt := range a.Config.Opts {
cmd.Config.Opts = append(cmd.Config.Opts, opt.Apply(cmd))
}
// Asserts - Global first, then Command
//
for _, assert := range r.Scope.Asserts {
cmd.Scope.AddAssert(assert)
}
for _, assert := range a.Config.Asserts {
cmd.Scope.AddAssert(assert.Apply(cmd.Scope))
}
r.Cmds = append(r.Cmds, cmd)
}

Expand All @@ -206,6 +304,7 @@ type CmdConfig struct {
Opts []*CmdOpt
Vars []scopeNode
Exports []*ScopeExportList
Asserts []*CmdAssert
}

// CmdOpt wraps a command option.
Expand All @@ -230,6 +329,24 @@ func (a *CmdOpt) Apply(c *runfile.RunCmd) *runfile.RunCmdOpt {
return opt
}

// CmdAssert wraps a command assertion.
//
type CmdAssert struct {
Line int
Test ScopeValueNode
Message ScopeValueNode
}

// Apply applies the node to the Scope.
//
func (a *CmdAssert) Apply(s *runfile.Scope) *runfile.Assert {
assert := &runfile.Assert{}
assert.Line = a.Line
assert.Test = a.Test.Apply(s)
assert.Message = strings.TrimSpace(a.Message.Apply(s))
return assert
}

// ScopeAttrAssignment wraps an attribute assignment.
//
type ScopeAttrAssignment struct {
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var EnableRunfileOverride = true
// TraceFn logs lexer transitions
//
func TraceFn(msg string, i interface{}) {
//noinspection GoBoolExpressions
if EnableFnTrace {
fnName := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
log.Println(msg, ":", fnName)
Expand Down
27 changes: 20 additions & 7 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import (

var tempDir string

func executeScript(shell string, script []string, args []string, env map[string]string, prefix string, out io.Writer) {
func executeScript(shell string, script []string, args []string, env map[string]string, prefix string, out io.Writer) int {
if shell == "" {
panic(config.ErrShell)
}
if len(script) == 0 {
return
return 0
}
tmpFile, err := tempFile(fmt.Sprintf("%s-%s-*.sh", prefix, shell))
if err != nil {
Expand Down Expand Up @@ -70,19 +70,32 @@ func executeScript(shell string, script []string, args []string, env map[string]
for k, v := range env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
_ = cmd.Run()
err = cmd.Run()
if err == nil {
return 0
}
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode()
}
panic(err)
}

// ExecuteCmdScript executes a command script.
//
func ExecuteCmdScript(shell string, script []string, args []string, env map[string]string) {
executeScript(shell, script, args, env, "cmd", os.Stdout)
func ExecuteCmdScript(shell string, script []string, args []string, env map[string]string) int {
return executeScript(shell, script, args, env, "cmd", os.Stdout)
}

// ExecuteSubCommand executes a command substitution.
//
func ExecuteSubCommand(shell string, command string, env map[string]string, out io.Writer) {
executeScript(shell, []string{command}, []string{}, env, "sub", out)
func ExecuteSubCommand(shell string, command string, env map[string]string, out io.Writer) int {
return executeScript(shell, []string{command}, []string{}, env, "sub", out)
}

// ExecuteTest will execute the test command against the supplied test string
//
func ExecuteTest(shell string, test string, env map[string]string) int {
return executeScript(shell, []string{test}, []string{}, env, "test", os.Stdout)
}

// tempFile
Expand Down
Loading

0 comments on commit 0da0b32

Please sign in to comment.