generate - testing

This commit is contained in:
Arpad Ryszka 2018-01-08 16:49:04 +01:00
parent 9e25538b94
commit 6178b0c7a1
12 changed files with 980 additions and 610 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
*.test *.test
*.out *.out
.coverprofile .coverprofile
.coverprofile-cmd
codecov codecov
cmd/treerack/treerack cmd/treerack/treerack

View File

@ -44,9 +44,11 @@ regenerate: $(SOURCES) $(PARSERS) head install
check: imports build $(PARSERS) check: imports build $(PARSERS)
go test -test.short -run ^Test go test -test.short -run ^Test
go test ./cmd/treerack -test.short -run ^Test
checkall: imports build $(PARSERS) checkall: imports build $(PARSERS)
go test go test
go test ./cmd/treerack
.coverprofile: $(SOURCES) imports .coverprofile: $(SOURCES) imports
go test -coverprofile .coverprofile go test -coverprofile .coverprofile
@ -57,6 +59,16 @@ cover: .coverprofile
showcover: .coverprofile showcover: .coverprofile
go tool cover -html .coverprofile go tool cover -html .coverprofile
.coverprofile-cmd: $(SOURCES) imports
go test ./cmd/treerack -coverprofile .coverprofile-cmd
cover-cmd: .coverprofile-cmd
go tool cover -func .coverprofile-cmd
showcover-cmd: .coverprofile-cmd
go tool cover -html .coverprofile-cmd
# command line interface not included
publishcoverage: .coverprofile publishcoverage: .coverprofile
curl -s https://codecov.io/bash -o codecov curl -s https://codecov.io/bash -o codecov
bash codecov -Zf .coverprofile bash codecov -Zf .coverprofile
@ -76,7 +88,7 @@ checkfmt: $(SOURCES)
@if [ "$$(gofmt -s -d $(SOURCES))" != "" ]; then false; else true; fi @if [ "$$(gofmt -s -d $(SOURCES))" != "" ]; then false; else true; fi
vet: vet:
go vet go vet ./...
precommit: regenerate fmt vet build checkall precommit: regenerate fmt vet build checkall

View File

@ -15,11 +15,11 @@ const syntaxFileUsage = "path to the syntax file in treerack format"
const syntaxStringUsage = "inline syntax in treerack format" const syntaxStringUsage = "inline syntax in treerack format"
const packageNameUsage = `package name of the generated code` const packageNameUsage = `package name of the generated Go code`
const exportUsage = `when the export flag is set, the generated code will have exported symbols to allow using it as a separate package` const exportUsage = `when the export flag is set, the generated code will have exported symbols to allow using it as a separate package`
const generateUsage = `treerack generate takes a syntax description from the standard input, or a file, or inline string, and generates code implementing a parser. It prints the parser code to the standard output.` const generateUsage = `treerack generate takes a syntax description from the standard input, or a file, or inline string, and generates parser code implementing the described syntax. It prints the parser code to the standard output.`
const generateExample = `Example: const generateExample = `Example:
treerack generate < syntax.treerack > parser.go` treerack generate -syntax syntax.treerack > parser.go`

View File

@ -0,0 +1 @@
foo = "bar"

View File

