* Add test command to tinygo
Этот коммит содержится в:
Carolyn Van Slyck 2019-06-18 03:23:59 -07:00 коммит произвёл Ron Evans
родитель a3d1f1a514
коммит 208e1719ad
9 изменённых файлов: 288 добавлений и 13 удалений

Просмотреть файл

@ -38,6 +38,7 @@ fmt:
fmt-check: fmt-check:
@unformatted=$$(gofmt -l $(FMT_PATHS)); [ -z "$$unformatted" ] && exit 0; echo "Unformatted:"; for fn in $$unformatted; do echo " $$fn"; done; exit 1 @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: gen-device-avr gen-device-nrf gen-device-sam gen-device-stm32
gen-device-avr: gen-device-avr:
@ -85,6 +86,9 @@ build/tinygo:
test: test:
CGO_CPPFLAGS="$(CGO_CPPFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" go test -v -tags byollvm . 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 .PHONY: smoketest smoketest-no-avr
smoketest: smoketest-no-avr smoketest: smoketest-no-avr
tinygo build -size short -o test.elf -target=arduino examples/blinky1 tinygo build -size short -o test.elf -target=arduino examples/blinky1

Просмотреть файл

@ -48,6 +48,12 @@ type Config struct {
TINYGOROOT string // GOROOT for TinyGo TINYGOROOT string // GOROOT for TinyGo
GOPATH string // GOPATH, like `go env GOPATH` GOPATH string // GOPATH, like `go env GOPATH`
BuildTags []string // build tags for TinyGo (empty means {Config.GOOS/Config.GOARCH}) 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 { type Compiler struct {
@ -214,7 +220,7 @@ func (c *Compiler) Compile(mainPath string) []error {
path = path[len(tinygoPath+"/src/"):] path = path[len(tinygoPath+"/src/"):]
} }
switch path { switch path {
case "machine", "os", "reflect", "runtime", "runtime/volatile", "sync": case "machine", "os", "reflect", "runtime", "runtime/volatile", "sync", "testing":
return path return path
default: default:
if strings.HasPrefix(path, "device/") || strings.HasPrefix(path, "examples/") { if strings.HasPrefix(path, "device/") || strings.HasPrefix(path, "examples/") {
@ -241,6 +247,7 @@ func (c *Compiler) Compile(mainPath string) []error {
CFlags: c.CFlags, CFlags: c.CFlags,
ClangHeaders: c.ClangHeaders, ClangHeaders: c.ClangHeaders,
} }
if strings.HasSuffix(mainPath, ".go") { if strings.HasSuffix(mainPath, ".go") {
_, err = lprogram.ImportFile(mainPath) _, err = lprogram.ImportFile(mainPath)
if err != nil { if err != nil {
@ -252,12 +259,13 @@ func (c *Compiler) Compile(mainPath string) []error {
return []error{err} return []error{err}
} }
} }
_, err = lprogram.Import("runtime", "") _, err = lprogram.Import("runtime", "")
if err != nil { if err != nil {
return []error{err} return []error{err}
} }
err = lprogram.Parse() err = lprogram.Parse(c.TestConfig.CompileTestBinary)
if err != nil { if err != nil {
return []error{err} return []error{err}
} }

Просмотреть файл

@ -1,6 +1,7 @@
package loader package loader
import ( import (
"bytes"
"errors" "errors"
"go/ast" "go/ast"
"go/build" "go/build"
@ -10,12 +11,15 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"text/template"
"github.com/tinygo-org/tinygo/cgo" "github.com/tinygo-org/tinygo/cgo"
) )
// Program holds all packages and some metadata about the program as a whole. // Program holds all packages and some metadata about the program as a whole.
type Program struct { type Program struct {
mainPkg string
Build *build.Context Build *build.Context
OverlayBuild *build.Context OverlayBuild *build.Context
OverlayPath func(path string) string 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 p.sorted = nil // invalidate the sorted order of packages
pkg := p.newPackage(buildPkg) pkg := p.newPackage(buildPkg)
p.Packages[buildPkg.ImportPath] = pkg p.Packages[buildPkg.ImportPath] = pkg
if p.mainPkg == "" {
p.mainPkg = buildPkg.ImportPath
}
return pkg, nil return pkg, nil
} }
@ -93,6 +102,11 @@ func (p *Program) ImportFile(path string) (*Package, error) {
p.sorted = nil // invalidate the sorted order of packages p.sorted = nil // invalidate the sorted order of packages
pkg := p.newPackage(buildPkg) pkg := p.newPackage(buildPkg)
p.Packages[buildPkg.ImportPath] = pkg p.Packages[buildPkg.ImportPath] = pkg
if p.mainPkg == "" {
p.mainPkg = buildPkg.ImportPath
}
return pkg, nil 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. // The returned error may be an Errors error, which contains a list of errors.
// //
// Idempotent. // Idempotent.
func (p *Program) Parse() error { func (p *Program) Parse(compileTestBinary bool) error {
includeTests := compileTestBinary
// Load all imports // Load all imports
for _, pkg := range p.Sorted() { for _, pkg := range p.Sorted() {
err := pkg.importRecursively() err := pkg.importRecursively(includeTests)
if err != nil { if err != nil {
if err, ok := err.(*ImportCycleError); ok { if err, ok := err.(*ImportCycleError); ok {
if pkg.ImportPath != err.Packages[0] { if pkg.ImportPath != err.Packages[0] {
@ -187,7 +203,14 @@ func (p *Program) Parse() error {
// Parse all packages. // Parse all packages.
for _, pkg := range p.Sorted() { 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 { if err != nil {
return err return err
} }
@ -204,6 +227,83 @@ func (p *Program) Parse() error {
return nil 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. // parseFile is a wrapper around parser.ParseFile.
func (p *Program) parseFile(path string, mode parser.Mode) (*ast.File, error) { func (p *Program) parseFile(path string, mode parser.Mode) (*ast.File, error) {
if p.fset == nil { 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. // Parse parses and typechecks this package.
// //
// Idempotent. // Idempotent.
func (p *Package) Parse() error { func (p *Package) Parse(includeTests bool) error {
if len(p.Files) != 0 { if len(p.Files) != 0 {
return nil return nil
} }
@ -242,7 +342,7 @@ func (p *Package) Parse() error {
return nil return nil
} }
files, err := p.parseFiles() files, err := p.parseFiles(includeTests)
if err != nil { if err != nil {
return err return err
} }
@ -281,11 +381,21 @@ func (p *Package) Check() error {
} }
// parseFiles parses the loaded list of files and returns this list. // 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. // TODO: do this concurrently.
var files []*ast.File var files []*ast.File
var fileErrs []error 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) f, err := p.parseFile(filepath.Join(p.Package.Dir, file), parser.ParseComments)
if err != nil { if err != nil {
fileErrs = append(fileErrs, err) fileErrs = append(fileErrs, err)
@ -320,6 +430,7 @@ func (p *Package) parseFiles() ([]*ast.File, error) {
if len(fileErrs) != 0 { if len(fileErrs) != 0 {
return nil, Errors{p, fileErrs} return nil, Errors{p, fileErrs}
} }
return files, nil return files, nil
} }
@ -340,9 +451,15 @@ func (p *Package) Import(to string) (*types.Package, error) {
// importRecursively() on the imported packages as well. // importRecursively() on the imported packages as well.
// //
// Idempotent. // Idempotent.
func (p *Package) importRecursively() error { func (p *Package) importRecursively(includeTests bool) error {
p.Importing = true 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" { if to == "C" {
// Do CGo processing in a later stage. // Do CGo processing in a later stage.
continue continue
@ -360,7 +477,7 @@ func (p *Package) importRecursively() error {
if importedPkg.Importing { if importedPkg.Importing {
return &ImportCycleError{[]string{p.ImportPath, importedPkg.ImportPath}, p.ImportPos[to]} return &ImportCycleError{[]string{p.ImportPath, importedPkg.ImportPath}, p.ImportPos[to]}
} }
err = importedPkg.importRecursively() err = importedPkg.importRecursively(false)
if err != nil { if err != nil {
if err, ok := err.(*ImportCycleError); ok { if err, ok := err.(*ImportCycleError); ok {
err.Packages = append([]string{p.ImportPath}, err.Packages...) err.Packages = append([]string{p.ImportPath}, err.Packages...)

33
main.go
Просмотреть файл

@ -54,6 +54,7 @@ type BuildConfig struct {
cFlags []string cFlags []string
ldFlags []string ldFlags []string
wasmAbi string wasmAbi string
testConfig compiler.TestConfig
} }
// Helper function for Compiler object. // Helper function for Compiler object.
@ -108,6 +109,7 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act
GOROOT: goroot, GOROOT: goroot,
GOPATH: getGopath(), GOPATH: getGopath(),
BuildTags: tags, BuildTags: tags,
TestConfig: config.testConfig,
} }
c, err := compiler.NewCompiler(pkgName, compilerConfig) c, err := compiler.NewCompiler(pkgName, compilerConfig)
if err != nil { 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 { func Flash(pkgName, target, port string, config *BuildConfig) error {
spec, err := LoadTarget(target) spec, err := LoadTarget(target)
if err != nil { if err != nil {
@ -656,6 +682,13 @@ func main() {
} }
err := Run(flag.Arg(0), *target, config) err := Run(flag.Arg(0), *target, config)
handleCompilerError(err) handleCompilerError(err)
case "test":
pkgRoot := "."
if flag.NArg() == 1 {
pkgRoot = flag.Arg(0)
}
err := Test(pkgRoot, *target, config)
handleCompilerError(err)
case "clean": case "clean":
// remove cache directory // remove cache directory
dir := cacheDir() dir := cacheDir()

Просмотреть файл

@ -92,7 +92,7 @@ func (v Value) Pointer() uintptr {
} }
func (v Value) IsValid() bool { func (v Value) IsValid() bool {
panic("unimplemented: (reflect.Value).IsValid()") return v.typecode != 0
} }
func (v Value) CanInterface() bool { func (v Value) CanInterface() bool {

6
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.
*/

77
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
}

14
tests/tinygotest/main.go Обычный файл
Просмотреть файл

@ -0,0 +1,14 @@
package main
import (
"fmt"
)
func main() {
Thing()
fmt.Println("normal main")
}
func Thing() {
fmt.Println("THING")
}

16
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) {
}