aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAyke van Laethem <[email protected]>2024-07-09 17:16:32 +0200
committerAyke <[email protected]>2024-07-13 13:26:26 +0200
commit8a357af3d81b86ada93a91fbd3b57d9c5fb34fc6 (patch)
tree08578e4fce0419eed350b33de51b0d5eef32d34e
parent7ac1ca0ae2a727329550eecf71814f08dccf0700 (diff)
downloadtinygo-8a357af3d81b86ada93a91fbd3b57d9c5fb34fc6.tar.gz
tinygo-8a357af3d81b86ada93a91fbd3b57d9c5fb34fc6.zip
all: add testing for compiler error messages
This is needed for some improvements I'm going to make next. This commit also refactors error handling slightly to make it more easily testable, this should hopefully not result in any actual changes in behavior.
-rw-r--r--errors_test.go84
-rw-r--r--main.go40
-rw-r--r--main_test.go2
-rw-r--r--testdata/errors/cgo.go15
-rw-r--r--testdata/errors/syntax.go7
-rw-r--r--testdata/errors/types.go12
6 files changed, 141 insertions, 19 deletions
diff --git a/errors_test.go b/errors_test.go
new file mode 100644
index 000000000..69c29148d
--- /dev/null
+++ b/errors_test.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/tinygo-org/tinygo/compileopts"
+)
+
+// Test the error messages of the TinyGo compiler.
+func TestErrors(t *testing.T) {
+ for _, name := range []string{
+ "cgo",
+ "syntax",
+ "types",
+ } {
+ t.Run(name, func(t *testing.T) {
+ testErrorMessages(t, "./testdata/errors/"+name+".go")
+ })
+ }
+}
+
+func testErrorMessages(t *testing.T, filename string) {
+ // Parse expected error messages.
+ expected := readErrorMessages(t, filename)
+
+ // Try to build a binary (this should fail with an error).
+ tmpdir := t.TempDir()
+ err := Build(filename, tmpdir+"/out", &compileopts.Options{
+ Target: "wasip1",
+ Semaphore: sema,
+ InterpTimeout: 180 * time.Second,
+ Debug: true,
+ VerifyIR: true,
+ Opt: "z",
+ })
+ if err == nil {
+ t.Fatal("expected to get a compiler error")
+ }
+
+ // Get the full ./testdata/errors directory.
+ wd, absErr := filepath.Abs("testdata/errors")
+ if absErr != nil {
+ t.Fatal(absErr)
+ }
+
+ // Write error message out as plain text.
+ var buf bytes.Buffer
+ printCompilerError(err, func(v ...interface{}) {
+ fmt.Fprintln(&buf, v...)
+ }, wd)
+ actual := strings.TrimRight(buf.String(), "\n")
+
+ // Check whether the error is as expected.
+ if actual != expected {
+ t.Errorf("expected error:\n%s\ngot:\n%s", indentText(expected, "> "), indentText(actual, "> "))
+ }
+}
+
+// Indent the given text with a given indentation string.
+func indentText(text, indent string) string {
+ return indent + strings.ReplaceAll(text, "\n", "\n"+indent)
+}
+
+// Read "// ERROR:" prefixed messages from the given file.
+func readErrorMessages(t *testing.T, file string) string {
+ data, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal("could not read input file:", err)
+ }
+
+ var errors []string
+ for _, line := range strings.Split(string(data), "\n") {
+ if strings.HasPrefix(line, "// ERROR: ") {
+ errors = append(errors, strings.TrimRight(line[len("// ERROR: "):], "\r\n"))
+ }
+ }
+ return strings.Join(errors, "\n")
+}
diff --git a/main.go b/main.go
index 4a23fa8cf..e18f12f1f 100644
--- a/main.go
+++ b/main.go
@@ -1293,10 +1293,9 @@ func usage(command string) {
// try to make the path relative to the current working directory. If any error
// occurs, this error is ignored and the absolute path is returned instead.
-func tryToMakePathRelative(dir string) string {
- wd, err := os.Getwd()
- if err != nil {
- return dir
+func tryToMakePathRelative(dir, wd string) string {
+ if wd == "" {
+ return dir // working directory not found
}
relpath, err := filepath.Rel(wd, dir)
if err != nil {
@@ -1307,28 +1306,25 @@ func tryToMakePathRelative(dir string) string {
// printCompilerError prints compiler errors using the provided logger function
// (similar to fmt.Println).
-//
-// There is one exception: interp errors may print to stderr unconditionally due
-// to limitations in the LLVM bindings.
-func printCompilerError(logln func(...interface{}), err error) {
+func printCompilerError(err error, logln func(...interface{}), wd string) {
switch err := err.(type) {
case types.Error:
- printCompilerError(logln, scanner.Error{
+ printCompilerError(scanner.Error{
Pos: err.Fset.Position(err.Pos),
Msg: err.Msg,
- })
+ }, logln, wd)
case scanner.Error:
if !strings.HasPrefix(err.Pos.Filename, filepath.Join(goenv.Get("GOROOT"), "src")) && !strings.HasPrefix(err.Pos.Filename, filepath.Join(goenv.Get("TINYGOROOT"), "src")) {
// This file is not from the standard library (either the GOROOT or
// the TINYGOROOT). Make the path relative, for easier reading.
// Ignore any errors in the process (falling back to the absolute
// path).
- err.Pos.Filename = tryToMakePathRelative(err.Pos.Filename)
+ err.Pos.Filename = tryToMakePathRelative(err.Pos.Filename, wd)
}
logln(err)
case scanner.ErrorList:
for _, scannerErr := range err {
- printCompilerError(logln, *scannerErr)
+ printCompilerError(*scannerErr, logln, wd)
}
case *interp.Error:
logln("#", err.ImportPath)
@@ -1346,7 +1342,7 @@ func printCompilerError(logln func(...interface{}), err error) {
case loader.Errors:
logln("#", err.Pkg.ImportPath)
for _, err := range err.Errs {
- printCompilerError(logln, err)
+ printCompilerError(err, logln, wd)
}
case loader.Error:
logln(err.Err.Error())
@@ -1356,7 +1352,7 @@ func printCompilerError(logln func(...interface{}), err error) {
}
case *builder.MultiError:
for _, err := range err.Errs {
- printCompilerError(logln, err)
+ printCompilerError(err, logln, wd)
}
default:
logln("error:", err)
@@ -1365,9 +1361,13 @@ func printCompilerError(logln func(...interface{}), err error) {
func handleCompilerError(err error) {
if err != nil {
- printCompilerError(func(args ...interface{}) {
+ wd, getwdErr := os.Getwd()
+ if getwdErr != nil {
+ wd = ""
+ }
+ printCompilerError(err, func(args ...interface{}) {
fmt.Fprintln(os.Stderr, args...)
- }, err)
+ }, wd)
os.Exit(1)
}
}
@@ -1769,9 +1769,13 @@ func main() {
stderr := (*testStderr)(buf)
passed, err := Test(pkgName, stdout, stderr, options, outpath)
if err != nil {
- printCompilerError(func(args ...interface{}) {
+ wd, getwdErr := os.Getwd()
+ if getwdErr != nil {
+ wd = ""
+ }
+ printCompilerError(err, func(args ...interface{}) {
fmt.Fprintln(stderr, args...)
- }, err)
+ }, wd)
}
if !passed {
select {
diff --git a/main_test.go b/main_test.go
index 238614330..d54af8634 100644
--- a/main_test.go
+++ b/main_test.go
@@ -380,7 +380,7 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c
return cmd.Run()
})
if err != nil {
- printCompilerError(t.Log, err)
+ printCompilerError(err, t.Log, "")
t.Fail()
return
}
diff --git a/testdata/errors/cgo.go b/testdata/errors/cgo.go
new file mode 100644
index 000000000..e0853cc85
--- /dev/null
+++ b/testdata/errors/cgo.go
@@ -0,0 +1,15 @@
+package main
+
+// #error hello
+// )))
+import "C"
+
+func main() {
+}
+
+// TODO: this error should be relative to the current directory (so cgo.go
+// instead of testdata/errors/cgo.go).
+
+// ERROR: # command-line-arguments
+// ERROR: testdata/errors/cgo.go:3:5: error: hello
+// ERROR: testdata/errors/cgo.go:4:4: error: expected identifier or '('
diff --git a/testdata/errors/syntax.go b/testdata/errors/syntax.go
new file mode 100644
index 000000000..48d9e732f
--- /dev/null
+++ b/testdata/errors/syntax.go
@@ -0,0 +1,7 @@
+package main
+
+func main(var) { // syntax error
+}
+
+// ERROR: # command-line-arguments
+// ERROR: syntax.go:3:11: expected ')', found 'var'
diff --git a/testdata/errors/types.go b/testdata/errors/types.go
new file mode 100644
index 000000000..491e2fe67
--- /dev/null
+++ b/testdata/errors/types.go
@@ -0,0 +1,12 @@
+package main
+
+func main() {
+ var a int
+ a = "foobar"
+ nonexisting()
+}
+
+// ERROR: # command-line-arguments
+// ERROR: types.go:5:6: cannot use "foobar" (untyped string constant) as int value in assignment
+// ERROR: types.go:6:2: undefined: nonexisting
+// ERROR: types.go:4:6: a declared and not used