@ -18,8 +18,6 @@ type generateOptions struct {
export bool export bool
} }
var isTest bool
func flagSet(o *generateOptions, output io.Writer) *flag.FlagSet { func flagSet(o *generateOptions, output io.Writer) *flag.FlagSet {
fs := flag.NewFlagSet("", flag.ContinueOnError) fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.Usage = func() {} fs.Usage = func() {}
@ -35,10 +33,12 @@ func helpGenerate() {
stdout(generateUsage) stdout(generateUsage)
stdout() stdout()
stdout("Options:") stdout("Options:")
fs := flagSet(&generateOptions{}, os.Stdout) fs := flagSet(&generateOptions{}, wout)
fs.PrintDefaults() fs.PrintDefaults()
stdout() stdout()
stdout(generateExample) stdout(generateExample)
stdout()
stdout(docRef)
} }
func flagError(fs *flag.FlagSet) { func flagError(fs *flag.FlagSet) {
@ -68,7 +68,7 @@ func generate(args []string) int {
} }
var options generateOptions var options generateOptions
fs := flagSet(&options, os.Stderr) fs := flagSet(&options, werr)
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
flagError(fs) flagError(fs)
return -1 return -1
@ -81,8 +81,7 @@ func generate(args []string) int {
var hasInput bool var hasInput bool
if options.syntaxFile == "" && options.syntax == "" { if options.syntaxFile == "" && options.syntax == "" {
fdint := int(os.Stdin.Fd()) hasInput = isTest && rin != nil || !isTest && !terminal.IsTerminal(0)
hasInput = !isTest && !terminal.IsTerminal(fdint)
} }
if !hasInput && options.syntaxFile == "" && options.syntax == "" { if !hasInput && options.syntaxFile == "" && options.syntax == "" {
@ -92,7 +91,7 @@ func generate(args []string) int {
var input io.Reader var input io.Reader
if hasInput { if hasInput {
input = os.Stdin input = rin
} else if options.syntaxFile != "" { } else if options.syntaxFile != "" {
f, err := os.Open(options.syntaxFile) f, err := os.Open(options.syntaxFile)
if err != nil { if err != nil {
@ -116,7 +115,7 @@ func generate(args []string) int {
goptions.PackageName = options.packageName goptions.PackageName = options.packageName
goptions.Export = options.export goptions.Export = options.export
if err := s.Generate(goptions, os.Stdout); err != nil { if err := s.Generate(goptions, wout); err != nil {
stderr(err) stderr(err)
return -1 return -1
} }

View File

@ -0,0 +1,132 @@
package main
import "testing"
func TestGenerate(t *testing.T) {
runMainTest(t,
mainTest{
title: "help",
args: []string{
"treerack", "generate", "-help",
},
stdout: []string{
generateUsage,
"-export",
"-package-name",
"-syntax",
"-syntax-string",
generateExample,
docRef,
},
},
mainTest{
title: "invalid flag",
args: []string{
"treerack", "generate", "-foo",
},
exit: -1,
stderr: []string{
"-export",
"-package-name",
"-syntax",
"-syntax-string",
},
},
mainTest{
title: "multiple inputs",
args: []string{
"treerack", "generate", "-syntax", "foo.treerack", "-syntax-string", `foo = "bar"`,
},
exit: -1,
stderr: []string{
"only one",
"-export",
"-package-name",
"-syntax",
"-syntax-string",
},
},
mainTest{
title: "no input",
args: []string{
"treerack", "generate",
},
exit: -1,
stderr: []string{
"missing syntax input",
"-export",
"-package-name",
"-syntax",
"-syntax-string",
},
},
mainTest{
title: "invalid input",
args: []string{
"treerack", "generate", "-syntax-string", "foo",
},
exit: -1,
stderr: []string{
"parse failed",
},
},
mainTest{
title: "file open fails",
args: []string{
"treerack", "generate", "-syntax", "noexist.treerack",
},
exit: -1,
stderr: []string{
"file",
},
},
mainTest{
title: "failing output",
args: []string{
"treerack", "generate", "-syntax-string", `foo = "bar"`,
},
failingOutput: true,
exit: -1,
},
mainTest{
title: "syntax as stdin",
args: []string{
"treerack", "generate", "-export", "-package-name", "foo",
},
stdin: `foo = "bar"`,
stdout: []string{
"package foo",
"func Parse",
},
},
mainTest{
title: "syntax as file",
args: []string{
"treerack", "generate", "-export", "-package-name", "foo", "-syntax", "foo_test.treerack",
},
stdout: []string{
"package foo",
"func Parse",
},
},
mainTest{
title: "syntax as string",
args: []string{
"treerack", "generate", "-export", "-package-name", "foo", "-syntax-string", `foo = "bar"`,
},
stdout: []string{
"package foo",
"func Parse",
},
},
)
}

29
cmd/treerack/io.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"fmt"
"io"
"os"
)
type exitFunc func(int)
var (
isTest bool
rin io.Reader = os.Stdin
wout io.Writer = os.Stdout
werr io.Writer = os.Stderr
exit exitFunc = func(code int) {
os.Exit(code)
}
)
func stdout(a ...interface{}) {
fmt.Fprintln(wout, a...)
}
func stderr(a ...interface{}) {
fmt.Fprintln(werr, a...)
}

View File

@ -1,8 +1,6 @@
package main package main
import ( import "os"
"os"
)
func mainHelp() { func mainHelp() {
stdout(summary) stdout(summary)
@ -17,21 +15,22 @@ func main() {
stderr("missing command") stderr("missing command")
stderr() stderr()
stderr(commandsHelp) stderr(commandsHelp)
stdout() stderr()
stdout(docRef) stderr(docRef)
os.Exit(-1) exit(-1)
return
} }
switch os.Args[1] { switch os.Args[1] {
case "generate": case "generate":
exit := generate(os.Args[2:]) code := generate(os.Args[2:])
os.Exit(exit) exit(code)
case "help", "-help": case "help", "-help":
mainHelp() mainHelp()
default: default:
stderr("invalid command") stderr("invalid command")
stderr() stderr()
stderr(commandsHelp) stderr(commandsHelp)
os.Exit(-1) exit(-1)
} }
} }

205
cmd/treerack/main_test.go Normal file
View File

@ -0,0 +1,205 @@
package main
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"strings"
"testing"
)
type mainTest struct {
title string
args []string
failingOutput bool
exit int
stdin string
stdout []string
stderr []string
}
type failingWriter struct{}
var errWriteFailed = errors.New("write failed")
func (w failingWriter) Write([]byte) (int, error) {
return 0, errWriteFailed
}
func init() {
isTest = true
}
func mockArgs(args ...string) (reset func()) {
original := os.Args
os.Args = args
reset = func() {
os.Args = original
}
return
}
func mockStdin(in string) (reset func()) {
original := rin
if in == "" {
rin = nil
} else {
rin = bytes.NewBufferString(in)
}
reset = func() {
rin = original
}
return
}
func mockOutput(w *io.Writer, failing bool) (out fmt.Stringer, reset func()) {
original := *w
reset = func() { *w = original }
if failing {
*w = failingWriter{}
return
}
var buf bytes.Buffer
*w = &buf
out = &buf
return
}
func mockStdout() (out fmt.Stringer, reset func()) {
return mockOutput(&wout, false)
}
func mockStderr() (out fmt.Stringer, reset func()) {
return mockOutput(&werr, false)
}
func mockFailingOutput() (reset func()) {
_, reset = mockOutput(&wout, true)
return
}
func mockExit() (code *int, reset func()) {
var exitCode int
code = &exitCode
original := exit
exit = func(c int) { exitCode = c }
reset = func() { exit = original }
return
}
func (mt mainTest) run(t *testing.T) {
test := func(t *testing.T) {
defer mockArgs(mt.args...)()
defer mockStdin(mt.stdin)()
var stdout fmt.Stringer
if mt.failingOutput {
defer mockFailingOutput()()
} else {
var reset func()
stdout, reset = mockStdout()
defer reset()
}
stderr, resetStderr := mockStderr()
defer resetStderr()
code, resetExit := mockExit()
defer resetExit()
main()
if *code != mt.exit {
t.Error("invalid exit code")
}
if stdout != nil {
for i := range mt.stdout {
if !strings.Contains(stdout.String(), mt.stdout[i]) {
t.Error("invalid output")
t.Log(stdout.String())
}
}
}
for i := range mt.stderr {
if !strings.Contains(stderr.String(), mt.stderr[i]) {
t.Error("invalid error output")
t.Log(stderr.String())
}
}
}
if mt.title == "" {
test(t)
} else {
t.Run(mt.title, test)
}
}
func runMainTest(t *testing.T, mt ...mainTest) {
for i := range mt {
mt[i].run(t)
}
}
func TestMissingCommand(t *testing.T) {
runMainTest(t,
mainTest{
args: []string{"treerack"},
exit: -1,
stderr: []string{
"missing command",
commandsHelp,
docRef,
},
},
)
}
func TestInvalidCommand(t *testing.T) {
runMainTest(t,
mainTest{
args: []string{
"treerack", "foo",
},
exit: -1,
stderr: []string{
"invalid command",
commandsHelp,
},
},
)
}
func TestHelp(t *testing.T) {
runMainTest(t,
mainTest{
title: "without dash",
args: []string{
"treerack", "help",
},
stdout: []string{
summary, commandsHelp, docRef,
},
},
mainTest{
title: "with dash",
args: []string{
"treerack", "-help",
},
stdout: []string{
summary, commandsHelp, docRef,
},
},
)
}

View File

@ -1,14 +0,0 @@
package main
import (
"fmt"
"os"
)
func stderr(a ...interface{}) {
fmt.Fprintln(os.Stderr, a...)
}
func stdout(a ...interface{}) {
fmt.Fprintln(os.Stderr, a...)
}

View File

@ -9,6 +9,10 @@ read, with error reporting
what was the bug with the large json from eskip? what was the bug with the large json from eskip?
[next] [next]
error reports
- take the last
simplify generator output
make generator output non-random (track parsers in a list in definition order)
missing tests, coverage: missing tests, coverage:
- validation - validation
- error cases - error cases
@ -36,6 +40,8 @@ test error report on invalid flag
report unused definitions report unused definitions
simplify generated output simplify generated output
parse hashed, storing only the results parse hashed, storing only the results
allchars: can have char sequence
input name: may be just dropped because completely controlled by the client
[optimization] [optimization]
try preallocate larger store chunks try preallocate larger store chunks

File diff suppressed because it is too large Load Diff