From 208e1719ad9a8e7ff94e8aac900909c37747a04f Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Tue, 18 Jun 2019 03:23:59 -0700 Subject: [PATCH] Add test command to tinygo (#243) * Add test command to tinygo --- Makefile | 4 + compiler/compiler.go | 12 ++- loader/loader.go | 137 +++++++++++++++++++++++++++++++--- main.go | 33 ++++++++ src/reflect/value.go | 2 +- src/testing/doc.go | 6 ++ src/testing/testing.go | 77 +++++++++++++++++++ tests/tinygotest/main.go | 14 ++++ tests/tinygotest/main_test.go | 16 ++++ 9 files changed, 288 insertions(+), 13 deletions(-) create mode 100644 src/testing/doc.go create mode 100644 src/testing/testing.go create mode 100644 tests/tinygotest/main.go create mode 100644 tests/tinygotest/main_test.go diff --git a/Makefile b/Makefile index c65fa7d2..04342dd1 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ fmt: fmt-check: @unformatted=$$(gofmt -l $(FMT_PATHS)); [ -z "$$unformatted" ] && exit 0; echo "Unformatted:"; for fn in $$unformatted; do echo " $$fn"; done; exit 1 + gen-device: gen-device-avr gen-device-nrf gen-device-sam gen-device-stm32 gen-device-avr: @@ -85,6 +86,9 @@ build/tinygo: test: CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" go test -v -tags byollvm . +tinygo-test: + cd tests/tinygotest && tinygo test + .PHONY: smoketest smoketest-no-avr smoketest: smoketest-no-avr tinygo build -size short -o test.elf -target=arduino examples/blinky1 diff --git a/compiler/compiler.go b/compiler/compiler.go index 27ee530b..93c3f496 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -48,6 +48,12 @@ type Config struct { TINYGOROOT string // GOROOT for TinyGo GOPATH string // GOPATH, like `go env GOPATH` BuildTags []string // build tags for TinyGo (empty means {Config.GOOS/Config.GOARCH}) + TestConfig TestConfig +} + +type TestConfig struct { + CompileTestBinary bool + // TODO: Filter the test functions to run, include verbose flag, etc } type Compiler struct { @@ -214,7 +220,7 @@ func (c *Compiler) Compile(mainPath string) []error { path = path[len(tinygoPath+"/src/"):] } switch path { - case "machine", "os", "reflect", "runtime", "runtime/volatile", "sync": + case "machine", "os", "reflect", "runtime", "runtime/volatile", "sync", "testing": return path default: if strings.HasPrefix(path, "device/") || strings.HasPrefix(path, "examples/") { @@ -241,6 +247,7 @@ func (c *Compiler) Compile(mainPath string) []error { CFlags: c.CFlags, ClangHeaders: c.ClangHeaders, } + if strings.HasSuffix(mainPath, ".go") { _, err = lprogram.ImportFile(mainPath) if err != nil { @@ -252,12 +259,13 @@ func (c *Compiler) Compile(mainPath string) []error { return []error{err} } } + _, err = lprogram.Import("runtime", "") if err != nil { return []error{err} } - err = lprogram.Parse() + err = lprogram.Parse(c.TestConfig.CompileTestBinary) if err != nil { return []error{err} } diff --git a/loader/loader.go b/loader/loader.go index cfa81e99..1cf37156 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -1,6 +1,7 @@ package loader import ( + "bytes" "errors" "go/ast" "go/build" @@ -10,12 +11,15 @@ import ( "os" "path/filepath" "sort" + "strings" + "text/template" "github.com/tinygo-org/tinygo/cgo" ) // Program holds all packages and some metadata about the program as a whole. type Program struct { + mainPkg string Build *build.Context OverlayBuild *build.Context OverlayPath func(path string) string @@ -64,6 +68,11 @@ func (p *Program) Import(path, srcDir string) (*Package, error) { p.sorted = nil // invalidate the sorted order of packages pkg := p.newPackage(buildPkg) p.Packages[buildPkg.ImportPath] = pkg + + if p.mainPkg == "" { + p.mainPkg = buildPkg.ImportPath + } + return pkg, nil } @@ -93,6 +102,11 @@ func (p *Program) ImportFile(path string) (*Package, error) { p.sorted = nil // invalidate the sorted order of packages pkg := p.newPackage(buildPkg) p.Packages[buildPkg.ImportPath] = pkg + + if p.mainPkg == "" { + p.mainPkg = buildPkg.ImportPath + } + return pkg, nil } @@ -171,10 +185,12 @@ func (p *Program) sort() { // The returned error may be an Errors error, which contains a list of errors. // // Idempotent. -func (p *Program) Parse() error { +func (p *Program) Parse(compileTestBinary bool) error { + includeTests := compileTestBinary + // Load all imports for _, pkg := range p.Sorted() { - err := pkg.importRecursively() + err := pkg.importRecursively(includeTests) if err != nil { if err, ok := err.(*ImportCycleError); ok { if pkg.ImportPath != err.Packages[0] { @@ -187,7 +203,14 @@ func (p *Program) Parse() error { // Parse all packages. for _, pkg := range p.Sorted() { - err := pkg.Parse() + err := pkg.Parse(includeTests) + if err != nil { + return err + } + } + + if compileTestBinary { + err := p.SwapTestMain() if err != nil { return err } @@ -204,6 +227,83 @@ func (p *Program) Parse() error { return nil } +func (p *Program) SwapTestMain() error { + var tests []string + + isTestFunc := func(f *ast.FuncDecl) bool { + // TODO: improve signature check + if strings.HasPrefix(f.Name.Name, "Test") && f.Name.Name != "TestMain" { + return true + } + return false + } + mainPkg := p.Packages[p.mainPkg] + for _, f := range mainPkg.Files { + for i, d := range f.Decls { + switch v := d.(type) { + case *ast.FuncDecl: + if isTestFunc(v) { + tests = append(tests, v.Name.Name) + } + if v.Name.Name == "main" { + // Remove main + if len(f.Decls) == 1 { + f.Decls = make([]ast.Decl, 0) + } else { + f.Decls[i] = f.Decls[len(f.Decls)-1] + f.Decls = f.Decls[:len(f.Decls)-1] + } + } + } + } + } + + // TODO: Check if they defined a TestMain and call it instead of testing.TestMain + const mainBody = `package main + +import ( + "testing" +) + +func main () { + m := &testing.M{ + Tests: []testing.TestToCall{ +{{range .TestFunctions}} + {Name: "{{.}}", Func: {{.}}}, +{{end}} + }, + } + + testing.TestMain(m) +} +` + tmpl := template.Must(template.New("testmain").Parse(mainBody)) + b := bytes.Buffer{} + tmplData := struct { + TestFunctions []string + }{ + TestFunctions: tests, + } + + err := tmpl.Execute(&b, tmplData) + if err != nil { + return err + } + path := filepath.Join(p.mainPkg, "$testmain.go") + + if p.fset == nil { + p.fset = token.NewFileSet() + } + + newMain, err := parser.ParseFile(p.fset, path, b.Bytes(), parser.AllErrors) + if err != nil { + return err + } + mainPkg.Files = append(mainPkg.Files, newMain) + + return nil +} + // parseFile is a wrapper around parser.ParseFile. func (p *Program) parseFile(path string, mode parser.Mode) (*ast.File, error) { if p.fset == nil { @@ -228,7 +328,7 @@ func (p *Program) parseFile(path string, mode parser.Mode) (*ast.File, error) { // Parse parses and typechecks this package. // // Idempotent. -func (p *Package) Parse() error { +func (p *Package) Parse(includeTests bool) error { if len(p.Files) != 0 { return nil } @@ -242,7 +342,7 @@ func (p *Package) Parse() error { return nil } - files, err := p.parseFiles() + files, err := p.parseFiles(includeTests) if err != nil { return err } @@ -281,11 +381,21 @@ func (p *Package) Check() error { } // parseFiles parses the loaded list of files and returns this list. -func (p *Package) parseFiles() ([]*ast.File, error) { +func (p *Package) parseFiles(includeTests bool) ([]*ast.File, error) { // TODO: do this concurrently. var files []*ast.File var fileErrs []error - for _, file := range p.GoFiles { + + var gofiles []string + if includeTests { + gofiles = make([]string, 0, len(p.GoFiles)+len(p.TestGoFiles)) + gofiles = append(gofiles, p.GoFiles...) + gofiles = append(gofiles, p.TestGoFiles...) + } else { + gofiles = p.GoFiles + } + + for _, file := range gofiles { f, err := p.parseFile(filepath.Join(p.Package.Dir, file), parser.ParseComments) if err != nil { fileErrs = append(fileErrs, err) @@ -320,6 +430,7 @@ func (p *Package) parseFiles() ([]*ast.File, error) { if len(fileErrs) != 0 { return nil, Errors{p, fileErrs} } + return files, nil } @@ -340,9 +451,15 @@ func (p *Package) Import(to string) (*types.Package, error) { // importRecursively() on the imported packages as well. // // Idempotent. -func (p *Package) importRecursively() error { +func (p *Package) importRecursively(includeTests bool) error { p.Importing = true - for _, to := range p.Package.Imports { + + imports := p.Package.Imports + if includeTests { + imports = append(imports, p.Package.TestImports...) + } + + for _, to := range imports { if to == "C" { // Do CGo processing in a later stage. continue @@ -360,7 +477,7 @@ func (p *Package) importRecursively() error { if importedPkg.Importing { return &ImportCycleError{[]string{p.ImportPath, importedPkg.ImportPath}, p.ImportPos[to]} } - err = importedPkg.importRecursively() + err = importedPkg.importRecursively(false) if err != nil { if err, ok := err.(*ImportCycleError); ok { err.Packages = append([]string{p.ImportPath}, err.Packages...) diff --git a/main.go b/main.go index 4eaa359c..97ea47d3 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,7 @@ type BuildConfig struct { cFlags []string ldFlags []string wasmAbi string + testConfig compiler.TestConfig } // Helper function for Compiler object. @@ -108,6 +109,7 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act GOROOT: goroot, GOPATH: getGopath(), BuildTags: tags, + TestConfig: config.testConfig, } c, err := compiler.NewCompiler(pkgName, compilerConfig) if err != nil { @@ -349,6 +351,30 @@ func Build(pkgName, outpath, target string, config *BuildConfig) error { }) } +func Test(pkgName, target string, config *BuildConfig) error { + spec, err := LoadTarget(target) + if err != nil { + return err + } + + spec.BuildTags = append(spec.BuildTags, "test") + config.testConfig.CompileTestBinary = true + return Compile(pkgName, ".elf", spec, config, func(tmppath string) error { + cmd := exec.Command(tmppath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + // Propagate the exit code + if err, ok := err.(*exec.ExitError); ok { + os.Exit(err.ExitCode()) + } + return &commandError{"failed to run compiled binary", tmppath, err} + } + return nil + }) +} + func Flash(pkgName, target, port string, config *BuildConfig) error { spec, err := LoadTarget(target) if err != nil { @@ -656,6 +682,13 @@ func main() { } err := Run(flag.Arg(0), *target, config) handleCompilerError(err) + case "test": + pkgRoot := "." + if flag.NArg() == 1 { + pkgRoot = flag.Arg(0) + } + err := Test(pkgRoot, *target, config) + handleCompilerError(err) case "clean": // remove cache directory dir := cacheDir() diff --git a/src/reflect/value.go b/src/reflect/value.go index 51128b81..577a944b 100644 --- a/src/reflect/value.go +++ b/src/reflect/value.go @@ -92,7 +92,7 @@ func (v Value) Pointer() uintptr { } func (v Value) IsValid() bool { - panic("unimplemented: (reflect.Value).IsValid()") + return v.typecode != 0 } func (v Value) CanInterface() bool { diff --git a/src/testing/doc.go b/src/testing/doc.go new file mode 100644 index 00000000..b5026d80 --- /dev/null +++ b/src/testing/doc.go @@ -0,0 +1,6 @@ +package testing + +/* + This is a sad stub of the upstream testing package because it doesn't compile + with tinygo right now. +*/ diff --git a/src/testing/testing.go b/src/testing/testing.go new file mode 100644 index 00000000..1d7ea051 --- /dev/null +++ b/src/testing/testing.go @@ -0,0 +1,77 @@ +package testing + +import ( + "bytes" + "fmt" + "io" + "os" +) + +// T is a test helper. +type T struct { + name string + output io.Writer + + // flags the test as having failed when non-zero + failed int +} + +// TestToCall is a reference to a test that should be called during a test suite run. +type TestToCall struct { + // Name of the test to call. + Name string + // Function reference to the test. + Func func(*T) +} + +// M is a test suite. +type M struct { + // tests is a list of the test names to execute + Tests []TestToCall +} + +// Run the test suite. +func (m *M) Run() int { + failures := 0 + for _, test := range m.Tests { + t := &T{ + name: test.Name, + output: &bytes.Buffer{}, + } + + fmt.Printf("=== RUN %s\n", test.Name) + test.Func(t) + + if t.failed == 0 { + fmt.Printf("--- PASS: %s\n", test.Name) + } else { + fmt.Printf("--- FAIL: %s\n", test.Name) + } + fmt.Println(t.output) + + failures += t.failed + } + + if failures > 0 { + fmt.Printf("exit status %d\n", failures) + fmt.Println("FAIL") + } + return failures +} + +func TestMain(m *M) { + os.Exit(m.Run()) +} + +// Error is equivalent to Log followed by Fail +func (t *T) Error(args ...interface{}) { + // This doesn't print the same as in upstream go, but works good enough + // TODO: buffer test output like go does + fmt.Fprintf(t.output, "\t") + fmt.Fprintln(t.output, args...) + t.Fail() +} + +func (t *T) Fail() { + t.failed = 1 +} diff --git a/tests/tinygotest/main.go b/tests/tinygotest/main.go new file mode 100644 index 00000000..84ed9700 --- /dev/null +++ b/tests/tinygotest/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" +) + +func main() { + Thing() + fmt.Println("normal main") +} + +func Thing() { + fmt.Println("THING") +} diff --git a/tests/tinygotest/main_test.go b/tests/tinygotest/main_test.go new file mode 100644 index 00000000..61ce2828 --- /dev/null +++ b/tests/tinygotest/main_test.go @@ -0,0 +1,16 @@ +package main + +import ( + "testing" // This is the tinygo testing package +) + +func TestFail1(t *testing.T) { + t.Error("TestFail1 failed because of stuff and things") +} + +func TestFail2(t *testing.T) { + t.Error("TestFail2 failed for reasons") +} + +func TestPass(t *testing.T) { +}