| // Copyright 2019 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 sample provides functionality for generating sample values of |
| // the types contained in the internal package. |
| package sample |
| |
| import ( |
| "context" |
| "fmt" |
| "go/parser" |
| "go/token" |
| "math" |
| "net/http" |
| "path" |
| "strings" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| "github.com/google/licensecheck" |
| "golang.org/x/pkgsite/internal" |
| "golang.org/x/pkgsite/internal/godoc" |
| "golang.org/x/pkgsite/internal/licenses" |
| "golang.org/x/pkgsite/internal/source" |
| "golang.org/x/pkgsite/internal/stdlib" |
| ) |
| |
| // These sample values can be used to construct test cases. |
| var ( |
| ModulePath = "github.com/valid/module_name" |
| RepositoryURL = "https://github.com/valid/module_name" |
| VersionString = "v1.0.0" |
| CommitTime = NowTruncated() |
| LicenseType = "MIT" |
| LicenseFilePath = "LICENSE" |
| NonRedistributableLicense = &licenses.License{ |
| Metadata: &licenses.Metadata{ |
| FilePath: "NONREDIST_LICENSE", |
| Types: []string{"UNKNOWN"}, |
| }, |
| Contents: []byte(`unknown`), |
| } |
| PackageName = "foo" |
| Suffix = "foo" |
| PackagePath = path.Join(ModulePath, Suffix) |
| V1Path = PackagePath |
| ReadmeFilePath = "README.md" |
| ReadmeContents = "readme" |
| GOOS = internal.All |
| GOARCH = internal.All |
| Doc = Documentation(GOOS, GOARCH, DocContents) |
| API = []*internal.Symbol{ |
| Constant, |
| Variable, |
| Function, |
| Type, |
| } |
| DocContents = ` |
| // Package p is a package. |
| // |
| // |
| // Links |
| // |
| // - pkg.go.dev, https://pkg.go.dev |
| package p |
| var V int |
| ` |
| Constant = &internal.Symbol{ |
| SymbolMeta: internal.SymbolMeta{ |
| Name: "Constant", |
| Synopsis: "const Constant", |
| Section: internal.SymbolSectionConstants, |
| Kind: internal.SymbolKindConstant, |
| }, |
| GOOS: internal.All, |
| GOARCH: internal.All, |
| } |
| Variable = &internal.Symbol{ |
| SymbolMeta: internal.SymbolMeta{ |
| Name: "Variable", |
| Synopsis: "var Variable", |
| Section: internal.SymbolSectionVariables, |
| Kind: internal.SymbolKindVariable, |
| }, |
| GOOS: internal.All, |
| GOARCH: internal.All, |
| } |
| Function = &internal.Symbol{ |
| SymbolMeta: internal.SymbolMeta{ |
| Name: "Function", |
| Synopsis: "func Function() error", |
| Section: internal.SymbolSectionFunctions, |
| Kind: internal.SymbolKindFunction, |
| }, |
| GOOS: internal.All, |
| GOARCH: internal.All, |
| } |
| FunctionNew = &internal.Symbol{ |
| SymbolMeta: internal.SymbolMeta{ |
| Name: "New", |
| Synopsis: "func New() *Type", |
| Section: internal.SymbolSectionTypes, |
| Kind: internal.SymbolKindFunction, |
| ParentName: "Type", |
| }, |
| GOOS: internal.All, |
| GOARCH: internal.All, |
| } |
| Type = &internal.Symbol{ |
| SymbolMeta: internal.SymbolMeta{ |
| Name: "Type", |
| Synopsis: "type Type struct", |
| Section: internal.SymbolSectionTypes, |
| Kind: internal.SymbolKindType, |
| }, |
| GOOS: internal.All, |
| GOARCH: internal.All, |
| Children: []*internal.SymbolMeta{ |
| &FunctionNew.SymbolMeta, |
| &Field, |
| &Method, |
| }, |
| } |
| Field = internal.SymbolMeta{ |
| Name: "Type.Field", |
| Synopsis: "field", |
| Section: internal.SymbolSectionTypes, |
| Kind: internal.SymbolKindField, |
| ParentName: "Type", |
| } |
| Method = internal.SymbolMeta{ |
| Name: "Type.Method", |
| Synopsis: "method", |
| Section: internal.SymbolSectionTypes, |
| Kind: internal.SymbolKindMethod, |
| ParentName: "Type", |
| } |
| ) |
| |
| // LicenseCmpOpts are options to use when comparing licenses with the cmp package. |
| var LicenseCmpOpts = []cmp.Option{ |
| cmp.Comparer(coveragePercentEqual), |
| cmpopts.IgnoreFields(licensecheck.Match{}, "Start", "End"), |
| } |
| |
| // coveragePercentEqual considers two floats the same if they are within 4 |
| // percentage points, and both are on the same side of 90% (our threshold). |
| func coveragePercentEqual(a, b float64) bool { |
| if (a >= 90) != (b >= 90) { |
| return false |
| } |
| return math.Abs(a-b) <= 4 |
| } |
| |
| // NowTruncated returns time.Now() truncated to Microsecond precision. |
| // |
| // This makes it easier to work with timestamps in PostgreSQL, which have |
| // Microsecond precision: |
| // |
| // https://www.postgresql.org/docs/9.1/datatype-datetime.html |
| func NowTruncated() time.Time { |
| return time.Now().In(time.UTC).Truncate(time.Microsecond) |
| } |
| |
| func DefaultModule() *internal.Module { |
| fp := constructFullPath(ModulePath, Suffix) |
| return AddPackage(Module(ModulePath, VersionString), UnitForPackage(fp, ModulePath, VersionString, path.Base(fp), true)) |
| } |
| |
| // Module creates a Module with the given path and version. |
| // The list of suffixes is used to create Units within the module. |
| func Module(modulePath, version string, suffixes ...string) *internal.Module { |
| mi := ModuleInfo(modulePath, version) |
| m := &internal.Module{ |
| ModuleInfo: *mi, |
| Licenses: Licenses(), |
| } |
| m.Units = []*internal.Unit{UnitForModuleRoot(mi)} |
| for _, s := range suffixes { |
| fp := constructFullPath(modulePath, s) |
| lp := UnitForPackage(fp, modulePath, VersionString, path.Base(fp), m.IsRedistributable) |
| if s != "" { |
| AddPackage(m, lp) |
| } else { |
| u := UnitForPackage(lp.Path, modulePath, version, lp.Name, lp.IsRedistributable) |
| m.Units[0].Documentation = u.Documentation |
| m.Units[0].Name = u.Name |
| } |
| } |
| if modulePath == stdlib.ModulePath { |
| m.Units[0].Readme = nil |
| } |
| // Fill in license contents. |
| for _, u := range m.Units { |
| u.LicenseContents = m.Licenses |
| } |
| return m |
| } |
| |
| func UnitForModuleRoot(m *internal.ModuleInfo) *internal.Unit { |
| u := &internal.Unit{ |
| UnitMeta: *UnitMeta(m.ModulePath, m.ModulePath, m.Version, "", m.IsRedistributable), |
| LicenseContents: Licenses(), |
| } |
| u.Readme = &internal.Readme{ |
| Filepath: ReadmeFilePath, |
| Contents: ReadmeContents, |
| } |
| return u |
| } |
| |
| // UnitForPackage constructs a unit with the given module path and suffix. |
| // |
| // If modulePath is the standard library, the package path is the |
| // suffix, which must not be empty. Otherwise, the package path |
| // is the concatenation of modulePath and suffix. |
| // |
| // The package name is last component of the package path. |
| func UnitForPackage(path, modulePath, version, name string, isRedistributable bool) *internal.Unit { |
| // Copy Doc because some tests modify it. |
| doc := *Doc |
| imps := Imports() |
| return &internal.Unit{ |
| UnitMeta: *UnitMeta(path, modulePath, version, name, isRedistributable), |
| Documentation: []*internal.Documentation{&doc}, |
| BuildContexts: []internal.BuildContext{{GOOS: doc.GOOS, GOARCH: doc.GOARCH}}, |
| LicenseContents: Licenses(), |
| Imports: imps, |
| NumImports: len(imps), |
| } |
| } |
| |
| func AddPackage(m *internal.Module, pkg *internal.Unit) *internal.Module { |
| if m.ModulePath != stdlib.ModulePath && !strings.HasPrefix(pkg.Path, m.ModulePath) { |
| panic(fmt.Sprintf("package path %q not a prefix of module path %q", |
| pkg.Path, m.ModulePath)) |
| } |
| AddUnit(m, UnitForPackage(pkg.Path, m.ModulePath, m.Version, pkg.Name, pkg.IsRedistributable)) |
| minLen := len(m.ModulePath) |
| if m.ModulePath == stdlib.ModulePath { |
| minLen = 1 |
| } |
| for pth := pkg.Path; len(pth) > minLen; pth = path.Dir(pth) { |
| found := false |
| for _, u := range m.Units { |
| if u.Path == pth { |
| found = true |
| break |
| } |
| } |
| if !found { |
| AddUnit(m, UnitEmpty(pth, m.ModulePath, m.Version)) |
| } |
| } |
| return m |
| } |
| |
| func PackageMeta(fullPath string) *internal.PackageMeta { |
| return &internal.PackageMeta{ |
| Path: fullPath, |
| IsRedistributable: true, |
| Name: path.Base(fullPath), |
| Synopsis: Doc.Synopsis, |
| Licenses: LicenseMetadata(), |
| } |
| } |
| |
| func ModuleInfo(modulePath, versionString string) *internal.ModuleInfo { |
| return &internal.ModuleInfo{ |
| ModulePath: modulePath, |
| Version: versionString, |
| CommitTime: CommitTime, |
| // Assume the module path is a GitHub-like repo name. |
| SourceInfo: source.NewGitHubInfo("https://"+modulePath, "", versionString), |
| IsRedistributable: true, |
| HasGoMod: true, |
| } |
| } |
| |
| func DefaultVersionMap() *internal.VersionMap { |
| return &internal.VersionMap{ |
| ModulePath: ModulePath, |
| RequestedVersion: VersionString, |
| ResolvedVersion: VersionString, |
| Status: http.StatusOK, |
| GoModPath: "", |
| Error: "", |
| } |
| } |
| |
| func AddUnit(m *internal.Module, u *internal.Unit) { |
| for _, e := range m.Units { |
| if e.Path == u.Path { |
| panic(fmt.Sprintf("module already has path %q", e.Path)) |
| } |
| } |
| m.Units = append(m.Units, u) |
| } |
| |
| func AddLicense(m *internal.Module, lic *licenses.License) { |
| m.Licenses = append(m.Licenses, lic) |
| dir := path.Dir(lic.FilePath) |
| if dir == "." { |
| dir = "" |
| } |
| for _, u := range m.Units { |
| if strings.TrimPrefix(u.Path, m.ModulePath+"/") == dir { |
| u.Licenses = append(u.Licenses, lic.Metadata) |
| u.LicenseContents = append(u.LicenseContents, lic) |
| } |
| } |
| } |
| |
| // ReplaceLicense replaces all licenses having the same file path as lic with lic. |
| func ReplaceLicense(m *internal.Module, lic *licenses.License) { |
| replaceLicense(lic, m.Licenses) |
| for _, u := range m.Units { |
| for i, lm := range u.Licenses { |
| if lm.FilePath == lic.FilePath { |
| u.Licenses[i] = lic.Metadata |
| } |
| } |
| replaceLicense(lic, u.LicenseContents) |
| } |
| } |
| |
| func replaceLicense(lic *licenses.License, lics []*licenses.License) { |
| for i, l := range lics { |
| if l.FilePath == lic.FilePath { |
| lics[i] = lic |
| } |
| } |
| } |
| |
| func UnitEmpty(path, modulePath, version string) *internal.Unit { |
| return &internal.Unit{ |
| UnitMeta: *UnitMeta(path, modulePath, version, "", true), |
| } |
| } |
| |
| func UnitMeta(path, modulePath, version, name string, isRedistributable bool) *internal.UnitMeta { |
| return &internal.UnitMeta{ |
| Path: path, |
| Name: name, |
| IsRedistributable: isRedistributable, |
| Licenses: LicenseMetadata(), |
| ModuleInfo: internal.ModuleInfo{ |
| ModulePath: modulePath, |
| Version: version, |
| CommitTime: NowTruncated(), |
| IsRedistributable: isRedistributable, |
| SourceInfo: source.NewGitHubInfo("https://"+modulePath, "", version), |
| }, |
| } |
| } |
| |
| func constructFullPath(modulePath, suffix string) string { |
| if modulePath != stdlib.ModulePath { |
| return path.Join(modulePath, suffix) |
| } |
| return suffix |
| } |
| |
| // Documentation returns a Documentation value for the given Go source. |
| // It panics if there are errors parsing or encoding the source. |
| func Documentation(goos, goarch, fileContents string) *internal.Documentation { |
| fset := token.NewFileSet() |
| pf, err := parser.ParseFile(fset, "sample.go", fileContents, parser.ParseComments) |
| if err != nil { |
| panic(err) |
| } |
| docPkg := godoc.NewPackage(fset, nil) |
| docPkg.AddFile(pf, true) |
| src, err := docPkg.Encode(context.Background()) |
| if err != nil { |
| panic(err) |
| } |
| |
| return &internal.Documentation{ |
| GOOS: goos, |
| GOARCH: goarch, |
| Synopsis: fmt.Sprintf("This is a package synopsis for GOOS=%s, GOARCH=%s", goos, goarch), |
| Source: src, |
| } |
| } |
| |
| func LicenseMetadata() []*licenses.Metadata { |
| return []*licenses.Metadata{ |
| { |
| Types: []string{LicenseType}, |
| FilePath: LicenseFilePath, |
| Coverage: licensecheck.Coverage{ |
| Percent: 100, |
| Match: []licensecheck.Match{{ID: "MIT", Type: 0}}, |
| }, |
| }, |
| } |
| } |
| |
| func Licenses() []*licenses.License { |
| return []*licenses.License{ |
| {Metadata: LicenseMetadata()[0], Contents: []byte(`Lorem Ipsum`)}, |
| } |
| } |
| |
| func Imports() []string { |
| return []string{"fmt", "path/to/bar"} |
| } |