cue/load: add Dependencies, rename Root and improve test

This sanatizes the repo in preparation of two changes
- fixing path bugs (these are exposed by the new tests)
- implementing the package additions of the spec

These two will be handled in separate CLs.

Change-Id: I5a42aa133b4fe9cf34e2f48b73fdfbf03a910242
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2947
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/build/instance.go b/cue/build/instance.go
index dcde72b..c0197a6 100644
--- a/cue/build/instance.go
+++ b/cue/build/instance.go
@@ -80,7 +80,16 @@
 	// from ancestor directories, up to the module file.
 	Dir string
 
-	Root string // module root directory ("" if unknown)
+	// Module defines the module name of a package. It must be defined if
+	// the packages within the directory structure of the module are to be
+	// imported by other packages, including those within the module.
+	Module string
+
+	// Root is the root of the directory hierarchy, it may be "" if this an
+	// instance has no imports.
+	// If Module != "", this corresponds to the module root.
+	// Root/pkg is the directory that holds third-party packages.
+	Root string // root directory of hierarchy ("" if unknown)
 
 	// AllTags are the build tags that can influence file selection in this
 	// directory.
@@ -106,6 +115,25 @@
 	Match      []string
 }
 
+// Dependencies reports all Instances on which this instance depends.
+func (inst *Instance) Dependencies() []*Instance {
+	// TODO: as cyclic dependencies are not allowed, we could just not check.
+	// Do for safety now and remove later if needed.
+	return appendDependencies(nil, inst, map[*Instance]bool{})
+}
+
+func appendDependencies(a []*Instance, inst *Instance, done map[*Instance]bool) []*Instance {
+	for _, d := range inst.Imports {
+		if done[d] {
+			continue
+		}
+		a = append(a, d)
+		done[d] = true
+		a = appendDependencies(a, d, done)
+	}
+	return a
+}
+
 // Abs converts relative path used in the one of the file fields to an
 // absolute one.
 func (inst *Instance) Abs(path string) string {
diff --git a/cue/load/config.go b/cue/load/config.go
index 2852270..3433680 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -148,6 +148,13 @@
 	return i
 }
 
+// Complete updates the configuration information. After calling complete,
+// the following invariants hold:
+//  - c.ModuleRoot != ""
+//  - c.Module is set to the module import prefix if there is a cue.mod file
+//    with the module property.
+//  - c.loader != nil
+//  - c.cache != ""
 func (c Config) complete() (cfg *Config, err error) {
 	// Each major CUE release should add a tag here.
 	// Old tags should not be removed. That is, the cue1.x tag is present
@@ -201,7 +208,7 @@
 			break
 		}
 		var r cue.Runtime
-		inst, err := r.Parse(mod, f)
+		inst, err := r.Compile(mod, f)
 		if err != nil {
 			return nil, errors.Wrapf(err, token.NoPos, "invalid cue.mod file")
 		}
