go.tools/cmd/stringer: add tests

Refactor a little to make testing easier.
Add golden tests and a check fo splitIntoRuns, which is the subtlest piece.
Still to come: execution tests.

Also fix a few issues in the generated code.

LGTM=gri
R=gri
CC=golang-codereviews, josharian
https://golang.org/cl/134450044
This commit is contained in:
Rob Pike 2014-09-04 14:16:59 -07:00
parent 196bd6741e
commit 9207f67279
3 changed files with 395 additions and 24 deletions

274
cmd/stringer/golden_test.go Normal file
View File

@ -0,0 +1,274 @@
// Copyright 2014 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.
// This file contains simple golden tests for various examples.
// Besides validating the results when the implementation changes,
// it provides a way to look at the generated code without having
// to execute the print statements in one's head.
package main
import (
"strings"
"testing"
)
// Golden represents a test case.
type Golden struct {
name string
input string // input; the package clause is provided when running the test.
output string // exected output.
}
var golden = []Golden{
{"day", day_in, day_out},
{"offset", offset_in, offset_out},
{"gap", gap_in, gap_out},
{"neg", neg_in, neg_out},
{"uneg", uneg_in, uneg_out},
{"map", map_in, map_out},
}
// Each example starts with "type XXX [u]int", with a single space separating them.
// Simple test: enumeration of type int starting at 0.
const day_in = `type Day int
const (
Monday Day = iota
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
)
`
const day_out = `
var (
_Day_indexes = [...]uint8{6, 13, 22, 30, 36, 44, 50}
_Day_names = "MondayTuesdayWednesdayThursdayFridaySaturdaySunday"
)
func (i Day) String() string {
if i < 0 || i >= Day(len(_Day_indexes)) {
return fmt.Sprintf("Day(%d)", i)
}
hi := _Day_indexes[i]
lo := uint8(0)
if i > 0 {
lo = _Day_indexes[i-1]
}
return _Day_names[lo:hi]
}
`
// Enumeration with an offset.
// Also includes a duplicate.
const offset_in = `type Number int
const (
_ Number = iota
One
Two
Three
AnotherOne = One // Duplicate; note that AnotherOne doesn't appear below.
)
`
const offset_out = `
var (
_Number_indexes = [...]uint8{3, 6, 11}
_Number_names = "OneTwoThree"
)
func (i Number) String() string {
i -= 1
if i < 0 || i >= Number(len(_Number_indexes)) {
return fmt.Sprintf("Number(%d)", i+1)
}
hi := _Number_indexes[i]
lo := uint8(0)
if i > 0 {
lo = _Number_indexes[i-1]
}
return _Number_names[lo:hi]
}
`
// Gaps and an offset.
const gap_in = `type Num int
const (
Two Num = 2
Three Num = 3
Five Num = 5
Six Num = 6
Seven Num = 7
Eight Num = 8
Nine Num = 9
Eleven Num = 11
)
`
const gap_out = `
var (
_Num_indexes_0 = [...]uint8{3, 8}
_Num_names_0 = "TwoThree"
_Num_indexes_1 = [...]uint8{4, 7, 12, 17, 21}
_Num_names_1 = "FiveSixSevenEightNine"
_Num_indexes_2 = [...]uint8{6}
_Num_names_2 = "Eleven"
)
func (i Num) String() string {
switch {
case 2 <= i && i < 3:
lo := uint8(0)
if i > 2 {
i -= 2
} else {
lo = _Num_indexes_0[i-1]
}
return _Num_names_0[lo:_Num_indexes_0[i]]
case 5 <= i && i < 9:
lo := uint8(0)
if i > 5 {
i -= 5
} else {
lo = _Num_indexes_1[i-1]
}
return _Num_names_1[lo:_Num_indexes_1[i]]
case i == 11:
return _Num_names_2
default:
return fmt.Sprintf("Num(%d)", i)
}
}
`
// Signed integers spanning zero.
const neg_in = `type Num int
const (
m_2 Num = -2 + iota
m_1
m0
m1
m2
)
`
const neg_out = `
var (
_Num_indexes = [...]uint8{3, 6, 8, 10, 12}
_Num_names = "m_2m_1m0m1m2"
)
func (i Num) String() string {
i -= -2
if i < 0 || i >= Num(len(_Num_indexes)) {
return fmt.Sprintf("Num(%d)", i+-2)
}
hi := _Num_indexes[i]
lo := uint8(0)
if i > 0 {
lo = _Num_indexes[i-1]
}
return _Num_names[lo:hi]
}
`
// Unsigned integers spanning zero.
const uneg_in = `type UNum uint
const (
m_2 UNum = ^UNum(0)-2
m_1
m0
m1
m2
)
`
const uneg_out = `
var (
_UNum_indexes = [...]uint8{3}
_UNum_names = "m_2"
)
func (i UNum) String() string {
i -= 18446744073709551613
if i >= UNum(len(_UNum_indexes)) {
return fmt.Sprintf("UNum(%d)", i+18446744073709551613)
}
hi := _UNum_indexes[i]
lo := uint8(0)
if i > 0 {
lo = _UNum_indexes[i-1]
}
return _UNum_names[lo:hi]
}
`
// Enough gaps to trigger a map implementation of the method.
// Also includes a duplicate to test that it doesn't cause problems
const map_in = `type Prime int
const (
p2 Prime = 2
p3 Prime = 3
p5 Prime = 5
p7 Prime = 7
p77 Prime = 7 // Duplicate; note that p77 doesn't appear below.
p11 Prime = 11
p13 Prime = 13
p17 Prime = 17
p19 Prime = 19
p23 Prime = 23
p29 Prime = 29
p37 Prime = 31
p41 Prime = 41
p43 Prime = 43
)
`
const map_out = `
var _Prime_map = map[Prime]string{
2: "p2",
3: "p3",
5: "p5",
7: "p7",
11: "p11",
13: "p13",
17: "p17",
19: "p19",
23: "p23",
29: "p29",
31: "p37",
41: "p41",
43: "p43",
}
func (i Prime) String() string {
if str, ok := _Prime_map[i]; ok {
return str
}
return fmt.Sprintf("Prime(%d)", i)
}
`
func TestGolden(t *testing.T) {
for _, test := range golden {
var g Generator
input := "package test\n" + test.input
file := test.name + ".go"
g.parsePackage(".", []string{file}, input)
// Extract the name and type of the constant from the first line.
tokens := strings.SplitN(test.input, " ", 3)
if len(tokens) != 3 {
t.Fatalf("%s: need type declaration on first line", test.name)
}
g.generate(tokens[1])
got := string(g.format())
if got != test.output {
t.Errorf("%s: got\n====\n%s====\nexpected\n====%s", test.name, got, test.output)
}
}
}

