third_party: add github.com/monopole/mdrip
mdrip is used by $V23_ROOT/www to execute code blocks in the website content.
This enables testing a tutorial's embedded code.
Change-Id: I3c1cad7ba4fa6fdc6275594caa9329c5f8c1c08b
diff --git a/go/src/github.com/gorilla/websocket/README.google b/go/src/github.com/gorilla/websocket/README.google
index 9646889..bc12d95 100644
--- a/go/src/github.com/gorilla/websocket/README.google
+++ b/go/src/github.com/gorilla/websocket/README.google
@@ -1,6 +1,6 @@
URL: https://github.com/gorilla/websocket/archive/92334662baa9cbebc2e6e68b8d56bc1233f85a4c.zip
Version: 92334662baa9cbebc2e6e68b8d56bc1233f85a4c
-License: Notice License
+License: MIT
License File: LICENSE
Description:
diff --git a/go/src/github.com/monopole/mdrip/.travis.yml b/go/src/github.com/monopole/mdrip/.travis.yml
new file mode 100644
index 0000000..4f2ee4d
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/.travis.yml
@@ -0,0 +1 @@
+language: go
diff --git a/go/src/github.com/monopole/mdrip/LICENSE b/go/src/github.com/monopole/mdrip/LICENSE
new file mode 100644
index 0000000..282fddc
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Jeff Regan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/go/src/github.com/monopole/mdrip/README.google b/go/src/github.com/monopole/mdrip/README.google
new file mode 100644
index 0000000..efa3e79
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/README.google
@@ -0,0 +1,10 @@
+URL: https://github.com/monopole/mdrip/archive/44d228784e161379153583e31bcc32d9a25796e3.zip
+Version: 44d228784e161379153583e31bcc32d9a25796e3
+License: Notice License
+License File: LICENSE
+
+Description:
+mdrip rips labeled command blocks from markdown files for execution.
+
+Local Modifications:
+No modifications.
diff --git a/go/src/github.com/monopole/mdrip/README.md b/go/src/github.com/monopole/mdrip/README.md
new file mode 100644
index 0000000..30b35fe
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/README.md
@@ -0,0 +1,94 @@
+# mdrip
+
+[![Build Status](https://travis-ci.org/monopole/mdrip.svg?branch=master)](https://travis-ci.org/monopole/mdrip)
+
+`mdrip` rips labeled command blocks from markdown files for execution.
+
+`mdrip` accepts one _label_ argument and any number of _file name_
+arguments, where the files are assumed to contain markdown. It scans
+the files for
+[fenced code blocks](https://help.github.com/articles/github-flavored-markdown/#fenced-code-blocks)
+immediately preceded by an HTML comment with embedded _@labels_.
+
+If one of the block labels matches the label argument to the command line, the associated block is extracted. Extracted blocks are emitted to `stdout`, or, if `--subshell` is specified, concatenated to run as a subprocess.
+
+This is a markdown-based instance of language-independent
+[literate programming](http://en.wikipedia.org/wiki/Literate_programming)
+(for perspective, see the latex-based
+[noweb](http://en.wikipedia.org/wiki/Noweb)).
+It's language independent because shell scripts can
+make, build and run programs in any programming language, via [_here_
+documents](http://tldp.org/LDP/abs/html/here-docs.html) and what not.
+
+
+## Build
+
+Assuming Go installed:
+
+```
+export MDRIP=~/mdrip
+GOPATH=$MDRIP/go go get github.com/monopole/mdrip
+GOPATH=$MDRIP/go go test github.com/monopole/mdrip/util
+$MDRIP/go/bin/mdrip # Shows usage.
+```
+
+## Example
+
+This [markdown coding tutorial](https://github.com/monopole/mdrip/blob/master/example_tutorial.md)
+(raw markdown
+[here](https://raw.githubusercontent.com/monopole/mdrip/master/example_tutorial.md))
+has bash code blocks that write, compile and run a Go program.
+
+Send code from that file to `stdout`:
+
+```
+$MDRIP/go/bin/mdrip lesson1 \
+ $MDRIP/go/src/github.com/monopole/mdrip/example_tutorial.md
+```
+
+Alternatively, run it's code in a subshell:
+```
+$MDRIP/go/bin/mdrip --subshell lesson1 \
+ $MDRIP/go/src/github.com/monopole/mdrip/example_tutorial.md
+```
+
+The above command has no output and exits with status zero if all the
+scripts labelled `@lesson1` in the given markdown succeed. On any
+failure, however, the command dumps a report and exits with non-zero
+status.
+
+This is one way to cover documentation with feature tests.
+Keeping code and documentation describing the code in the same file makes it much easier to keep them in sync.
+
+
+## Details
+
+A _script_ is a sequence of code blocks with a common label. If a
+block has multiple labels, it can be incorporated into multiple
+scripts. If a block has no label, it's ignored. The number of
+scripts that can be extracted from a set of markdown files equals the
+number of unique labels.
+
+If code blocks are in bash syntax, and the tool is itself running
+in a bash shell, then piping `mdrip` output to `source /dev/stdin` is
+equivalent to a human copy/pasting code blocks to their own shell
+prompt. In this scenario, an error in block _N_ will not stop
+execution of block _N+1_. To instead stop on error, pipe the output
+to `bash -e`.
+
+Alternatively, the tool can itself run extracted code in a bash subshell like this
+
+> `mdrip --subshell someLabel file1.md file2.md ...`
+
+If that command fails, so did something in a command block. `mdrip` reports which block failed and what it's `stdout` and `stderr` saw, while otherwise capturing and discarding subshell output.
+
+There's no notion of encapsulation. Also, there's no automatic cleanup. A block that does cleanup can be added to the markdown.
+
+### Special labels
+
+ * The first label on a block is slightly special, in that it's
+reported as the block name for logging. But like any label
+it can be used for selection too.
+
+ * The @sleep label causes mdrip to insert a `sleep 2` command after the block. Appropriate if one is starting a server in the background in that block.
+
diff --git a/go/src/github.com/monopole/mdrip/bad.md b/go/src/github.com/monopole/mdrip/bad.md
new file mode 100644
index 0000000..39ae6b1
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/bad.md
@@ -0,0 +1,22 @@
+<!-- @bad @good @sleep -->
+```
+nc -l 8000 &
+PID=$!
+```
+
+<!-- @good -->
+```
+kill $PID
+```
+
+<!-- @bad -->
+```
+echo "Don't forget to: killall nc"
+```
+
+<!-- @bad @good -->
+```
+echo "About to trigger a failure."
+echo ${DONT_EXIST?} > /dev/null
+```
+
diff --git a/go/src/github.com/monopole/mdrip/example_tutorial.md b/go/src/github.com/monopole/mdrip/example_tutorial.md
new file mode 100644
index 0000000..4de47d5
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/example_tutorial.md
@@ -0,0 +1,48 @@
+First do some setup:
+
+<!-- @init @lesson1 @cleanup -->
+```
+DEMO_DIR=/tmp/mdrip_example
+mkdir -p $DEMO_DIR/src/example
+```
+
+Write a *Go* function...
+
+<!-- @makeAdder @lesson1 -->
+```
+ cat - <<EOF >$DEMO_DIR/src/example/add.go
+package main
+
+func add(x, y int) (int) { return x + y }
+EOF
+echo "the next command intended to fail"
+badCommandToTriggerTestFailure
+```
+
+...and a main program to call it:
+
+<!-- @makeMain @lesson1 -->
+```
+ cat - <<EOF >$DEMO_DIR/src/example/main.go
+package main
+
+import "fmt"
+
+func main() {
+ comment this line to avoid compiler error
+ fmt.Printf("Calling add on 1 and 2 yields %d.\n", add(1, 2))
+}
+EOF
+echo "The following compile should fail."
+GOPATH=$DEMO_DIR go install example
+$DEMO_DIR/bin/example
+```
+
+Copy/paste the above into a shell to build and run your *Go* program.
+
+Clean up with this command:
+
+<!-- @cleanup @lesson1 @sleep -->
+```
+/bin/rm -rf $DEMO_DIR
+```
diff --git a/go/src/github.com/monopole/mdrip/main.go b/go/src/github.com/monopole/mdrip/main.go
new file mode 100644
index 0000000..bbc5f70
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/main.go
@@ -0,0 +1,172 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "github.com/monopole/mdrip/util"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+ "time"
+)
+
+var blockTimeOut = flag.Duration("blockTimeOut", 7*time.Second,
+ "The max amount of time to wait for a command block to exit.")
+
+// dumpBucket emits the contents of a util.ScriptBucket.
+//
+// If n <= 0, dump everything, else only dump the first n blocks. n
+// is 1 relative, i.e., if you want the first two blocks dumped, pass
+// n==2, not n==1.
+func dumpBucket(label string, bucket *util.ScriptBucket, n int) {
+ fmt.Printf("#\n# Script @%s from %s \n#\n", label, bucket.GetFileName())
+ delimFmt := "#" + strings.Repeat("-", 70) + "# %s %d\n"
+ for i, block := range bucket.GetScript() {
+ if n > 0 && i >= n {
+ break
+ }
+ fmt.Printf(delimFmt, "Start", i+1)
+ fmt.Printf("echo \"Block '%s' (%d/%d in %s) of %s\"\n####\n",
+ block.GetLabels()[0], i+1, len(bucket.GetScript()), label, bucket.GetFileName())
+ fmt.Print(block.GetCodeText())
+ fmt.Printf(delimFmt, "End", i+1)
+ fmt.Println()
+ }
+}
+
+// emitStraightScript simply prints the contents of scriptBuckets.
+func emitStraightScript(label string, scriptBuckets []*util.ScriptBucket) {
+ for _, bucket := range scriptBuckets {
+ dumpBucket(label, bucket, 0)
+ }
+ fmt.Printf("echo \" \"\n")
+ fmt.Printf("echo \"All done. No errors.\"\n")
+}
+
+// emitPreambledScript emits the first script normally, then emit it
+// again, as well as the the remaining scripts, so that they run in a
+// subshell.
+//
+// This allows the aggregrate script to be structured as 1) a preamble
+// initialization script that impacts the environment of the active
+// shell, followed by 2) a script that executes as a subshell that
+// exits on error. An exit in (2) won't cause the active shell (most
+// likely a terminal) to close.
+//
+// The first script must be able to complete without exit on error
+// because its not running as a subshell. So it should just set
+// environment variables and/or define shell funtions.
+func emitPreambledScript(label string, scriptBuckets []*util.ScriptBucket, n int) {
+ dumpBucket(label, scriptBuckets[0], n)
+ delim := "HANDLED_SCRIPT"
+ fmt.Printf(" bash -e <<'%s'\n", delim)
+ fmt.Printf("function handledTrouble() {\n")
+ fmt.Printf(" echo \" \"\n")
+ fmt.Printf(" echo \"Unable to continue!\"\n")
+ fmt.Printf(" exit 1\n")
+ fmt.Printf("}\n")
+ fmt.Printf("trap handledTrouble INT TERM\n")
+ emitStraightScript(label, scriptBuckets)
+ fmt.Printf("%s\n", delim)
+}
+
+func usage() {
+ fmt.Fprintf(os.Stderr, "\nUsage: %s {label} {fileName}...\n", os.Args[0])
+ flag.PrintDefaults()
+ fmt.Fprintf(os.Stderr,
+ `
+Reads markdown files, extracts code blocks with a given @label, and
+either runs them in a subshell or emits them to stdout.
+
+If the markdown file contains
+
+ Blah blah blah.
+ <!-- @goHome @foo -->
+ '''
+ cd $HOME
+ '''
+ Blah blah blah.
+ <!-- @echoApple @apple -->
+ '''
+ echo "an apple a day keeps the doctor away"
+ '''
+ Blah blah blah.
+ <!-- @echoCloseStar @foo @baz -->
+ '''
+ echo "Proxima Centauri"
+ '''
+ Blah blah blah.
+
+then the command '{this} foo {fileName}' emits:
+
+ cd $HOME
+ echo "Proxima Centauri"
+
+Pipe output to 'source /dev/stdin' to run it directly.
+
+Use --subshell to run the blocks in a subshell leaving your current
+shell env vars and pwd unchanged. The code blocks can, however, do
+anything to your computer that you can.
+`)
+}
+
+func main() {
+ flag.Usage = usage
+ preambled := flag.Int("preambled", -1,
+ "Place all scripts in a subshell, preambled by the first {n} blocks in the first script.")
+ subshell := flag.Bool("subshell", false,
+ "Run extracted blocks in subshell (leaves your env vars and pwd unchanged).")
+ swallow := flag.Bool("swallow", false,
+ "Swallow errors from subshell (non-zero exit only on problems in driver code).")
+ flag.Parse()
+ if *swallow && !*subshell {
+ fmt.Fprintf(os.Stderr, "Makes no sense to specify --swallow but not --subshell.\n")
+ usage()
+ os.Exit(1)
+ }
+ if flag.NArg() < 2 {
+ usage()
+ os.Exit(1)
+ }
+ label := flag.Arg(0)
+ scriptBuckets := make([]*util.ScriptBucket, flag.NArg()-1)
+
+ for i := 1; i < flag.NArg(); i++ {
+ fileName := flag.Arg(i)
+ contents, err := ioutil.ReadFile(fileName)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Unable to read %q\n", fileName)
+ usage()
+ os.Exit(2)
+ }
+ m := util.Parse(string(contents))
+ script, ok := m[label]
+ if !ok {
+ fmt.Fprintf(os.Stderr, "No block labelled %q in file %q.\n", label, fileName)
+ os.Exit(3)
+ }
+ scriptBuckets[i-1] = util.NewScriptBucket(fileName, script)
+ }
+
+ if len(scriptBuckets) < 1 {
+ return
+ }
+
+ if !*subshell {
+ if *preambled >= 0 {
+ emitPreambledScript(label, scriptBuckets, *preambled)
+ } else {
+ emitStraightScript(label, scriptBuckets)
+ }
+ return
+ }
+
+ result := util.RunInSubShell(scriptBuckets, *blockTimeOut)
+ if result.GetProblem() != nil {
+ util.Complain(result, label)
+ if !*swallow {
+ log.Fatal(result.GetProblem())
+ }
+ }
+}
diff --git a/go/src/github.com/monopole/mdrip/util/buff_scanner.go b/go/src/github.com/monopole/mdrip/util/buff_scanner.go
new file mode 100644
index 0000000..d116cf0
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/util/buff_scanner.go
@@ -0,0 +1,104 @@
+package util
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "time"
+)
+
+// Special strings that might appear in shell output, signalling
+// things to the stream processors.
+const MsgHappy = "MDRIP_HAPPY_Completed_command_block"
+const MsgError = "MDRIP_ERROR_Problem_while_executing_command_block"
+const MsgTimeout = "MDRIP_TIMEOUT_Command_block_did_not_finish_in_allotted_time"
+
+// BuffScanner returns a channel to which it will write lines of text.
+//
+// The text is harvested from an io stream, which will be read until
+// the io stream hits EOF or otherwise closes - at which point the
+// returned channel is closed.
+//
+// If the io stream blocks for longer than the given wait time, the
+// function will send a special line of text to the channel and close
+// it.
+func BuffScanner(wait time.Duration, label string, stream io.ReadCloser, debug bool) <-chan string {
+ chLine := make(chan string, 1)
+ xScanner := func() <-chan string {
+ chBuffLine := make(chan string, 1)
+ go func() {
+ defer close(chBuffLine)
+ scanner := bufio.NewScanner(stream)
+ if debug {
+ fmt.Printf("DEBUG: xScanner: %s - starting up\n", label)
+ }
+ for scanner.Scan() {
+ if debug {
+ fmt.Printf("DEBUG: xScanner: %s - calling Text\n", label)
+ }
+ line := scanner.Text()
+ if debug {
+ fmt.Printf("DEBUG: xScanner: %s - got \"%s\"\n", label, line)
+ }
+ chBuffLine <- line
+ if debug {
+ fmt.Printf("DEBUG: xScanner: %s - handed \"%s\" to channel\n", label, line)
+ }
+ }
+
+ if debug {
+ fmt.Printf("DEBUG: xScanner: %s - exitted Scan loop\n", label)
+ }
+ if err := scanner.Err(); err != nil {
+ if debug {
+ fmt.Printf("DEBUG: xScanner: %s - error : %s\n", label, err.Error())
+ }
+ chBuffLine <- MsgError + " : " + err.Error()
+ }
+ if debug {
+ fmt.Printf("DEBUG: xScanner: %s - completely done\n", label)
+ }
+ }()
+ return chBuffLine
+ }
+
+ chBuffLine := xScanner()
+
+ go func() {
+ defer close(chLine)
+ for {
+ if debug {
+ fmt.Printf("DEBUG: buffScanner: %s - top of loop\n", label)
+ }
+ select {
+ case line, ok := <-chBuffLine:
+ if ok {
+ if debug {
+ fmt.Printf("DEBUG: buffScanner: %s - got line, sending on\n", label)
+ }
+ chLine <- line
+ if debug {
+ fmt.Printf("DEBUG: buffScanner: %s - sent line\n", label)
+ }
+ } else {
+ if debug {
+ fmt.Printf("DEBUG: buffScanner: %s - done reading the stream\n", label)
+ }
+ chBuffLine = nil
+ return
+ }
+ case <-time.After(wait):
+ chLine <- MsgTimeout
+ if debug {
+ fmt.Printf("DEBUG: buffScanner: %s - timed out\n", label)
+ }
+ return
+ }
+ }
+
+ if debug {
+ fmt.Printf("DEBUG: buffScanner: returning chLine\n")
+ }
+ }()
+ return chLine
+}
diff --git a/go/src/github.com/monopole/mdrip/util/buff_scanner_test.go b/go/src/github.com/monopole/mdrip/util/buff_scanner_test.go
new file mode 100644
index 0000000..43c4773
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/util/buff_scanner_test.go
@@ -0,0 +1,129 @@
+package util
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "testing"
+ "time"
+)
+
+type stalledReader struct {
+ bytes.Buffer
+}
+
+func (stalledReader) Read(p []byte) (n int, err error) {
+ time.Sleep(5 * time.Second)
+ return 0, nil
+}
+func (stalledReader) Close() error { return nil }
+
+func TestStalledReader(t *testing.T) {
+ foo := stalledReader{}
+ chOut := BuffScanner(1*time.Second, "heythere", foo, true)
+
+ line, ok := <-chOut
+ if !ok {
+ t.Fail()
+ }
+ want := MsgTimeout
+ if line != want {
+ t.Errorf("got \n\t%v\nwant\n\t%v", line, want)
+ }
+
+ line, ok = <-chOut
+ if ok {
+ t.Fail()
+ }
+}
+
+type bustedReader struct {
+ bytes.Buffer
+}
+
+func (bustedReader) Read(p []byte) (n int, err error) {
+ return 0, nil
+}
+
+func (bustedReader) Close() error { return nil }
+
+func TestBustedReader(t *testing.T) {
+ foo := bustedReader{}
+ chOut := BuffScanner(1*time.Second, "heythere", foo, true)
+
+ line, ok := <-chOut
+ if !ok {
+ t.Fail()
+ }
+ want := MsgError + " : multiple Read calls return no data or error"
+ if line != want {
+ t.Errorf("got \n\t%v\nwant\n\t%v", line, want)
+ }
+
+ line, ok = <-chOut
+ if ok {
+ t.Fail()
+ }
+}
+
+type simpleReader struct {
+ io.Reader
+}
+
+func (simpleReader) Close() error { return nil }
+
+func TestSimpleReader(t *testing.T) {
+ foo1 := simpleReader{bytes.NewBufferString("beans and\nrice")}
+ chOut := BuffScanner(1*time.Second, "heythere", foo1, true)
+
+ line, ok := <-chOut
+ if !ok {
+ t.Fail()
+ }
+ want := "beans and"
+ if line != want {
+ t.Errorf("got \n\t%v\nwant\n\t%v", line, want)
+ }
+
+ line, ok = <-chOut
+ if !ok {
+ t.Fail()
+ }
+ want = "rice"
+ if line != want {
+ t.Errorf("got \n\t%v\nwant\n\t%v", line, want)
+ }
+
+ line, ok = <-chOut
+ if ok {
+ t.Fail()
+ }
+}
+
+// An example main.
+func main() {
+ {
+ foo := simpleReader{bytes.NewBufferString("beans and\nrice")}
+ chOut := BuffScanner(1*time.Second, "heythere", foo, true)
+ for line := range chOut {
+ fmt.Println(line)
+ }
+ fmt.Println("-----------------------")
+ }
+ {
+ foo := stalledReader{}
+ chOut := BuffScanner(1*time.Second, "heythere", foo, true)
+ for line := range chOut {
+ fmt.Println(line)
+ }
+ fmt.Println("-----------------------")
+ }
+ {
+ foo := bustedReader{}
+ chOut := BuffScanner(1*time.Second, "heythere", foo, true)
+ for line := range chOut {
+ fmt.Println(line)
+ }
+ fmt.Println("-----------------------")
+ }
+}
diff --git a/go/src/github.com/monopole/mdrip/util/lexer.go b/go/src/github.com/monopole/mdrip/util/lexer.go
new file mode 100644
index 0000000..e766337
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/util/lexer.go
@@ -0,0 +1,318 @@
+// Other than the custom stateFn's, much of this is copied from
+// https://golang.org/src/pkg/text/template/parse/lex.go. Cannot use
+// stuct embedding to reuse, since all the good parts are private.
+
+package util
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "unicode/utf8"
+)
+
+type Pos int
+
+type CommandBlock struct {
+ labels []string
+ codeText string
+}
+
+func (x CommandBlock) GetLabels() []string {
+ return x.labels
+}
+func (x CommandBlock) GetCodeText() string {
+ return x.codeText
+}
+
+type item struct {
+ typ itemType // Type of this item.
+ val string // The value of this item.
+}
+
+func (i item) String() string {
+ switch {
+ case i.typ == itemEOF:
+ return "EOF"
+ case i.typ == itemError:
+ return i.val
+ case i.typ == itemBlockLabel:
+ return string(labelMarker) + i.val
+ case i.typ == itemCommandBlock:
+ return "--------\n" + i.val + "--------\n"
+ case len(i.val) > 10:
+ return fmt.Sprintf("%.30s...", i.val)
+ }
+ return fmt.Sprintf("%s", i.val)
+}
+
+type itemType int
+
+const (
+ itemError itemType = iota
+ itemBlockLabel // Label for a command block
+ itemCommandBlock // All lines between codeFence marks
+ itemEOF
+)
+
+const (
+ labelMarker = '@'
+ commentOpen = "<!--"
+ commentClose = "-->"
+ codeFence = "```\n"
+)
+
+const eof = -1
+
+type stateFn func(*lexer) stateFn
+
+type lexer struct {
+ input string // string being scanned
+ state stateFn // the next lexing function to enter
+ pos Pos // current position in 'input'
+ start Pos // start of this item
+ width Pos // width of last rune read
+ items chan item // channel of scanned items
+}
+
+// next returns the next rune in the input.
+func (l *lexer) next() rune {
+ if int(l.pos) >= len(l.input) {
+ l.width = 0
+ return eof
+ }
+ r, w := utf8.DecodeRuneInString(l.input[l.pos:])
+ l.width = Pos(w)
+ l.pos += l.width
+ return r
+}
+
+func (l *lexer) peek() rune {
+ r := l.next()
+ l.backup()
+ return r
+}
+
+func (l *lexer) backup() {
+ l.pos -= l.width
+}
+
+func (l *lexer) emit(t itemType) {
+ l.items <- item{t, l.input[l.start:l.pos]}
+ l.start = l.pos
+}
+
+func (l *lexer) ignore() {
+ l.start = l.pos
+}
+
+// Consumes the next rune if it's from the valid set.
+func (l *lexer) accept(valid string) bool {
+ if strings.IndexRune(valid, l.next()) >= 0 {
+ return true
+ }
+ l.backup()
+ return false
+}
+
+// Consumes a run of runes from the valid set
+func (l *lexer) acceptRun(valid string) {
+ // is the next character of the input an element
+ // of the (defining) 'valid' set of runes (a string).
+ for strings.IndexRune(valid, l.next()) >= 0 {
+ }
+ l.backup()
+}
+
+func (l *lexer) acceptWord() {
+ l.acceptRun("012345789abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+}
+
+// errorf returns an error token and terminates the scan by passing
+// back a nil pointer that will be the next state, terminating l.nextItem.
+func (l *lexer) errorf(format string, args ...interface{}) stateFn {
+ l.items <- item{itemError, fmt.Sprintf(format, args...)}
+ return nil
+}
+
+// nextItem returns the next item from the input.
+func (l *lexer) nextItem() item {
+ item := <-l.items
+ return item
+}
+
+// newLex creates a new scanner for the input string.
+func newLex(input string) *lexer {
+ l := &lexer{
+ input: input,
+ items: make(chan item),
+ }
+ go l.run()
+ return l
+}
+
+func (l *lexer) run() {
+ for l.state = lexText; l.state != nil; {
+ l.state = l.state(l)
+ }
+}
+
+func isSpace(r rune) bool {
+ return r == ' ' || r == '\t'
+}
+
+func isEndOfLine(r rune) bool {
+ return r == '\r' || r == '\n'
+}
+
+// lexText scans until an opening comment delimiter.
+func lexText(l *lexer) stateFn {
+ for {
+ if strings.HasPrefix(l.input[l.pos:], commentOpen) {
+ return lexPutativeComment
+ }
+ if l.next() == eof {
+ l.ignore()
+ l.emit(itemEOF)
+ return nil
+ }
+ }
+}
+
+// Move to lexing a command block intended for a particular script, or to
+// lexing a simple comment. Comment opener known to be present.
+func lexPutativeComment(l *lexer) stateFn {
+ l.pos += Pos(len(commentOpen))
+ for {
+ switch r := l.next(); {
+ case isSpace(r):
+ l.ignore()
+ case r == labelMarker:
+ l.backup()
+ return lexBlockLabels
+ default:
+ l.backup()
+ return lexCommentRemainder
+ }
+ }
+}
+
+// lexCommentRemainder assumes a comment opener was read,
+// and eats everything up to and including the comment closer.
+func lexCommentRemainder(l *lexer) stateFn {
+ i := strings.Index(l.input[l.pos:], commentClose)
+ if i < 0 {
+ return l.errorf("unclosed comment")
+ }
+ l.pos += Pos(i + len(commentClose))
+ l.ignore()
+ return lexText
+}
+
+// lexBlockLabels scans a string like "@1 @hey" emitting the labels
+// "1" and "hey". LabelMarker known to be present.
+func lexBlockLabels(l *lexer) stateFn {
+ for {
+ switch r := l.next(); {
+ case r == eof || isEndOfLine(r):
+ return l.errorf("unclosed block label sequence")
+ case isSpace(r):
+ l.ignore()
+ case r == labelMarker:
+ l.ignore()
+ l.acceptWord()
+ if l.width == 0 {
+ return l.errorf("empty block label")
+ }
+ l.emit(itemBlockLabel)
+ default:
+ l.backup()
+ if !strings.HasPrefix(l.input[l.pos:], commentClose) {
+ return l.errorf("improperly closed block label sequence")
+ }
+ l.pos += Pos(len(commentClose))
+ l.ignore()
+ l.acceptRun(" \t")
+ l.ignore()
+ r := l.next()
+ if r != '\n' && r != '\r' {
+ return l.errorf("Expected command block marker at start of line.")
+ }
+ l.ignore()
+ if !strings.HasPrefix(l.input[l.pos:], codeFence) {
+ return l.errorf("Expected command block mark, got: " + l.input[l.pos:])
+ }
+ return lexCommandBlock
+ }
+ }
+ return lexText
+}
+
+// lexCommandBlock scans a command block. Initial marker known to be present.
+func lexCommandBlock(l *lexer) stateFn {
+ l.pos += Pos(len(codeFence))
+ l.ignore()
+ for {
+ if strings.HasPrefix(l.input[l.pos:], codeFence) {
+ if l.pos > l.start {
+ l.emit(itemCommandBlock)
+ }
+ l.pos += Pos(len(codeFence))
+ l.ignore()
+ return lexText
+ }
+ if l.next() == eof {
+ return l.errorf("unclosed command block")
+ }
+ }
+}
+
+func shouldSleep(labels []string) bool {
+ for _, label := range labels {
+ if label == "sleep" {
+ return true
+ }
+ }
+ return false
+}
+
+// Parse lexes the incoming string into a mapping from block label to
+// CommandBlock array. The labels are the strings after a labelMarker in
+// a comment preceding a command block. Arrays hold command blocks in the
+// order they appeared in the input.
+func Parse(s string) (result map[string][]*CommandBlock) {
+ result = make(map[string][]*CommandBlock)
+ currentLabels := make([]string, 0, 10)
+ l := newLex(s)
+ for {
+ item := l.nextItem()
+ switch {
+ case item.typ == itemEOF || item.typ == itemError:
+ return
+ case item.typ == itemBlockLabel:
+ currentLabels = append(currentLabels, item.val)
+ case item.typ == itemCommandBlock:
+ if len(currentLabels) == 0 {
+ fmt.Println("Have an unlabelled command block:\n " + item.val)
+ os.Exit(1)
+ }
+ // If the command block has a 'sleep' label, add a brief sleep
+ // at the end. This is hack to give servers placed in the
+ // background time to start.
+ if shouldSleep(currentLabels) {
+ item.val = item.val + "sleep 2s # Added by mdrip\n"
+ }
+ newBlock := &CommandBlock{currentLabels, item.val}
+ for _, label := range currentLabels {
+ blocks, ok := result[label]
+ if ok {
+ blocks = append(blocks, newBlock)
+ } else {
+ blocks = []*CommandBlock{newBlock}
+ }
+ result[label] = blocks
+ }
+ currentLabels = make([]string, 0, 10)
+ }
+ }
+}
diff --git a/go/src/github.com/monopole/mdrip/util/lexer_test.go b/go/src/github.com/monopole/mdrip/util/lexer_test.go
new file mode 100644
index 0000000..3347549
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/util/lexer_test.go
@@ -0,0 +1,90 @@
+package util
+
+import (
+ "fmt"
+ "testing"
+)
+
+type lexTest struct {
+ name string // Name of the sub-test.
+ input string // Input string to be lexed.
+ want []item // Expected items produced by lexer.
+}
+
+const (
+ block1 = "echo $PATH\n" +
+ "echo $GOPATH"
+ block2 = "kill -9 $pid"
+)
+
+var (
+ tEOF = item{itemEOF, ""}
+)
+
+var lexTests = []lexTest{
+ {"empty", "", []item{tEOF}},
+ {"spaces", " \t\n", []item{tEOF}},
+ {"text", "blah blah blinkity blah", []item{tEOF}},
+ {"comment1", "<!-- -->", []item{tEOF}},
+ {"comment2", "a <!-- --> b", []item{tEOF}},
+ {"block1", "aa <!-- @1 -->\n" +
+ "```\n" + block1 + "```\n bbb",
+ []item{{itemBlockLabel, "1"},
+ {itemCommandBlock, block1},
+ tEOF}},
+ {"block2", "aa <!-- @1 @2-->\n" +
+ "```\n" + block1 + "```\n bb cc\n" +
+ "dd <!-- @3 @4-->\n" +
+ "```\n" + block2 + "```\n ee ff\n",
+ []item{
+ {itemBlockLabel, "1"},
+ {itemBlockLabel, "2"},
+ {itemCommandBlock, block1},
+ {itemBlockLabel, "3"},
+ {itemBlockLabel, "4"},
+ {itemCommandBlock, block2},
+ tEOF}},
+}
+
+// collect gathers the emitted items into a slice.
+func collect(t *lexTest) (items []item) {
+ l := newLex(t.input)
+ for {
+ item := l.nextItem()
+ items = append(items, item)
+ if item.typ == itemEOF || item.typ == itemError {
+ break
+ }
+ }
+ return
+}
+
+func equal(i1, i2 []item) bool {
+ if len(i1) != len(i2) {
+ return false
+ }
+ for k := range i1 {
+ if i1[k].typ != i2[k].typ {
+ fmt.Printf("types not equal - got : %s\n", i1[k].typ)
+ fmt.Printf("types not equal - want: %s\n", i2[k].typ)
+ fmt.Printf("\n")
+ return false
+ }
+ if i1[k].val != i2[k].val {
+ fmt.Printf("vals not equal - got : %q\n", i1[k].val)
+ fmt.Printf("vals not equal - want: %q\n", i2[k].val)
+ fmt.Printf("\n")
+ return false
+ }
+ }
+ return true
+}
+
+func TestLex(t *testing.T) {
+ for _, test := range lexTests {
+ got := collect(&test)
+ if !equal(got, test.want) {
+ t.Errorf("%s:\ngot\n\t%+v\nexpected\n\t%v", test.name, got, test.want)
+ }
+ }
+}
diff --git a/go/src/github.com/monopole/mdrip/util/pid.go b/go/src/github.com/monopole/mdrip/util/pid.go
new file mode 100644
index 0000000..62051ff
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/util/pid.go
@@ -0,0 +1,40 @@
+package util
+
+import (
+ "errors"
+ "fmt"
+ "os/exec"
+ "strconv"
+ "strings"
+)
+
+// getProcessGroupId purports to get a process group Id common to all
+// subprocesses of its pid argument.
+//
+// There should be a better way to do this.
+//
+// Goal is to be able to support killing any subprocesses created by
+// RunInSubShell. At the moment, its up to command script authors to
+// clean up after themselves.
+func getProcesssGroupId(pid int) (int, error) {
+ // /bin/ps -o pid,pgid,rgid,ppid,cmd
+ // /bin/ps -o pgid=12492 --no-headers
+ cmdOut, execErr := exec.Command(
+ "/bin/ps", "--pid", strconv.Itoa(pid), "-o", "pgid", "--no-headers").Output()
+ groupId := strings.TrimSpace(string(cmdOut))
+ if execErr != nil || len(groupId) < 1 {
+ return 0, errors.New(
+ "Unable to yank groupId from ps command: " + groupId + " " + execErr.Error())
+ }
+ pgid, convErr := strconv.Atoi(groupId)
+ if convErr != nil {
+ return 0, convErr
+ }
+ return pgid, nil
+}
+
+// An attempt to kill any and all child processes.
+func killProcesssGroup(pgid int) {
+ killer := exec.Command("/bin/kill", "-TERM", "--", fmt.Sprintf("-%v", pgid))
+ killer.Start()
+}
diff --git a/go/src/github.com/monopole/mdrip/util/runner.go b/go/src/github.com/monopole/mdrip/util/runner.go
new file mode 100644
index 0000000..bb0abbd
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/util/runner.go
@@ -0,0 +1,318 @@
+package util
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+)
+
+// Should switch to a logging package that supports levels,
+// e.g. https://github.com/golang/glog
+var debug = flag.Bool("debug", false,
+ "If true, dump more information during run.")
+
+// check reports the error fatally if its non-nil.
+func check(msg string, err error) {
+ if err != nil {
+ fmt.Printf("Problem with %s\n", msg, err)
+ log.Fatal(err)
+ }
+}
+
+// blockOutput pairs the output collected from a stream (i.e. stderr
+// or stdout) as a result of executing all or part of a command block
+// with a bool indicating if the output is associated with shell
+// success or shell failure. Output can appear on stderr without
+// neccessarily being associated with shell failure.
+type blockOutput struct {
+ success bool
+ output string
+}
+
+// accumulateOutput returns a channel to which it writes objects that
+// contain what purport to be the entire output of one command block.
+//
+// To do so, it accumulates strings off a channel representing command
+// block output until the channel closes, or until a string arrives
+// that matches a particular pattern.
+//
+// On the happy path, strings are accumulated and every so often sent
+// out with a success == true flag attached. This continues until the
+// input channel closes.
+//
+// On a sad path, an accumulation of strings is sent with a success ==
+// false flag attached, and the function exits early, before it's
+// input channel closes.
+func accumulateOutput(prefix string, in <-chan string) <-chan *blockOutput {
+ out := make(chan *blockOutput)
+ var accum bytes.Buffer
+ go func() {
+ defer close(out)
+ for line := range in {
+ if strings.HasPrefix(line, MsgTimeout) {
+ accum.WriteString("\n" + line + "\n")
+ accum.WriteString("A subprocess might still be running.\n")
+ if *debug {
+ fmt.Printf("DEBUG: accumulateOutput %s: Timeout return.\n", prefix)
+ }
+ out <- &blockOutput{false, accum.String()}
+ return
+ }
+ if strings.HasPrefix(line, MsgError) {
+ accum.WriteString(line + "\n")
+ if *debug {
+ fmt.Printf("DEBUG: accumulateOutput %s: Error return.\n", prefix)
+ }
+ out <- &blockOutput{false, accum.String()}
+ return
+ }
+ if strings.HasPrefix(line, MsgHappy) {
+ if *debug {
+ fmt.Printf("DEBUG: accumulateOutput %s: %s\n", prefix, line)
+ }
+ out <- &blockOutput{true, accum.String()}
+ accum.Reset()
+ } else {
+ if *debug {
+ fmt.Printf("DEBUG: accumulateOutput %s: Accumulating [%s]\n", prefix, line)
+ }
+ accum.WriteString(line + "\n")
+ }
+ }
+
+ if *debug {
+ fmt.Printf("DEBUG: accumulateOutput %s: <--- This channel has closed.\n", prefix)
+ }
+ trailing := strings.TrimSpace(accum.String())
+ if len(trailing) > 0 {
+ if *debug {
+ fmt.Printf(
+ "DEBUG: accumulateOutput %s: Erroneous (missing-happy) output [%s]\n", prefix, accum.String())
+ }
+ out <- &blockOutput{false, accum.String()}
+ } else {
+ if *debug {
+ fmt.Printf("DEBUG: accumulateOutput %s: Nothing trailing.\n", prefix)
+ }
+ }
+ }()
+ return out
+}
+
+// ScriptResult pairs blockOutput with meta data about shell execution.
+type ScriptResult struct {
+ blockOutput
+ fileName string // File in which the error occurred.
+ index int // Command block index.
+ block *CommandBlock // Content of actual command block.
+ problem error // Error, if any.
+ message string // Detailed error message, if any.
+}
+
+func (x ScriptResult) GetFileName() string {
+ return x.fileName
+}
+
+func (x ScriptResult) GetProblem() error {
+ return x.problem
+}
+
+// ScriptBucket associates a list of commandBlocks with the name of the
+// file they came from.
+type ScriptBucket struct {
+ fileName string
+ script []*CommandBlock
+}
+
+func (x ScriptBucket) GetFileName() string {
+ return x.fileName
+}
+func (x ScriptBucket) GetScript() []*CommandBlock {
+ return x.script
+}
+func NewScriptBucket(fileName string, script []*CommandBlock) *ScriptBucket {
+ return &ScriptBucket{fileName, script}
+}
+
+// userBehavior acts like a command line user.
+//
+// It writes command blocks to shell, then waits after each block to
+// see if the block worked. If the block appeared to complete without
+// error, the routine sends the next block, else it exits early.
+func userBehavior(stdIn io.Writer, scriptBuckets []*ScriptBucket, blockTimeout time.Duration,
+ stdOut, stdErr io.ReadCloser) (errResult *ScriptResult) {
+ emptyArray := []string{}
+
+ chOut := BuffScanner(blockTimeout, "stdout", stdOut, *debug)
+ chErr := BuffScanner(1*time.Minute, "stderr", stdErr, *debug)
+
+ chAccOut := accumulateOutput("stdOut", chOut)
+ chAccErr := accumulateOutput("stdErr", chErr)
+
+ errResult = &ScriptResult{blockOutput{false, ""}, "", -1, &CommandBlock{emptyArray, ""}, nil, ""}
+ for _, bucket := range scriptBuckets {
+ for i, block := range bucket.script {
+ blockName := block.labels[0]
+ fmt.Printf("Running %s (%d/%d) from %s\n",
+ blockName, i+1, len(bucket.script), bucket.fileName)
+ if *debug {
+ fmt.Printf("DEBUG: userBehavior: sending \"%s\"\n", block.codeText)
+ }
+ _, err := stdIn.Write([]byte(block.codeText))
+ check("write script", err)
+ if *debug {
+ fmt.Printf("DEBUG: userBehavior: sending happy\n")
+ }
+ _, err = stdIn.Write([]byte("\necho " + MsgHappy + " " + blockName + "\n"))
+ check("write msgHappy", err)
+
+ result := <-chAccOut
+
+ if result == nil || !result.success {
+ // A nil result means stdout has closed early because a
+ // sub-subprocess failed.
+ if result == nil {
+ if *debug {
+ fmt.Printf("DEBUG: userBehavior: stdout Result == nil.\n")
+ // fmt.Printf("DEBUG: userBehavior: sending warning to stdErr\n")
+ }
+ // chErr <- MsgError + " : early termination; stdout has closed."
+ } else {
+ if *debug {
+ fmt.Printf("DEBUG: userBehavior: stdout Result: %s\n", result.output)
+ }
+ // Shell may still be alive despite a failure (e.g. an mdrip
+ // imposed timeout). Maybe send exit.
+ exitShell(stdIn)
+ errResult.output = result.output
+ errResult.message = result.output
+ }
+ errResult.fileName = bucket.fileName
+ errResult.index = i
+ errResult.block = block
+ fillErrResult(chAccErr, errResult)
+ return
+ }
+ }
+ }
+ exitShell(stdIn)
+ fmt.Printf("All done, no errors triggered.\n")
+ return
+}
+
+// fillErrResult fills an instance of ScriptResult.
+func fillErrResult(chAccErr <-chan *blockOutput, errResult *ScriptResult) {
+ result := <-chAccErr
+ if result == nil {
+ if *debug {
+ fmt.Printf("DEBUG: userBehavior: stderr Result == nil.\n")
+ }
+ errResult.problem = errors.New("unknown")
+ return
+ }
+ errResult.problem = errors.New(result.output)
+ errResult.message = result.output
+ if *debug {
+ fmt.Printf("DEBUG: userBehavior: stderr Result: %s\n", result.output)
+ }
+}
+
+func exitShell(stdIn io.Writer) {
+ if *debug {
+ fmt.Printf("DEBUG: userBehavior: exiting subshell.\n")
+ }
+ stdIn.Write([]byte("exit\n"))
+ // Don't check for error - it either works, or we'll have
+ // already reported a failed shell.
+}
+
+func dumpCapturedOutput(name, delim, output string) {
+ fmt.Fprintf(os.Stderr, "\n%s capture:\n", name)
+ fmt.Fprintf(os.Stderr, delim)
+ fmt.Fprintf(os.Stderr, output)
+ fmt.Fprintf(os.Stderr, "\n")
+ fmt.Fprintf(os.Stderr, delim)
+}
+
+// Complain spits the contents of a ScriptResult to stderr.
+func Complain(result *ScriptResult, label string) {
+ delim := strings.Repeat("-", 70) + "\n"
+ fmt.Fprintf(os.Stderr, "Error in block '%s' (#%d of script '%s') in %s:\n",
+ result.block.labels[0], result.index+1, label, result.fileName)
+ fmt.Fprintf(os.Stderr, delim)
+ fmt.Fprintf(os.Stderr, string(result.block.codeText))
+ fmt.Fprintf(os.Stderr, delim)
+ dumpCapturedOutput("Stdout", delim, result.output)
+ if len(result.message) > 0 {
+ dumpCapturedOutput("Stderr", delim, result.message)
+ }
+}
+
+// RunInSubShell runs command blocks in a subprocess, stopping and
+// reporting on any error. The subprocess runs with the -e flag, so
+// it will abort if any sub-subprocess (any command) fails.
+//
+// Command blocks are strings presumably holding code from some shell
+// language. The strings may be more complex than single commands
+// delimitted by linefeeds - e.g. blocks that operate on HERE
+// documents, or multi-line commands using line continuation via '\',
+// quotes or curly brackets.
+//
+// This function itself is not a shell interpreter, so it has no idea
+// if one line of text from a command block is an individual command
+// or part of something else.
+//
+// Error reporting works by discarding output from command blocks that
+// succeeded, and only reporting the contents of stdout and stderr
+// when the subprocess exits on error.
+func RunInSubShell(scriptBuckets []*ScriptBucket, blockTimeout time.Duration) (
+ result *ScriptResult) {
+ // Adding "-e" to force the subshell to die on any error.
+ shell := exec.Command("bash", "-e")
+
+ stdOut, err := shell.StdoutPipe()
+ check("out pipe", err)
+
+ stdErr, err := shell.StderrPipe()
+ check("err pipe", err)
+
+ stdIn, err := shell.StdinPipe()
+ check("in pipe", err)
+
+ err = shell.Start()
+ check("shell start", err)
+
+ pid := shell.Process.Pid
+ if *debug {
+ fmt.Printf("DEBUG: RunInSubShell: pid = %d\n", pid)
+ }
+ pgid, err := getProcesssGroupId(pid)
+ if err == nil {
+ if *debug {
+ fmt.Printf("DEBUG: RunInSubShell: pgid = %d\n", pgid)
+ }
+ }
+
+ result = userBehavior(stdIn, scriptBuckets, blockTimeout, stdOut, stdErr)
+
+ if *debug {
+ fmt.Printf("DEBUG: RunInSubShell: Waiting for shell to end.\n")
+ }
+ waitError := shell.Wait()
+ if result.problem == nil {
+ result.problem = waitError
+ }
+ if *debug {
+ fmt.Printf("DEBUG: RunInSubShell: Shell done.\n")
+ }
+
+ // killProcesssGroup(pgid)
+ return
+}
diff --git a/go/src/github.com/monopole/mdrip/util/runner_test.go b/go/src/github.com/monopole/mdrip/util/runner_test.go
new file mode 100644
index 0000000..9bffcba
--- /dev/null
+++ b/go/src/github.com/monopole/mdrip/util/runner_test.go
@@ -0,0 +1,100 @@
+package util
+
+import (
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+var emptyArray []string = []string{}
+var emptyCommandBlock *CommandBlock = &CommandBlock{emptyArray, ""}
+
+const timeoutSeconds = 1
+
+func TestRunnerWithNothing(t *testing.T) {
+ if RunInSubShell([]*ScriptBucket{}, timeoutSeconds*time.Second).problem != nil {
+ t.Fail()
+ }
+}
+
+func doIt(blocks []*CommandBlock) *ScriptResult {
+ return RunInSubShell([]*ScriptBucket{&ScriptBucket{"iAmFileName", blocks}}, timeoutSeconds*time.Second)
+}
+
+func TestRunnerWithGoodStuff(t *testing.T) {
+ labels := []string{"foo", "bar"}
+ blocks := []*CommandBlock{
+ &CommandBlock{labels, "echo kale\ndate\n"},
+ &CommandBlock{labels, "echo beans\necho apple\n"},
+ &CommandBlock{labels, "echo hasta\necho la vista\n"}}
+ result := doIt(blocks)
+ if result.problem != nil {
+ t.Fail()
+ }
+}
+
+func checkFail(t *testing.T, got, want *ScriptResult) {
+ if got.problem == nil {
+ t.Fail()
+ }
+ if got.index != want.index {
+ t.Errorf("%s got\n\t%v\nwant\n\t%v", "script", got.index, want.index)
+ }
+ if !strings.Contains(got.message, want.message) {
+ t.Errorf("%s got\n\t%v\nwant\n\t%v", "message", got.message, want.message)
+ }
+}
+
+func TestStartWithABadCommand(t *testing.T) {
+ want := &ScriptResult{
+ blockOutput{false, "dunno"},
+ "fileNameTestStartWithABadCommand",
+ 0,
+ emptyCommandBlock,
+ nil,
+ "bash: line 1: notagoodcommand: command not found"}
+
+ labels := []string{"foo", "bar"}
+ blocks := []*CommandBlock{
+ &CommandBlock{labels, "notagoodcommand\ndate\n"},
+ &CommandBlock{labels, "echo beans\necho cheese\n"}}
+ checkFail(t, doIt(blocks), want)
+}
+
+func TestBadCommandInTheMiddle(t *testing.T) {
+ want := &ScriptResult{
+ blockOutput{false, "dunno"},
+ "fileNameTestBadCommandInTheMiddle",
+ 2,
+ emptyCommandBlock,
+ nil,
+ "bash: line 9: lochNessMonster: command not found"}
+
+ labels := []string{"foo", "bar"}
+
+ blocks := []*CommandBlock{
+ &CommandBlock{labels, "echo tofu\ndate\n"},
+ &CommandBlock{labels, "echo beans\necho kale\n"},
+ &CommandBlock{labels, "lochNessMonster\n"},
+ &CommandBlock{labels, "echo hasta\necho la vista\n"}}
+
+ checkFail(t, doIt(blocks), want)
+}
+
+func TestTimeOut(t *testing.T) {
+ want := &ScriptResult{
+ blockOutput{false, "dunno"},
+ "fileNameTestTimeOut",
+ 0,
+ emptyCommandBlock,
+ nil,
+ MsgTimeout}
+
+ labels := []string{"foo", "bar"}
+ // Go to sleep for twice the length of the timeout.
+ blocks := []*CommandBlock{
+ &CommandBlock{labels, "date\nsleep " + strconv.Itoa(timeoutSeconds+2) + "\necho kale"},
+ &CommandBlock{labels, "echo beans\necho cheese\n"}}
+ checkFail(t, doIt(blocks), want)
+}