Skip to content

Add test command to tinygo #243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 18, 2019
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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/") {
Expand All @@ -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 {
Expand All @@ -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}
}
Expand Down
137 changes: 127 additions & 10 deletions loader/loader.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package loader

import (
"bytes"
"errors"
"go/ast"
"go/build"
Expand All @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies that the first import sets the mainPkg, which seems a bit unclean to me as it may be unexpected that the order of imports matters here (for example, things would be different if you imported the runtime package first). I think it would be more explicit if mainPkg is exported and set in compiler/compiler.go.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a change that could be made in a follow-up PR? It's not actually based on the import statement order. The compiler calls ldprogram.Import or ldprogram.ImportFile, depending on whether it is testing a package or a single go file, that is how the mainPkg is being set.

I'm feeling pressured to get this merged quickly (understandable, sorry it's taken so long) and as you can tell I don't have time to work on this anymore. I tried to export and set the package from the compiler, but it doesn't have all the information to set it. So I'd have to replicate logic from the loader and this would slow down getting the PR merged even more.

}

return pkg, nil
}

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

}

return pkg, nil
}

Expand Down Expand Up @@ -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] {
Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -320,6 +430,7 @@ func (p *Package) parseFiles() ([]*ast.File, error) {
if len(fileErrs) != 0 {
return nil, Errors{p, fileErrs}
}

return files, nil
}

Expand All @@ -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
Expand All @@ -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...)
Expand Down
33 changes: 33 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type BuildConfig struct {
cFlags []string
ldFlags []string
wasmAbi string
testConfig compiler.TestConfig
}

// Helper function for Compiler object.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/reflect/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions src/testing/doc.go
Original file line number Diff line number Diff line change
@@ -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.
*/
Loading