package bind import ( "bytes" "flag" "go/ast" "go/build" "go/importer" "go/parser" "go/token" "go/types" "io" "io/ioutil" "log" "os" "os/exec" "path" "path/filepath" "runtime" "strings" "testing" "golang.org/x/mobile/internal/importers" "golang.org/x/mobile/internal/importers/java" "golang.org/x/mobile/internal/importers/objc" ) func init() { log.SetFlags(log.Lshortfile) } var updateFlag = flag.Bool("update", false, "Update the golden files.") var tests = []string{ "", // The universe package with the error type. "testdata/basictypes.go", "testdata/structs.go", "testdata/interfaces.go", "testdata/issue10788.go", "testdata/issue12328.go", "testdata/issue12403.go", "testdata/issue29559.go", "testdata/keywords.go", "testdata/try.go", "testdata/vars.go", "testdata/ignore.go", "testdata/doc.go", "testdata/underscores.go", } var javaTests = []string{ "testdata/java.go", "testdata/classes.go", } var objcTests = []string{ "testdata/objc.go", "testdata/objcw.go", } var fset = token.NewFileSet() func fileRefs(t *testing.T, filename string, pkgPrefix string) *importers.References { f, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) if err != nil { t.Fatalf("%s: %v", filename, err) } refs, err := importers.AnalyzeFile(f, pkgPrefix) if err != nil { t.Fatalf("%s: %v", filename, err) } fakePath := path.Dir(filename) for i := range refs.Embedders { refs.Embedders[i].PkgPath = fakePath } return refs } func typeCheck(t *testing.T, filename string, gopath string) (*types.Package, *ast.File) { f, err := parser.ParseFile(fset, filename, nil, parser.AllErrors|parser.ParseComments) if err != nil { t.Fatalf("%s: %v", filename, err) } pkgName := filepath.Base(filename) pkgName = strings.TrimSuffix(pkgName, ".go") // typecheck and collect typechecker errors var conf types.Config conf.Error = func(err error) { t.Error(err) } if gopath != "" { conf.Importer = importer.Default() oldDefault := build.Default defer func() { build.Default = oldDefault }() build.Default.GOPATH = gopath } pkg, err := conf.Check(pkgName, fset, []*ast.File{f}, nil) if err != nil { t.Fatal(err) } return pkg, f } // diff runs the command "diff a b" and returns its output func diff(a, b string) string { var buf bytes.Buffer var cmd *exec.Cmd switch runtime.GOOS { case "plan9": cmd = exec.Command("/bin/diff", "-c", a, b) default: cmd = exec.Command("/usr/bin/diff", "-u", a, b) } cmd.Stdout = &buf cmd.Stderr = &buf cmd.Run() return buf.String() } func writeTempFile(t *testing.T, name string, contents []byte) string { f, err := ioutil.TempFile("", name) if err != nil { t.Fatal(err) } if _, err := f.Write(contents); err != nil { t.Fatal(err) } if err := f.Close(); err != nil { t.Fatal(err) } return f.Name() } func TestGenObjc(t *testing.T) { for _, filename := range tests { var pkg *types.Package var file *ast.File if filename != "" { pkg, file = typeCheck(t, filename, "") } var buf bytes.Buffer g := &ObjcGen{ Generator: &Generator{ Printer: &Printer{Buf: &buf, IndentEach: []byte("\t")}, Fset: fset, Files: []*ast.File{file}, Pkg: pkg, }, } if pkg != nil { g.AllPkg = []*types.Package{pkg} } g.Init(nil) testcases := []struct { suffix string gen func() error }{ { ".objc.h.golden", g.GenH, }, { ".objc.m.golden", g.GenM, }, { ".objc.go.h.golden", g.GenGoH, }, } for _, tc := range testcases { buf.Reset() if err := tc.gen(); err != nil { t.Errorf("%s: %v", filename, err) continue } out := writeTempFile(t, "generated"+tc.suffix, buf.Bytes()) defer os.Remove(out) var golden string if filename != "" { golden = filename[:len(filename)-len(".go")] } else { golden = "testdata/universe" } golden += tc.suffix if diffstr := diff(golden, out); diffstr != "" { t.Errorf("%s: does not match Objective-C golden:\n%s", filename, diffstr) if *updateFlag { t.Logf("Updating %s...", golden) err := exec.Command("/bin/cp", out, golden).Run() if err != nil { t.Errorf("Update failed: %s", err) } } } } } } func genObjcPackages(t *testing.T, dir string, cg *ObjcWrapper) { pkgBase := filepath.Join(dir, "src", "ObjC") if err := os.MkdirAll(pkgBase, 0700); err != nil { t.Fatal(err) } for i, jpkg := range cg.Packages() { pkgDir := filepath.Join(pkgBase, jpkg) if err := os.MkdirAll(pkgDir, 0700); err != nil { t.Fatal(err) } pkgFile := filepath.Join(pkgDir, "package.go") cg.Buf.Reset() cg.GenPackage(i) if err := ioutil.WriteFile(pkgFile, cg.Buf.Bytes(), 0600); err != nil { t.Fatal(err) } } cg.Buf.Reset() cg.GenInterfaces() clsFile := filepath.Join(pkgBase, "interfaces.go") if err := ioutil.WriteFile(clsFile, cg.Buf.Bytes(), 0600); err != nil { t.Fatal(err) } cmd := exec.Command( "go", "install", "-pkgdir="+filepath.Join(dir, "pkg", build.Default.GOOS+"_"+build.Default.GOARCH), "ObjC/...", ) cmd.Env = append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("failed to go install the generated ObjC wrappers: %v: %s", err, string(out)) } } func genJavaPackages(t *testing.T, dir string, cg *ClassGen) { buf := cg.Buf cg.Buf = new(bytes.Buffer) pkgBase := filepath.Join(dir, "src", "Java") if err := os.MkdirAll(pkgBase, 0700); err != nil { t.Fatal(err) } for i, jpkg := range cg.Packages() { pkgDir := filepath.Join(pkgBase, jpkg) if err := os.MkdirAll(pkgDir, 0700); err != nil { t.Fatal(err) } pkgFile := filepath.Join(pkgDir, "package.go") cg.Buf.Reset() cg.GenPackage(i) if err := ioutil.WriteFile(pkgFile, cg.Buf.Bytes(), 0600); err != nil { t.Fatal(err) } io.Copy(buf, cg.Buf) } cg.Buf.Reset() cg.GenInterfaces() clsFile := filepath.Join(pkgBase, "interfaces.go") if err := ioutil.WriteFile(clsFile, cg.Buf.Bytes(), 0600); err != nil { t.Fatal(err) } io.Copy(buf, cg.Buf) cg.Buf = buf cmd := exec.Command( "go", "install", "-pkgdir="+filepath.Join(dir, "pkg", build.Default.GOOS+"_"+build.Default.GOARCH), "Java/...", ) cmd.Env = append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("failed to go install the generated Java wrappers: %v: %s", err, string(out)) } } func TestGenJava(t *testing.T) { allTests := tests if java.IsAvailable() { allTests = append(append([]string{}, allTests...), javaTests...) } for _, filename := range allTests { var pkg *types.Package var file *ast.File var buf bytes.Buffer var cg *ClassGen var classes []*java.Class if filename != "" { refs := fileRefs(t, filename, "Java/") imp := &java.Importer{} var err error classes, err = imp.Import(refs) if err != nil { t.Fatal(err) } tmpGopath := "" if len(classes) > 0 { tmpGopath, err = ioutil.TempDir(os.TempDir(), "gomobile-bind-test-") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpGopath) cg = &ClassGen{ Printer: &Printer{ IndentEach: []byte("\t"), Buf: new(bytes.Buffer), }, } cg.Init(classes, refs.Embedders) genJavaPackages(t, tmpGopath, cg) cg.Buf = &buf } pkg, file = typeCheck(t, filename, tmpGopath) } g := &JavaGen{ Generator: &Generator{ Printer: &Printer{Buf: &buf, IndentEach: []byte(" ")}, Fset: fset, Files: []*ast.File{file}, Pkg: pkg, }, } if pkg != nil { g.AllPkg = []*types.Package{pkg} } g.Init(classes) testCases := []struct { suffix string gen func() error }{ { ".java.golden", func() error { for i := range g.ClassNames() { if err := g.GenClass(i); err != nil { return err } } return g.GenJava() }, }, { ".java.c.golden", func() error { if cg != nil { cg.GenC() } return g.GenC() }, }, { ".java.h.golden", func() error { if cg != nil { cg.GenH() } return g.GenH() }, }, } for _, tc := range testCases { buf.Reset() if err := tc.gen(); err != nil { t.Errorf("%s: %v", filename, err) continue } out := writeTempFile(t, "generated"+tc.suffix, buf.Bytes()) defer os.Remove(out) var golden string if filename != "" { golden = filename[:len(filename)-len(".go")] } else { golden = "testdata/universe" } golden += tc.suffix if diffstr := diff(golden, out); diffstr != "" { t.Errorf("%s: does not match Java golden:\n%s", filename, diffstr) if *updateFlag { t.Logf("Updating %s...", golden) if err := exec.Command("/bin/cp", out, golden).Run(); err != nil { t.Errorf("Update failed: %s", err) } } } } } } func TestGenGo(t *testing.T) { for _, filename := range tests { var buf bytes.Buffer var pkg *types.Package if filename != "" { pkg, _ = typeCheck(t, filename, "") } testGenGo(t, filename, &buf, pkg) } } func TestGenGoJavaWrappers(t *testing.T) { if !java.IsAvailable() { t.Skipf("java is not available") } for _, filename := range javaTests { var buf bytes.Buffer refs := fileRefs(t, filename, "Java/") imp := &java.Importer{} classes, err := imp.Import(refs) if err != nil { t.Fatal(err) } tmpGopath, err := ioutil.TempDir(os.TempDir(), "gomobile-bind-test-") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpGopath) cg := &ClassGen{ Printer: &Printer{ IndentEach: []byte("\t"), Buf: &buf, }, } cg.Init(classes, refs.Embedders) genJavaPackages(t, tmpGopath, cg) pkg, _ := typeCheck(t, filename, tmpGopath) cg.GenGo() testGenGo(t, filename, &buf, pkg) } } func TestGenGoObjcWrappers(t *testing.T) { if runtime.GOOS != "darwin" { t.Skipf("can only generate objc wrappers on darwin") } for _, filename := range objcTests { var buf bytes.Buffer refs := fileRefs(t, filename, "ObjC/") types, err := objc.Import(refs) if err != nil { t.Fatal(err) } tmpGopath, err := ioutil.TempDir(os.TempDir(), "gomobile-bind-test-") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpGopath) cg := &ObjcWrapper{ Printer: &Printer{ IndentEach: []byte("\t"), Buf: &buf, }, } var genNames []string for _, emb := range refs.Embedders { genNames = append(genNames, emb.Name) } cg.Init(types, genNames) genObjcPackages(t, tmpGopath, cg) pkg, _ := typeCheck(t, filename, tmpGopath) cg.GenGo() testGenGo(t, filename, &buf, pkg) } } func testGenGo(t *testing.T, filename string, buf *bytes.Buffer, pkg *types.Package) { conf := &GeneratorConfig{ Writer: buf, Fset: fset, Pkg: pkg, } if pkg != nil { conf.AllPkg = []*types.Package{pkg} } if err := GenGo(conf); err != nil { t.Errorf("%s: %v", filename, err) return } out := writeTempFile(t, "go", buf.Bytes()) defer os.Remove(out) golden := filename if golden == "" { golden = "testdata/universe" } golden += ".golden" if diffstr := diff(golden, out); diffstr != "" { t.Errorf("%s: does not match Go golden:\n%s", filename, diffstr) if *updateFlag { t.Logf("Updating %s...", golden) if err := exec.Command("/bin/cp", out, golden).Run(); err != nil { t.Errorf("Update failed: %s", err) } } } } func TestCustomPrefix(t *testing.T) { const datafile = "testdata/customprefix.go" pkg, file := typeCheck(t, datafile, "") type testCase struct { golden string gen func(w io.Writer) error } var buf bytes.Buffer jg := &JavaGen{ JavaPkg: "com.example", Generator: &Generator{ Printer: &Printer{Buf: &buf, IndentEach: []byte(" ")}, Fset: fset, AllPkg: []*types.Package{pkg}, Files: []*ast.File{file}, Pkg: pkg, }, } jg.Init(nil) testCases := []testCase{ { "testdata/customprefix.java.golden", func(w io.Writer) error { buf.Reset() for i := range jg.ClassNames() { if err := jg.GenClass(i); err != nil { return err } } if err := jg.GenJava(); err != nil { return err } _, err := io.Copy(w, &buf) return err }, }, { "testdata/customprefix.java.h.golden", func(w io.Writer) error { buf.Reset() if err := jg.GenH(); err != nil { return err } _, err := io.Copy(w, &buf) return err }, }, { "testdata/customprefix.java.c.golden", func(w io.Writer) error { buf.Reset() if err := jg.GenC(); err != nil { return err } _, err := io.Copy(w, &buf) return err }, }, } for _, pref := range []string{"EX", ""} { og := &ObjcGen{ Prefix: pref, Generator: &Generator{ Printer: &Printer{Buf: &buf, IndentEach: []byte(" ")}, Fset: fset, AllPkg: []*types.Package{pkg}, Pkg: pkg, }, } og.Init(nil) testCases = append(testCases, []testCase{ { "testdata/customprefix" + pref + ".objc.go.h.golden", func(w io.Writer) error { buf.Reset() if err := og.GenGoH(); err != nil { return err } _, err := io.Copy(w, &buf) return err }, }, { "testdata/customprefix" + pref + ".objc.h.golden", func(w io.Writer) error { buf.Reset() if err := og.GenH(); err != nil { return err } _, err := io.Copy(w, &buf) return err }, }, { "testdata/customprefix" + pref + ".objc.m.golden", func(w io.Writer) error { buf.Reset() if err := og.GenM(); err != nil { return err } _, err := io.Copy(w, &buf) return err }, }, }...) } for _, tc := range testCases { var buf bytes.Buffer if err := tc.gen(&buf); err != nil { t.Errorf("generating %s: %v", tc.golden, err) continue } out := writeTempFile(t, "generated", buf.Bytes()) defer os.Remove(out) if diffstr := diff(tc.golden, out); diffstr != "" { t.Errorf("%s: generated file does not match:\b%s", tc.golden, diffstr) if *updateFlag { t.Logf("Updating %s...", tc.golden) err := exec.Command("/bin/cp", out, tc.golden).Run() if err != nil { t.Errorf("Update failed: %s", err) } } } } } func TestLowerFirst(t *testing.T) { testCases := []struct { in, want string }{ {"", ""}, {"Hello", "hello"}, {"HelloGopher", "helloGopher"}, {"hello", "hello"}, {"ID", "id"}, {"IDOrName", "idOrName"}, {"ΓειαΣας", "γειαΣας"}, } for _, tc := range testCases { if got := lowerFirst(tc.in); got != tc.want { t.Errorf("lowerFirst(%q) = %q; want %q", tc.in, got, tc.want) } } } // Test that typeName work for anonymous qualified fields. func TestSelectorExprTypeName(t *testing.T) { e, err := parser.ParseExprFrom(fset, "", "struct { bytes.Buffer }", 0) if err != nil { t.Fatal(err) } ft := e.(*ast.StructType).Fields.List[0].Type if got, want := typeName(ft), "Buffer"; got != want { t.Errorf("got: %q; want %q", got, want) } }