internal/godoc: preserve Scopes

It turns out that ast.Scopes are used to render doc, so preserve them.

Also, tear down the queue after each integration test: because of
retrying, the queue would still be active from previous tests, making
the output confusing for debugging.

Change-Id: I3f3c59d355c0e121e1d8866e621ed830d80e9b9c
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/259717
Trust: Jonathan Amsterdam <jba@google.com>
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/godoc/encode.go b/internal/godoc/encode.go
index 366563e..d0c7c32 100644
--- a/internal/godoc/encode.go
+++ b/internal/godoc/encode.go
@@ -12,6 +12,7 @@
 	"go/ast"
 	"go/token"
 	"io"
+	"sort"
 
 	"golang.org/x/pkgsite/internal/derrors"
 )
@@ -164,8 +165,6 @@
 // in the tree itself. We assign these numbers as well, and store the numbers
 // in a separate field of File.
 func removeCycles(f *File) {
-	f.AST.Scope.Objects = nil // doc doesn't use scopes
-
 	// First pass: assign every Decl, Spec and Ident a number.
 	// Since these aren't shared and Inspect is deterministic,
 	// this walk will produce the same sequence of Decls after encoding/decoding.
@@ -227,6 +226,21 @@
 		}
 	}
 	f.AST.Unresolved = nil
+
+	// Remember only those scope items that have been assigned a number; the others
+	// are not relevant to doc (unexported functions, for instance).
+	f.ScopeItems = nil
+	for name, obj := range f.AST.Scope.Objects {
+		if num, ok := obj.Data.(int); ok {
+			f.ScopeItems = append(f.ScopeItems, scopeItem{name, num})
+		}
+	}
+	// Sort for deterministic encoding.
+	sort.Slice(f.ScopeItems, func(i, j int) bool {
+		return f.ScopeItems[i].Name < f.ScopeItems[j].Name
+	})
+	f.AST.Scope.Objects = nil
+
 }
 
 // fixupObjects re-establishes the original Object and Decl relationships of the
@@ -268,7 +282,6 @@
 			id.Obj = objs[num]
 		case num == len(objs):
 			// A new object; fix it up and remember it.
-			obj.Data = nil
 			if obj.Decl != nil {
 				obj.Decl = decls[obj.Decl.(int)]
 			}
@@ -285,6 +298,13 @@
 		f.AST.Unresolved[i] = decls[num].(*ast.Ident)
 	}
 	f.UnresolvedNums = nil
+
+	// Fix up file scope objects.
+	f.AST.Scope.Objects = map[string]*ast.Object{}
+	for _, item := range f.ScopeItems {
+		f.AST.Scope.Objects[item.Name] = objs[item.Num]
+	}
+	f.ScopeItems = nil
 }
 
 // isRelevantDecl reports whether n is a Node for a declaration relevant to
diff --git a/internal/godoc/encode_test.go b/internal/godoc/encode_test.go
index 28a1b64..d5d315a 100644
--- a/internal/godoc/encode_test.go
+++ b/internal/godoc/encode_test.go
@@ -13,7 +13,7 @@
 	"testing"
 )
 