diff --git a/cue/load/import.go b/cue/load/import.go
index 46c44f6..de14a70 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -199,6 +199,8 @@
 
 func updateDirs(c *Config, p *build.Instance, path, srcDir string, mode importMode) errors.Error {
 	p.DisplayPath = path
+	p.Root = c.ModuleRoot
+	p.Module = c.Module
 
 	isLocal := isLocalImport(path)
 	p.Local = isLocal
diff --git a/cue/load/loader_test.go b/cue/load/loader_test.go
index 949c3e9..26625f6 100644
--- a/cue/load/loader_test.go
+++ b/cue/load/loader_test.go
@@ -16,18 +16,18 @@
 
 import (
 	"bytes"
-	"fmt"
 	"os"
 	"path/filepath"
 	"strconv"
 	"strings"
 	"testing"
+	"text/template"
 	"unicode"
 
 	"cuelang.org/go/cue"
-	build "cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/format"
 	"cuelang.org/go/internal/str"
+	"github.com/kylelemons/godebug/diff"
 )
 
 // TestLoad is an end-to-end test.
@@ -36,102 +36,207 @@
 	if err != nil {
 		t.Fatal(err)
 	}
+	testdataDir := filepath.Join(cwd, testdata)
+	dirCfg := &Config{Dir: testdataDir}
+
 	args := str.StringList
 	testCases := []struct {
+		cfg  *Config
 		args []string
 		want string
-		err  string
 	}{{
+		// Even though the directory is called testdata, the last path in
+		// the module is test. So "package test" is correctly the default
+		// package of this directory.
+		cfg:  dirCfg,
 		args: nil,
 		want: `
-test: test.cue (1 files)
-	sub: sub/sub.cue (1 files)`,
+path:   example.org/test
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata
+files:
+    $CWD/testdata/test.cue
+imports:
+    example.org/test/sub: $CWD/testdata/sub/sub.cue`,
 	}, {
+		// Even though the directory is called testdata, the last path in
+		// the module is test. So "package test" is correctly the default
+		// package of this directory.
+		cfg:  dirCfg,
 		args: args("."),
 		want: `
-test: test.cue (1 files)
-	sub: sub/sub.cue (1 files)`,
+path:   example.org/test
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata
+files:
+    $CWD/testdata/test.cue
+imports:
+    example.org/test/sub: $CWD/testdata/sub/sub.cue`,
 	}, {
+		// TODO:
+		// - paths are incorrect, should be example.org/test/other:main and
+		//   example.org/test/other/file, respectively.
+		// - referenced import path of files is wrong.
+		cfg:  dirCfg,
 		args: args("./other/..."),
 		want: `
-main: other/main.cue (1 files)
-	file: other/file/file.cue (1 files);main: other/main.cue (1 files)
-	file: other/file/file.cue (1 files)`,
+path:   example.org/test
+module: example.org/test
+root:   $CWD/testdata/
+dir:    $CWD/testdata/other
+files:
+	$CWD/testdata/other/main.cue
+imports:
+	./file: $CWD/testdata/other/file/file.cue
+
+path:   example.org/test/file
+module: example.org/test
+root:   $CWD/testdata/
+dir:    $CWD/testdata/other/file
+files:
+	$CWD/testdata/other/file/file.cue`,
 	}, {
+		cfg:  dirCfg,
 		args: args("./anon"),
-		want: ":  (0 files)",
-		err:  "build constraints exclude all CUE files",
+		want: `
+err:    build constraints exclude all CUE files in ./anon
+path:   example.org/test/anon
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata/anon`,
 	}, {
+		// TODO:
+		// - paths are incorrect, should be example.org/test/other:main and
+		//   example.org/test/other/file, respectively.
+		cfg:  dirCfg,
 		args: args("./other"),
 		want: `
-main: other/main.cue (1 files)
-	file: other/file/file.cue (1 files)`,
+path:   example.org/test/other
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata/other
+files:
+	$CWD/testdata/other/main.cue
+imports:
+	./file: $CWD/testdata/other/file/file.cue`,
 	}, {
+		// TODO:
+		// - incorrect path, should be example.org/test/hello:test
+		cfg:  dirCfg,
 		args: args("./hello"),
 		want: `
-test: test.cue hello/test.cue (2 files)
-	sub: sub/sub.cue (1 files)`,
+path:   example.org/test/hello
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata/hello
+files:
+	$CWD/testdata/test.cue
+	$CWD/testdata/hello/test.cue
+imports:
+	example.org/test/sub: $CWD/testdata/sub/sub.cue`,
 	}, {
+		cfg:  dirCfg,
 		args: args("./anon.cue", "./other/anon.cue"),
-		want: ": ./anon.cue ./other/anon.cue (2 files)",
+		want: `
+path:   ""
+module: ""
+root:   $CWD/testdata
+dir:    $CWD/testdata
+files:
+	$CWD/testdata/anon.cue
+	$CWD/testdata/other/anon.cue`,
 	}, {
+		cfg: dirCfg,
 		// Absolute file is normalized.
 		args: args(filepath.Join(cwd, "testdata", "anon.cue")),
-		want: ": ./anon.cue (1 files)",
+		want: `
+path:   ""
+module: ""
+root:   $CWD/testdata
+dir:    $CWD/testdata
+files:
+	$CWD/testdata/anon.cue`,
 	}, {
+		// NOTE: dir should probably be set to $CWD/testdata, but either way.
+		cfg:  dirCfg,
 		args: args("non-existing"),
-		want: ":  (0 files)",
-		err:  `cannot find package "non-existing"`,
+		want: `
+err:    cannot find package "non-existing"
+path:   ""
+module: example.org/test
+root:   $CWD/testdata
+dir:    non-existing `,
 	}, {
+		cfg:  dirCfg,
 		args: args("./empty"),
-		want: ":  (0 files)",
-		err:  `no CUE files in ./empty`,
+		want: `
+err:    no CUE files in ./empty
+path:   example.org/test/empty
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata/empty`,
 	}, {
+		cfg:  dirCfg,
 		args: args("./imports"),
 		want: `
-imports: imports/imports.cue (1 files)
-	catch: pkg/acme.com/catch/catch.cue (1 files)
-	helper: pkg/acme.com/helper/helper.cue (1 files)`,
-		err: ``,
+path:   example.org/test/imports
+module: example.org/test
+root:   $CWD/testdata
+dir:    $CWD/testdata/imports
+files:
+	$CWD/testdata/imports/imports.cue
+imports:
+	acme.com/catch: $CWD/testdata/pkg/acme.com/catch/catch.cue
+	acme.com/helper: $CWD/testdata/pkg/acme.com/helper/helper.cue`,
 	}}
 	for i, tc := range testCases {
 		t.Run(strconv.Itoa(i)+"/"+strings.Join(tc.args, ":"), func(t *testing.T) {
-			c := &Config{Dir: filepath.Join(cwd, testdata)}
-			pkgs := Instances(tc.args, c)
+			pkgs := Instances(tc.args, tc.cfg)
 
-			var errs, data []string
-			for _, p := range pkgs {
-				if p.Err != nil {
-					errs = append(errs, p.Err.Error())
-				}
-				got := strings.TrimSpace(pkgInfo(pkgs[0]))
-				data = append(data, got)
+			buf := &bytes.Buffer{}
+			err := pkgInfo.Execute(buf, pkgs)
+			if err != nil {
+				t.Fatal(err)
 			}
 
-			if err := strings.Join(errs, ";"); err == "" != (tc.err == "") ||
-				err != "" && !strings.Contains(err, tc.err) {
-				t.Errorf("error:\n got: %v\nwant: %v", err, tc.err)
-			}
-			got := strings.Join(data, ";")
+			got := strings.TrimSpace(buf.String())
+			got = strings.Replace(got, cwd, "$CWD", -1)
 			// Make test work with Windows.
 			got = strings.Replace(got, string(filepath.Separator), "/", -1)
+
 			want := strings.TrimSpace(tc.want)
+			want = strings.Replace(want, "\t", "    ", -1)
 			if got != want {
-				t.Errorf("got:\n%v\nwant:\n%v", got, want)
+				t.Errorf("\n%s", diff.Diff(got, want))
+				t.Logf("\n%s", got)
 			}
 		})
 	}
 }
 
-func pkgInfo(p *build.Instance) string {
-	b := &bytes.Buffer{}
-	fmt.Fprintf(b, "%s: %s (%d files)\n",
-		p.PkgName, strings.Join(p.CUEFiles, " "), len(p.Files))
-	for _, p := range p.Imports {
-		fmt.Fprintf(b, "\t%s\n", pkgInfo(p))
-	}
-	return b.String()
-}
+var pkgInfo = template.Must(template.New("pkg").Parse(`
+{{- range . -}}
+{{- if .Err}}err:    {{.Err}}{{end}}
+path:   {{if .ImportPath}}{{.ImportPath}}{{else}}""{{end}}
+module: {{if .Module}}{{.Module}}{{else}}""{{end}}
+root:   {{.Root}}
+dir:    {{.Dir}}
+{{if .Files -}}
+files:
+{{- range .Files}}
+    {{.Filename}}
+{{- end -}}
+{{- end}}
+{{if .Imports -}}
+imports:
+{{- range .Dependencies}}
+    {{.ImportPath}}:{{range .Files}} {{.Filename}}{{end}}
+{{- end}}
+{{end -}}
+{{- end -}}
+`))
 
 func TestOverlays(t *testing.T) {
 	cwd, _ := os.Getwd()