github.com/google/skylark@v0.0.0-20181101142754-a5f7082aabed/internal/chunkedfile/chunkedfile.go (about) 1 // Copyright 2017 The Bazel 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 5 // Package chunkedfile provides utilities for testing that source code 6 // errors are reported in the appropriate places. 7 // 8 // A chunked file consists of several chunks of input text separated by 9 // "---" lines. Each chunk is an input to the program under test, such 10 // as an evaluator. Lines containing "###" are interpreted as 11 // expectations of failure: the following text is a Go string literal 12 // denoting a regular expression that should match the failure message. 13 // 14 // Example: 15 // 16 // x = 1 / 0 ### "division by zero" 17 // --- 18 // x = 1 19 // print(x + "") ### "int + string not supported" 20 // 21 // A client test feeds each chunk of text into the program under test, 22 // then calls chunk.GotError for each error that actually occurred. Any 23 // discrepancy between the actual and expected errors is reported using 24 // the client's reporter, which is typically a testing.T. 25 package chunkedfile 26 27 import ( 28 "fmt" 29 "io/ioutil" 30 "regexp" 31 "strconv" 32 "strings" 33 ) 34 35 const debug = false 36 37 // A Chunk is a portion of a source file. 38 // It contains a set of expected errors. 39 type Chunk struct { 40 Source string 41 filename string 42 report Reporter 43 wantErrs map[int]*regexp.Regexp 44 } 45 46 // Reporter is implemented by *testing.T. 47 type Reporter interface { 48 Errorf(format string, args ...interface{}) 49 } 50 51 // Read parses a chunked file and returns its chunks. 52 // It reports failures using the reporter. 53 func Read(filename string, report Reporter) (chunks []Chunk) { 54 data, err := ioutil.ReadFile(filename) 55 if err != nil { 56 report.Errorf("%s", err) 57 return 58 } 59 linenum := 1 60 for i, chunk := range strings.Split(string(data), "\n---\n") { 61 chunk := string(chunk) 62 if debug { 63 fmt.Printf("chunk %d at line %d: %s\n", i, linenum, chunk) 64 } 65 // Pad with newlines so the line numbers match the original file. 66 src := strings.Repeat("\n", linenum-1) + chunk 67 68 wantErrs := make(map[int]*regexp.Regexp) 69 70 // Parse comments of the form: 71 // ### "expected error". 72 lines := strings.Split(chunk, "\n") 73 for j := 0; j < len(lines); j, linenum = j+1, linenum+1 { 74 line := lines[j] 75 hashes := strings.Index(line, "###") 76 if hashes < 0 { 77 continue 78 } 79 rest := strings.TrimSpace(line[hashes+len("###"):]) 80 pattern, err := strconv.Unquote(rest) 81 if err != nil { 82 report.Errorf("%s:%d: not a quoted regexp: %s", filename, linenum, rest) 83 continue 84 } 85 rx, err := regexp.Compile(pattern) 86 if err != nil { 87 report.Errorf("%s:%d: %v", filename, linenum, err) 88 continue 89 } 90 wantErrs[linenum] = rx 91 if debug { 92 fmt.Printf("\t%d\t%s\n", linenum, rx) 93 } 94 } 95 linenum++ 96 97 chunks = append(chunks, Chunk{src, filename, report, wantErrs}) 98 } 99 return chunks 100 } 101 102 // GotError should be called by the client to report an error at a particular line. 103 // GotError reports unexpected errors to the chunk's reporter. 104 func (chunk *Chunk) GotError(linenum int, msg string) { 105 if rx, ok := chunk.wantErrs[linenum]; ok { 106 delete(chunk.wantErrs, linenum) 107 if !rx.MatchString(msg) { 108 chunk.report.Errorf("%s:%d: error %q does not match pattern %q", chunk.filename, linenum, msg, rx) 109 } 110 } else { 111 chunk.report.Errorf("%s:%d: unexpected error: %v", chunk.filename, linenum, msg) 112 } 113 } 114 115 // Done should be called by the client to indicate that the chunk has no more errors. 116 // Done reports expected errors that did not occur to the chunk's reporter. 117 func (chunk *Chunk) Done() { 118 for linenum, rx := range chunk.wantErrs { 119 chunk.report.Errorf("%s:%d: expected error matching %q", chunk.filename, linenum, rx) 120 } 121 }