whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,536 @@
// Copyright 2023 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.
//go:generate go run . -w
//go:build go1.22
// Package configgen generates the upload config file stored in the config.json
// file of golang.org/x/telemetry/config based on the chartconfig stored in
// config.txt.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"go/version"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"golang.org/x/mod/semver"
"golang.org/x/telemetry/internal/chartconfig"
"golang.org/x/telemetry/internal/telemetry"
)
var (
write = flag.Bool("w", false, "if set, write the config file; otherwise, print to stdout")
force = flag.Bool("f", false, "if set, force the write of the config file even if the current content is still valid")
// SamplingRate is the fraction of otherwise uploadable reports that will be uploaded
SamplingRate = 1.0
)
func main() {
flag.Parse()
gcfgs, err := chartconfig.Load()
if err != nil {
log.Fatal(err)
}
// The padding heuristics below are based on the example of gopls.
//
// The goal is to pad enough versions for a quarter.
uCfg, err := generate(gcfgs, padding{
// 6 releases into the future translates to approximately three months for gopls.
releases: 6,
// We may release gopls 1.0, but won't release 2.0 in a three month timespan!
maj: 1,
// We don't usually do more than one minor release a month.
majmin: 3,
// Since golang/go#55267, which committed to adhering to semver, gopls
// hasn't had more than 5 patches per minor version.
patch: 6,
// Gopls has never had more than 4 prereleases.
pre: 4,
})
if err != nil {
log.Fatal(err)
}
cfgJSON, err := json.MarshalIndent(uCfg, "", "\t")
if err != nil {
log.Fatal(err)
}
if !*write {
fmt.Println(string(cfgJSON))
os.Exit(0)
}
configFile, err := configFile()
if err != nil {
log.Fatalf("finding config file: %v", err)
}
if !*force {
currentCfg, err := readConfig(configFile)
if err != nil {
log.Fatal(err)
}
// Guarantee that we have enough padding to do two patches releases tomorrow.
minCfg, err := generate(gcfgs, padding{
releases: 2,
maj: 1,
majmin: 1, // we're not ever going to do more than one major/minor release in a day
patch: 2,
pre: 2, // in a single day, we wouldn't prep more than two prereleases per version
})
if err != nil {
log.Fatal(err)
}
if contains(currentCfg, minCfg) {
fmt.Fprintln(os.Stderr, "not writing the config file as it is still valid; use -f to force")
os.Exit(0)
}
}
if err := os.WriteFile(configFile, cfgJSON, 0666); err != nil {
log.Fatal(err)
}
}
// configFile returns the path to the x/telemetry/config config.json file in
// this repo.
//
// The file must already exist: this won't be a valid location if running from
// the module cache; this functionality only works when executed from the
// telemetry repo.
func configFile() (string, error) {
out, err := exec.Command("go", "list", "-f", "{{.Dir}}", "golang.org/x/telemetry/internal/configgen").Output()
if err != nil {
return "", err
}
dir := strings.TrimSpace(string(out))
configFile := filepath.Join(dir, "..", "..", "config", "config.json")
if _, err := os.Stat(configFile); err != nil {
return "", err
}
return configFile, nil
}
func readConfig(file string) (*telemetry.UploadConfig, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("reading config file: %v", err)
}
cfg := new(telemetry.UploadConfig)
if err := json.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("unmarshalling config file: %v", err)
}
return cfg, nil
}
// generate computes the upload config from chart configs and module
// information, returning the resulting formatted JSON.
func generate(gcfgs []chartconfig.ChartConfig, padding padding) (*telemetry.UploadConfig, error) {
ucfg := &telemetry.UploadConfig{
GOOS: goos(),
GOARCH: goarch(),
// the probability of uploading a report
SampleRate: SamplingRate,
}
var err error
ucfg.GoVersion, err = goVersions()
if err != nil {
return nil, fmt.Errorf("querying go info: %v", err)
}
for i, r := range gcfgs {
if err := ValidateChartConfig(r); err != nil {
// TODO(rfindley): this is a poor way to identify the faulty record. We
// should probably store position information in the ChartConfig.
return nil, fmt.Errorf("chart config #%d (%q): %v", i, r.Title, err)
}
}
var (
programs = make(map[string]*telemetry.ProgramConfig) // package path -> config
minVersions = make(map[string]string) // package path -> min version required, or "" for all
)
for _, gcfg := range gcfgs {
pcfg := programs[gcfg.Program]
if pcfg == nil {
pcfg = &telemetry.ProgramConfig{
Name: gcfg.Program,
}
programs[gcfg.Program] = pcfg
minVersions[gcfg.Program] = gcfg.Version
}
minVersions[gcfg.Program] = minVersion(minVersions[gcfg.Program], gcfg.Version)
ccfg := telemetry.CounterConfig{
Name: gcfg.Counter,
Rate: 1.0, // TODO(rfindley): how should rate be configured?
Depth: gcfg.Depth,
}
if gcfg.Depth > 0 {
pcfg.Stacks = append(pcfg.Stacks, ccfg)
} else {
pcfg.Counters = append(pcfg.Counters, ccfg)
}
}
for _, p := range programs {
minVersion := minVersions[p.Name]
// Collect eligible program versions. If p is a toolchain tool (cmd/go,
// cmd/compile, etc), these come out of the Go versions queried above.
// Otherwise, they come from the proxy.
//
// In both of these cases, the versions should be valid, but we verify
// anyway as otherwise the version comparison is meaningless.
// (and in fact, there is an invalid go1.9.2rc2 version in the proxy)
if telemetry.IsToolchainProgram(p.Name) {
// Note: no need to pad versions for toolchain programs, since the
// toolchain is released infrequently.
// (and in any case, version padding only works for semantic versions)
for _, v := range ucfg.GoVersion {
if !version.IsValid(v) {
// The proxy toolchain versions list go1.9.2rc2, which is invalid.
// Skip it.
continue
}
if minVersion == "" || version.Compare(minVersion, v) <= 0 {
p.Versions = append(p.Versions, v)
}
}
} else {
versions, err := listProxyVersions(p.Name)
if err != nil {
return nil, fmt.Errorf("listing versions for %q: %v", p.Name, err)
}
// Filter proxy versions in place.
i := 0
for _, v := range versions {
if !semver.IsValid(v) {
return nil, fmt.Errorf("invalid semver %q returned from proxy for %q", v, p.Name)
}
if minVersion == "" || semver.Compare(minVersion, v) <= 0 {
versions[i] = v
i++
}
}
p.Versions = padVersions(versions[:i], prereleasesForProgram(p.Name), padding)
}
ucfg.Programs = append(ucfg.Programs, p)
}
sort.Slice(ucfg.Programs, func(i, j int) bool {
return ucfg.Programs[i].Name < ucfg.Programs[j].Name
})
return ucfg, nil
}
// contains reports whether outer contains all program versions of inner, and
// is otherwise equivalent to inner.
func contains(outer, inner *telemetry.UploadConfig) bool {
if !slices.Equal(outer.GOARCH, inner.GOARCH) {
return false
}
if !slices.Equal(outer.GOOS, inner.GOOS) {
return false
}
if !slices.Equal(outer.GoVersion, inner.GoVersion) {
return false
}
for _, pi := range inner.Programs {
i := slices.IndexFunc(outer.Programs, func(po *telemetry.ProgramConfig) bool {
return po.Name == pi.Name
})
if i < 0 {
return false
}
po := outer.Programs[i]
if !sliceContains(po.Versions, pi.Versions) {
return false
}
if !slices.Equal(po.Counters, pi.Counters) {
return false
}
if !slices.Equal(po.Stacks, pi.Stacks) {
return false
}
}
for _, po := range outer.Programs {
if !slices.ContainsFunc(inner.Programs, func(pi *telemetry.ProgramConfig) bool {
return pi.Name == po.Name
}) {
return false
}
}
return true
}
func sliceContains[T comparable](outer, inner []T) bool {
m := toMap(outer)
for _, v := range inner {
if !m[v] {
return false
}
}
return true
}
func toMap[T comparable](s []T) map[T]bool {
m := make(map[T]bool)
for _, v := range s {
m[v] = true
}
return m
}
// prereleasesForProgram returns the set of prereleases to use for the provided
// program. We may need to customize this for the conventions of different
// programs.
func prereleasesForProgram(program string) []string {
// Surely eight prereleases is enough for any program... :)
return []string{"pre.1", "pre.2", "pre.3", "pre.4", "pre.5", "pre.6", "pre.7", "pre.8"}
}
// minVersion returns the lesser semantic version of v1 and v2.
//
// As a special case, the empty string is treated as an absolute minimum
// (empty => all versions are greater).
func minVersion(v1, v2 string) string {
if v1 == "" || v2 == "" {
return ""
}
if semver.Compare(v1, v2) > 0 {
return v2
}
return v1
}
// goos returns a sorted slice of known GOOS values.
func goos() []string {
var gooses []string
for goos := range knownOS {
gooses = append(gooses, goos)
}
sort.Strings(gooses)
return gooses
}
// goarch returns a sorted slice of known GOARCH values.
func goarch() []string {
var arches []string
for arch := range knownArch {
arches = append(arches, arch)
}
sort.Strings(arches)
return arches
}
// goInfo queries the proxy for information about go distributions, including
// versions, GOOS, and GOARCH values.
func goVersions() ([]string, error) {
// Trick: read Go distribution information from the module versions of
// golang.org/toolchain. These define the set of valid toolchains, and
// therefore are a reasonable source for version information.
//
// A more authoritative source for this information may be
// https://go.dev/dl?mode=json&include=all.
proxyVersions, err := listProxyVersions("golang.org/toolchain")
if err != nil {
return nil, fmt.Errorf("listing toolchain versions: %v", err)
}
var goVersionRx = regexp.MustCompile(`^-(go.+)\.[^.]+-[^.]+$`)
verSet := make(map[string]struct{})
for _, v := range proxyVersions {
pre := semver.Prerelease(v)
match := goVersionRx.FindStringSubmatch(pre)
if match == nil {
return nil, fmt.Errorf("proxy version %q does not match prerelease regexp %q", v, goVersionRx)
}
verSet[match[1]] = struct{}{}
}
var vers []string
for v := range verSet {
vers = append(vers, v)
}
sort.Sort(byGoVersion(vers))
return vers, nil
}
type byGoVersion []string
func (vs byGoVersion) Len() int { return len(vs) }
func (vs byGoVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
func (vs byGoVersion) Less(i, j int) bool {
cmp := version.Compare(vs[i], vs[j])
if cmp != 0 {
return cmp < 0
}
// To ensure that we have a stable sort, order equivalent Go versions lexically.
return vs[i] < vs[j]
}
// versionsForTesting contains versions to use for testing, rather than
// querying the proxy.
var versionsForTesting map[string][]string
// listProxyVersions queries the Go module mirror for published versions of the
// given modulePath.
//
// modulePath must be lower-case (or already escaped): this function doesn't do
// any escaping of upper-cased letters, as is required by the proxy prototol
// (https://go.dev/ref/mod#goproxy-protocol).
func listProxyVersions(modulePath string) ([]string, error) {
if vers, ok := versionsForTesting[modulePath]; ok {
return vers, nil
}
cmd := exec.Command("go", "list", "-m", "--versions", modulePath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("listing versions: %v (stderr: %v)", err, stderr.String())
}
fields := strings.Fields(strings.TrimSpace(string(out)))
if len(fields) == 0 {
return nil, fmt.Errorf("invalid version list output: %q", string(out))
}
return fields[1:], nil
}
// padding defines constraints on additional versions to pad.
//
// These constraints help restrict version padding to "reasonable" versions,
// based on heuristics such as "we never do more than 3 prereleases for a given
// version" or "we never have more than 5 patch versions" or "we can't do more
// than 10 total releases over that time period". See the field documentation
// for details.
type padding struct {
releases int // bounds on the total number of releases
maj int // bounds the number of new major versions
majmin int // bounds the number of new major+minor versions
patch int // bounds the number of new patch versions
pre int // the number of prereleases to pad per release
}
// padVersions pads the existing version list with potential next versions, so
// that we don't have to wait an additional day to start getting reports for a
// newly tagged version.
//
// The prereleases argument may be supplied to provide a set of potential
// prerelease candidates. For example, if the program releases prereleases of
// the form "-pre.N", prereleases should be {"pre.1", "pre.2", ...}. For each
// potential next release version, the next two prerelease versions will be
// selected out of the provided set of prereleases.
func padVersions(versions []string, prereleasePatterns []string, padding padding) []string {
versions = slices.Clone(versions)
semver.Sort(versions)
latestRelease := "v0.0.0"
all := make(map[string]bool) // for de-duplicating padded versions
for _, v := range versions {
cv := semver.Canonical(v)
all[cv] = true
if semver.Prerelease(cv) == "" && semver.Compare(latestRelease, cv) < 0 {
latestRelease = cv
}
}
parsedLatest, ok := parseSemver(latestRelease)
if !ok {
// "can't happen", since the latest release version should always be canonical.
panic(fmt.Sprintf("unable to parse latest release version %q", latestRelease))
}
// Pad the latest version only.
//
// This assumes that the program in question doesn't patch older releases
// (as is the case with gopls). If that assumption ever changes, we may need
// to apply padding to older versions as well.
versionsToPad := []semversion{parsedLatest}
var maj, min, patch int
for _, toPad := range versionsToPad {
for majPadding := 0; majPadding <= padding.maj; majPadding++ {
maj = toPad.major + majPadding
for minPadding := 0; minPadding+majPadding <= padding.majmin; minPadding++ {
if majPadding == 0 {
min = toPad.minor + minPadding
} else {
min = minPadding
}
for patchPadding := 0; patchPadding <= padding.patch; patchPadding++ {
releases := majPadding + minPadding + patchPadding
if releases == 0 || releases > padding.releases {
continue
}
if majPadding == 0 && minPadding == 0 {
patch = toPad.patch + patchPadding
} else {
patch = patchPadding
}
v := fmt.Sprintf("v%d.%d.%d", maj, min, patch)
if all[v] {
// This guard is future proofing: we may have seen this version
// before if we are ever padding something other than the latest
// version.
continue
}
versions = append(versions, v)
// We may already have prereleases at this version. Don't pad
// additional prereleases, under the assumption that we don't
// typically have more than padding.pre prereleases.
nextPrerelease := 0
for i, patt := range prereleasePatterns {
pre := fmt.Sprintf("%s-%s", v, patt)
if all[pre] {
nextPrerelease = i + 1
}
}
for i := nextPrerelease; i < len(prereleasePatterns) && i < padding.pre; i++ {
pre := fmt.Sprintf("%s-%s", v, prereleasePatterns[i])
versions = append(versions, pre)
}
}
}
}
}
semver.Sort(versions)
return versions
}
// version is a parsed semantic version.
type semversion struct {
major, minor, patch int
pre string
}
// parseSemver attempts to parse semver components out of the provided semver
// v. If v is not valid semver in canonical form, parseSemver returns _, _, _,
// _, false.
func parseSemver(v string) (_ semversion, ok bool) {
var parsed semversion
v, parsed.pre, _ = strings.Cut(v, "-")
if _, err := fmt.Sscanf(v, "v%d.%d.%d", &parsed.major, &parsed.minor, &parsed.patch); err == nil {
ok = true
}
return parsed, ok
}
@@ -0,0 +1,256 @@
// Copyright 2023 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.
//go:build go1.22
package main
import (
_ "embed"
"reflect"
"sort"
"testing"
"golang.org/x/telemetry/internal/chartconfig"
"golang.org/x/telemetry/internal/telemetry"
)
func TestGenerate(t *testing.T) {
defer func(vers map[string][]string) {
versionsForTesting = vers
}(versionsForTesting)
versionsForTesting = map[string][]string{
"golang.org/toolchain": {"v0.0.1-go1.21.0.linux-arm", "v0.0.1-go1.20.linux-arm"},
"golang.org/x/tools/gopls": {"v0.13.0", "v0.14.0", "v0.15.0-pre.1", "v0.15.0"},
}
const raw = `
title: Editor Distribution
counter: gopls/editor:{emacs,vim,vscode,other}
description: measure editor distribution for gopls users.
type: partition
issue: https://go.dev/issue/61038
program: golang.org/x/tools/gopls
version: v0.14.0
`
gcfgs, err := chartconfig.Parse([]byte(raw))
if err != nil {
t.Fatal(err)
}
got, err := generate(gcfgs, padding{2, 1, 1, 2, 2})
if err != nil {
t.Fatal(err)
}
want := telemetry.UploadConfig{
GOOS: goos(),
GOARCH: goarch(),
SampleRate: SamplingRate,
GoVersion: []string{"go1.20", "go1.21.0"},
Programs: []*telemetry.ProgramConfig{{
Name: "golang.org/x/tools/gopls",
Versions: []string{
"v0.14.0",
"v0.15.0-pre.1",
"v0.15.0",
"v0.15.1-pre.1",
"v0.15.1-pre.2",
"v0.15.1",
"v0.15.2-pre.1",
"v0.15.2-pre.2",
"v0.15.2",
"v0.16.0-pre.1",
"v0.16.0-pre.2",
"v0.16.0",
"v0.16.1-pre.1",
"v0.16.1-pre.2",
"v0.16.1",
"v1.0.0-pre.1",
"v1.0.0-pre.2",
"v1.0.0",
"v1.0.1-pre.1",
"v1.0.1-pre.2",
"v1.0.1",
},
Counters: []telemetry.CounterConfig{{
Name: "gopls/editor:{emacs,vim,vscode,other}",
Rate: 1.0,
}},
}},
}
if !reflect.DeepEqual(*got, want) {
if len(got.Programs) != len(want.Programs) {
t.Errorf("generate(): got %d programs, want %d", len(got.Programs), len(want.Programs))
} else {
for i, gotp := range got.Programs {
want := *want.Programs[i]
if !reflect.DeepEqual(*gotp, want) {
t.Errorf("generate() program #%d =\n%+v\nwant:\n%+v", i, *gotp, want)
}
}
}
t.Errorf("generate() =\n%+v\nwant:\n%+v", *got, want)
}
}
func TestByGoVersion_Less(t *testing.T) {
got := []string{
"go1.21.0",
"go1.21rc1",
"go1.9",
"go1.9rc1",
"go1.6",
"go1.6beta1",
}
want := []string{
"go1.6beta1",
"go1.6",
"go1.9rc1",
"go1.9",
"go1.21rc1",
"go1.21.0",
}
sort.Sort(byGoVersion(got))
if !reflect.DeepEqual(got, want) {
t.Errorf("sort.Sort(byGoVersion(got)) = %v, want %v", got, want)
}
}
func TestContains(t *testing.T) {
baseline := func() *telemetry.UploadConfig {
return &telemetry.UploadConfig{
GOOS: goos(),
GOARCH: goarch(),
GoVersion: []string{"go1.20", "go1.21.0"},
Programs: []*telemetry.ProgramConfig{{
Name: "golang.org/x/tools/gopls",
Versions: []string{
"v0.14.0",
"v0.15.0-pre.1",
"v0.15.0",
"v0.15.1-pre.1",
"v0.15.1-pre.2",
"v0.15.1",
},
Counters: []telemetry.CounterConfig{{
Name: "gopls/editor:{emacs,vim,vscode,other}",
Rate: 1.0,
}},
}},
}
}
tests := []struct {
name string
outerMut, innerMut func(*telemetry.UploadConfig)
want bool
}{
{
"additional arch",
func(cfg *telemetry.UploadConfig) { cfg.GOARCH = append(cfg.GOARCH, "fake") },
func(cfg *telemetry.UploadConfig) {},
false,
},
{
"additional program",
func(cfg *telemetry.UploadConfig) { cfg.Programs = append(cfg.Programs, new(telemetry.ProgramConfig)) },
func(cfg *telemetry.UploadConfig) {},
false,
},
{
"additional counter",
func(cfg *telemetry.UploadConfig) {
cfg.Programs[0].Counters = append(cfg.Programs[0].Counters, telemetry.CounterConfig{})
},
func(cfg *telemetry.UploadConfig) {},
false,
},
{
"additional version",
func(cfg *telemetry.UploadConfig) {
cfg.Programs[0].Versions = append(cfg.Programs[0].Versions, "v99.99.99")
},
func(cfg *telemetry.UploadConfig) {},
true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
outer := baseline()
test.outerMut(outer)
inner := baseline()
test.innerMut(inner)
if got := contains(outer, inner); got != test.want {
t.Errorf("contains(...) = %v, want %v", got, test.want)
}
})
}
}
func TestPadVersions(t *testing.T) {
tests := []struct {
versions []string
prereleasePatterns []string
padding padding
want []string
}{
{
nil,
[]string{"pre.1"},
padding{1, 1, 1, 1, 2},
[]string{
"v0.0.1-pre.1",
"v0.0.1",
"v0.1.0-pre.1",
"v0.1.0",
"v1.0.0-pre.1",
"v1.0.0",
},
},
{
[]string{"v0.8.3", "v0.9.1", "v0.9.2", "v1.0.0", "v1.0.1", "v1.0.2-pre.1", "v1.0.2-pre.2", "v1.0.2-pre.3"},
[]string{"pre.1", "pre.2", "pre.3", "pre.4"},
padding{2, 1, 2, 2, 2},
[]string{
"v0.8.3",
"v0.9.1",
"v0.9.2",
"v1.0.0",
"v1.0.1",
"v1.0.2-pre.1",
"v1.0.2-pre.2",
"v1.0.2-pre.3",
"v1.0.2",
"v1.0.3-pre.1",
"v1.0.3-pre.2",
"v1.0.3",
"v1.1.0-pre.1",
"v1.1.0-pre.2",
"v1.1.0",
"v1.1.1-pre.1",
"v1.1.1-pre.2",
"v1.1.1",
"v1.2.0-pre.1",
"v1.2.0-pre.2",
"v1.2.0",
"v2.0.0-pre.1",
"v2.0.0-pre.2",
"v2.0.0",
"v2.0.1-pre.1",
"v2.0.1-pre.2",
"v2.0.1",
"v2.1.0-pre.1",
"v2.1.0-pre.2",
"v2.1.0",
},
},
}
for _, test := range tests {
got := padVersions(test.versions, test.prereleasePatterns, test.padding)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("padVersions(%v, %v) =\n%v\nwant:\n%v", test.versions, test.prereleasePatterns, got, test.want)
}
}
}
@@ -0,0 +1,60 @@
// Copyright 2023 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.
//go:build go1.22
package main
// knownOS is the list of past, present, and future known GOOS values.
// Do not remove from this list, as it is used for filename matching.
// If you add an entry to this list, look at unixOS, below.
var knownOS = map[string]bool{
"aix": true,
"android": true,
"darwin": true,
"dragonfly": true,
"freebsd": true,
"hurd": true,
"illumos": true,
"ios": true,
"js": true,
"linux": true,
"nacl": true,
"netbsd": true,
"openbsd": true,
"plan9": true,
"solaris": true,
"wasip1": true,
"windows": true,
"zos": true,
}
// knownArch is the list of past, present, and future known GOARCH values.
// Do not remove from this list, as it is used for filename matching.
var knownArch = map[string]bool{
"386": true,
"amd64": true,
"amd64p32": true,
"arm": true,
"armbe": true,
"arm64": true,
"arm64be": true,
"loong64": true,
"mips": true,
"mipsle": true,
"mips64": true,
"mips64le": true,
"mips64p32": true,
"mips64p32le": true,
"ppc": true,
"ppc64": true,
"ppc64le": true,
"riscv": true,
"riscv64": true,
"s390": true,
"s390x": true,
"sparc": true,
"sparc64": true,
"wasm": true,
}
@@ -0,0 +1,55 @@
// Copyright 2023 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.
//go:build go1.22
package main
import (
"errors"
"fmt"
"go/version"
"golang.org/x/mod/semver"
"golang.org/x/telemetry/internal/chartconfig"
"golang.org/x/telemetry/internal/telemetry"
)
// ValidateChartConfig checks that a ChartConfig is complete and coherent,
// returning an error describing all problems encountered, or nil.
func ValidateChartConfig(cfg chartconfig.ChartConfig) error {
var errs []error
reportf := func(format string, args ...any) {
errs = append(errs, fmt.Errorf(format, args...))
}
if cfg.Title == "" {
reportf("title must be set")
}
if len(cfg.Issue) == 0 {
reportf("at least one issue is required")
}
if cfg.Program == "" {
reportf("program must be set")
}
if cfg.Counter == "" {
reportf("counter must be set")
}
if cfg.Type == "" {
reportf("type must be set")
}
if cfg.Depth < 0 {
reportf("invalid depth %d: must be non-negative", cfg.Depth)
}
if cfg.Depth != 0 && cfg.Type != "stack" {
reportf("depth can only be set for \"stack\" chart types")
}
valid := semver.IsValid
if telemetry.IsToolchainProgram(cfg.Program) {
valid = version.IsValid
}
if cfg.Version != "" && !valid(cfg.Version) {
reportf("%q is not a valid version (must be a go version or semver)", cfg.Version)
}
return errors.Join(errs...)
}
@@ -0,0 +1,81 @@
// Copyright 2023 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.
//go:build go1.22
package main
import (
"strings"
"testing"
"golang.org/x/telemetry/internal/chartconfig"
)
func TestLoadedChartsAreValid(t *testing.T) {
// Test that we can actually load the chart config.
charts, err := chartconfig.Load()
if err != nil {
t.Errorf("Load() failed: %v", err)
}
for i, chart := range charts {
if err := ValidateChartConfig(chart); err != nil {
t.Errorf("Chart %d is invalid: %v", i, err)
}
}
}
func TestValidateOK(t *testing.T) {
// A minimally valid chart config.
const input = `
title: Editor Distribution
counter: gopls/editor:{emacs,vim,vscode,other}
type: partition
issue: https://go.dev/issue/12345
program: golang.org/x/tools/gopls
`
records, err := chartconfig.Parse([]byte(input))
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("Parse(%q) returned %d records, want exactly 1", input, len(records))
}
if err := ValidateChartConfig(records[0]); err != nil {
t.Errorf("Validate(%q) = %v, want nil", input, err)
}
}
func TestValidate(t *testing.T) {
tests := map[string][]string{ // input -> want errors
// validation of mandatory fields
"description:bar": {"title", "program", "issue", "counter", "type"},
// validation of semver intervals
"version:1.2.3.4": {"semver"},
// valid of stack configuration
"depth:-1": {"non-negative", "stack"},
}
for input, wantErrs := range tests {
records, err := chartconfig.Parse([]byte(input))
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("Parse(%q) returned %d records, want exactly 1", input, len(records))
}
err = ValidateChartConfig(records[0])
if err == nil {
t.Fatalf("Validate(%q) succeeded unexpectedly", input)
}
errs := err.Error()
for _, want := range wantErrs {
if !strings.Contains(errs, want) {
t.Errorf("Validate(%q) = %v, want containing %q", input, err, want)
}
}
}
}