Skip to content

feat(gopackagesdriver): add base test case for go packages driver #3743

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
Nov 9, 2023
1 change: 1 addition & 0 deletions go/tools/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ filegroup(
"//go/tools/bzltestutil:all_files",
"//go/tools/coverdata:all_files",
"//go/tools/go_bin_runner:all_files",
"//go/tools/gopackagesdriver:all_files",
],
visibility = ["//visibility:public"],
)
12 changes: 12 additions & 0 deletions go/tools/bazel_testing/bazel_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,23 @@ func RunBazel(args ...string) error {
// If the command starts but exits with a non-zero status, a *StderrExitError
// will be returned which wraps the original *exec.ExitError.
func BazelOutput(args ...string) ([]byte, error) {
return BazelOutputWithInput(nil, args...)
}

// BazelOutputWithInput invokes a bazel command with a list of arguments and
// an input stream and returns the content of stdout.
//
// If the command starts but exits with a non-zero status, a *StderrExitError
// will be returned which wraps the original *exec.ExitError.
func BazelOutputWithInput(stdin io.Reader, args ...string) ([]byte, error) {
cmd := BazelCmd(args...)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd.Stdout = stdout
cmd.Stderr = stderr
if stdin != nil {
cmd.Stdin = stdin
}
err := cmd.Run()
if eErr, ok := err.(*exec.ExitError); ok {
eErr.Stderr = stderr.Bytes()
Expand Down
12 changes: 10 additions & 2 deletions go/tools/gopackagesdriver/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
load("//go:def.bzl", "go_binary", "go_library")
load(":aspect.bzl", "bazel_supports_canonical_label_literals")
load("//go/private:common.bzl", "RULES_GO_REPO_NAME")

go_library(
Expand All @@ -16,7 +15,9 @@ go_library(
"utils.go",
],
importpath = "github.com/bazelbuild/rules_go/go/tools/gopackagesdriver",
visibility = ["//visibility:private"],
visibility = [
"//tests/integration/gopackagesdriver:__pkg__",
],
)

go_binary(
Expand All @@ -29,3 +30,10 @@ go_binary(
"rulesGoRepositoryName": RULES_GO_REPO_NAME,
},
)

filegroup(
name = "all_files",
testonly = True,
srcs = glob(["**"]),
visibility = ["//visibility:public"],
)
6 changes: 6 additions & 0 deletions go/tools/gopackagesdriver/bazel.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Bazel struct {
workspaceRoot string
bazelStartupFlags []string
info map[string]string
version string
}

// Minimal BEP structs to access the build outputs
Expand All @@ -55,6 +56,7 @@ func NewBazel(ctx context.Context, bazelBin, workspaceRoot string, bazelStartupF
bazelBin: bazelBin,
workspaceRoot: workspaceRoot,
bazelStartupFlags: bazelStartupFlags,
version: "6",
}
if err := b.fillInfo(ctx); err != nil {
return nil, fmt.Errorf("unable to query bazel info: %w", err)
Expand All @@ -73,6 +75,10 @@ func (b *Bazel) fillInfo(ctx context.Context) error {
parts := strings.SplitN(strings.TrimSpace(scanner.Text()), ":", 2)
b.info[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
release := strings.Split(b.info["release"], " ")
if len(release) == 2 {
b.version = release[1]
}
return nil
}

Expand Down
12 changes: 11 additions & 1 deletion go/tools/gopackagesdriver/bazel_json_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
)

Expand Down Expand Up @@ -238,7 +239,7 @@ func (b *BazelJSONBuilder) Build(ctx context.Context, labels []string, mode Load
ret := []string{}
for _, f := range files {
if strings.HasSuffix(f, ".pkg.json") {
ret = append(ret, f)
ret = append(ret, cleanPath(f))
}
}

Expand All @@ -253,3 +254,12 @@ func (b *BazelJSONBuilder) PathResolver() PathResolverFunc {
return p
}
}

func cleanPath(p string) string {
// On Windows the paths may contain a starting `\`, this would make them not resolve
if runtime.GOOS == "windows" && p[0] == '\\' {
return p[1:]
}

return p
}
4 changes: 2 additions & 2 deletions go/tools/gopackagesdriver/json_packages_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ type JSONPackagesDriver struct {
registry *PackageRegistry
}

func NewJSONPackagesDriver(jsonFiles []string, prf PathResolverFunc) (*JSONPackagesDriver, error) {
func NewJSONPackagesDriver(jsonFiles []string, prf PathResolverFunc, bazelVersion string) (*JSONPackagesDriver, error) {
jpd := &JSONPackagesDriver{
registry: NewPackageRegistry(),
registry: NewPackageRegistry(bazelVersion),
}

for _, f := range jsonFiles {
Expand Down
2 changes: 1 addition & 1 deletion go/tools/gopackagesdriver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func run() (*driverResponse, error) {
return emptyResponse, fmt.Errorf("unable to build JSON files: %w", err)
}

driver, err := NewJSONPackagesDriver(jsonFiles, bazelJsonBuilder.PathResolver())
driver, err := NewJSONPackagesDriver(jsonFiles, bazelJsonBuilder.PathResolver(), bazel.version)
if err != nil {
return emptyResponse, fmt.Errorf("unable to load JSON files: %w", err)
}
Expand Down
26 changes: 24 additions & 2 deletions go/tools/gopackagesdriver/packageregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ package main

import (
"fmt"
"strconv"
"strings"
)

type PackageRegistry struct {
packagesByID map[string]*FlatPackage
stdlib map[string]string
bazelVersion []int
}

func NewPackageRegistry(pkgs ...*FlatPackage) *PackageRegistry {
func NewPackageRegistry(bazelVersion string, pkgs ...*FlatPackage) *PackageRegistry {
pr := &PackageRegistry{
packagesByID: map[string]*FlatPackage{},
stdlib: map[string]string{},
bazelVersion: parseVersion(bazelVersion),
}
pr.Add(pkgs...)
return pr
Expand Down Expand Up @@ -88,7 +91,10 @@ func (pr *PackageRegistry) Match(labels []string) ([]string, []*FlatPackage) {
roots := map[string]struct{}{}

for _, label := range labels {
if !strings.HasPrefix(label, "@") {
// When packagesdriver is ran from rules go, rulesGoRepositoryName will just be @
if pr.bazelVersion[0] >= 6 &&
!strings.HasPrefix(label, "@") {
// Canonical labels is only since Bazel 6.0.0
label = fmt.Sprintf("@%s", label)
}

Expand Down Expand Up @@ -119,3 +125,19 @@ func (pr *PackageRegistry) Match(labels []string) ([]string, []*FlatPackage) {

return retRoots, retPkgs
}

func parseVersion(v string) []int {
parts := strings.Split(v, ".")
version := make([]int, len(parts))

var err error
for i, p := range parts {
version[i], err = strconv.Atoi(p)
if err != nil {
// Failsafe default
return []int{6, 0, 0}
}
}

return version
}
13 changes: 13 additions & 0 deletions tests/integration/gopackagesdriver/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
load("//go/tools/bazel_testing:def.bzl", "go_bazel_test")

go_bazel_test(
name = "gopackagesdriver_test",
size = "enormous",
srcs = ["gopackagesdriver_test.go"],
rule_files = [
"//:all_files",
],
deps = [
"@io_bazel_rules_go//go/tools/gopackagesdriver:gopackagesdriver_lib",
]
)
8 changes: 8 additions & 0 deletions tests/integration/gopackagesdriver/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Go Packages Driver

gopackagesdriver_test
--------------------
Verifies that the output of the go packages driver includes the correct output.

Go x/tools is very sensitive to inaccuracies in the package output, so we should
validate each added feature against what is expected by x/tools.
126 changes: 126 additions & 0 deletions tests/integration/gopackagesdriver/gopackagesdriver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package gopackagesdriver_test

import (
"encoding/json"
"path"
"strings"
"testing"

"github.com/bazelbuild/rules_go/go/tools/bazel_testing"
gpd "github.com/bazelbuild/rules_go/go/tools/gopackagesdriver"
)

type response struct {
Roots []string `json:",omitempty"`
Packages []*gpd.FlatPackage
}

func TestMain(m *testing.M) {
bazel_testing.TestMain(m, bazel_testing.Args{
Main: `
-- BUILD.bazel --
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "hello",
srcs = ["hello.go"],
importpath = "example.com/hello",
visibility = ["//visibility:public"],
)

-- hello.go --
package hello

import "os"

func main() {
fmt.Fprintln(os.Stderr, "Hello World!")
}
`,
})
}

const (
osPkgID = "@io_bazel_rules_go//stdlib:os"
)

func TestBaseFileLookup(t *testing.T) {
reader := strings.NewReader("{}")
out, err := bazel_testing.BazelOutputWithInput(reader, "run", "@io_bazel_rules_go//go/tools/gopackagesdriver", "--", "file=hello.go")
if err != nil {
t.Errorf("Unexpected error: %w", err.Error())
return
}
var resp response
err = json.Unmarshal(out, &resp)
if err != nil {
t.Errorf("Failed to unmarshal packages driver response: %w\n%w", err.Error(), out)
return
}

t.Run("roots", func(t *testing.T) {
if len(resp.Roots) != 1 {
t.Errorf("Expected 1 package root: %+v", resp.Roots)
return
}

if !strings.HasSuffix(resp.Roots[0], "//:hello") {
t.Errorf("Unexpected package id: %q", resp.Roots[0])
return
}
})

t.Run("package", func(t *testing.T) {
var pkg *gpd.FlatPackage
for _, p := range resp.Packages {
if p.ID == resp.Roots[0] {
pkg = p
}
}

if pkg == nil {
t.Errorf("Expected to find %q in resp.Packages", resp.Roots[0])
return
}

if len(pkg.CompiledGoFiles) != 1 || len(pkg.GoFiles) != 1 ||
path.Base(pkg.GoFiles[0]) != "hello.go" || path.Base(pkg.CompiledGoFiles[0]) != "hello.go" {
t.Errorf("Expected to find 1 file (hello.go) in (Compiled)GoFiles:\n%+v", pkg)
return
}

if pkg.Standard {
t.Errorf("Expected package to not be Standard:\n%+v", pkg)
return
}

if len(pkg.Imports) != 1 {
t.Errorf("Expected one import:\n%+v", pkg)
return
}

if pkg.Imports["os"] != osPkgID {
t.Errorf("Expected os import to map to %q:\n%+v", osPkgID, pkg)
return
}
})

t.Run("dependency", func(t *testing.T) {
var osPkg *gpd.FlatPackage
for _, p := range resp.Packages {
if p.ID == osPkgID {
osPkg = p
}
}

if osPkg == nil {
t.Errorf("Expected os package to be included:\n%+v", osPkg)
return
}

if !osPkg.Standard {
t.Errorf("Expected os import to be standard:\n%+v", osPkg)
return
}
})
}