133 lines
5.0 KiB
Go
133 lines
5.0 KiB
Go
package check
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// A pull request's failed test, its whole output read from the build step that said it (the operator, 2026-10-30: the
|
|
// verdict cuts a failure to its last lines, and its cause was guessed). The verdict names the step, its build module and
|
|
// machine and the check it ran for (delivery.FailedFirst); the build module's log tool reads the step's log from its own
|
|
// stream (mesh-sdk buildstep.LogTool). The verdict is found through the pull request's candidate, or, once no candidate
|
|
// holds it, as the newest verdict that names the pull request's failure.
|
|
|
|
// FailureStep is where a failure's whole output is: the step id, its build module and machine, the run it ran for, and the
|
|
// call that reads it.
|
|
type FailureStep struct {
|
|
ID string `json:"id"`
|
|
Module string `json:"module"`
|
|
Machine string `json:"machine,omitempty"`
|
|
Run string `json:"run"`
|
|
Read string `json:"read"`
|
|
}
|
|
|
|
// FailureRead is a pull request's failed test: the verdict that names it, the test and the words the verdict keeps, the
|
|
// step that holds the rest, and the first page of its log (Output, the log tool's answer), or why there is none.
|
|
type FailureRead struct {
|
|
Pull string `json:"pull"`
|
|
Verdict string `json:"verdict"`
|
|
Test string `json:"test,omitempty"`
|
|
Words string `json:"words,omitempty"`
|
|
Step *FailureStep `json:"step,omitempty"`
|
|
Output json.RawMessage `json:"output,omitempty"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// logToolOf is a build module's log tool: mesh-build-go's build_go_log.
|
|
func logToolOf(module string) string {
|
|
lang := strings.TrimPrefix(module, "mesh-build-")
|
|
return module + ".build_" + lang + "_log"
|
|
}
|
|
|
|
// failureOf is the failure a verdict names of the pull request pull: the first that names it, or, in a verdict that names
|
|
// no pull request (written before), the first failure.
|
|
func failureOf(v delivery.Verdict, pull string) (delivery.FailedFirst, bool) {
|
|
for _, f := range append([]delivery.FailedFirst{v.Failed}, v.Failures...) {
|
|
if f.Pull == pull && f.Test != "" {
|
|
return f, true
|
|
}
|
|
}
|
|
if v.Failed.Pull == "" && v.Failed.Test != "" {
|
|
return v.Failed, true
|
|
}
|
|
return delivery.FailedFirst{}, false
|
|
}
|
|
|
|
// Failure answers a pull request's failed test, with the page of its log from the stream sequence from on (0 the start).
|
|
func (r Reader) Failure(repository string, number int, from uint64) (FailureRead, error) {
|
|
pull := fmt.Sprintf("%s#%d", repository, number)
|
|
out := FailureRead{Pull: pull}
|
|
var v delivery.Verdict
|
|
found := false
|
|
if read, err := r.Pull(repository, number); err == nil && len(read.Verdict) > 0 {
|
|
if v, err = delivery.DecodeVerdict(read.Verdict); err != nil {
|
|
return out, fmt.Errorf("%s holds what is not a verdict: %w", read.Key, err)
|
|
}
|
|
out.Verdict, found = read.Key, true
|
|
} else if err != nil && !strings.HasPrefix(err.Error(), "no candidate") {
|
|
return out, err
|
|
}
|
|
if _, ok := failureOf(v, pull); !found || v.State != delivery.VerdictFail || !ok {
|
|
// No candidate holds it, or its candidate's verdict did not fail of it: the newest verdict that names its failure.
|
|
key, last, err := r.lastFailure(pull)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
if key == "" {
|
|
if found {
|
|
out.Note = "its verdict names no failed test of it, and no other verdict does"
|
|
return out, nil
|
|
}
|
|
return out, fmt.Errorf("no candidate holds %s, and no verdict names a failed test of it", pull)
|
|
}
|
|
out.Verdict, v = key, last
|
|
}
|
|
f, _ := failureOf(v, pull)
|
|
out.Test, out.Words = f.Test, f.Words
|
|
if f.Step == "" {
|
|
out.Note = "the verdict names no build step that kept the whole output (a build module or a checker from before it)"
|
|
return out, nil
|
|
}
|
|
out.Step = &FailureStep{ID: f.Step, Module: f.Module, Machine: f.Machine, Run: f.Run,
|
|
Read: fmt.Sprintf("%s {run: %s, step: %s}", logToolOf(f.Module), f.Run, f.Step)}
|
|
if r.Logs == nil {
|
|
out.Note = "the build module's log cannot be asked here"
|
|
return out, nil
|
|
}
|
|
page, err := r.Logs(logToolOf(f.Module), map[string]any{"run": f.Run, "step": f.Step, "from": from})
|
|
if err != nil {
|
|
out.Note = "its log could not be read: " + err.Error()
|
|
return out, nil
|
|
}
|
|
out.Output = page
|
|
return out, nil
|
|
}
|
|
|
|
// lastFailure is the newest verdict that names a failed test of pull, by its step id (an id begins with its time), and
|
|
// its key; "" when none does among the first MaxReads.
|
|
func (r Reader) lastFailure(pull string) (string, delivery.Verdict, error) {
|
|
rows, _, err := r.Verdicts("")
|
|
if err != nil {
|
|
return "", delivery.Verdict{}, err
|
|
}
|
|
var key, newest string
|
|
var last delivery.Verdict
|
|
for _, row := range rows {
|
|
v, err := delivery.DecodeVerdict(row.Verdict)
|
|
if err != nil || v.State != delivery.VerdictFail {
|
|
continue
|
|
}
|
|
f, ok := failureOf(v, pull)
|
|
if !ok || f.Pull != pull {
|
|
continue
|
|
}
|
|
if key == "" || len(f.Step) > len(newest) || (len(f.Step) == len(newest) && f.Step > newest) {
|
|
key, newest, last = row.Key, f.Step, v
|
|
}
|
|
}
|
|
return key, last, nil
|
|
}
|