// Copyright 2017 The Bazel 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 chunkedfile provides utilities for testing that source code // errors are reported in the appropriate places. // // A chunked file consists of several chunks of input text separated by // "---" lines. Each chunk is an input to the program under test, such // as an evaluator. Lines containing "###" are interpreted as // expectations of failure: the following text is a Go string literal // denoting a regular expression that should match the failure message. // // Example: // // x = 1 / 0 ### "division by zero" // --- // x = 1 // print(x + "") ### "int + string not supported" // // A client test feeds each chunk of text into the program under test, // then calls chunk.GotError for each error that actually occurred. Any // discrepancy between the actual and expected errors is reported using // the client's reporter, which is typically a testing.T. package chunkedfile // import "go.starlark.net/internal/chunkedfile" import ( "fmt" "io/ioutil" "regexp" "strconv" "strings" ) const debug = false // A Chunk is a portion of a source file. // It contains a set of expected errors. type Chunk struct { Source string filename string report Reporter wantErrs map[int]*regexp.Regexp } // Reporter is implemented by *testing.T. type Reporter interface { Errorf(format string, args ...interface{}) } // Read parses a chunked file and returns its chunks. // It reports failures using the reporter. // // Error messages of the form "file.star:line:col: ..." are prefixed // by a newline so that the Go source position added by (*testing.T).Errorf // appears on a separate line so as not to confused editors. func Read(filename string, report Reporter) (chunks []Chunk) { data, err := ioutil.ReadFile(filename) if err != nil { report.Errorf("%s", err) return } linenum := 1 for i, chunk := range strings.Split(string(data), "\n---\n") { if debug { fmt.Printf("chunk %d at line %d: %s\n", i, linenum, chunk) } // Pad with newlines so the line numbers match the original file. src := strings.Repeat("\n", linenum-1) + chunk wantErrs := make(map[int]*regexp.Regexp) // Parse comments of the form: // ### "expected error". lines := strings.Split(chunk, "\n") for j := 0; j < len(lines); j, linenum = j+1, linenum+1 { line := lines[j] hashes := strings.Index(line, "###") if hashes < 0 { continue } rest := strings.TrimSpace(line[hashes+len("###"):]) pattern, err := strconv.Unquote(rest) if err != nil { report.Errorf("\n%s:%d: not a quoted regexp: %s", filename, linenum, rest) continue } rx, err := regexp.Compile(pattern) if err != nil { report.Errorf("\n%s:%d: %v", filename, linenum, err) continue } wantErrs[linenum] = rx if debug { fmt.Printf("\t%d\t%s\n", linenum, rx) } } linenum++ chunks = append(chunks, Chunk{src, filename, report, wantErrs}) } return chunks } // GotError should be called by the client to report an error at a particular line. // GotError reports unexpected errors to the chunk's reporter. func (chunk *Chunk) GotError(linenum int, msg string) { if rx, ok := chunk.wantErrs[linenum]; ok { delete(chunk.wantErrs, linenum) if !rx.MatchString(msg) { chunk.report.Errorf("\n%s:%d: error %q does not match pattern %q", chunk.filename, linenum, msg, rx) } } else { chunk.report.Errorf("\n%s:%d: unexpected error: %v", chunk.filename, linenum, msg) } } // Done should be called by the client to indicate that the chunk has no more errors. // Done reports expected errors that did not occur to the chunk's reporter. func (chunk *Chunk) Done() { for linenum, rx := range chunk.wantErrs { chunk.report.Errorf("\n%s:%d: expected error matching %q", chunk.filename, linenum, rx) } }