github.com/lab47/exprcore@v0.0.0-20210525052339-fb7d6bd9331e/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 // import "github.com/lab47/exprcore/internal/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 // 54 // Error messages of the form "file.star:line:col: ..." are prefixed 55 // by a newline so that the Go source position added by (*testing.T).Errorf 56 // appears on a separate line so as not to confused editors. 57 func Read(filename string, report Reporter) (chunks []Chunk) { 58 data, err := ioutil.ReadFile(filename) 59 if err != nil { 60 report.Errorf("%s", err) 61 return 62 } 63 linenum := 1 64 for i, chunk := range strings.Split(string(data), "\n---\n") { 65 if debug { 66 fmt.Printf("chunk %d at line %d: %s\n", i, linenum, chunk) 67 } 68 // Pad with newlines so the line numbers match the original file. 69 src := strings.Repeat("\n", linenum-1) + chunk 70 71 wantErrs := make(map[int]*regexp.Regexp) 72 73 // Parse comments of the form: 74 // ### "expected error". 75 lines := strings.Split(chunk, "\n") 76 for j := 0; j < len(lines); j, linenum = j+1, linenum+1 { 77 line := lines[j] 78 hashes := strings.Index(line, "###") 79 if hashes < 0 { 80 continue 81 } 82 rest := strings.TrimSpace(line[hashes+len("###"):]) 83 pattern, err := strconv.Unquote(rest) 84 if err != nil { 85 report.Errorf("\n%s:%d: not a quoted regexp: %s", filename, linenum, rest) 86 continue 87 } 88 rx, err := regexp.Compile(pattern) 89 if err != nil { 90 report.Errorf("\n%s:%d: %v", filename, linenum, err) 91 continue 92 } 93 wantErrs[linenum] = rx 94 if debug { 95 fmt.Printf("\t%d\t%s\n", linenum, rx) 96 } 97 } 98 linenum++ 99 100 chunks = append(chunks, Chunk{src, filename, report, wantErrs}) 101 } 102 return chunks 103 } 104 105 // GotError should be called by the client to report an error at a particular line. 106 // GotError reports unexpected errors to the chunk's reporter. 107 func (chunk *Chunk) GotError(linenum int, msg string) { 108 if rx, ok := chunk.wantErrs[linenum]; ok { 109 delete(chunk.wantErrs, linenum) 110 if !rx.MatchString(msg) { 111 chunk.report.Errorf("\n%s:%d: error %q does not match pattern %q", chunk.filename, linenum, msg, rx) 112 } 113 } else { 114 chunk.report.Errorf("\n%s:%d: unexpected error: %v", chunk.filename, linenum, msg) 115 } 116 } 117 118 // Done should be called by the client to indicate that the chunk has no more errors. 119 // Done reports expected errors that did not occur to the chunk's reporter. 120 func (chunk *Chunk) Done() { 121 for linenum, rx := range chunk.wantErrs { 122 chunk.report.Errorf("\n%s:%d: expected error matching %q", chunk.filename, linenum, rx) 123 } 124 }