package test import ( "bytes" "errors" "fmt" "io/ioutil" "log" "regexp" "strconv" "strings" ) // errorCheck matches errors in outStr against comments in source files. // For each line of the source files which should generate an error, // there should be a comment of the form // ERROR "regexp". // If outStr has an error for a line which has no such comment, // this function will report an error. // Likewise if outStr does not have an error for a line which has a comment, // or if the error message does not match the . // The syntax is Perl but it's best to stick to egrep. // // Sources files are supplied as fullshort slice. // It consists of pairs: full path to source file and its base name. //nolint:gocyclo,funlen func errorCheck(outStr string, wantAuto bool, fullshort ...string) (err error) { var errs []error out := splitOutput(outStr, wantAuto) // Cut directory name. for i := range out { for j := 0; j < len(fullshort); j += 2 { full, short := fullshort[j], fullshort[j+1] out[i] = strings.Replace(out[i], full, short, -1) } } var want []wantedError for j := 0; j < len(fullshort); j += 2 { full, short := fullshort[j], fullshort[j+1] want = append(want, wantedErrors(full, short)...) } for _, we := range want { var errmsgs []string if we.auto { errmsgs, out = partitionStrings("", out) } else { errmsgs, out = partitionStrings(we.prefix, out) } if len(errmsgs) == 0 { errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr)) continue } matched := false n := len(out) var textsToMatch []string for _, errmsg := range errmsgs { // Assume errmsg says "file:line: foo". // Cut leading "file:line: " to avoid accidental matching of file name instead of message. text := errmsg if i := strings.Index(text, " "); i >= 0 { text = text[i+1:] } if we.re.MatchString(text) { matched = true } else { out = append(out, errmsg) textsToMatch = append(textsToMatch, text) } } if !matched { err := fmt.Errorf("%s:%d: no match for %#q vs %q in:\n\t%s", we.file, we.lineNum, we.reStr, textsToMatch, strings.Join(out[n:], "\n\t")) errs = append(errs, err) continue } } if len(out) > 0 { errs = append(errs, fmt.Errorf("unmatched errors")) for _, errLine := range out { errs = append(errs, fmt.Errorf("%s", errLine)) } } if len(errs) == 0 { return nil } if len(errs) == 1 { return errs[0] } var buf bytes.Buffer fmt.Fprintf(&buf, "\n") for _, err := range errs { fmt.Fprintf(&buf, "%s\n", err.Error()) } return errors.New(buf.String()) } func splitOutput(out string, wantAuto bool) []string { // gc error messages continue onto additional lines with leading tabs. // Split the output at the beginning of each line that doesn't begin with a tab. // lines are impossible to match so those are filtered out. var res []string for _, line := range strings.Split(out, "\n") { line = strings.TrimSuffix(line, "\r") // normalize Windows output if strings.HasPrefix(line, "\t") { //nolint:gocritic res[len(res)-1] += "\n" + line } else if strings.HasPrefix(line, "go tool") || strings.HasPrefix(line, "#") || !wantAuto && strings.HasPrefix(line, "") { continue } else if strings.TrimSpace(line) != "" { res = append(res, line) } } return res } // matchPrefix reports whether s starts with file name prefix followed by a :, // and possibly preceded by a directory name. func matchPrefix(s, prefix string) bool { i := strings.Index(s, ":") if i < 0 { return false } j := strings.LastIndex(s[:i], "/") s = s[j+1:] if len(s) <= len(prefix) || s[:len(prefix)] != prefix { return false } if s[len(prefix)] == ':' { return true } return false } func partitionStrings(prefix string, strs []string) (matched, unmatched []string) { for _, s := range strs { if matchPrefix(s, prefix) { matched = append(matched, s) } else { unmatched = append(unmatched, s) } } return } type wantedError struct { reStr string re *regexp.Regexp lineNum int auto bool // match line file string prefix string } var ( errRx = regexp.MustCompile(`// (?:GC_)?ERROR (.*)`) errAutoRx = regexp.MustCompile(`// (?:GC_)?ERRORAUTO (.*)`) errQuotesRx = regexp.MustCompile(`"([^"]*)"`) lineRx = regexp.MustCompile(`LINE(([+-])([0-9]+))?`) ) // wantedErrors parses expected errors from comments in a file. //nolint:nakedret,gocyclo,funlen func wantedErrors(file, short string) (errs []wantedError) { cache := make(map[string]*regexp.Regexp) src, err := ioutil.ReadFile(file) if err != nil { log.Fatal(err) } for i, line := range strings.Split(string(src), "\n") { lineNum := i + 1 if strings.Contains(line, "////") { // double comment disables ERROR continue } var auto bool m := errAutoRx.FindStringSubmatch(line) if m != nil { auto = true } else { m = errRx.FindStringSubmatch(line) } if m == nil { continue } all := m[1] mm := errQuotesRx.FindAllStringSubmatch(all, -1) if mm == nil { log.Fatalf("%s:%d: invalid errchk line: %s", file, lineNum, line) } for _, m := range mm { replacedOnce := false rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string { if replacedOnce { return m } replacedOnce = true n := lineNum if strings.HasPrefix(m, "LINE+") { delta, _ := strconv.Atoi(m[5:]) n += delta } else if strings.HasPrefix(m, "LINE-") { delta, _ := strconv.Atoi(m[5:]) n -= delta } return fmt.Sprintf("%s:%d", short, n) }) re := cache[rx] if re == nil { var err error re, err = regexp.Compile(rx) if err != nil { log.Fatalf("%s:%d: invalid regexp \"%#q\" in ERROR line: %v", file, lineNum, rx, err) } cache[rx] = re } prefix := fmt.Sprintf("%s:%d", short, lineNum) errs = append(errs, wantedError{ reStr: rx, re: re, prefix: prefix, auto: auto, lineNum: lineNum, file: short, }) } } return }