Major refactoring to use go structs rather than files / folders

master v1.0.0
Jason T. Lenz 1 year ago
parent eaf7c1a330
commit e3fe0f47a1
  1. 9
      .build.yml
  2. 2
      LICENSE
  3. 58
      Makefile
  4. 207
      README.md
  5. 8
      cmd/Makefile
  6. 79
      cmd/foo_src/main.go
  7. 91
      cmd/foo_test.go
  8. 2
      go.mod
  9. 45
      template/Makefile.got
  10. 19
      template/README.md
  11. 183
      template/README.md.got
  12. 263
      testcli.go
  13. 24
      tests/example.sh
  14. 14
      tests/example_test.go
  15. 1
      tests/invalid-more-than-one-argument/tCmd
  16. 1
      tests/invalid-more-than-one-argument/tExit
  17. 6
      tests/invalid-more-than-one-argument/tStderr
  18. 0
      tests/invalid-more-than-one-argument/tStdout
  19. 1
      tests/invalid-no-arguments/tCmd
  20. 1
      tests/invalid-no-arguments/tExit
  21. 6
      tests/invalid-no-arguments/tStderr
  22. 0
      tests/invalid-no-arguments/tStdout
  23. 1
      tests/valid-a-tests/0-a/tCmd
  24. 1
      tests/valid-a-tests/0-a/tExit
  25. 0
      tests/valid-a-tests/0-a/tStderr
  26. 1
      tests/valid-a-tests/0-a/tStdout
  27. 1
      tests/valid-a-tests/1-a/tCmd
  28. 1
      tests/valid-a-tests/1-a/tExit
  29. 0
      tests/valid-a-tests/1-a/tStderr
  30. 1
      tests/valid-a-tests/1-a/tStdout
  31. 1
      tests/valid-a-tests/5-a/tCmd
  32. 1
      tests/valid-a-tests/5-a/tExit
  33. 0
      tests/valid-a-tests/5-a/tStderr
  34. 1
      tests/valid-a-tests/5-a/tStdout
  35. 1
      tests/valid-check-file/tCmd
  36. 1
      tests/valid-check-file/tExample.check
  37. 1
      tests/valid-check-file/tExit
  38. 0
      tests/valid-check-file/tStderr
  39. 0
      tests/valid-check-file/tStdout
  40. 1
      tests/valid-checkRegex-file/tCmd
  41. 1
      tests/valid-checkRegex-file/tExample.checkRegex
  42. 1
      tests/valid-checkRegex-file/tExit
  43. 0
      tests/valid-checkRegex-file/tStderr
  44. 0
      tests/valid-checkRegex-file/tStdout
  45. 1
      tests/valid-regex/tCmd
  46. 1
      tests/valid-regex/tExit
  47. 0
      tests/valid-regex/tStderr
  48. 1
      tests/valid-regex/tStdoutRegex

@ -0,0 +1,9 @@
image: alpine/edge
packages:
- go
sources:
- https://git.sr.ht/~lenzj/testcli
tasks:
- test: |
cd testcli/cmd
make check

@ -1,4 +1,4 @@
Copyright (c) 2020 Jason T. Lenz. All rights reserved.
Copyright (c) 2021 Jason T. Lenz. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are

@ -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:

@ -1,171 +1,73 @@
# testcli
Package **testcli** is a golang utility package for testing command line
interfaces ([CLI](https://en.wikipedia.org/wiki/Command-line_interface)). 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.
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 testcli 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.
Package **testcli** is a golang helper utility for testing command line
applications ([CLI](https://en.wikipedia.org/wiki/Command-line_interface)) using
the standard go testing framework.
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.
For example within a "foo_test.go" file:
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 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)
}
A full example demonstrating additional ways to use testcli is contained within
the "cmd" folder of this package.
## 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.
func Run(t *testing.T, clt T)
Run executes a single command line test.
```
## Example usage
A full example demontrating the use of testcli is contained within the "tests"
folder of this package. A simple single test example is described below.
### Example folder structure
## Types
```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/testcli"
"testing"
)
func TestExample(t *testing.T) {
testcli.RunTests(t, "./example.sh")
type FT struct {
Path string // Path to file
TContent string // Regex to check against file contents
}
```
#### 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/testcli/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)
testcli.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/testcli/tests 0.01s
FT defines a file path and regex to check against the file contents.
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
}
T defines an individual command line test case.
```
## Running the full package tests
```text
$ cd cmd
$ make check
--or--
$ cd tests && go test -v
```
## Contributing
@ -175,17 +77,14 @@ me is by following the instructions in the link below. Thank you!
<https://blog.lenzplace.org/contact>
## Versioning
I follow the [SemVer](http://semver.org/) strategy for versioning. The latest
version is listed in the [releases](/lenzj/testcli/releases) section.
## License
This project is licensed under a BSD two clause license - see the
[LICENSE](LICENSE) file for details.
<!-- vim:set ts=4 sw=4 et 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,3 +1,3 @@
module git.lenzplace.org/lenzj/testcli
go 1.12
go 1.16

@ -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,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,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 @@
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 @@
{{.cli}} "The rain in Spain falls mainly in the plain"

@ -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 @@
{{.cli}} "there are three lowercase \"a\"'s in this sentence" > tExample.result

@ -1 +0,0 @@
{{.cli}} "The rain in Spain falls mainly in the plain"
Loading…
Cancel
Save