|  | // 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. | 
|  |  | 
|  | package http2 | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "encoding/xml" | 
|  | "flag" | 
|  | "fmt" | 
|  | "io" | 
|  | "os" | 
|  | "reflect" | 
|  | "regexp" | 
|  | "sort" | 
|  | "strconv" | 
|  | "strings" | 
|  | "sync" | 
|  | "testing" | 
|  | ) | 
|  |  | 
|  | var coverSpec = flag.Bool("coverspec", false, "Run spec coverage tests") | 
|  |  | 
|  | // The global map of sentence coverage for the http2 spec. | 
|  | var defaultSpecCoverage specCoverage | 
|  |  | 
|  | var loadSpecOnce sync.Once | 
|  |  | 
|  | func loadSpec() { | 
|  | if f, err := os.Open("testdata/draft-ietf-httpbis-http2.xml"); err != nil { | 
|  | panic(err) | 
|  | } else { | 
|  | defaultSpecCoverage = readSpecCov(f) | 
|  | f.Close() | 
|  | } | 
|  | } | 
|  |  | 
|  | // covers marks all sentences for section sec in defaultSpecCoverage. Sentences not | 
|  | // "covered" will be included in report outputted by TestSpecCoverage. | 
|  | func covers(sec, sentences string) { | 
|  | loadSpecOnce.Do(loadSpec) | 
|  | 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 len(a) > 0 { | 
|  | if len(b) == 0 { | 
|  | return false | 
|  | } | 
|  | x, y := atoi(a[0]), atoi(b[0]) | 
|  | if x == y { | 
|  | a, b = a[1:], b[1:] | 
|  | continue | 
|  | } | 
|  | return x < y | 
|  | } | 
|  | if len(b) > 0 { | 
|  | 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 struct { | 
|  | coverage map[specPart]bool | 
|  | d        *xml.Decoder | 
|  | } | 
|  |  | 
|  | func joinSection(sec []int) string { | 
|  | s := fmt.Sprintf("%d", sec[0]) | 
|  | for _, n := range sec[1:] { | 
|  | s = fmt.Sprintf("%s.%d", s, n) | 
|  | } | 
|  | return s | 
|  | } | 
|  |  | 
|  | func (sc specCoverage) readSection(sec []int) { | 
|  | var ( | 
|  | buf = new(bytes.Buffer) | 
|  | sub = 0 | 
|  | ) | 
|  | for { | 
|  | tk, err := sc.d.Token() | 
|  | if err != nil { | 
|  | if err == io.EOF { | 
|  | return | 
|  | } | 
|  | panic(err) | 
|  | } | 
|  | switch v := tk.(type) { | 
|  | case xml.StartElement: | 
|  | if skipElement(v) { | 
|  | if err := sc.d.Skip(); err != nil { | 
|  | panic(err) | 
|  | } | 
|  | if v.Name.Local == "section" { | 
|  | sub++ | 
|  | } | 
|  | break | 
|  | } | 
|  | switch v.Name.Local { | 
|  | case "section": | 
|  | sub++ | 
|  | sc.readSection(append(sec, sub)) | 
|  | case "xref": | 
|  | buf.Write(sc.readXRef(v)) | 
|  | } | 
|  | case xml.CharData: | 
|  | if len(sec) == 0 { | 
|  | break | 
|  | } | 
|  | buf.Write(v) | 
|  | case xml.EndElement: | 
|  | if v.Name.Local == "section" { | 
|  | sc.addSentences(joinSection(sec), buf.String()) | 
|  | return | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (sc specCoverage) readXRef(se xml.StartElement) []byte { | 
|  | var b []byte | 
|  | for { | 
|  | tk, err := sc.d.Token() | 
|  | if err != nil { | 
|  | panic(err) | 
|  | } | 
|  | switch v := tk.(type) { | 
|  | case xml.CharData: | 
|  | if b != nil { | 
|  | panic("unexpected CharData") | 
|  | } | 
|  | b = []byte(string(v)) | 
|  | case xml.EndElement: | 
|  | if v.Name.Local != "xref" { | 
|  | panic("expected </xref>") | 
|  | } | 
|  | if b != nil { | 
|  | return b | 
|  | } | 
|  | sig := attrSig(se) | 
|  | switch sig { | 
|  | case "target": | 
|  | return []byte(fmt.Sprintf("[%s]", attrValue(se, "target"))) | 
|  | case "fmt-of,rel,target", "fmt-,,rel,target": | 
|  | return []byte(fmt.Sprintf("[%s, %s]", attrValue(se, "target"), attrValue(se, "rel"))) | 
|  | case "fmt-of,sec,target", "fmt-,,sec,target": | 
|  | return []byte(fmt.Sprintf("[section %s of %s]", attrValue(se, "sec"), attrValue(se, "target"))) | 
|  | case "fmt-of,rel,sec,target": | 
|  | return []byte(fmt.Sprintf("[section %s of %s, %s]", attrValue(se, "sec"), attrValue(se, "target"), attrValue(se, "rel"))) | 
|  | default: | 
|  | panic(fmt.Sprintf("unknown attribute signature %q in %#v", sig, fmt.Sprintf("%#v", se))) | 
|  | } | 
|  | default: | 
|  | panic(fmt.Sprintf("unexpected tag %q", v)) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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 { | 
|  | sc := specCoverage{ | 
|  | coverage: map[specPart]bool{}, | 
|  | d:        xml.NewDecoder(r)} | 
|  | sc.readSection(nil) | 
|  | return sc | 
|  | } | 
|  |  | 
|  | func (sc specCoverage) addSentences(sec string, sentence string) { | 
|  | for _, s := range parseSentences(sentence) { | 
|  | sc.coverage[specPart{sec, s}] = false | 
|  | } | 
|  | } | 
|  |  | 
|  | func (sc specCoverage) cover(sec string, sentence string) { | 
|  | for _, s := range parseSentences(sentence) { | 
|  | p := specPart{sec, s} | 
|  | if _, ok := sc.coverage[p]; !ok { | 
|  | panic(fmt.Sprintf("Not found in spec: %q, %q", sec, s)) | 
|  | } | 
|  | sc.coverage[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 TestSpecCoverage(t *testing.T) { | 
|  | if !*coverSpec { | 
|  | t.Skip() | 
|  | } | 
|  |  | 
|  | loadSpecOnce.Do(loadSpec) | 
|  |  | 
|  | var ( | 
|  | list     []specPart | 
|  | cv       = defaultSpecCoverage.coverage | 
|  | total    = len(cv) | 
|  | complete = 0 | 
|  | ) | 
|  |  | 
|  | for sp, touched := range defaultSpecCoverage.coverage { | 
|  | if touched { | 
|  | complete++ | 
|  | } else { | 
|  | list = append(list, sp) | 
|  | } | 
|  | } | 
|  | sort.Stable(bySpecSection(list)) | 
|  |  | 
|  | if testing.Short() && len(list) > 5 { | 
|  | list = list[:5] | 
|  | } | 
|  |  | 
|  | for _, p := range list { | 
|  | t.Errorf("\tSECTION %s: %s", p.section, p.sentence) | 
|  | } | 
|  |  | 
|  | t.Logf("%d/%d (%d%%) sentences covered", complete, total, (complete/total)*100) | 
|  | } | 
|  |  | 
|  | func attrSig(se xml.StartElement) string { | 
|  | var names []string | 
|  | for _, attr := range se.Attr { | 
|  | if attr.Name.Local == "fmt" { | 
|  | names = append(names, "fmt-"+attr.Value) | 
|  | } else { | 
|  | names = append(names, attr.Name.Local) | 
|  | } | 
|  | } | 
|  | sort.Strings(names) | 
|  | return strings.Join(names, ",") | 
|  | } | 
|  |  | 
|  | func attrValue(se xml.StartElement, attr string) string { | 
|  | for _, a := range se.Attr { | 
|  | if a.Name.Local == attr { | 
|  | return a.Value | 
|  | } | 
|  | } | 
|  | panic("unknown attribute " + attr) | 
|  | } | 
|  |  | 
|  | func TestSpecPartLess(t *testing.T) { | 
|  | tests := []struct { | 
|  | sec1, sec2 string | 
|  | want       bool | 
|  | }{ | 
|  | {"6.2.1", "6.2", false}, | 
|  | {"6.2", "6.2.1", true}, | 
|  | {"6.10", "6.10.1", true}, | 
|  | {"6.10", "6.1.1", false}, // 10, not 1 | 
|  | {"6.1", "6.1", false},    // equal, so not less | 
|  | } | 
|  | for _, tt := range tests { | 
|  | got := (specPart{tt.sec1, "foo"}).Less(specPart{tt.sec2, "foo"}) | 
|  | if got != tt.want { | 
|  | t.Errorf("Less(%q, %q) = %v; want %v", tt.sec1, tt.sec2, got, tt.want) | 
|  | } | 
|  | } | 
|  | } |