-func TestEncodeDecodeASTFiles(t *testing.T) {
+func TestEncodeDecodePackage(t *testing.T) {
 	// Verify that we can encode and decode the Go files in this directory.
 	p, err := packageForDir(".", true)
 	if err != nil {
diff --git a/internal/godoc/godoc.go b/internal/godoc/godoc.go
index f1af28a..eb66d43 100644
--- a/internal/godoc/godoc.go
+++ b/internal/godoc/godoc.go
@@ -32,9 +32,17 @@
 
 // A File contains everything needed about a source file to render documentation.
 type File struct {
-	Name           string // full file pathname relative to zip content directory
-	AST            *ast.File
-	UnresolvedNums []int // used to handle sharing of unresolved identifiers
+	Name string // full file pathname relative to zip content directory
+	AST  *ast.File
+	// The following fields are only for encoding and decoding. They are public
+	// only because gob requires them to be. Clients should ignore them.
+	UnresolvedNums []int       // used to handle sharing of unresolved identifiers
+	ScopeItems     []scopeItem // sorted by name for deterministic encoding
+}
+
+type scopeItem struct {
+	Name string
+	Num  int
 }
 
 // NewPackage returns a new Package with the given fset and set of module package paths.
diff --git a/internal/testing/integration/frontend_doc_render_test.go b/internal/testing/integration/frontend_doc_render_test.go
index 3c53e2e..dd74ec5 100644
--- a/internal/testing/integration/frontend_doc_render_test.go
+++ b/internal/testing/integration/frontend_doc_render_test.go
@@ -5,9 +5,13 @@
 package integration
 
 import (
+	"bufio"
+	"bytes"
 	"context"
+	"fmt"
 	"io/ioutil"
 	"net/http"
+	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -39,8 +43,9 @@
 					const C = 123
 
 					// F is a function.
-					func F(t time.Time, s string) T {
-						_ = C
+					func F(t time.Time, s string) (T, u) {
+						x := 3
+						x = C
 					}
 			`,
 			"file2.go": `
@@ -48,38 +53,28 @@
 
 					var V = C
 					type T int
+					type u int
 			`,
 		},
 	}
 
-	commonExps := []string{
-		internal.ExperimentRemoveUnusedAST,
-		internal.ExperimentInsertPackageSource,
-		internal.ExperimentSidenav,
-		internal.ExperimentUnitPage,
-	}
+	// Process with saving the source.
+	processVersions(
+		experiment.NewContext(context.Background(), internal.ExperimentUnitPage, internal.ExperimentRemoveUnusedAST, internal.ExperimentInsertPackageSource),
+		t, []*proxy.Module{m})
 
-	ctx := experiment.NewContext(context.Background(), commonExps...)
-	processVersions(ctx, t, []*proxy.Module{m})
-
-	getDoc := func(exps ...string) string {
-		t.Helper()
-
-		ectx := experiment.NewContext(ctx, append(exps, commonExps...)...)
-		ts := setupFrontend(ectx, t, nil)
-		url := ts.URL + "/" + m.ModulePath
-		return getPage(ectx, t, url)
-	}
-
-	workerDoc := getDoc()
-	frontendDoc := getDoc(internal.ExperimentFrontendRenderDoc)
-
+	workerDoc := getDoc(t, m.ModulePath)
+	frontendDoc := getDoc(t, m.ModulePath, internal.ExperimentUnitPage, internal.ExperimentFrontendRenderDoc, internal.ExperimentInsertPackageSource)
 	if diff := cmp.Diff(workerDoc, frontendDoc); diff != "" {
 		t.Errorf("mismatch (-worker, +frontend):\n%s", diff)
 	}
 }
 
-func getPage(ctx context.Context, t *testing.T, url string) string {
+func getDoc(t *testing.T, modulePath string, exps ...string) string {
+	ctx := experiment.NewContext(context.Background(),
+		append(exps, internal.ExperimentSidenav, internal.ExperimentUnitPage)...)
+	ts := setupFrontend(ctx, t, nil)
+	url := ts.URL + "/" + modulePath
 	resp, err := http.Get(url)
 	if err != nil {
 		t.Fatalf("%s: %v", url, err)
@@ -92,5 +87,14 @@
 	if err != nil {
 		t.Fatalf("%s: %v", url, err)
 	}
-	return string(content)
+	// Remove surrounding whitespace from lines.
+	scan := bufio.NewScanner(bytes.NewReader(content))
+	var b strings.Builder
+	for scan.Scan() {
+		fmt.Fprintln(&b, strings.TrimSpace(scan.Text()))
+	}
+	if scan.Err() != nil {
+		t.Fatal(scan.Err())
+	}
+	return b.String()
 }
diff --git a/internal/testing/integration/frontend_test.go b/internal/testing/integration/frontend_test.go
index c9ac5f2..4aedf81 100644
--- a/internal/testing/integration/frontend_test.go
+++ b/internal/testing/integration/frontend_test.go
@@ -213,13 +213,15 @@
 // TODO(https://github.com/golang/go/issues/40098): factor out this code reduce
 // duplication
 func setupQueue(ctx context.Context, t *testing.T, proxyModules []*proxy.Module, experimentNames ...string) (queue.Queue, func()) {
+	cctx, cancel := context.WithCancel(ctx)
 	proxyClient, teardown := proxy.SetupTestClient(t, proxyModules)
 	sourceClient := source.NewClient(1 * time.Second)
-	q := queue.NewInMemory(ctx, 1, experimentNames,
-		func(ctx context.Context, mpath, version string) (int, error) {
+	q := queue.NewInMemory(cctx, 1, experimentNames,
+		func(ctx context.Context, mpath, version string) (_ int, err error) {
 			return frontend.FetchAndUpdateState(ctx, mpath, version, proxyClient, sourceClient, testDB)
 		})
 	return q, func() {
+		cancel()
 		teardown()
 	}
 }