blob: 4fb2d5f64427a834c218a425f2bbcbf08ba6617a [file] [log] [blame]
// Copyright 2013 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 importer
import (
"bufio"
"bytes"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"testing"
"time"
"llvm.org/llgo/third_party/gotools/go/gcimporter"
"llvm.org/llgo/third_party/gotools/go/types"
)
var fset = token.NewFileSet()
var tests = []string{
`package p`,
// consts
`package p; const X = true`,
`package p; const X, y, Z = true, false, 0 != 0`,
`package p; const ( A float32 = 1<<iota; B; C; D)`,
`package p; const X = "foo"`,
`package p; const X string = "foo"`,
`package p; const X = 0`,
`package p; const X = -42`,
`package p; const X = 3.14159265`,
`package p; const X = -1e-10`,
`package p; const X = 1.2 + 2.3i`,
`package p; const X = -1i`,
`package p; import "math"; const Pi = math.Pi`,
`package p; import m "math"; const Pi = m.Pi`,
// types
`package p; type T int`,
`package p; type T [10]int`,
`package p; type T []int`,
`package p; type T struct{}`,
`package p; type T struct{x int}`,
`package p; type T *int`,
`package p; type T func()`,
`package p; type T *T`,
`package p; type T interface{}`,
`package p; type T interface{ foo() }`,
`package p; type T interface{ m() T }`,
// TODO(gri) disabled for now - import/export works but
// types.Type.String() used in the test cannot handle cases
// like this yet
// `package p; type T interface{ m() interface{T} }`,
`package p; type T map[string]bool`,
`package p; type T chan int`,
`package p; type T <-chan complex64`,
`package p; type T chan<- map[int]string`,
// test case for issue 8177
`package p; type T1 interface { F(T2) }; type T2 interface { T1 }`,
// vars
`package p; var X int`,
`package p; var X, Y, Z struct{f int "tag"}`,
// funcs
`package p; func F()`,
`package p; func F(x int, y struct{}) bool`,
`package p; type T int; func (*T) F(x int, y struct{}) T`,
// selected special cases
`package p; type T int`,
`package p; type T uint8`,
`package p; type T byte`,
`package p; type T error`,
`package p; import "net/http"; type T http.Client`,
`package p; import "net/http"; type ( T1 http.Client; T2 struct { http.Client } )`,
`package p; import "unsafe"; type ( T1 unsafe.Pointer; T2 unsafe.Pointer )`,
`package p; import "unsafe"; type T struct { p unsafe.Pointer }`,
}
func TestImportSrc(t *testing.T) {
for _, src := range tests {
pkg, err := pkgForSource(src)
if err != nil {
t.Errorf("typecheck failed: %s", err)
continue
}
testExportImport(t, pkg, "")
}
}
func TestImportStdLib(t *testing.T) {
start := time.Now()
libs, err := stdLibs()
if err != nil {
t.Fatalf("could not compute list of std libraries: %s", err)
}
if len(libs) < 100 {
t.Fatalf("only %d std libraries found - something's not right", len(libs))
}
// make sure printed go/types types and gc-imported types
// can be compared reasonably well
types.GcCompatibilityMode = true
var totSize, totGcSize int
for _, lib := range libs {
// limit run time for short tests
if testing.Short() && time.Since(start) >= 750*time.Millisecond {
return
}
pkg, err := pkgForPath(lib)
switch err := err.(type) {
case nil:
// ok
case *build.NoGoError:
// no Go files - ignore
continue
default:
t.Errorf("typecheck failed: %s", err)
continue
}
size, gcsize := testExportImport(t, pkg, lib)
if gcsize == 0 {
// if gc import didn't happen, assume same size
// (and avoid division by zero below)
gcsize = size
}
if testing.Verbose() {
fmt.Printf("%s\t%d\t%d\t%d%%\n", lib, size, gcsize, int(float64(size)*100/float64(gcsize)))
}
totSize += size
totGcSize += gcsize
}
if testing.Verbose() {
fmt.Printf("\n%d\t%d\t%d%%\n", totSize, totGcSize, int(float64(totSize)*100/float64(totGcSize)))
}
types.GcCompatibilityMode = false
}
func testExportImport(t *testing.T, pkg0 *types.Package, path string) (size, gcsize int) {
data := ExportData(pkg0)
size = len(data)
imports := make(map[string]*types.Package)
n, pkg1, err := ImportData(imports, data)
if err != nil {
t.Errorf("package %s: import failed: %s", pkg0.Name(), err)
return
}
if n != size {
t.Errorf("package %s: not all input data consumed", pkg0.Name())
return
}
s0 := pkgString(pkg0)
s1 := pkgString(pkg1)
if s1 != s0 {
t.Errorf("package %s: \nimport got:\n%s\nwant:\n%s\n", pkg0.Name(), s1, s0)
}
// If we have a standard library, compare also against the gcimported package.
if path == "" {
return // not std library
}
gcdata, err := gcExportData(path)
if err != nil {
if pkg0.Name() == "main" {
return // no export data present for main package
}
t.Errorf("package %s: couldn't get export data: %s", pkg0.Name(), err)
}
gcsize = len(gcdata)
imports = make(map[string]*types.Package)
pkg2, err := gcImportData(imports, gcdata, path)
if err != nil {
t.Errorf("package %s: gcimport failed: %s", pkg0.Name(), err)
return
}
s2 := pkgString(pkg2)
if s2 != s0 {
t.Errorf("package %s: \ngcimport got:\n%s\nwant:\n%s\n", pkg0.Name(), s2, s0)
}
return
}
func pkgForSource(src string) (*types.Package, error) {
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
return nil, err
}
return typecheck("import-test", f)
}
func pkgForPath(path string) (*types.Package, error) {
// collect filenames
ctxt := build.Default
pkginfo, err := ctxt.Import(path, "", 0)
if err != nil {
return nil, err
}
filenames := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
// parse files
files := make([]*ast.File, len(filenames))
for i, filename := range filenames {
var err error
files[i], err = parser.ParseFile(fset, filepath.Join(pkginfo.Dir, filename), nil, 0)
if err != nil {
return nil, err
}
}
return typecheck(path, files...)
}
var defaultConf = types.Config{
// we only care about exports and thus can ignore function bodies
IgnoreFuncBodies: true,
// work around C imports if possible
FakeImportC: true,
// strconv exports IntSize as a constant. The type-checker must
// use the same word size otherwise the result of the type-checker
// and gc imports is different. We don't care about alignment
// since none of the tests have exported constants depending
// on alignment (see also issue 8366).
Sizes: &types.StdSizes{WordSize: strconv.IntSize / 8, MaxAlign: 8},
}
func typecheck(path string, files ...*ast.File) (*types.Package, error) {
return defaultConf.Check(path, fset, files, nil)
}
// pkgString returns a string representation of a package's exported interface.
func pkgString(pkg *types.Package) string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "package %s\n", pkg.Name())
scope := pkg.Scope()
for _, name := range scope.Names() {
if exported(name) {
obj := scope.Lookup(name)
buf.WriteString(obj.String())
switch obj := obj.(type) {
case *types.Const:
// For now only print constant values if they are not float
// or complex. This permits comparing go/types results with
// gc-generated gcimported package interfaces.
info := obj.Type().Underlying().(*types.Basic).Info()
if info&types.IsFloat == 0 && info&types.IsComplex == 0 {
fmt.Fprintf(&buf, " = %s", obj.Val())
}
case *types.TypeName:
// Print associated methods.
// Basic types (e.g., unsafe.Pointer) have *types.Basic
// type rather than *types.Named; so we need to check.
if typ, _ := obj.Type().(*types.Named); typ != nil {
if n := typ.NumMethods(); n > 0 {
// Sort methods by name so that we get the
// same order independent of whether the
// methods got imported or coming directly
// for the source.
// TODO(gri) This should probably be done
// in go/types.
list := make([]*types.Func, n)
for i := 0; i < n; i++ {
list[i] = typ.Method(i)
}
sort.Sort(byName(list))
buf.WriteString("\nmethods (\n")
for _, m := range list {
fmt.Fprintf(&buf, "\t%s\n", m)
}
buf.WriteString(")")
}
}
}
buf.WriteByte('\n')
}
}
return buf.String()
}
var stdLibRoot = filepath.Join(runtime.GOROOT(), "src") + string(filepath.Separator)
// The following std libraries are excluded from the stdLibs list.
var excluded = map[string]bool{
"builtin": true, // contains type declarations with cycles
"unsafe": true, // contains fake declarations
}
// stdLibs returns the list of standard library package paths.
func stdLibs() (list []string, err error) {
err = filepath.Walk(stdLibRoot, func(path string, info os.FileInfo, err error) error {
if err == nil && info.IsDir() {
// testdata directories don't contain importable libraries
if info.Name() == "testdata" {
return filepath.SkipDir
}
pkgPath := path[len(stdLibRoot):] // remove stdLibRoot
if len(pkgPath) > 0 && !excluded[pkgPath] {
list = append(list, pkgPath)
}
}
return nil
})
return
}
type byName []*types.Func
func (a byName) Len() int { return len(a) }
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
// gcExportData returns the gc-generated export data for the given path.
// It is based on a trimmed-down version of gcimporter.Import which does
// not do the actual import, does not handle package unsafe, and assumes
// that path is a correct standard library package path (no canonicalization,
// or handling of local import paths).
func gcExportData(path string) ([]byte, error) {
filename, id := gcimporter.FindPkg(path, "")
if filename == "" {
return nil, fmt.Errorf("can't find import: %s", path)
}
if id != path {
panic("path should be canonicalized")
}
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
buf := bufio.NewReader(f)
if err = gcimporter.FindExportData(buf); err != nil {
return nil, err
}
var data []byte
for {
line, err := buf.ReadBytes('\n')
if err != nil {
return nil, err
}
data = append(data, line...)
// export data ends in "$$\n"
if len(line) == 3 && line[0] == '$' && line[1] == '$' {
return data, nil
}
}
}
func gcImportData(imports map[string]*types.Package, data []byte, path string) (*types.Package, error) {
filename := fmt.Sprintf("<filename for %s>", path) // so we have a decent error message if necessary
return gcimporter.ImportData(imports, filename, path, bufio.NewReader(bytes.NewBuffer(data)))
}