whatcanGOwrong
This commit is contained in:
+536
@@ -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
|
||||
}
|
||||
+256
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -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,
|
||||
}
|
||||
+55
@@ -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...)
|
||||
}
|
||||
+81
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user