go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/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 "go.starlark.net/internal/chunkedfile" 26 27 import ( 28 "fmt" 29 "os" 30 "regexp" 31 "runtime" 32 "strconv" 33 "strings" 34 ) 35 36 const debug = false 37 38 // A Chunk is a portion of a source file. 39 // It contains a set of expected errors. 40 type Chunk struct { 41 Source string 42 filename string 43 report Reporter 44 wantErrs map[int]*regexp.Regexp 45 } 46 47 // Reporter is implemented by *testing.T. 48 type Reporter interface { 49 Errorf(format string, args ...interface{}) 50 } 51 52 // Read parses a chunked file and returns its chunks. 53 // It reports failures using the reporter. 54 // 55 // Error messages of the form "file.star:line:col: ..." are prefixed 56 // by a newline so that the Go source position added by (*testing.T).Errorf 57 // appears on a separate line so as not to confused editors. 58 func Read(filename string, report Reporter) (chunks []Chunk) { 59 data, err := os.ReadFile(filename) 60 if err != nil { 61 report.Errorf("%s", err) 62 return 63 } 64 linenum := 1 65 66 eol := "\n" 67 if runtime.GOOS == "windows" { 68 eol = "\r\n" 69 } 70 71 for i, chunk := range strings.Split(string(data), eol+"---"+eol) { 72 if debug { 73 fmt.Printf("chunk %d at line %d: %s\n", i, linenum, chunk) 74 } 75 // Pad with newlines so the line numbers match the original file. 76 src := strings.Repeat("\n", linenum-1) + chunk 77 78 wantErrs := make(map[int]*regexp.Regexp) 79 80 // Parse comments of the form: 81 // ### "expected error". 82 lines := strings.Split(chunk, "\n") 83 for j := 0; j < len(lines); j, linenum = j+1, linenum+1 { 84 line := lines[j] 85 hashes := strings.Index(line, "###") 86 if hashes < 0 { 87 continue 88 } 89 rest := strings.TrimSpace(line[hashes+len("###"):]) 90 pattern, err := strconv.Unquote(rest) 91 if err != nil { 92 report.Errorf("\n%s:%d: not a quoted regexp: %s", filename, linenum, rest) 93 continue 94 } 95 rx, err := regexp.Compile(pattern) 96 if err != nil { 97 report.Errorf("\n%s:%d: %v", filename, linenum, err) 98 continue 99 } 100 wantErrs[linenum] = rx 101 if debug { 102 fmt.Printf("\t%d\t%s\n", linenum, rx) 103 } 104 } 105 linenum++ 106 107 chunks = append(chunks, Chunk{src, filename, report, wantErrs}) 108 } 109 return chunks 110 } 111 112 // GotError should be called by the client to report an error at a particular line. 113 // GotError reports unexpected errors to the chunk's reporter. 114 func (chunk *Chunk) GotError(linenum int, msg string) { 115 if rx, ok := chunk.wantErrs[linenum]; ok { 116 delete(chunk.wantErrs, linenum) 117 if !rx.MatchString(msg) { 118 chunk.report.Errorf("\n%s:%d: error %q does not match pattern %q", chunk.filename, linenum, msg, rx) 119 } 120 } else { 121 chunk.report.Errorf("\n%s:%d: unexpected error: %v", chunk.filename, linenum, msg) 122 } 123 } 124 125 // Done should be called by the client to indicate that the chunk has no more errors. 126 // Done reports expected errors that did not occur to the chunk's reporter. 127 func (chunk *Chunk) Done() { 128 for linenum, rx := range chunk.wantErrs { 129 chunk.report.Errorf("\n%s:%d: expected error matching %q", chunk.filename, linenum, rx) 130 } 131 }