// Copyright 2014 The Go Authors. // See https://code.google.com/p/go/source/browse/CONTRIBUTORS // Licensed under the same terms as Go itself: // https://code.google.com/p/go/source/browse/LICENSE package http2 import ( "encoding/xml" "flag" "fmt" "io" "os" "reflect" "regexp" "sort" "strconv" "strings" "testing" ) var coverSpec = flag.Bool("coverspec", false, "Run spec coverage tests") // The global map of sentence coverage for the http2 spec. var defaultSpecCoverage specCoverage func init() { f, err := os.Open("testdata/draft-ietf-httpbis-http2.xml") if err != nil { panic(err) } defaultSpecCoverage = readSpecCov(f) } // specCover marks all sentences for section sec in defaultSpecCoverage. Sentences not // "covered" will be included in report outputed by TestSpecCoverage. func specCover(sec, sentences string) { defaultSpecCoverage.cover(sec, sentences) } type specPart struct { section string sentence string } func (ss specPart) Less(oo specPart) bool { atoi := func(s string) int { n, err := strconv.Atoi(s) if err != nil { panic(err) } return n } a := strings.Split(ss.section, ".") b := strings.Split(oo.section, ".") for i := 0; i < len(a); i++ { if i >= len(b) { return false } x, y := atoi(a[i]), atoi(b[i]) if x < y { return true } } return false } type bySpecSection []specPart func (a bySpecSection) Len() int { return len(a) } func (a bySpecSection) Less(i, j int) bool { return a[i].Less(a[j]) } func (a bySpecSection) Swap(i, j int) { a[i], a[j] = a[j], a[i] } type specCoverage map[specPart]bool func readSection(sc specCoverage, d *xml.Decoder, sec []int) { sub := 0 for { tk, err := d.Token() if err != nil { if err == io.EOF { return } panic(err) } switch v := tk.(type) { case xml.StartElement: if skipElement(v) { if err := d.Skip(); err != nil { panic(err) } break } if v.Name.Local == "section" { sub++ readSection(sc, d, append(sec, sub)) } case xml.CharData: if len(sec) == 0 { break } ssec := fmt.Sprintf("%d", sec[0]) for _, n := range sec[1:] { ssec = fmt.Sprintf("%s.%d", ssec, n) } sc.addSentences(ssec, string(v)) case xml.EndElement: if v.Name.Local == "section" { return } } } } var skipAnchor = map[string]bool{ "intro": true, "Overview": true, } var skipTitle = map[string]bool{ "Acknowledgements": true, "Change Log": true, "Document Organization": true, "Conventions and Terminology": true, } func skipElement(s xml.StartElement) bool { switch s.Name.Local { case "artwork": return true case "section": for _, attr := range s.Attr { switch attr.Name.Local { case "anchor": if skipAnchor[attr.Value] || strings.HasPrefix(attr.Value, "changes.since.") { return true } case "title": if skipTitle[attr.Value] { return true } } } } return false } func readSpecCov(r io.Reader) specCoverage { d := xml.NewDecoder(r) sc := specCoverage{} readSection(sc, d, nil) return sc } func (sc specCoverage) addSentences(sec string, sentence string) { for _, s := range parseSentences(sentence) { sc[specPart{sec, s}] = false } } func (sc specCoverage) cover(sec string, sentence string) { for _, s := range parseSentences(sentence) { p := specPart{sec, s} if _, ok := sc[p]; !ok { panic(fmt.Sprintf("Not found in spec: %q, %q", sec, s)) } sc[specPart{sec, s}] = true } } var whitespaceRx = regexp.MustCompile(`\s+`) func parseSentences(sens string) []string { sens = strings.TrimSpace(sens) if sens == "" { return nil } ss := strings.Split(whitespaceRx.ReplaceAllString(sens, " "), ". ") for i, s := range ss { s = strings.TrimSpace(s) if !strings.HasSuffix(s, ".") { s += "." } ss[i] = s } return ss } func TestSpecParseSentences(t *testing.T) { tests := []struct { ss string want []string }{ {"Sentence 1. Sentence 2.", []string{ "Sentence 1.", "Sentence 2.", }}, {"Sentence 1. \nSentence 2.\tSentence 3.", []string{ "Sentence 1.", "Sentence 2.", "Sentence 3.", }}, } for i, tt := range tests { got := parseSentences(tt.ss) if !reflect.DeepEqual(got, tt.want) { t.Errorf("%d: got = %q, want %q", i, got, tt.want) } } } func TestSpecBuildCoverageTable(t *testing.T) { testdata := `
Foo. Sentence 1. Sentence 2 . Sentence 3.
Bar.
Baz.
` got := readSpecCov(strings.NewReader(testdata)) want := specCoverage{ specPart{"1", "Foo."}: false, specPart{"1", "Sentence 1."}: false, specPart{"1", "Sentence 2."}: false, specPart{"1", "Sentence 3."}: false, specPart{"2", "Bar."}: false, specPart{"2.1", "Baz."}: false, } if !reflect.DeepEqual(got, want) { t.Errorf("got = %+v, want %+v", got, want) } } func TestSpecUncovered(t *testing.T) { testdata := `
Foo. Sentence 1.
` sp := readSpecCov(strings.NewReader(testdata)) sp.cover("1", "Foo. Sentence 1.") want := specCoverage{ specPart{"1", "Foo."}: true, specPart{"1", "Sentence 1."}: true, } if !reflect.DeepEqual(sp, want) { t.Errorf("got = %+v, want %+v", sp, want) } defer func() { if err := recover(); err == nil { t.Error("expected panic") } }() sp.cover("1", "Not in spec.") } func TestSpecCoverage(t *testing.T) { if !*coverSpec { t.Skip("skipping spec coverage without -coverspec") } var notCovered bySpecSection for p, covered := range defaultSpecCoverage { if !covered { notCovered = append(notCovered, p) } } if len(notCovered) == 0 { return } sort.Sort(notCovered) const shortLen = 5 if testing.Short() && len(notCovered) > shortLen { notCovered = notCovered[:shortLen] } t.Logf("COVER REPORT:") fails := 0 for _, p := range notCovered { t.Errorf("\tSECTION %s: %s", p.section, p.sentence) fails++ } t.Logf("%d sections not covered", fails) }