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.
25package chunkedfile // import "go.starlark.net/internal/chunkedfile"
26
27import (
28	"fmt"
29	"io/ioutil"
30	"regexp"
31	"strconv"
32	"strings"
33)
34
35const debug = false
36
37// A Chunk is a portion of a source file.
38// It contains a set of expected errors.
39type 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.
47type 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.
57func 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.
107func (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.
120func (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}
125