View File

@ -20,11 +20,11 @@
// type Pill int
//
// const (
// Undefined Pill = iota
// Placebo Pill = iota
// Aspirin
// Ibuprofen
// Acetaminophen
// Paracetamol = Acetaminophen
// Paracetamol
// Acetaminophen = Paracetamol
// )
//
// running this command
@ -45,8 +45,8 @@
// //go:generate go tool stringer -type=Pill
// TODO: do we install this as a tool or as a binary?
//
// If multiple contants have the same value, the lexically first matching name will
// be used (in the example, Paracetamol will print as "Acetaminophen").
// If multiple constants have the same value, the lexically first matching name will
// be used (in the example, Acetaminophen will print as "Paracetamol").
//
// With no arguments, it processes the package in the current directory.
// Otherwise, the arguments must name a single directory holding a Go package
@ -142,14 +142,7 @@ func main() {
}
// Format the output.
src, err := format.Source(g.buf.Bytes())
if err != nil {
// Should never happen, but can arise when developing this code.
// The user can compile the output to see the error.
log.Printf("warning: internal error: invalid Go generated: %s", err)
log.Printf("warning: compile the package to analyze the error")
src = g.buf.Bytes()
}
src := g.format()
// Write to file.
outputName := *output
@ -157,7 +150,7 @@ func main() {
baseName := fmt.Sprintf("%s_string.go", types[0])
outputName = filepath.Join(dir, strings.ToLower(baseName))
}
err = ioutil.WriteFile(outputName, src, 0644)
err := ioutil.WriteFile(outputName, src, 0644)
if err != nil {
log.Fatalf("writing output: %s", err)
}
@ -214,12 +207,12 @@ func (g *Generator) parsePackageDir(directory string) {
// names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
names = append(names, pkg.SFiles...)
names = prefixDirectory(directory, names)
g.parsePackage(directory, names)
g.parsePackage(directory, names, "")
}
// parsePackageFiles parses the package occupying the named files.
func (g *Generator) parsePackageFiles(names []string) {
g.parsePackage(".", names)
g.parsePackage(".", names, "")
}
// prefixDirectory places the directory name on the beginning of each name in the list.
@ -235,7 +228,9 @@ func prefixDirectory(directory string, names []string) []string {
}
// doPackage analyzes the single package constructed from the named files.
func (g *Generator) parsePackage(directory string, names []string) {
// If text is non-nil, it is a string to be used instead of the content of the file,
// to be used for testing. doPackage exits if there is an error.
func (g *Generator) parsePackage(directory string, names []string, text interface{}) {
var files []*File
var astFiles []*ast.File
g.pkg = new(Package)
@ -244,7 +239,7 @@ func (g *Generator) parsePackage(directory string, names []string) {
if !strings.HasSuffix(name, ".go") {
continue
}
parsedFile, err := parser.ParseFile(fs, name, nil, 0)
parsedFile, err := parser.ParseFile(fs, name, text, 0)
if err != nil {
log.Fatalf("parsing package: %s: %s", name, err)
}
@ -323,6 +318,19 @@ func (g *Generator) generate(typeName string) {
func splitIntoRuns(values []Value) [][]Value {
// We use stable sort so the lexically first name is chosen for equal elements.
sort.Stable(byValue(values))
// Remove duplicates. Stable sort has put the one we want to print first,
// so use that one. The String method won't care about which named constant
// was the argument, so the first name for the given value is the only one to keep.
// We need to do this because identical values would cause the switch or map
// to fail to compile.
j := 1
for i := 1; i < len(values); i++ {
if values[i].value != values[i-1].value {
values[j] = values[i]
j++
}
}
values = values[:j]
runs := make([][]Value, 0, 10)
for len(values) > 0 {
// One contiguous sequence per outer loop.
@ -336,6 +344,19 @@ func splitIntoRuns(values []Value) [][]Value {
return runs
}
// format returns the gofmt-ed contents of the Generator's buffer.
func (g *Generator) format() []byte {
src, err := format.Source(g.buf.Bytes())
if err != nil {
// Should never happen, but can arise when developing this code.
// The user can compile the output to see the error.
log.Printf("warning: internal error: invalid Go generated: %s", err)
log.Printf("warning: compile the package to analyze the error")
return g.buf.Bytes()
}
return src
}
// Value represents a declared constant.
type Value struct {
name string // The name of the constant.
@ -470,7 +491,7 @@ func (g *Generator) declareIndexAndNameVars(run []Value, typeName string, suffix
indexes[i] = b.Len()
}
names := b.String()
g.Printf("\t_%s_indexes%s = []uint%d{", typeName, suffix, usize(len(names)))
g.Printf("\t_%s_indexes%s = [...]uint%d{", typeName, suffix, usize(len(names)))
for i, v := range indexes {
if i > 0 {
g.Printf(", ")
@ -553,7 +574,7 @@ func (g *Generator) buildMultipleRuns(runs [][]Value, typeName string) {
g.Printf("\t\treturn _%s_names_%d\n", typeName, i)
continue
}
g.Printf("\tcase %s < i && i < %s:\n", &values[0], &values[len(values)-1])
g.Printf("\tcase %s <= i && i < %s:\n", &values[0], &values[len(values)-1])
g.Printf("\t\tlo := uint%d(0)\n", usize(len(values)))
g.Printf("\t\tif i > %s {\n", &values[0])
g.Printf("\t\t\ti -= %s\n", &values[0])
@ -583,10 +604,9 @@ func (g *Generator) buildMap(runs [][]Value, typeName string) {
// Argument to format is the type name.
const stringMap = `func (i %[1]s) String() string {
str, ok := _%[1]s_map[i]
if !ok {
return fmt.Sprintf("%[1]s(%%d)", i)
if str, ok := _%[1]s_map[i]; ok {
return str
}
return str
return fmt.Sprintf("%[1]s(%%d)", i)
}
`

77
cmd/stringer/util_test.go Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2014 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.
// This file contains tests for some of the internal functions.
package main
import (
"fmt"
"testing"
)
// Helpers to save typing in the test cases.
type u []uint64
type uu [][]uint64
type SplitTest struct {
input u
output uu
signed bool
}
var (
m2 = uint64(2)
m1 = uint64(1)
m0 = uint64(0)
m_1 = ^uint64(0) // -1 when signed.
m_2 = ^uint64(0) - 1 // -2 when signed.
)
var splitTests = []SplitTest{
// No need for a test for the empty case; that's picked off before splitIntoRuns.
// Single value.
{u{1}, uu{u{1}}, false},
// Out of order.
{u{3, 2, 1}, uu{u{1, 2, 3}}, true},
// Out of order.
{u{3, 2, 1}, uu{u{1, 2, 3}}, false},
// A gap at the beginning.
{u{1, 33, 32, 31}, uu{u{1}, u{31, 32, 33}}, true},
// A gap in the middle, in mixed order.
{u{33, 7, 32, 31, 9, 8}, uu{u{7, 8, 9}, u{31, 32, 33}}, true},
// Gaps throughout
{u{33, 44, 1, 32, 45, 31}, uu{u{1}, u{31, 32, 33}, u{44, 45}}, true},
// Unsigned values spanning 0.
{u{m1, m0, m_1, m2, m_2}, uu{u{m0, m1, m2}, u{m_2, m_1}}, false},
// Signed values spanning 0
{u{m1, m0, m_1, m2, m_2}, uu{u{m_2, m_1, m0, m1, m2}}, true},
}
func TestSplitIntoRuns(t *testing.T) {
Outer:
for n, test := range splitTests {
values := make([]Value, len(test.input))
for i, v := range test.input {
values[i] = Value{"", v, test.signed, fmt.Sprint(v)}
}
runs := splitIntoRuns(values)
if len(runs) != len(test.output) {
t.Errorf("#%d: %v: got %d runs; expected %d", n, test.input, len(runs), len(test.output))
continue
}
for i, run := range runs {
if len(run) != len(test.output[i]) {
t.Errorf("#%d: got %v; expected %v", n, runs, test.output)
continue Outer
}
for j, v := range run {
if v.value != test.output[i][j] {
t.Errorf("#%d: got %v; expected %v", n, runs, test.output)
continue Outer
}
}
}
}
}