146 lines
5.8 KiB
Go
146 lines
5.8 KiB
Go
package buildstep
|
|
|
|
// The failure contract of a build step's test level (novox/hq issue 1901, its thorough fix): what a build module says of
|
|
// each test that failed, the same for every language. Each build module used to parse its own test runner's output, and
|
|
// a fix in one never reached the others: a failed Go test kept a t.Logf line, a failed node --test run kept a passing
|
|
// '✔' line of its first file. The parsers that fill the contract live here (gotest.go, tap.go) and the conformance kit
|
|
// (package conformance) holds every build module to them with captured runner output.
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/sdkversion"
|
|
)
|
|
|
|
// Failure is one test that failed, or a package or file that failed with no test to name (a build that failed, a test
|
|
// program that panicked, a test file that did not load). The JSON names are the ones mesh-build-go and the checker have
|
|
// always used; File is new and an older reader ignores it.
|
|
type Failure struct {
|
|
// Package is the Go package's import path, or the TypeScript package's directory in the workspace.
|
|
Package string `json:"package"`
|
|
// Test names the failing test, or the tests that were running when the program stopped; "" when none can be named.
|
|
Test string `json:"test,omitempty"`
|
|
// File is where the failing test is (a Go test's file and line is in its Cause; node names the test's file:line:col).
|
|
File string `json:"file,omitempty"`
|
|
// Cause is the one line that says why: the assertion's or the error's first line, a "panic:" line, a build's first
|
|
// error, or words saying how the program stopped. **Never empty, and never a passing line** (PassingLine).
|
|
Cause string `json:"cause,omitempty"`
|
|
// Output is the failing test's own output, its last lines up to OutputBytes, Cause first: an older checker reads
|
|
// only its first line.
|
|
Output string `json:"output,omitempty"`
|
|
}
|
|
|
|
// OutputBytes is the most a failure keeps of its output, and LineBytes the most of one line: four kilobytes holds a long
|
|
// assertion message and stays far under a bus message's or a verdict's bound.
|
|
const (
|
|
OutputBytes = 4096
|
|
LineBytes = 1024
|
|
)
|
|
|
|
// By names a build module and the SDK it was built with, for a result's "by": the checker names it when a failure hides
|
|
// its cause ("the build module hid the failure (<by>)").
|
|
func By(module string) string {
|
|
return module + " with mesh-sdk " + sdkversion.Version()
|
|
}
|
|
|
|
// PassingLine says a line is a test runner's word for a test or a package that passed: node's '✔', TAP's and go test's
|
|
// "ok", go test's "PASS" and "--- PASS". Such a line is never a failure's cause, nor part of a failure's output.
|
|
func PassingLine(l string) bool {
|
|
t := strings.TrimSpace(l)
|
|
switch {
|
|
case t == "PASS", t == "ok", strings.HasPrefix(t, "✔"), strings.HasPrefix(t, "--- PASS"),
|
|
strings.HasPrefix(t, "ok "), strings.HasPrefix(t, "ok\t"):
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Hidden says how a failure hides why it failed, or "" when it does not: it has no cause, or its cause is a passing line.
|
|
// The checker says it loudly (issue 1901): a build module that sends such a failure does not keep the contract.
|
|
func Hidden(f Failure) string {
|
|
c := strings.TrimSpace(f.Cause)
|
|
switch {
|
|
case c == "":
|
|
return "it sent no cause"
|
|
case PassingLine(c):
|
|
return fmt.Sprintf("its cause is a passing line (%q)", CutLine(c))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Clip joins the last lines of a failure's output that fit OutputBytes, each cut to LineBytes, and says how many earlier
|
|
// lines it left out. **Its first line is the cause**, put ahead when the kept lines do not start with it, or, with no
|
|
// cause and lines left out, the first non-empty line, then the marker: an older checker reads only the first line.
|
|
// Passing lines are dropped.
|
|
func Clip(lines []string, cause string) string {
|
|
own := make([]string, 0, len(lines))
|
|
for _, l := range lines {
|
|
if !PassingLine(l) {
|
|
own = append(own, l)
|
|
}
|
|
}
|
|
lines = own
|
|
size, from := 0, len(lines)
|
|
for from > 0 {
|
|
n := len(CutLine(lines[from-1])) + 1
|
|
if size+n > OutputBytes && from < len(lines) {
|
|
break
|
|
}
|
|
size += n
|
|
from--
|
|
}
|
|
kept := make([]string, 0, len(lines)-from+2)
|
|
head := strings.TrimSpace(cause)
|
|
for i := 0; head == "" && i < from; i++ {
|
|
head = strings.TrimSpace(lines[i])
|
|
}
|
|
if head != "" && (from > 0 || len(lines) == 0 || strings.TrimSpace(lines[from]) != head) {
|
|
kept = append(kept, CutLine(head))
|
|
}
|
|
if from > 0 {
|
|
kept = append(kept, fmt.Sprintf("... (%d earlier lines)", from))
|
|
}
|
|
for _, l := range lines[from:] {
|
|
kept = append(kept, CutLine(l))
|
|
}
|
|
return strings.Join(kept, "\n")
|
|
}
|
|
|
|
// CutLine cuts a line to LineBytes, on a character boundary.
|
|
func CutLine(l string) string {
|
|
if len(l) <= LineBytes {
|
|
return l
|
|
}
|
|
return strings.ToValidUTF8(l[:LineBytes], "") + "..."
|
|
}
|
|
|
|
// ConsoleFailure is the failure of a command that is no test runner (go vet, gofmt, tsc, a build script): its cause is the
|
|
// first line that names an error (tsc's "error TS", go vet's "file.go:12:3: ..."), else the first line that is not a
|
|
// heading ("# <package>", npm's "> script"), and its output the command's lines without passing ones.
|
|
func ConsoleFailure(pkg, test, out string) Failure {
|
|
var lines []string
|
|
for _, l := range strings.Split(strings.ReplaceAll(stripANSI(out), "\r\n", "\n"), "\n") {
|
|
if t := strings.TrimSpace(l); t != "" && !PassingLine(t) {
|
|
lines = append(lines, strings.TrimRight(l, " \t"))
|
|
}
|
|
}
|
|
cause := ""
|
|
for _, l := range lines {
|
|
if t := strings.TrimSpace(l); strings.Contains(t, "error") || strings.Contains(t, "Error") || goPosition.MatchString(t) {
|
|
cause = t
|
|
break
|
|
}
|
|
}
|
|
for _, l := range lines {
|
|
if t := strings.TrimSpace(l); cause == "" && !strings.HasPrefix(t, "# ") && !strings.HasPrefix(t, "> ") && !strings.HasPrefix(t, "npm ") {
|
|
cause = t
|
|
}
|
|
}
|
|
if cause == "" {
|
|
cause = test + " failed and wrote nothing that says why"
|
|
}
|
|
cause = CutLine(cause)
|
|
return Failure{Package: pkg, Test: test, Cause: cause, Output: Clip(lines, cause)}
|
|
}
|