// Copyright 2017 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main_test import ( "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "testing" "golang.org/x/tools/internal/testenv" ) type test struct { offset, from, to string // specify the arguments fileSpecified bool // true if the offset or from args specify a specific file pkgs map[string][]string wantErr bool wantOut string // a substring expected to be in the output packages map[string][]string // a map of the package name to the files contained within, which will be numbered by i.go where i is the index } // Test that renaming that would modify cgo files will produce an error and not modify the file. func TestGeneratedFiles(t *testing.T) { testenv.NeedsTool(t, "go") testenv.NeedsTool(t, "cgo") tmp, bin, cleanup := buildGorename(t) defer cleanup() srcDir := filepath.Join(tmp, "src") err := os.Mkdir(srcDir, os.ModePerm) if err != nil { t.Fatal(err) } var env = []string{fmt.Sprintf("GOPATH=%s", tmp)} for _, envVar := range os.Environ() { if !strings.HasPrefix(envVar, "GOPATH=") { env = append(env, envVar) } } // gorename currently requires GOPATH mode. env = append(env, "GO111MODULE=off") // Testing renaming in packages that include cgo files: for iter, renameTest := range []test{ { // Test: variable not used in any cgo file -> no error from: `"mytest"::f`, to: "g", packages: map[string][]string{ "mytest": []string{`package mytest; func f() {}`, `package mytest // #include import "C" func z() {C.puts(nil)}`}, }, wantErr: false, wantOut: "Renamed 1 occurrence in 1 file in 1 package.", }, { // Test: to name used in cgo file -> rename error from: `"mytest"::f`, to: "g", packages: map[string][]string{ "mytest": []string{`package mytest; func f() {}`, `package mytest // #include import "C" func g() {C.puts(nil)}`}, }, wantErr: true, wantOut: "conflicts with func in same block", }, { // Test: from name in package in cgo file -> error from: `"mytest"::f`, to: "g", packages: map[string][]string{ "mytest": []string{`package mytest // #include import "C" func f() { C.puts(nil); } `}, }, wantErr: true, wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:", }, { // Test: from name in cgo file -> error from: filepath.Join("mytest", "0.go") + `::f`, to: "g", fileSpecified: true, packages: map[string][]string{ "mytest": []string{`package mytest // #include import "C" func f() { C.puts(nil); } `}, }, wantErr: true, wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:", }, { // Test: offset in cgo file -> identifier in cgo error offset: filepath.Join("main", "0.go") + `:#78`, to: "bar", fileSpecified: true, wantErr: true, packages: map[string][]string{ "main": {`package main // #include import "C" import "fmt" func main() { foo := 1 C.close(2) fmt.Println(foo) } `}, }, wantOut: "cannot rename identifiers in generated file containing DO NOT EDIT marker:", }, { // Test: from identifier appears in cgo file in another package -> error from: `"test"::Foo`, to: "Bar", packages: map[string][]string{ "test": []string{ `package test func Foo(x int) (int){ return x * 2 } `, }, "main": []string{ `package main import "test" import "fmt" // #include import "C" func fun() { x := test.Foo(3) C.close(3) fmt.Println(x) } `, }, }, wantErr: true, wantOut: "gorename: refusing to modify generated file containing DO NOT EDIT marker:", }, { // Test: from identifier doesn't appear in cgo file that includes modified package -> rename successful from: `"test".Foo::x`, to: "y", packages: map[string][]string{ "test": []string{ `package test func Foo(x int) (int){ return x * 2 } `, }, "main": []string{ `package main import "test" import "fmt" // #include import "C" func fun() { x := test.Foo(3) C.close(3) fmt.Println(x) } `, }, }, wantErr: false, wantOut: "Renamed 2 occurrences in 1 file in 1 package.", }, { // Test: from name appears in cgo file in same package -> error from: `"mytest"::f`, to: "g", packages: map[string][]string{ "mytest": []string{`package mytest; func f() {}`, `package mytest // #include import "C" func z() {C.puts(nil); f()}`, `package mytest // #include import "C" func foo() {C.close(3); f()}`, }, }, wantErr: true, wantOut: "gorename: refusing to modify generated files containing DO NOT EDIT marker:", }, { // Test: from name in file, identifier not used in cgo file -> rename successful from: filepath.Join("mytest", "0.go") + `::f`, to: "g", fileSpecified: true, packages: map[string][]string{ "mytest": []string{`package mytest; func f() {}`, `package mytest // #include import "C" func z() {C.puts(nil)}`}, }, wantErr: false, wantOut: "Renamed 1 occurrence in 1 file in 1 package.", }, { // Test: from identifier imported to another package but does not modify cgo file -> rename successful from: `"test".Foo`, to: "Bar", packages: map[string][]string{ "test": []string{ `package test func Foo(x int) (int){ return x * 2 } `, }, "main": []string{ `package main // #include import "C" func fun() { C.close(3) } `, `package main import "test" import "fmt" func g() { fmt.Println(test.Foo(3)) } `, }, }, wantErr: false, wantOut: "Renamed 2 occurrences in 2 files in 2 packages.", }, } { // Write the test files testCleanup := setUpPackages(t, srcDir, renameTest.packages) // Set up arguments var args []string var arg, val string if renameTest.offset != "" { arg, val = "-offset", renameTest.offset } else { arg, val = "-from", renameTest.from } prefix := fmt.Sprintf("%d: %s %q -to %q", iter, arg, val, renameTest.to) if renameTest.fileSpecified { // add the src dir to the value of the argument val = filepath.Join(srcDir, val) } args = append(args, arg, val, "-to", renameTest.to) // Run command cmd := exec.Command(bin, args...) cmd.Args[0] = "gorename" cmd.Env = env // Check the output out, err := cmd.CombinedOutput() // errors should result in no changes to files if err != nil { if !renameTest.wantErr { t.Errorf("%s: received unexpected error %s", prefix, err) } // Compare output if ok := strings.Contains(string(out), renameTest.wantOut); !ok { t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut) } // Check that no files were modified if modified := modifiedFiles(t, srcDir, renameTest.packages); len(modified) != 0 { t.Errorf("%s: files unexpectedly modified: %s", prefix, modified) } } else { if !renameTest.wantErr { if ok := strings.Contains(string(out), renameTest.wantOut); !ok { t.Errorf("%s: unexpected command output: %s (want: %s)", prefix, out, renameTest.wantOut) } } else { t.Errorf("%s: command succeeded unexpectedly, output: %s", prefix, out) } } testCleanup() } } // buildGorename builds the gorename executable. // It returns its path, and a cleanup function. func buildGorename(t *testing.T) (tmp, bin string, cleanup func()) { if runtime.GOOS == "android" { t.Skipf("the dependencies are not available on android") } tmp, err := ioutil.TempDir("", "gorename-regtest-") if err != nil { t.Fatal(err) } defer func() { if cleanup == nil { // probably, go build failed. os.RemoveAll(tmp) } }() bin = filepath.Join(tmp, "gorename") if runtime.GOOS == "windows" { bin += ".exe" } cmd := exec.Command("go", "build", "-o", bin) if err := cmd.Run(); err != nil { t.Fatalf("Building gorename: %v", err) } return tmp, bin, func() { os.RemoveAll(tmp) } } // setUpPackages sets up the files in a temporary directory provided by arguments. func setUpPackages(t *testing.T, dir string, packages map[string][]string) (cleanup func()) { var pkgDirs []string for pkgName, files := range packages { // Create a directory for the package. pkgDir := filepath.Join(dir, pkgName) pkgDirs = append(pkgDirs, pkgDir) if err := os.Mkdir(pkgDir, os.ModePerm); err != nil { t.Fatal(err) } // Write the packages files for i, val := range files { file := filepath.Join(pkgDir, strconv.Itoa(i)+".go") if err := ioutil.WriteFile(file, []byte(val), os.ModePerm); err != nil { t.Fatal(err) } } } return func() { for _, dir := range pkgDirs { os.RemoveAll(dir) } } } // modifiedFiles returns a list of files that were renamed (without the prefix dir). func modifiedFiles(t *testing.T, dir string, packages map[string][]string) (results []string) { for pkgName, files := range packages { pkgDir := filepath.Join(dir, pkgName) for i, val := range files { file := filepath.Join(pkgDir, strconv.Itoa(i)+".go") // read file contents and compare to val if contents, err := ioutil.ReadFile(file); err != nil { t.Fatalf("File missing: %s", err) } else if string(contents) != val { results = append(results, strings.TrimPrefix(dir, file)) } } } return results }