parent
eaf7c1a330
commit
e3fe0f47a1
@ -0,0 +1,9 @@ |
||||
image: alpine/edge |
||||
packages: |
||||
- go |
||||
sources: |
||||
- https://git.sr.ht/~lenzj/testcli |
||||
tasks: |
||||
- test: | |
||||
cd testcli/cmd |
||||
make check |
@ -1,58 +0,0 @@ |
||||
.POSIX: |
||||
|
||||
PNAME = testcli
|
||||
|
||||
RTEMPLATE ?= ../repo-template
|
||||
|
||||
all: check |
||||
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
.PHONY: all |
||||
|
||||
#---Test/Check Section---
|
||||
|
||||
TESTDIR = tests
|
||||
|
||||
check: |
||||
cd $(TESTDIR) && go test -v
|
||||
|
||||
cleanCheck: |
||||
find $(TESTDIR) -name '*.result' -delete
|
||||
|
||||
.PHONY: check cleanCheck |
||||
|
||||
#---Generate Main Documents---
|
||||
|
||||
regenDocMain: |
||||
pgot -i ":$(RTEMPLATE)" -o README.md template/README.md.got
|
||||
pgot -i ":$(RTEMPLATE)" -o LICENSE $(RTEMPLATE)/LICENSE.src/BSD-2-clause.got
|
||||
|
||||
.PHONY: regenDocMain |
||||
|
||||
#---Generate Makefile---
|
||||
|
||||
regenMakefile: |
||||
pgot -i ":$(RTEMPLATE)" -o Makefile template/Makefile.got
|
||||
|
||||
.PHONY: regenMakefile |
||||
|
||||
#---Lint Helper Target---
|
||||
|
||||
lint: |
||||
@find . -path ./.git -prune -or \
|
||||
-type f -and -not -name 'Makefile' \
|
||||
-exec grep -Hn '<no value>' '{}' ';'
|
||||
|
||||
.PHONY: lint |
||||
|
||||
#---TODO Helper Target---
|
||||
|
||||
todo: |
||||
@find . -path ./.git -prune -or \
|
||||
-type f -and -not -name 'Makefile' \
|
||||
-exec grep -Hn TODO '{}' ';'
|
||||
|
||||
.PHONY: todo |
||||
|
||||
# vim:set noet tw=80:
|
@ -0,0 +1,8 @@ |
||||
check: foo foo_test.go |
||||
go test -v
|
||||
|
||||
foo: foo_src/main.go |
||||
go build -o foo foo_src/main.go
|
||||
|
||||
clean: |
||||
rm -f foo foo_output.txt
|
@ -0,0 +1,79 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"flag" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"os" |
||||
"path/filepath" |
||||
) |
||||
|
||||
const Version = "1.7.2" |
||||
|
||||
func main() { |
||||
// Get application name as executed from command prompt
|
||||
appName := filepath.Base(os.Args[0]) |
||||
|
||||
// Set up formatting for error messages
|
||||
log.SetFlags(0) |
||||
log.SetPrefix(appName + ": ") |
||||
|
||||
// Parse command line
|
||||
flag.Usage = func() { |
||||
fmt.Fprintf(flag.CommandLine.Output(), |
||||
"Usage: %s [OPTION]... [FILE]\n"+ |
||||
"This is a simple utility to test the \"testcli\" golang package\n"+ |
||||
"and also demonstrate it's use. It replaces any instance of \"foo\"\n"+ |
||||
"in the input stream with \"foobar\" before sending to the output\n"+ |
||||
"stream. Note that if [FILE] is not specified on the command line\n"+ |
||||
"the utility reads from stdin.\n"+ |
||||
"Options:\n", appName) |
||||
flag.PrintDefaults() |
||||
} |
||||
var oflag = flag.String("o", "-", "file to output to rather than stdout") |
||||
var vflag = flag.Bool("v", false, "display "+appName+" version") |
||||
|
||||
flag.Parse() |
||||
|
||||
// Display application version if requested
|
||||
if *vflag { |
||||
fmt.Println(appName + " " + Version) |
||||
os.Exit(0) |
||||
} |
||||
|
||||
// Prepare input and output streams
|
||||
var ( |
||||
input io.Reader |
||||
output io.Writer |
||||
err error |
||||
) |
||||
|
||||
switch flag.NArg() { |
||||
case 0: |
||||
input = os.Stdin |
||||
case 1: |
||||
infile := flag.Arg(0) |
||||
if input, err = os.Open(infile); err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
default: |
||||
log.Fatalln("Too many arguments specified!") |
||||
} |
||||
|
||||
switch *oflag { |
||||
case "-": |
||||
output = os.Stdout |
||||
default: |
||||
if output, err = os.Create(*oflag); err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
} |
||||
|
||||
content, err := io.ReadAll(input) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
output.Write(bytes.ReplaceAll(content, []byte("foo"), []byte("foobar"))) |
||||
} |
@ -0,0 +1,91 @@ |
||||
// Copyright (c) 2021 Jason T. Lenz. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
package foo_test |
||||
|
||||
import ( |
||||
"git.lenzplace.org/lenzj/testcli" |
||||
"os/exec" |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
func TestFooVersion(t *testing.T) { |
||||
clt := testcli.T{ |
||||
Cmd: exec.Cmd{ |
||||
Path: "./foo", |
||||
Args: []string{"foo", "-v"}, |
||||
}, |
||||
TStdout: `^foo 1.7.2\n$`, |
||||
} |
||||
testcli.Run(t, clt) |
||||
} |
||||
|
||||
func TestManyFoo(t *testing.T) { |
||||
clTests := []struct { |
||||
name string |
||||
test testcli.T |
||||
}{ |
||||
{ |
||||
name: "version", |
||||
test: testcli.T{ |
||||
Cmd: exec.Cmd{ |
||||
Path: "./foo", |
||||
Args: []string{"foo", "-v"}, |
||||
}, |
||||
TStdout: `^foo 1.7.2\n$`, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "bad option", |
||||
test: testcli.T{ |
||||
Cmd: exec.Cmd{ |
||||
Path: "./foo", |
||||
Args: []string{"foo", "-z"}, |
||||
}, |
||||
TStderr: `(?m)^flag provided but not defined: -z$.*$`, |
||||
TExit: 2, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "standard input", |
||||
test: testcli.T{ |
||||
Cmd: exec.Cmd{ |
||||
Path: "./foo", |
||||
Stdin: strings.NewReader("Foo,foo,fOO,bar\n"), |
||||
}, |
||||
TStdout: `^Foo,foobar,fOO,bar\n$`, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "input from file", |
||||
test: testcli.T{ |
||||
Cmd: exec.Cmd{ |
||||
Path: "./foo", |
||||
Args: []string{"foo", "foo_test.go"}, |
||||
}, |
||||
TStdout: `(?m)^.*^package foobar_test$.*$`, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "output to file", |
||||
test: testcli.T{ |
||||
Cmd: exec.Cmd{ |
||||
Path: "./foo", |
||||
Args: []string{"foo", "-o", "foo_output.txt"}, |
||||
Stdin: strings.NewReader("Foo,foo,fOO,bar\n"), |
||||
}, |
||||
TFiles: []testcli.FT{ |
||||
{ |
||||
Path: "foo_output.txt", |
||||
TContent: `^Foo,foobar,fOO,bar\n$`, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
for _, clt := range clTests { |
||||
t.Run(clt.name, func(t *testing.T) { testcli.Run(t, clt.test) }) |
||||
} |
||||
} |
@ -1,45 +0,0 @@ |
||||
;;; |
||||
{ |
||||
"pgotInclude" : [
|
||||
"global.got",
|
||||
"Makefile.src/mk-rm.got",
|
||||
"Makefile.src/mk-docMain.got",
|
||||
"Makefile.src/mk-mkFile.got",
|
||||
"Makefile.src/mk-lint.got",
|
||||
"Makefile.src/mk-todo.got"
|
||||
]
|
||||
} |
||||
;;; |
||||
.POSIX: |
||||
|
||||
PNAME = testcli
|
||||
|
||||
RTEMPLATE ?= ../repo-template
|
||||
|
||||
all: check |
||||
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
.PHONY: all |
||||
|
||||
#---Test/Check Section---
|
||||
|
||||
TESTDIR = tests
|
||||
|
||||
check: |
||||
cd $(TESTDIR) && go test -v
|
||||
|
||||
cleanCheck: |
||||
find $(TESTDIR) -name '*.result' -delete
|
||||
|
||||
.PHONY: check cleanCheck |
||||
|
||||
{{template "mk-docMain" .}} |
||||
|
||||
{{template "mk-mkFile" .}} |
||||
|
||||
{{template "mk-lint" .}} |
||||
|
||||
{{template "mk-todo" .}} |
||||
|
||||
# vim:set noet tw=80:
|
@ -1,19 +0,0 @@ |
||||
# Purpose |
||||
|
||||
This "template" folder contains templates and associates scripts to generate |
||||
standard repository files such as README.md and LICENSE. This is primarily |
||||
used by the author to ensure consistency across repositories and can safely be |
||||
ignored by most end users and contributors. |
||||
|
||||
# Usage |
||||
|
||||
If you do desire to use or process templates in this folder make sure you have |
||||
cloned the git repository at <https://git.lenzplace.org/lenzj/repo-template>. |
||||
The cloned "repo-template" respository should ideally exist one level outside |
||||
this repository root. I.E. ../repo-template |
||||
|
||||
Once this is in place you can regenerate the documents from templates using the |
||||
following command from the main repository folder: |
||||
|
||||
$ make regenMakefile |
||||
$ make regenDocMain |
@ -1,183 +0,0 @@ |
||||
;;; |
||||
{ |
||||
"rname": "testcli", |
||||
"pgotInclude": [ "README.src/all.got" ] |
||||
} |
||||
;;; |
||||
# {{.rname}} |
||||
|
||||
Package **{{.rname}}** is a golang utility package for testing command line |
||||
interfaces ([CLI](https://en.wikipedia.org/wiki/Command-line_interface)). When |
||||
using {{.rname}}, each CLI test exists within its own file system folder. All test |
||||
folders for a specific CLI are typically contained within a main folder which |
||||
the {{.rname}} package "walks" entering all subdirs and executing each CLI test |
||||
within a folder. Test results are tracked and displayed using the golang |
||||
standard testing infrastructure. |
||||
|
||||
Each test folder must contain the following text files: |
||||
|
||||
```text |
||||
* tCmd : The CLI command to be executed including parameters and |
||||
options. Within this file any '{{`{{.cli}}`}}' string is replaced |
||||
by the CLI being tested. If a test folder is multiple levels |
||||
deep within the file tree the relative CLI path is adjusted |
||||
accordingly before executing tCmd. |
||||
* tStdout : The expected stdout |
||||
* tStderr : The expected sterr |
||||
* tExit : The exit code |
||||
``` |
||||
|
||||
The following are optional files: |
||||
|
||||
```text |
||||
* t*.check : Any file matching t*.check is compared directly against the |
||||
same filename t*.result. This can be used to check the |
||||
output of any files generated by the CLI. |
||||
``` |
||||
|
||||
Alternately, any of the output filenames above may end with "Regex" to do a |
||||
regular expression match rather than a direct string comparison. This can be |
||||
useful to check output for which a portion changes often. For example log file |
||||
output that contains the date or time at the beginning of an output line. |
||||
|
||||
```text |
||||
* tStdoutRegex : Contains a regular expression string to match against |
||||
the expected stdout |
||||
* t*.checkRegex |
||||
* etc. ... |
||||
``` |
||||
|
||||
The following file names are not used directly by {{.rname}} but are named as |
||||
follows by convention if needed: |
||||
|
||||
```text |
||||
* tStdin : Any text intended to be fed as stdin to the CLI. Typically |
||||
this is accomplished within tCmd such as "{{`{{.cli}}`}} < tStdin" |
||||
``` |
||||
|
||||
Note that test folder names serve as the test description and are displayed |
||||
when a test fails or when using "go test -v". Also note that folders can be |
||||
nested as many levels deep as desired to categorize and group tests. |
||||
|
||||
## Functions |
||||
|
||||
```text |
||||
func RunTests(t *testing.T, cliPath string) |
||||
RunTests iterates through all of the tests at or below the current path |
||||
using the specified CLI. The CLI path can be absolute or relative. |
||||
``` |
||||
|
||||
## Example usage |
||||
|
||||
A full example demontrating the use of {{.rname}} is contained within the "tests" |
||||
folder of this package. A simple single test example is described below. |
||||
|
||||
### Example folder structure |
||||
|
||||
```text |
||||
tests |
||||
|-- example.sh |
||||
|-- example_test.go |
||||
|-- 5-a |
||||
|-- tCmd |
||||
|-- tExit |
||||
|-- tStderr |
||||
|-- tStdout |
||||
``` |
||||
|
||||
### Example files |
||||
|
||||
#### tests/example.sh (description) |
||||
|
||||
```text |
||||
Purpose: A trivial demonstration script. The argument string is processed |
||||
and printed to stdout. Everything up to and including the first |
||||
letter "a" is deleted, and everything after and including the |
||||
last letter "a" is also deleted. |
||||
Usage: example.sh string |
||||
``` |
||||
|
||||
#### tests/example_test.go (contents) |
||||
|
||||
```text |
||||
package example_test |
||||
|
||||
import ( |
||||
"git.lenzplace.org/lenzj/{{.rname}}" |
||||
"testing" |
||||
) |
||||
|
||||
func TestExample(t *testing.T) { |
||||
{{.rname}}.RunTests(t, "./example.sh") |
||||
} |
||||
``` |
||||
|
||||
#### tests/5-a/tCmd (contents) |
||||
|
||||
```text |
||||
{{`{{.cli}}`}} "The rain in Spain falls mainly in the plain" |
||||
``` |
||||
|
||||
#### tests/5-a/tExit (contents) |
||||
|
||||
```text |
||||
0 |
||||
``` |
||||
|
||||
#### tests/5-a/tStderr (contents empty) |
||||
|
||||
```text |
||||
File is empty |
||||
``` |
||||
|
||||
#### tests/5-a/tStdout (contents) |
||||
|
||||
```text |
||||
in in Spain falls mainly in the pl |
||||
``` |
||||
|
||||
### Example passing test output |
||||
|
||||
```text |
||||
$ go test -v |
||||
=== RUN TestExample |
||||
=== RUN TestExample/5-a |
||||
--- PASS: TestExample (0.01s) |
||||
--- PASS: TestExample/5-a (0.01s) |
||||
PASS |
||||
ok git.lenzplace.org/lenzj/{{.rname}}/tests 0.01s |
||||
``` |
||||
|
||||
### Example failing test output |
||||
|
||||
Changed the last "a" in tests/5-a/tCmd to "A". |
||||
|
||||
```text |
||||
$ go test -v |
||||
=== RUN TestExample |
||||
=== RUN TestExample/5-a |
||||
--- FAIL: TestExample (0.01s) |
||||
--- FAIL: TestExample/5-a (0.01s) |
||||
{{.rname}}.go:137: stdout: |
||||
expected "in in Spain falls mainly in the pl\n" |
||||
received "in in Spain falls m\n" |
||||
FAIL |
||||
exit status 1 |
||||
FAIL git.lenzplace.org/lenzj/{{.rname}}/tests 0.01s |
||||
``` |
||||
|
||||
## Running the full package tests |
||||
|
||||
```text |
||||
$ make check |
||||
--or-- |
||||
$ cd tests && go test -v |
||||
``` |
||||
|
||||
{{template "rd-contributing" .}} |
||||
|
||||
{{template "rd-versioning" .}} |
||||
|
||||
{{template "rd-license" .}} |
||||
|
||||
<!-- vim:set ts=4 sw=4 et tw=80: --> |
@ -1,196 +1,137 @@ |
||||
// Copyright (c) 2020 Jason T. Lenz. All rights reserved.
|
||||
// Copyright (c) 2021 Jason T. Lenz. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
// Package testcli is a helper utility for testing golang command line
|
||||
// applications (CLI). When using testcli, each CLI test exists within its own
|
||||
// file system folder. All test folders for a specific CLI are typically
|
||||
// contained within a main folder which the testcli package "walks" entering all
|
||||
// subdirs and executing each CLI test within a folder. Test results are
|
||||
// tracked and displayed using the golang standard testing infrastructure.
|
||||
// Package testcli is a helper utility for testing command line applications
|
||||
// (CLI) using the standard go testing framework.
|
||||
//
|
||||
// Each test folder must contain the following text files:
|
||||
// * tCmd : The CLI command to be executed including parameters and
|
||||
// options. Within this file any '{{.cli}}' string is replaced
|
||||
// by the CLI being tested. If a test folder is multiple levels
|
||||
// deep within the file tree the relative CLI path is adjusted
|
||||
// accordingly before executing tCmd.
|
||||
// * tStdout : The expected stdout
|
||||
// * tStderr : The expected sterr
|
||||
// * tExit : The exit code
|
||||
// Tests are created by populating a T structure and then passing it to the Run
|
||||
// function. The T structure must contain at a minimum the command to be
|
||||
// executed. The remaining items within the T structure are optional and have
|
||||
// reasonable defaults in typical use cases.
|
||||
//
|
||||
// The following are optional files:
|
||||
// * t*.check : Any file matching t*.check is compared directly against the
|
||||
// same filename t*.result. This can be used to check the
|
||||
// output of any files generated by the CLI.
|
||||
// For example within a "foo_test.go" file:
|
||||
//
|
||||
// Alternately, any of the output filenames above may end with "Regex" to do a
|
||||
// regular expression match rather than a direct string comparison. This can
|
||||
// be useful to check output for which a portion changes often. For example
|
||||
// log file output that contains the date or time at the beginning of an output
|
||||
// line.
|
||||
// * tStdoutRegex : Contains a regular expression string to match against
|
||||
// the expected stdout
|
||||
// * t*.checkRegex
|
||||
// * etc. ...
|
||||
// func TestFooVersion(t *testing.T) {
|
||||
// clt := testcli.T{
|
||||
// Cmd: exec.Cmd{
|
||||
// Path: "./foo",
|
||||
// Args: []string{"foo", "-v"},
|
||||
// },
|
||||
// TStdout: `^foo 1.7.2\n$`,
|
||||
// }
|
||||
// testcli.Run(t, clt)
|
||||
// }
|
||||
//
|
||||
// The following file names are not used directly by testcli but are named as
|
||||
// follows by convention if needed:
|
||||
// * tStdin : Any text intended to be fed as stdin to the CLI. Typically
|
||||
// this is accomplished within tCmd such as "{{.cli}} < tStdin"
|
||||
// func TestFooBadOption(t *testing.T) {
|
||||
// clt := testcli.T{
|
||||
// Cmd: exec.Cmd{
|
||||
// Path: "./foo",
|
||||
// Args: []string{"foo", "-z"},
|
||||
// },
|
||||
// TStderr: `(?m)^flag provided but not defined: -z$.*$`,
|
||||
// TExit: 2,
|
||||
// }
|
||||
// testcli.Run(t, clt)
|
||||
// }
|
||||
//
|
||||
// Note that test folder names serve as the test description and are displayed
|
||||
// when a test fails or when using "go test -v". Also note that folders can be
|
||||
// nested as many levels deep as desired to categorize and group tests.
|
||||
//
|
||||
// A full example demontrating the use of testcli is contained within the
|
||||
// "tests" folder of this package.
|
||||
// A full example demontrating additional ways to use testcli is contained
|
||||
// within the "cmd" folder of this package.
|
||||
package testcli |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"bytes" |
||||
"errors" |
||||
"io" |
||||
"os" |
||||
"os/exec" |
||||
"path/filepath" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
const ( |
||||
cliReplace = "{{.cli}}" |
||||
checkExt = ".check" |
||||
resultExt = ".result" |
||||
) |
||||
// T defines an individual command line test case.
|
||||
type T struct { |
||||
Cmd exec.Cmd // The command line details to execute
|
||||
TStdout string // Regex to check against stdout
|
||||
TStderr string // Regex to check against stderr
|
||||
TFiles []FT // An optional array of output files to check
|
||||
TExit int // The expected exit code
|
||||
} |
||||
|
||||
// RunTests iterates through all of the tests at or below the current path
|
||||
// using the specified CLI. The CLI path can be absolute or relative.
|
||||
func RunTests(t *testing.T, cliPath string) { |
||||
// tCmd is the filename which contains a test
|
||||
const tCmd = "tCmd" |
||||
// Check that command to be tested exists.
|
||||
cliAbs, err := filepath.Abs(cliPath) |
||||
if err != nil { |
||||
panic("Unable to convert relative path to absolute") |
||||
} |
||||
_, err = exec.LookPath(cliAbs) |
||||
if err != nil { |
||||
panic(cliPath + " does not exist or is not executable") |
||||
} |
||||
// Get system shell used to execute scripts
|
||||
shell, ok := os.LookupEnv("SHELL") |
||||
if !ok { |
||||
panic("Unable to identify shell to execute test command") |
||||
} |
||||
// Walk dir tree looking for tCmd files
|
||||
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error { |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if filepath.Base(path) == tCmd { |
||||
testPathAbs, err := filepath.Abs(filepath.Dir(path)) |
||||
if err != nil { |
||||
panic("Unable to get absolute test path") |
||||
} |
||||
cliRel, err := filepath.Rel(testPathAbs, cliAbs) |
||||
if err != nil { |
||||
panic("Unable to get relative command line path") |
||||
} |
||||
err = runOneTest(t, shell, cliRel, path) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
}) |
||||
if err != nil { |
||||
panic("Unable to walk directory tree") |
||||
} |
||||
// FT defines a file path and regex to check against the file contents.
|
||||
type FT struct { |
||||
Path string // Path to file
|
||||
TContent string // Regex to check against file contents
|
||||
} |
||||
|
||||
func runOneTest(t *testing.T, shell, cliRel, testFile string) error { |
||||
testPath := filepath.Dir(testFile) |
||||
t.Run(testPath, func(t *testing.T) { |
||||
// Read in tCmd
|
||||
tCmd := strings.TrimSpace(fileToString(testFile)) |
||||
// Transform cliReplace to true app path
|
||||
tCmd = strings.Replace(tCmd, cliReplace, cliRel, -1) |
||||
// Execute tCmd in shell and save stdout, stderr, exit
|
||||
cmd := exec.Command(shell, "-c", tCmd) |
||||
var stdout, stderr strings.Builder |
||||
cmd.Stdout = &stdout |
||||
cmd.Stderr = &stderr |
||||
cmd.Dir = testPath |
||||
err := cmd.Run() |
||||
// Run executes a single command line test.
|
||||
func Run(t *testing.T, clt T) { |
||||
var stdout, stderr bytes.Buffer |
||||
var exitCode int |
||||
clt.Cmd.Stdout = &stdout |
||||
clt.Cmd.Stderr = &stderr |
||||
err := clt.Cmd.Run() |
||||
|
||||
var exitCode string |
||||
if err != nil { |
||||
if exitError, ok := err.(*exec.ExitError); ok { |
||||
exitCode = strconv.Itoa(exitError.ExitCode()) |
||||
// Get exit code as an int
|
||||
if err != nil { |
||||
if exitError, ok := err.(*exec.ExitError); ok { |
||||
exitCode = exitError.ExitCode() |
||||
} else { |
||||
if clt.Cmd.Path == "" { |
||||
t.Error("Empty test command!") |
||||
} else { |
||||
panic("Error getting exit code") |
||||
t.Errorf("Malformed test command %q\n %q", clt.Cmd.Path, err) |
||||
} |
||||
} else { |
||||
exitCode = "0" |
||||
return |
||||
} |
||||
checkExpected(t, testPath, "tExit", exitCode) |
||||
|
||||
checkExpected(t, testPath, "tStdout", stdout.String()) |
||||
} else { |
||||
exitCode = 0 |
||||
} |
||||
|
||||
checkExpected(t, testPath, "tStderr", stderr.String()) |
||||
if err = regexpByte(clt.TStdout, stdout.Bytes()); err != nil { |
||||
t.Error("Stdout " + err.Error()) |
||||
} |
||||
if err = regexpByte(clt.TStderr, stderr.Bytes()); err != nil { |
||||
t.Error("Stderr " + err.Error()) |
||||
} |
||||
if clt.TExit != exitCode { |
||||
t.Errorf("Expected exit code %d rather than %d", clt.TExit, exitCode) |
||||
} |
||||
|
||||
// Compare any t*.[check|checkRegex] files against t*.result files
|
||||
r := regexp.MustCompile(`^t.*\.(check|checkRegex)$`) |
||||
err = filepath.Walk(testPath, func(path string, info os.FileInfo, err error) error { |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// Is it a check file?
|
||||
if r.MatchString(filepath.Base(path)) { |
||||
fDir := filepath.Dir(path) |
||||
fBase := filepath.Base(path) |
||||
fNoext := strings.TrimSuffix(fBase, filepath.Ext(fBase)) |
||||
tResult := fileToString(fDir + "/" + fNoext + resultExt) |
||||
checkExpected(t, fDir, fNoext+checkExt, tResult) |
||||
} |
||||
return nil |
||||
}) |
||||
if err != nil { |
||||
panic("Unable to walk directory tree for t*.[check|checkRegex] files") |
||||
for _, ft := range clt.TFiles { |
||||
if err = regexpFile(ft.TContent, ft.Path); err != nil { |
||||
t.Error(err.Error()) |
||||
} |
||||
}) |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
// Check the actual against the expected output.
|
||||
func checkExpected(t *testing.T, testPath, fName, actual string) { |
||||
fNameR := fName + "Regex" |
||||
fPath := testPath + "/" + fName |
||||
fPathR := testPath + "/" + fNameR |
||||
if _, err := os.Stat(fPath); err == nil { |
||||
// Direct string comparison
|
||||
fCheck := fileToString(fPath) |
||||
if fCheck != actual { |
||||
t.Errorf(fName+":\n expected %q\n received %q", fCheck, actual) |
||||
} |
||||
} else if _, err := os.Stat(fPathR); err == nil { |
||||
// Regex comparison
|
||||
fCheck := fileToString(fPathR) |
||||
r := regexp.MustCompile(fCheck) |
||||
if !r.MatchString(actual) { |
||||
t.Errorf(fNameR+":\n regex %q\n did not match %q", fCheck, actual) |
||||
} |
||||
} else { |
||||
panic("Test file " + testPath + "[" + fName + "|" + fNameR + "] not found.") |
||||
// helper function to test a regexp against a []byte
|
||||
func regexpByte(rs string, b []byte) error { |
||||
r, err := regexp.Compile(rs) |
||||
if err != nil { |
||||
return errors.New("regexp compile error") |
||||
} |
||||
if !r.Match(b) { |
||||
return errors.New("regexp did not match") |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Read the specified file and return it as a string.
|
||||
func fileToString(fPath string) string { |
||||
fByte, err := ioutil.ReadFile(fPath) |
||||
// helper function to test a regexp against the contents of a file
|
||||
func regexpFile(rstring string, filePath string) error { |
||||
file, err := os.Open(filePath) |
||||
if err != nil { |
||||
panic("Unable to read " + fPath) |
||||
return errors.New("Could not open file " + filePath) |
||||
} |
||||
return string(fByte) |
||||
content, rerr := io.ReadAll(file) |
||||
if err = file.Close(); err != nil { |
||||
return errors.New("could not close file " + filePath) |
||||
} |
||||
if rerr != nil { |
||||
return errors.New("could not read file " + filePath) |
||||
} |
||||
if err = regexpByte(rstring, content); err != nil { |
||||
return errors.New(err.Error() + " for file \"" + filePath + "\"") |
||||
} |
||||
return nil |
||||
} |
||||
|
@ -1,24 +0,0 @@ |
||||
#!/bin/sh |
||||
|
||||
displayUsage () { |
||||
echo "Purpose: A trivial demonstration script. The argument string is processed" >&2 |
||||
echo " and printed to stdout. Everything up to and including the first" >&2 |
||||
echo " letter \"a\" is deleted, and everything after and including the" >&2 |
||||
echo " last letter \"a\" is also deleted." >&2 |
||||
echo "Usage: $NAME string" >&2 |
||||
} |
||||
|
||||
NAME=${0##*/} |
||||
|
||||
# Check that one and only one argument is passed |
||||
if [ $# -ne 1 ] |
||||
then |
||||
echo "Error: Incorrect number of arguments" >&2 |
||||
displayUsage |
||||
exit 1 |
||||
fi |
||||
|
||||
OUTPUT=${1#*a} |
||||
OUTPUT=${OUTPUT%a*} |
||||
|
||||
echo $OUTPUT |
@ -1,14 +0,0 @@ |
||||
// Copyright (c) 2019 Jason T. Lenz. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
package example_test |
||||
|
||||
import ( |
||||
"git.lenzplace.org/lenzj/testcli" |
||||
"testing" |
||||
) |
||||
|
||||
func TestExample(t *testing.T) { |
||||
testcli.RunTests(t, "./example.sh") |
||||
} |
@ -1 +0,0 @@ |
||||
{{.cli}} "the first argument" "a second argument" "a third argument" |
@ -1 +0,0 @@ |
||||
1 |
@ -1,6 +0,0 @@ |
||||
Error: Incorrect number of arguments |
||||
Purpose: A trivial demonstration script. The argument string is processed |
||||
and printed to stdout. Everything up to and including the first |
||||
letter "a" is deleted, and everything after and including the |
||||
last letter "a" is also deleted. |
||||
Usage: example.sh string |
@ -1 +0,0 @@ |
||||
{{.cli}} |
@ -1 +0,0 @@ |
||||
1 |
@ -1,6 +0,0 @@ |
||||
Error: Incorrect number of arguments |
||||
Purpose: A trivial demonstration script. The argument string is processed |
||||
and printed to stdout. Everything up to and including the first |
||||
letter "a" is deleted, and everything after and including the |
||||
last letter "a" is also deleted. |
||||
Usage: example.sh string |
@ -1 +0,0 @@ |
||||
{{.cli}} "there Are no lowercAse A's in this sentence" |
@ -1 +0,0 @@ |
||||
0 |
@ -1 +0,0 @@ |
||||
there Are no lowercAse A's in this sentence |
@ -1 +0,0 @@ |
||||
{{.cli}} "the quick brown fox jumped over the lazy dog" |
@ -1 +0,0 @@ |
||||
0 |
@ -1 +0,0 @@ |
||||
zy dog |
@ -1 +0,0 @@ |
||||
{{.cli}} "The rain in Spain falls mainly in the plain" |
@ -1 +0,0 @@ |
||||
0 |
@ -1 +0,0 @@ |
||||
in in Spain falls mainly in the pl |
@ -1 +0,0 @@ |
||||
{{.cli}} "there are three lowercase \"a\"'s in this sentence" > tExample.result |
@ -1 +0,0 @@ |
||||
re three lowercase " |
@ -1 +0,0 @@ |
||||
0 |
@ -1 +0,0 @@ |
||||
{{.cli}} "there are three lowercase \"a\"'s in this sentence" > tExample.result |
@ -1 +0,0 @@ |
||||
^re.*case " |
@ -1 +0,0 @@ |
||||
0 |
@ -1 +0,0 @@ |
||||
{{.cli}} "The rain in Spain falls mainly in the plain" |
@ -1 +0,0 @@ |
||||
0 |
@ -1 +0,0 @@ |
||||
in in.*the pl |
Loading…
Reference in new issue