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  }