| // 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) |
| } |
| } |
| } |