1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package metrics_test
6
7import (
8	"bytes"
9	"flag"
10	"fmt"
11	"go/ast"
12	"go/doc"
13	"go/doc/comment"
14	"go/format"
15	"go/parser"
16	"go/token"
17	"internal/diff"
18	"os"
19	"regexp"
20	"runtime/metrics"
21	"sort"
22	"strings"
23	"testing"
24	_ "unsafe"
25)
26
27// Implemented in the runtime.
28//
29//go:linkname runtime_readMetricNames
30func runtime_readMetricNames() []string
31
32func TestNames(t *testing.T) {
33	// Note that this regexp is promised in the package docs for Description. Do not change.
34	r := regexp.MustCompile("^(?P<name>/[^:]+):(?P<unit>[^:*/]+(?:[*/][^:*/]+)*)$")
35	all := metrics.All()
36	for i, d := range all {
37		if !r.MatchString(d.Name) {
38			t.Errorf("name %q does not match regexp %#q", d.Name, r)
39		}
40		if i > 0 && all[i-1].Name >= all[i].Name {
41			t.Fatalf("allDesc not sorted: %s ≥ %s", all[i-1].Name, all[i].Name)
42		}
43	}
44
45	names := runtime_readMetricNames()
46	sort.Strings(names)
47	samples := make([]metrics.Sample, len(names))
48	for i, name := range names {
49		samples[i].Name = name
50	}
51	metrics.Read(samples)
52
53	for _, d := range all {
54		for len(samples) > 0 && samples[0].Name < d.Name {
55			t.Errorf("%s: reported by runtime but not listed in All", samples[0].Name)
56			samples = samples[1:]
57		}
58		if len(samples) == 0 || d.Name < samples[0].Name {
59			t.Errorf("%s: listed in All but not reported by runtime", d.Name)
60			continue
61		}
62		if samples[0].Value.Kind() != d.Kind {
63			t.Errorf("%s: runtime reports %v but All reports %v", d.Name, samples[0].Value.Kind(), d.Kind)
64		}
65		samples = samples[1:]
66	}
67}
68
69func wrap(prefix, text string, width int) string {
70	doc := &comment.Doc{Content: []comment.Block{&comment.Paragraph{Text: []comment.Text{comment.Plain(text)}}}}
71	pr := &comment.Printer{TextPrefix: prefix, TextWidth: width}
72	return string(pr.Text(doc))
73}
74
75func formatDesc(t *testing.T) string {
76	var b strings.Builder
77	for i, d := range metrics.All() {
78		if i > 0 {
79			fmt.Fprintf(&b, "\n")
80		}
81		fmt.Fprintf(&b, "%s\n", d.Name)
82		fmt.Fprintf(&b, "%s", wrap("\t", d.Description, 80-2*8))
83	}
84	return b.String()
85}
86
87var generate = flag.Bool("generate", false, "update doc.go for go generate")
88
89func TestDocs(t *testing.T) {
90	want := formatDesc(t)
91
92	src, err := os.ReadFile("doc.go")
93	if err != nil {
94		t.Fatal(err)
95	}
96	fset := token.NewFileSet()
97	f, err := parser.ParseFile(fset, "doc.go", src, parser.ParseComments)
98	if err != nil {
99		t.Fatal(err)
100	}
101	fdoc := f.Doc
102	if fdoc == nil {
103		t.Fatal("no doc comment in doc.go")
104	}
105	pkg, err := doc.NewFromFiles(fset, []*ast.File{f}, "runtime/metrics")
106	if err != nil {
107		t.Fatal(err)
108	}
109	if pkg.Doc == "" {
110		t.Fatal("doc.NewFromFiles lost doc comment")
111	}
112	doc := new(comment.Parser).Parse(pkg.Doc)
113	expectCode := false
114	foundCode := false
115	updated := false
116	for _, block := range doc.Content {
117		switch b := block.(type) {
118		case *comment.Heading:
119			expectCode = false
120			if b.Text[0] == comment.Plain("Supported metrics") {
121				expectCode = true
122			}
123		case *comment.Code:
124			if expectCode {
125				foundCode = true
126				if b.Text != want {
127					if !*generate {
128						t.Fatalf("doc comment out of date; use go generate to rebuild\n%s", diff.Diff("old", []byte(b.Text), "want", []byte(want)))
129					}
130					b.Text = want
131					updated = true
132				}
133			}
134		}
135	}
136
137	if !foundCode {
138		t.Fatalf("did not find Supported metrics list in doc.go")
139	}
140	if updated {
141		fmt.Fprintf(os.Stderr, "go test -generate: writing new doc.go\n")
142		var buf bytes.Buffer
143		buf.Write(src[:fdoc.Pos()-f.FileStart])
144		buf.WriteString("/*\n")
145		buf.Write(new(comment.Printer).Comment(doc))
146		buf.WriteString("*/")
147		buf.Write(src[fdoc.End()-f.FileStart:])
148		src, err := format.Source(buf.Bytes())
149		if err != nil {
150			t.Fatal(err)
151		}
152		if err := os.WriteFile("doc.go", src, 0666); err != nil {
153			t.Fatal(err)
154		}
155	} else if *generate {
156		fmt.Fprintf(os.Stderr, "go test -generate: doc.go already up-to-date\n")
157	}
158}
159