blob: 6a2e8f4d8a2f77d5d48f22d53a71bb3232f3169c [file] [log] [blame]
// Copyright 2015 The Vanadium 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
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"v.io/jiri"
"v.io/jiri/gitutil"
"v.io/jiri/jiritest"
"v.io/jiri/project"
"v.io/jiri/runutil"
"v.io/jiri/tool"
"v.io/x/devtools/tooldata"
)
func writeFileOrDie(t *testing.T, jirix *jiri.X, path, contents string) {
if err := jirix.NewSeq().WriteFile(path, []byte(contents), 0644).Done(); err != nil {
t.Fatalf("WriteFile(%v, %v) failed: %v", path, contents, err)
}
}
// setupAPITest sets up the test environment and returns a FakeJiriRoot
// representing the environment that was created, along with a cleanup closure
// that should be deferred.
func setupAPITest(t *testing.T) (*jiritest.FakeJiriRoot, func()) {
// Capture JIRI_ROOT, using a relative path. We use this to find the
// third_party repository below.
realRoot, err := filepath.Abs(filepath.Join("..", "..", "..", "..", "..", "..", ".."))
if err != nil {
t.Fatal(err)
}
// Set up a fake jiri environment, with a test project.
fake, cleanupFake := jiritest.NewFakeJiriRoot(t)
if err := fake.CreateRemoteProject("test"); err != nil {
t.Fatal(err)
}
if err := fake.AddProject(project.Project{
Name: "test",
Path: "test",
Remote: fake.Projects["test"],
}); err != nil {
t.Fatal(err)
}
// Set up a third_party project, based on the real root. We need the real
// third_party sources in order for buildGotools to work.
if err := fake.CreateRemoteProject("third_party"); err != nil {
t.Fatal(err)
}
if err := fake.AddProject(project.Project{
Name: "third_party",
Path: "third_party",
Remote: filepath.Join(realRoot, "third_party"),
}); err != nil {
t.Fatal(err)
}
if err := fake.UpdateUniverse(false); err != nil {
t.Fatal(err)
}
// Build gotools for use in the rest of the api tests.
gotoolsPath, cleanupGotools, err := buildGotools(fake.X)
if err != nil {
t.Fatalf("buildGotools failed: %v", err)
}
gotoolsBinPathFlag = gotoolsPath
return fake, func() {
if err := cleanupGotools(); err != nil {
t.Fatal(err)
}
cleanupFake()
gotoolsBinPathFlag = ""
}
}
// TestPublicAPICheckError checks that the public API check fails for
// a CL that introduces changes to the public API.
func TestPublicAPICheckError(t *testing.T) {
fake, cleanup := setupAPITest(t)
defer cleanup()
config := tooldata.NewConfig(tooldata.APICheckProjectsOpt(map[string]struct{}{"test": struct{}{}}))
if err := tooldata.SaveConfig(fake.X, config); err != nil {
t.Fatalf("%v", err)
}
branch := "my-branch"
projectPath := filepath.Join(fake.X.Root, "test")
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
// Simulate an API with an existing public function called TestFunction.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, ".api"), `# This is a comment that should be ignored
pkg main, func TestFunction()
`)
// Write a change that un-exports TestFunction.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, "file.go"), `package main
func testFunction() {
}`)
commitMessage := "Commit file.go"
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CommitFile("file.go", commitMessage); err != nil {
t.Fatalf("%v", err)
}
var buf bytes.Buffer
fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &buf})
if err := doAPICheck(fake.X, []string{"test"}, true); err != nil {
t.Fatalf("doAPICheck failed: %v", err)
} else if buf.String() == "" {
t.Fatalf("doAPICheck detected no changes, but some were expected")
}
}
// TestPublicAPICheckOk checks that the public API check succeeds for
// a CL that introduces no changes to the public API.
func TestPublicAPICheckOk(t *testing.T) {
fake, cleanup := setupAPITest(t)
defer cleanup()
config := tooldata.NewConfig(tooldata.APICheckProjectsOpt(map[string]struct{}{"test": struct{}{}}))
if err := tooldata.SaveConfig(fake.X, config); err != nil {
t.Fatalf("%v", err)
}
branch := "my-branch"
projectPath := filepath.Join(fake.X.Root, "test")
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
// Simulate an API with an existing public function called TestFunction.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, ".api"), `# This is a comment that should be ignored
pkg main, func TestFunction()
`)
// Write a change that un-exports TestFunction.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, "file.go"), `package main
func TestFunction() {
}`)
commitMessage := "Commit file.go"
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CommitFile("file.go", commitMessage); err != nil {
t.Fatalf("%v", err)
}
var buf bytes.Buffer
fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &buf})
if err := doAPICheck(fake.X, []string{"test"}, true); err != nil {
t.Fatalf("doAPICheck failed: %v", err)
} else if buf.String() != "" {
t.Fatalf("doAPICheck detected changes, but none were expected: %s", buf.String())
}
}
// TestPublicAPIMissingAPIFile ensures that the check will fail if a 'required
// check' project has a missing .api file and a non-empty public API.
func TestPublicAPIMissingAPIFile(t *testing.T) {
fake, cleanup := setupAPITest(t)
defer cleanup()
config := tooldata.NewConfig(tooldata.APICheckProjectsOpt(map[string]struct{}{"test": struct{}{}}))
if err := tooldata.SaveConfig(fake.X, config); err != nil {
t.Fatalf("%v", err)
}
branch := "my-branch"
projectPath := filepath.Join(fake.X.Root, "test")
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
// Write a go file with a public API and no corresponding .api file.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, "file.go"), `package main
func TestFunction() {
}`)
commitMessage := "Commit file.go"
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CommitFile("file.go", commitMessage); err != nil {
t.Fatalf("%v", err)
}
var buf bytes.Buffer
fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &buf})
if err := doAPICheck(fake.X, []string{"test"}, true); err != nil {
t.Fatalf("doAPICheck failed: %v", err)
} else if buf.String() == "" {
t.Fatalf("doAPICheck should have failed, but did not")
} else if !strings.Contains(buf.String(), "could not read the package's .api file") {
t.Fatalf("doAPICheck failed, but not for the expected reason: %s", buf.String())
}
}
// TestPublicAPIMissingAPIFileNoPublicAPI ensures that the check will pass if a
// 'required check' project has a missing .api but the public API is empty.
func TestPublicAPIMissingAPIFileNoPublicAPI(t *testing.T) {
fake, cleanup := setupAPITest(t)
defer cleanup()
config := tooldata.NewConfig(tooldata.APICheckProjectsOpt(map[string]struct{}{"test": struct{}{}}))
if err := tooldata.SaveConfig(fake.X, config); err != nil {
t.Fatalf("%v", err)
}
branch := "my-branch"
projectPath := filepath.Join(fake.X.Root, "test")
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
// Write a go file with a public API and no corresponding .api file.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, "file.go"), `package main
func testFunction() {
}`)
commitMessage := "Commit file.go"
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CommitFile("file.go", commitMessage); err != nil {
t.Fatalf("%v", err)
}
var buf bytes.Buffer
fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &buf})
if err := doAPICheck(fake.X, []string{"test"}, true); err != nil {
t.Fatalf("doAPICheck failed: %v", err)
} else if output := buf.String(); output != "" {
t.Fatalf("doAPICheck should have passed, but did not: %s", output)
}
}
// TestPublicAPIMissingAPIFileNotRequired ensures that the check will
// not fail if a 'required check' project has a missing .api file but
// that API file is in an 'internal' package.
func TestPublicAPIMissingAPIFileNotRequired(t *testing.T) {
fake, cleanup := setupAPITest(t)
defer cleanup()
config := tooldata.NewConfig(tooldata.APICheckProjectsOpt(map[string]struct{}{"test": struct{}{}}))
if err := tooldata.SaveConfig(fake.X, config); err != nil {
t.Fatalf("%v", err)
}
branch := "my-branch"
projectPath := filepath.Join(fake.X.Root, "test")
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
// Write a go file with a public API and no corresponding .api file.
if err := os.Mkdir(filepath.Join(projectPath, "internal"), 0744); err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
testFilePath := filepath.Join(projectPath, "internal", "file.go")
writeFileOrDie(t, fake.X, testFilePath, `package main
func TestFunction() {
}`)
commitMessage := "Commit file.go"
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CommitFile(testFilePath, commitMessage); err != nil {
t.Fatalf("%v", err)
}
var buf bytes.Buffer
fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &buf})
if err := doAPICheck(fake.X, []string{"test"}, true); err != nil {
t.Fatalf("doAPICheck failed: %v", err)
} else if buf.String() != "" {
t.Fatalf("doAPICheck should have passed, but did not: %s", buf.String())
}
}
// TestPublicAPIUpdate checks that the api update command correctly
// updates the API definition.
func TestPublicAPIUpdate(t *testing.T) {
fake, cleanup := setupAPITest(t)
defer cleanup()
if err := tooldata.SaveConfig(fake.X, tooldata.NewConfig()); err != nil {
t.Fatalf("%v", err)
}
branch := "my-branch"
projectPath := filepath.Join(fake.X.Root, "test")
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
// Simulate an API with an existing public function called TestFunction.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, ".api"), `# This is a comment that should be ignored
pkg main, func TestFunction()
`)
// Write a change that changes TestFunction to TestFunction1.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, "file.go"), `package main
func TestFunction1() {
}`)
commitMessage := "Commit file.go"
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CommitFile("file.go", commitMessage); err != nil {
t.Fatalf("%v", err)
}
var out bytes.Buffer
fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &out, Stderr: &out})
if err := runAPIFix(fake.X, []string{"test"}); err != nil {
t.Fatalf("should have succeeded but did not: %v\n%v", err, out.String())
}
contents, err := readAPIFileContents(fake.X, filepath.Join(projectPath, ".api"))
if err != nil {
t.Fatalf("%v", err)
}
if got, want := string(contents), "pkg main, func TestFunction1()\n"; got != want {
t.Fatalf("expected %s, got %s", want, got)
}
// Now write a change that changes TestFunction1 to testFunction1.
// There should be no more public API left and the .api file should be
// removed.
writeFileOrDie(t, fake.X, filepath.Join(projectPath, "file.go"), `package main
func testFunction1() {
}`)
if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectPath)).CommitFile("file.go", commitMessage); err != nil {
t.Fatalf("%v", err)
}
out.Reset()
if err := runAPIFix(fake.X, []string{"test"}); err != nil {
t.Fatalf("should have succeeded but did not: %v", err)
}
if _, err := fake.X.NewSeq().Stat(filepath.Join(projectPath, ".api")); err == nil {
t.Fatalf(".api file exists when it should have been removed: %v", err)
} else if !runutil.IsNotExist(err) {
t.Fatalf("%v", err)
}
}