internal/frontend: add version history

Symbol history is added to the versions page, for when a symbol is first
introduced to the package API.

For golang/go#37102

Change-Id: I22b7bc959a464dc38fe0bd39244c997fd51370f1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/298309
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
Julie Qiu 2021-03-09 01:46:04 -05:00
Родитель ff0a2dbc60
Коммит a2f4692c7e
15 изменённых файлов: 984 добавлений и 22 удалений

Просмотреть файл

@ -1460,11 +1460,14 @@ pre,
}
.Versions td:nth-child(1) {
padding-right: 3rem;
vertical-align: top;
}
.Versions td:nth-child(2) {
border-right: 1px solid var(--gray-8);
padding-right: 1rem;
text-align: right;
border-right: 1px solid var(--gray-8);
vertical-align: top;
white-space: nowrap;
}
.Versions td:nth-child(3) {
padding-left: 1rem;
@ -1477,6 +1480,12 @@ pre,
.Versions-major {
font-weight: 600;
}
.Versions-symbolChild {
padding-left: 2rem;
}
.Versions-symbolBuilds {
font-size: 0.75rem;
}
.Imports-list {
list-style: none;

Просмотреть файл

@ -47,8 +47,28 @@
</td>
<td>
<div class="Versions-commitTime">{{$v.CommitTime}}</div>
<div>
{{range $v.Symbols}}
{{template "symbol" .}}
{{range .Children}}
<div class="Versions-symbolChild">{{template "symbol" .}}</div>
{{end}}
{{end}}
</div>
</td>
</tr>
{{end}}
{{end}}
{{end}}
{{define "symbol"}}
<div>
{{if .New}}
{{if eq .Kind "Method"}}method{{else if eq .Kind "Field"}}field{{end}}
<a href="{{.Link}}">{{end}}{{.Synopsis}} {{if .New}}</a>
{{end}}
{{if .Builds}}
<span class="Versions-symbolBuilds">{{range $i, $b := .Builds}}{{if $i}}, {{end}}{{$b}}{{end}}</span>
{{end}}
</div>
{{end}}

Просмотреть файл

@ -4,6 +4,8 @@
package internal
import "fmt"
// A BuildContext describes a build context for the Go tool: information needed
// to build a Go package. For our purposes, we only care about the information
// that affects documentation generated from the package.
@ -11,6 +13,11 @@ type BuildContext struct {
GOOS, GOARCH string
}
// String returns a string formatted representation of the build context.
func (b BuildContext) String() string {
return fmt.Sprintf("%s/%s", b.GOOS, b.GOARCH)
}
// All represents all values for a build context element (GOOS or GOARCH).
const All = "all"

Просмотреть файл

@ -6,25 +6,27 @@
package internal
const (
ExperimentCommandTOC = "command-toc"
ExperimentInsertSymbols = "insert-symbols"
ExperimentInsertSymbolHistory = "insert-symbol-history"
ExperimentInteractivePlayground = "interactive-playground"
ExperimentNotAtLatest = "not-at-latest"
ExperimentRetractions = "retractions"
ExperimentUnitMetaWithLatest = "unit-meta-with-latest"
ExperimentCommandTOC = "command-toc"
ExperimentInsertSymbolHistory = "insert-symbol-history"
ExperimentInsertSymbols = "insert-symbols"
ExperimentInteractivePlayground = "interactive-playground"
ExperimentNotAtLatest = "not-at-latest"
ExperimentRetractions = "retractions"
ExperimentSymbolHistoryVersionsPage = "symbol-history-versions-page"
ExperimentUnitMetaWithLatest = "unit-meta-with-latest"
)
// Experiments represents all of the active experiments in the codebase and
// a description of each experiment.
var Experiments = map[string]string{
ExperimentCommandTOC: "Enable the table of contents for command documention pages.",
ExperimentInsertSymbols: "Insert data into symbols, package_symbols, and documentation_symbols.",
ExperimentInsertSymbolHistory: "Insert symbol history data into the symbol_history table.",
ExperimentInteractivePlayground: "Enable interactive example playground on the unit page.",
ExperimentNotAtLatest: "Enable the display of a 'not at latest' badge.",
ExperimentRetractions: "Retrieve and display retraction and deprecation information.",
ExperimentUnitMetaWithLatest: "Use latest-version information for GetUnitMeta.",
ExperimentCommandTOC: "Enable the table of contents for command documention pages.",
ExperimentInsertSymbolHistory: "Insert symbol history data into the symbol_history table.",
ExperimentInsertSymbols: "Insert data into symbols, package_symbols, and documentation_symbols.",
ExperimentInteractivePlayground: "Enable interactive example playground on the unit page.",
ExperimentNotAtLatest: "Enable the display of a 'not at latest' badge.",
ExperimentRetractions: "Retrieve and display retraction and deprecation information.",
ExperimentSymbolHistoryVersionsPage: "Show package API history on the versions page.",
ExperimentUnitMetaWithLatest: "Use latest-version information for GetUnitMeta.",
}
// Experiment holds data associated with an experimental feature for frontend

208
internal/frontend/symbol.go Normal file
Просмотреть файл

@ -0,0 +1,208 @@
// Copyright 2021 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 frontend
import (
"fmt"
"sort"
"strings"
"golang.org/x/pkgsite/internal"
)
// Symbol is an element in the package API. A symbol can be a constant,
// variable, function, type, field or method.
type Symbol struct {
// Name is name of the symbol. At a given package version, name must be
// unique.
Name string
// Synopsis is the one line description of the symbol that is displayed.
Synopsis string
// Link is the link to the symbol name on pkg.go.dev.
Link string
// Section is the section that a symbol appears in.
Section internal.SymbolSection
// Kind is the type of a symbol, which is either a constant, variable,
// function, type, field or method.
Kind internal.SymbolKind
// Children contain the child symbols for this symbol. This will
// only be populated when the SymbolType is "Type". For example, the
// children of net/http.Handler are FileServer, NotFoundHandler,
// RedirectHandler, StripPrefix, and TimeoutHandler. Each child
// symbol will have ParentName set to the Name of this type.
Children []*Symbol
// Builds lists all of the build contexts supported by the symbol, it is
// only available for limited set of builds. If the symbol supports all
// build contexts, Builds will be nil.
Builds []string
// New indicates that the symbol is new as of the version where it is
// present. For example, if type Client was introduced in v1.0.0 and
// Client.Timeout was introduced in v1.1.0, New will be false for Client
// and true for Client.Timeout if this Symbol corresponds to v1.1.0.
New bool
}
// symbolsForVersions returns an array of symbols for use in the VersionSummary
// of the specified version.
func symbolsForVersion(pkgURLPath string, symbolsAtVersion map[string]*internal.UnitSymbol) []*Symbol {
nameToSymbol := map[string]*Symbol{}
for _, us := range symbolsAtVersion {
s, ok := nameToSymbol[us.Name]
if !ok {
s = &Symbol{
Name: us.Name,
Synopsis: formatSynopsis(us.Synopsis, us.ParentName, us.Kind),
Link: symbolLink(pkgURLPath, us.Name),
Section: us.Section,
Kind: us.Kind,
New: true,
Builds: symbolBuilds(us),
}
} else if !s.New && us.Kind == internal.SymbolKindType {
// It's possible that a symbol was already created if this is a parent
// symbol and we called addSymbol on the child symbol first. In that
// case, a parent symbol would have been created where s.New is set to
// false and s.Synopsis is set to the one created in createParent.
// Update those fields instead of overwritting the struct, since the
// struct's Children field would have already been populated.
s.New = true
s.Synopsis = us.Synopsis
}
if us.ParentName == us.Name {
// s is not a child symbol of a type, so add it to the map and
// continue.
nameToSymbol[us.Name] = s
continue
}
// s is a child symbol of a parent type, so append it to the Children field
// of the parent type.
parent, ok := nameToSymbol[us.ParentName]
if !ok {
parent = createParent(us, pkgURLPath)
nameToSymbol[us.ParentName] = parent
}
parent.Children = append(parent.Children, s)
}
return sortSymbols(nameToSymbol)
}
func symbolLink(pkgURLPath, name string) string {
return fmt.Sprintf("%s#%s", pkgURLPath, name)
}
func symbolBuilds(us *internal.UnitSymbol) []string {
if us.InAll() {
return nil
}
var builds []string
for _, b := range us.BuildContexts() {
builds = append(builds, b.String())
}
return builds
}
// createParent creates a parent symbol for the provided unit symbol. This is
// used when us is a child of a symbol that may have been introduced at a
// different version. The symbol created will have New set to false, since this
// function is only used when a parent symbol is not found for the unit symbol,
// which means it was not introduced at the same version.
func createParent(us *internal.UnitSymbol, pkgURLPath string) *Symbol {
var synopsis string
if strings.Contains(us.Synopsis, fmt.Sprintf("type %s struct", us.ParentName)) {
synopsis = fmt.Sprintf("type %s struct", us.ParentName)
} else if strings.Contains(us.Synopsis, fmt.Sprintf("type %s interface", us.ParentName)) {
synopsis = fmt.Sprintf("type %s interface", us.ParentName)
} else {
synopsis = fmt.Sprintf("type %s", us.ParentName)
}
s := &Symbol{
Name: us.ParentName,
Synopsis: synopsis,
Link: symbolLink(pkgURLPath, us.ParentName),
Section: internal.SymbolSectionTypes,
Kind: internal.SymbolKindType,
Builds: symbolBuilds(us),
}
return s
}
// formatSynopsis removes the leading "type <ParentName> <struct/interface>,"
// string for the synopsis. These strings are added to prevent conflicts
// between the same synopsis for different symbols (for example, if two
// different interfaces both had a Foo() string method) in the database, since
// the synopsis is used as a piece of a unique constraint in the database.
//
// Trailing brackets are removed from types.
// TODO(https://golang.org/issue/37102): Change the unique constraint on
// package_symbols and store the desired synopsis. Then remove this function.
func formatSynopsis(syn, parentName string, kind internal.SymbolKind) string {
switch kind {
case internal.SymbolKindType:
return strings.TrimSuffix(syn, "{}")
case internal.SymbolKindField:
return strings.TrimPrefix(syn, fmt.Sprintf("type %s struct, ", parentName))
case internal.SymbolKindMethod:
return strings.TrimPrefix(syn, fmt.Sprintf("type %s interface, ", parentName))
}
return syn
}
// sortSymbols returns an array of symbols in order of
// (1) Constants (2) Variables (3) Functions and (4) Types.
// Within each section, symbols are sorted alphabetically by name.
// In the types sections, aside from interfaces, child symbols are sorted in
// order of (1) Fields (2) Constants (3) Variables (4) Functions and (5)
// Methods. For interfaces, child symbols are sorted in order of
// (1) Methods (2) Constants (3) Variables and (4) Functions.
func sortSymbols(nameToSymbol map[string]*Symbol) []*Symbol {
sm := map[internal.SymbolSection][]*Symbol{}
for _, parent := range nameToSymbol {
sm[parent.Section] = append(sm[parent.Section], parent)
cm := map[internal.SymbolKind][]*Symbol{}
parent.Synopsis = strings.TrimSuffix(parent.Synopsis, "{ ... }")
for _, c := range parent.Children {
cm[c.Kind] = append(cm[c.Kind], c)
}
for _, syms := range cm {
sortSymbolsGroup(syms)
}
symbols := append(append(append(
cm[internal.SymbolKindField],
cm[internal.SymbolKindConstant]...),
cm[internal.SymbolKindVariable]...),
cm[internal.SymbolKindFunction]...)
if strings.Contains(parent.Synopsis, "interface") {
parent.Children = append(cm[internal.SymbolKindMethod], symbols...)
} else {
parent.Children = append(symbols, cm[internal.SymbolKindMethod]...)
}
}
for _, syms := range sm {
sortSymbolsGroup(syms)
}
return append(append(append(
sm[internal.SymbolSectionConstants],
sm[internal.SymbolSectionVariables]...),
sm[internal.SymbolSectionFunctions]...),
sm[internal.SymbolSectionTypes]...)
}
func sortSymbolsGroup(syms []*Symbol) {
sort.Slice(syms, func(i, j int) bool {
return syms[i].Synopsis < syms[j].Synopsis
})
for _, s := range syms {
sort.Strings(s.Builds)
}
}

Просмотреть файл

@ -18,6 +18,7 @@ import (
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/symbol"
"golang.org/x/pkgsite/internal/version"
)
@ -77,6 +78,7 @@ type VersionSummary struct {
Version string
Retracted bool
RetractionRationale string
Symbols []*Symbol
}
func fetchVersionsDetails(ctx context.Context, ds internal.DataSource, fullPath, modulePath string) (*VersionsDetails, error) {
@ -89,6 +91,15 @@ func fetchVersionsDetails(ctx context.Context, ds internal.DataSource, fullPath,
if err != nil {
return nil, err
}
outVersionToNameToUnitSymbol := map[string]map[string]*internal.UnitSymbol{}
if experiment.IsActive(ctx, internal.ExperimentSymbolHistoryVersionsPage) {
versionToNameToUnitSymbols, err := db.GetPackageSymbols(ctx, fullPath, modulePath)
if err != nil {
return nil, err
}
outVersionToNameToUnitSymbol = symbol.IntroducedHistory(versionToNameToUnitSymbols)
}
linkify := func(mi *internal.ModuleInfo) string {
// Here we have only version information, but need to construct the full
// import path of the package corresponding to this version.
@ -100,7 +111,7 @@ func fetchVersionsDetails(ctx context.Context, ds internal.DataSource, fullPath,
}
return constructUnitURL(versionPath, mi.ModulePath, linkVersion(mi.Version, mi.ModulePath))
}
return buildVersionDetails(ctx, modulePath, versions, linkify), nil
return buildVersionDetails(ctx, modulePath, versions, outVersionToNameToUnitSymbol, linkify), nil
}
// pathInVersion constructs the full import path of the package corresponding
@ -127,7 +138,11 @@ func pathInVersion(v1Path string, mi *internal.ModuleInfo) string {
// versions tab, organizing major versions into those that have the same module
// path as the package version under consideration, and those that don't. The
// given versions MUST be sorted first by module path and then by semver.
func buildVersionDetails(ctx context.Context, currentModulePath string, modInfos []*internal.ModuleInfo, linkify func(v *internal.ModuleInfo) string) *VersionsDetails {
func buildVersionDetails(ctx context.Context,
currentModulePath string,
modInfos []*internal.ModuleInfo,
versionToNameToSymbol map[string]map[string]*internal.UnitSymbol,
linkify func(v *internal.ModuleInfo) string) *VersionsDetails {
// lists organizes versions by VersionListKey. Note that major version isn't
// sufficient as a key: there are packages contained in the same major
// version of different modules, for example github.com/hashicorp/vault/api,
@ -179,6 +194,9 @@ func buildVersionDetails(ctx context.Context, currentModulePath string, modInfos
vs.Retracted = mi.Retracted
vs.RetractionRationale = mi.RetractionRationale
}
if nts, ok := versionToNameToSymbol[mi.Version]; ok {
vs.Symbols = symbolsForVersion(linkify(mi), nts)
}
if _, ok := lists[key]; !ok {
seenLists = append(seenLists, key)
}

Просмотреть файл

@ -10,6 +10,7 @@ import (
"github.com/google/go-cmp/cmp"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/testing/sample"
"golang.org/x/pkgsite/internal/version"
@ -44,9 +45,6 @@ func versionSummaries(path string, versions []string, linkify func(path, version
}
func TestFetchPackageVersionsDetails(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout*2)
defer cancel()
var (
v2Path = "test.com/module/v2/foo"
v1Path = "test.com/module/foo"
@ -161,6 +159,9 @@ func TestFetchPackageVersionsDetails(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout*2)
ctx = experiment.NewContext(ctx, internal.ExperimentInsertSymbols, internal.ExperimentSymbolHistoryVersionsPage)
defer cancel()
defer postgres.ResetTestDB(testDB, t)
for _, v := range tc.modules {

84
internal/proxy/testdata/symbols@v1.0.0.txtar поставляемый Normal file
Просмотреть файл

@ -0,0 +1,84 @@
A module used for testing the symbols logic.
-- go.mod --
module example.com/symbols
-- README.md --
This is the README for a test module.
-- LICENSE --
$MITLicense
-- symbols.go --
package symbols
// const
const C = 1
// const iota
const (
AA = iota + 1
_
BB
CC
)
type Num int
const (
DD Num = iota
_
EE
FF
)
// var
var V = 2
// Multiple variables on the same line.
var A, B string
// func
func F() {}
// type
type T int
// typeConstant
const CT T = 3
// typeVariable
var VT T
// multi-line var
var (
ErrA = errors.New("error A")
ErrB = errors.New("error B")
)
// typeFunc
func TF() T { return T(0) }
// method
// BUG(uid): this verifies that notes are rendered
func (T) M() {}
type S1 struct {
F int // field
}
type S2 struct {
S1 // embedded struct; should have an id
}
type I1 interface {
M1()
}
type I2 interface {
I1 // embedded interface; should not have an id
}
type (
Int int
)

108
internal/proxy/testdata/symbols@v1.1.0.txtar поставляемый Normal file
Просмотреть файл

@ -0,0 +1,108 @@
A module used for testing the symbols logic.
-- go.mod --
module example.com/symbols
-- README.md --
This is the README for a test module.
-- LICENSE --
$MITLicense
-- symbols.go --
package symbols
// const
const C = 1
// const iota
const (
AA = iota + 1
_
BB
CC
)
type Num int
const (
DD Num = iota
_
EE
FF
)
// var
var V = 2
// Multiple variables on the same line.
var A, B string
// func
func F() {}
// type
type T int
// typeConstant
const CT T = 3
// typeVariable
var VT T
// multi-line var
var (
ErrA = errors.New("error A")
ErrB = errors.New("error B")
)
// typeFunc
func TF() T { return T(0) }
// method
// BUG(uid): this verifies that notes are rendered
func (T) M() {}
type S1 struct {
F int // field
}
type S2 struct {
S1 // embedded struct; should have an id
G int
}
type I1 interface {
M1()
}
type I2 interface {
I1 // embedded interface; should not have an id
M2()
}
type (
Int int
String bool
)
-- hello/hello.go --
// +build linux darwin
// +build amd64
package hello
// Hello returns a greeting.
func Hello() string {
return "Hello"
}
-- hello/hello_js.go --
// +build js,wasm
package hello
// HelloJS returns a greeting when the build context is js/wasm.
func HelloJS() string {
return "Hello"
}

95
internal/proxy/testdata/symbols@v1.2.0.txtar поставляемый Normal file
Просмотреть файл

@ -0,0 +1,95 @@
A module used for testing the symbols logic.
-- go.mod --
module example.com/symbols
-- README.md --
This is the README for a test module.
-- LICENSE --
$MITLicense
-- symbols.go --
package symbols
// const
const C = 1
// const iota
const (
AA = iota + 1
_
BB
CC
)
type Num int
const (
DD Num = iota
_
EE
FF
)
// var
var V = 2
// Multiple variables on the same line.
var A, B string
// func
func F() {}
// type
type T int
// typeConstant
const CT T = 3
// typeVariable
var VT T
// multi-line var
var (
ErrA = errors.New("error A")
ErrB = errors.New("error B")
)
// typeFunc
func TF() T { return T(0) }
// method
// BUG(uid): this verifies that notes are rendered
func (T) M() {}
type S1 struct {
F int // field
}
type S2 struct {
S1 // embedded struct; should have an id
G int
}
type I1 interface {
M1()
}
type I2 interface {
I1 // embedded interface; should not have an id
M2()
}
type (
Int int
String bool
)
-- hello/hello.go --
package hello
// Hello returns a greeting.
func Hello() string {
return "Hello"
}

Просмотреть файл

@ -29,7 +29,7 @@ const (
)
// Symbol is an element in the package API. A symbol can be a constant,
// variable, function of type.
// variable, function, or type.
type Symbol struct {
// Name is the name of the symbol.
Name string

Просмотреть файл

@ -0,0 +1,354 @@
// Copyright 2021 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 integration
import "golang.org/x/pkgsite/internal/frontend"
var versionsPageHello = []*frontend.VersionList{
{
VersionListKey: frontend.VersionListKey{
ModulePath: "example.com/symbols",
Major: "v1",
},
Versions: []*frontend.VersionSummary{
{
CommitTime: "Jan 30, 2019",
Link: "/example.com/symbols@v1.2.0/hello",
Retracted: false,
RetractionRationale: "",
Version: "v1.2.0",
Symbols: []*frontend.Symbol{
{
Name: "Hello",
Synopsis: "func Hello() string",
Link: "/example.com/symbols@v1.2.0/hello#Hello",
New: true,
Section: "Functions",
Kind: "Function",
},
},
},
{
CommitTime: "Jan 30, 2019",
Link: "/example.com/symbols@v1.1.0/hello",
Retracted: false,
RetractionRationale: "",
Version: "v1.1.0",
Symbols: []*frontend.Symbol{
{
Name: "Hello",
Synopsis: "func Hello() string",
Link: "/example.com/symbols@v1.1.0/hello#Hello",
New: true,
Section: "Functions",
Kind: "Function",
Builds: []string{"darwin/amd64", "linux/amd64"},
},
{
Name: "HelloJS",
Synopsis: "func HelloJS() string",
Link: "/example.com/symbols@v1.1.0/hello#HelloJS",
New: true,
Section: "Functions",
Kind: "Function",
Builds: []string{"js/wasm"},
},
},
},
},
},
}
var versionsPageSymbols = []*frontend.VersionList{
{
VersionListKey: frontend.VersionListKey{
ModulePath: "example.com/symbols",
Major: "v1",
},
Versions: []*frontend.VersionSummary{
{
CommitTime: "Jan 30, 2019",
Link: "/example.com/symbols@v1.2.0",
Version: "v1.2.0",
},
{
CommitTime: "Jan 30, 2019",
Link: "/example.com/symbols@v1.1.0",
Version: "v1.1.0",
Symbols: []*frontend.Symbol{
{
Name: "I2",
Synopsis: "type I2 interface",
Link: "/example.com/symbols@v1.1.0#I2",
Section: "Types",
Kind: "Type",
Children: []*frontend.Symbol{
{
Name: "I2.M2",
Synopsis: "M2 func()",
Link: "/example.com/symbols@v1.1.0#I2.M2",
New: true,
Section: "Types",
Kind: "Method",
},
},
},
{
Name: "S2",
Synopsis: "type S2 struct",
Link: "/example.com/symbols@v1.1.0#S2",
Section: "Types",
Kind: "Type",
Children: []*frontend.Symbol{
{
Name: "S2.G",
Synopsis: "G int",
Link: "/example.com/symbols@v1.1.0#S2.G",
New: true,
Section: "Types",
Kind: "Field",
},
},
},
{
Name: "String",
Synopsis: "type String bool",
Link: "/example.com/symbols@v1.1.0#String",
New: true,
Section: "Types",
Kind: "Type",
},
},
},
{
CommitTime: "Jan 30, 2019",
Link: "/example.com/symbols@v1.0.0",
Retracted: false,
RetractionRationale: "",
Version: "v1.0.0",
Symbols: []*frontend.Symbol{
{
Name: "AA",
Synopsis: "const AA",
Link: "/example.com/symbols@v1.0.0#AA",
New: true,
Section: "Constants",
Kind: "Constant",
},
{
Name: "BB",
Synopsis: "const BB",
Link: "/example.com/symbols@v1.0.0#BB",
New: true,
Section: "Constants",
Kind: "Constant",
},
{
Name: "C",
Synopsis: "const C",
Link: "/example.com/symbols@v1.0.0#C",
New: true,
Section: "Constants",
Kind: "Constant",
},
{
Name: "CC",
Synopsis: "const CC",
Link: "/example.com/symbols@v1.0.0#CC",
New: true,
Section: "Constants",
Kind: "Constant",
},
{
Name: "A",
Synopsis: "var A string",
Link: "/example.com/symbols@v1.0.0#A",
New: true,
Section: "Variables",
Kind: "Variable",
},
{
Name: "B",
Synopsis: "var B string",
Link: "/example.com/symbols@v1.0.0#B",
New: true,
Section: "Variables",
Kind: "Variable",
},
{
Name: "ErrA",
Synopsis: `var ErrA = errors.New("error A")`,
Link: "/example.com/symbols@v1.0.0#ErrA",
New: true,
Section: "Variables",
Kind: "Variable",
},
{
Name: "ErrB",
Synopsis: `var ErrB = errors.New("error B")`,
Link: "/example.com/symbols@v1.0.0#ErrB",
New: true,
Section: "Variables",
Kind: "Variable",
},
{
Name: "V",
Synopsis: "var V = 2",
Link: "/example.com/symbols@v1.0.0#V",
New: true,
Section: "Variables",
Kind: "Variable",
},
{
Name: "F",
Synopsis: "func F()",
Link: "/example.com/symbols@v1.0.0#F",
New: true,
Section: "Functions",
Kind: "Function",
},
{
Name: "I1",
Synopsis: "type I1 interface",
Link: "/example.com/symbols@v1.0.0#I1",
New: true,
Section: "Types",
Kind: "Type",
Children: []*frontend.Symbol{
{
Name: "I1.M1",
Synopsis: "M1 func()",
Link: "/example.com/symbols@v1.0.0#I1.M1",
New: true,
Section: "Types",
Kind: "Method",
},
},
},
{
Name: "I2",
Synopsis: "type I2 interface",
Link: "/example.com/symbols@v1.0.0#I2",
New: true,
Section: "Types",
Kind: "Type",
},
{
Name: "Int",
Synopsis: "type Int int",
Link: "/example.com/symbols@v1.0.0#Int",
New: true,
Section: "Types",
Kind: "Type",
},
{
Name: "Num",
Synopsis: "type Num int",
Link: "/example.com/symbols@v1.0.0#Num",
New: true,
Section: "Types",
Kind: "Type",
Children: []*frontend.Symbol{
{
Name: "DD",
Synopsis: "const DD",
Link: "/example.com/symbols@v1.0.0#DD",
New: true,
Section: "Types",
Kind: "Constant",
},
{
Name: "EE",
Synopsis: "const EE",
Link: "/example.com/symbols@v1.0.0#EE",
New: true,
Section: "Types",
Kind: "Constant",
},
{
Name: "FF",
Synopsis: "const FF",
Link: "/example.com/symbols@v1.0.0#FF",
New: true,
Section: "Types",
Kind: "Constant",
},
},
},
{
Name: "S1",
Synopsis: "type S1 struct",
Link: "/example.com/symbols@v1.0.0#S1",
New: true,
Section: "Types",
Kind: "Type",
Children: []*frontend.Symbol{
{
Name: "S1.F",
Synopsis: "F int",
Link: "/example.com/symbols@v1.0.0#S1.F",
New: true,
Section: "Types",
Kind: "Field",
},
},
},
{
Name: "S2",
Synopsis: "type S2 struct",
Link: "/example.com/symbols@v1.0.0#S2",
New: true,
Section: "Types",
Kind: "Type",
},
{
Name: "T",
Synopsis: "type T int",
Link: "/example.com/symbols@v1.0.0#T",
New: true,
Section: "Types",
Kind: "Type",
Children: []*frontend.Symbol{
{
Name: "CT",
Synopsis: "const CT",
Link: "/example.com/symbols@v1.0.0#CT",
New: true,
Section: "Types",
Kind: "Constant",
},
{
Name: "VT",
Synopsis: "var VT T",
Link: "/example.com/symbols@v1.0.0#VT",
New: true,
Section: "Types",
Kind: "Variable",
},
{
Name: "TF",
Synopsis: "func TF() T",
Link: "/example.com/symbols@v1.0.0#TF",
New: true,
Section: "Types",
Kind: "Function",
},
{
Name: "T.M",
Synopsis: "func (T) M()",
Link: "/example.com/symbols@v1.0.0#T.M",
New: true,
Section: "Types",
Kind: "Method",
},
},
},
},
},
},
},
}

Просмотреть файл

@ -39,9 +39,13 @@ func TestFrontendDocRender(t *testing.T) {
}
func getDoc(t *testing.T, modulePath string, exps ...string) string {
return getFrontendPage(t, "/"+modulePath, exps...)
}
func getFrontendPage(t *testing.T, urlPath string, exps ...string) string {
ctx := experiment.NewContext(context.Background(), exps...)
ts := setupFrontend(ctx, t, nil, nil)
url := ts.URL + "/" + modulePath
url := ts.URL + urlPath
resp, err := http.Get(url)
if err != nil {
t.Fatalf("%s: %v", url, err)

Просмотреть файл

@ -36,6 +36,7 @@ func setupFrontend(ctx context.Context, t *testing.T, q queue.Queue, rc *redis.C
ThirdPartyPath: "../../../third_party",
AppVersionLabel: "",
Queue: q,
ServeStats: true,
})
if err != nil {
t.Fatal(err)

Просмотреть файл

@ -0,0 +1,51 @@
// Copyright 2021 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 integration
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/frontend"
"golang.org/x/pkgsite/internal/postgres"
)
func TestFrontendVersionsPage(t *testing.T) {
defer postgres.ResetTestDB(testDB, t)
exps := []string{
internal.ExperimentInsertSymbols,
internal.ExperimentSymbolHistoryVersionsPage,
}
processVersions(
experiment.NewContext(context.Background(), exps...),
t, testModules)
const modulePath = "example.com/symbols"
for _, test := range []struct {
name, pkgPath string
want []*frontend.VersionList
}{
{"versions page symbols - one version all symbols", modulePath, versionsPageSymbols},
{"versions page hello - multi GOOS", modulePath + "/hello", versionsPageHello},
} {
t.Run(test.name, func(t *testing.T) {
urlPath := fmt.Sprintf("/%s?tab=versions&m=json", test.pkgPath)
body := getFrontendPage(t, urlPath, exps...)
var got frontend.VersionsDetails
if err := json.Unmarshal([]byte(body), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if diff := cmp.Diff(test.want, got.ThisModule); diff != "" {
t.Errorf("mismatch (-want, got):\n%s", diff)
}
})
}
}