A build or check failed now and then on 'Could not resolve host: novox.internal' between clones that resolved the same name, and its whole delivery failed with it. A clone whose lookup failed reached nothing, so it is cloned again with a growing pause for up to 60 s, each retry said in the build's log as the resolver not answering; past the bound it fails saying so. Any other git failure fails at once, as before. Covers a module's own clone, an artifact's context and a check's repositories.
163 lines
6.2 KiB
Go
163 lines
6.2 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Issue 1773: a build or a check failed now and then on "Could not resolve host: novox.internal" while cloning from
|
|
// the forge, between clones that resolved the same name, and its whole delivery failed with it. A clone that fails on
|
|
// the name lookup is tried again within a bound, says each retry in the build's log as the resolver's failure, and
|
|
// only past the bound fails, saying the resolver did not answer. Any other failure of git fails at once, as before.
|
|
|
|
const lookupFailure = "exit status 128\nfatal: unable to access 'http://novox.internal:20000/novox/x.git/': " +
|
|
"Could not resolve host: novox.internal"
|
|
|
|
// failingClones is a runner whose first n forge clones fail with failure, then clone as r does; n < 0 fails every one.
|
|
type failingClones struct {
|
|
r *recorded
|
|
n int
|
|
failure string
|
|
clones int
|
|
}
|
|
|
|
func (f *failingClones) run(ctx context.Context, dir, name string, args ...string) (string, error) {
|
|
if name == "git" && isACloneOf(args) {
|
|
f.clones++
|
|
if f.n < 0 || f.clones <= f.n {
|
|
return "", fmt.Errorf("git %s: %s", strings.Join(args, " "), f.failure)
|
|
}
|
|
}
|
|
return f.r.run(ctx, dir, name, args...)
|
|
}
|
|
|
|
func isACloneOf(args []string) bool {
|
|
for _, a := range args {
|
|
if a == "clone" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// buildLog collects what a build says in its own log.
|
|
func buildLog() (Log, func() []string) {
|
|
var mu sync.Mutex
|
|
var lines []string
|
|
return func(step, message string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
lines = append(lines, step+": "+message)
|
|
}, func() []string {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return append([]string(nil), lines...)
|
|
}
|
|
}
|
|
|
|
const nothingToBuild = `{"module":"x","version":"1"}`
|
|
|
|
// 1773/AC1: a clone failing twice on the lookup, then working, is tried again, each retry said, and the build goes on.
|
|
func TestACloneThatFailsOnTheLookupIsTriedAgainAndTheBuildGoesOn(t *testing.T) {
|
|
quickLookups(t)
|
|
r, workspace := aRepository(t, nothingToBuild, nil)
|
|
f := &failingClones{r: r, n: 2, failure: lookupFailure}
|
|
log, said := buildLog()
|
|
_, err := Build(context.Background(), f.run, r, "http://novox.internal:20000/novox/x.git", "", "", workspace, nil,
|
|
Npmrc{}, GitCredential{}, log)
|
|
if err != nil {
|
|
t.Fatalf("a clone the resolver failed twice, then answered, ended the build: %v", err)
|
|
}
|
|
if f.clones != 3 {
|
|
t.Errorf("the clone ran %d times, want 3 (two lookups failed, the third worked)", f.clones)
|
|
}
|
|
retries := 0
|
|
for _, line := range said() {
|
|
if strings.Contains(line, "the resolver did not answer") && strings.Contains(line, "novox.internal") {
|
|
retries++
|
|
}
|
|
}
|
|
if retries != 2 {
|
|
t.Errorf("the build's log said %d retries as the resolver's failure, want 2:\n%s", retries,
|
|
strings.Join(said(), "\n"))
|
|
}
|
|
}
|
|
|
|
// 1773/AC2: a lookup that keeps failing past the bound fails the build, saying the resolver did not answer.
|
|
func TestACloneWhoseLookupNeverAnswersFailsSayingTheResolverDidNotAnswer(t *testing.T) {
|
|
quickLookups(t)
|
|
r, workspace := aRepository(t, nothingToBuild, nil)
|
|
f := &failingClones{r: r, n: -1, failure: lookupFailure}
|
|
_, err := Build(context.Background(), f.run, r, "http://novox.internal:20000/novox/x.git", "", "", workspace, nil,
|
|
Npmrc{}, GitCredential{}, nil)
|
|
if err == nil {
|
|
t.Fatal("a clone whose lookup never answered built")
|
|
}
|
|
if !strings.Contains(err.Error(), "the resolver did not answer") ||
|
|
!strings.Contains(err.Error(), "http://novox.internal:20000/novox/x.git") {
|
|
t.Errorf("the failure does not say the resolver did not answer for the forge's URL: %v", err)
|
|
}
|
|
if f.clones < 2 {
|
|
t.Errorf("the clone ran %d times; a failed lookup is tried again within the bound", f.clones)
|
|
}
|
|
}
|
|
|
|
// 1773/AC3: any other failure of git (authentication, a missing repository) is not tried again and fails at once.
|
|
func TestACloneFailingForAnotherReasonIsNotTriedAgain(t *testing.T) {
|
|
quickLookups(t)
|
|
for _, failure := range []string{
|
|
"exit status 128\nfatal: Authentication failed for 'http://novox.internal:20000/novox/x.git/'",
|
|
"exit status 128\nremote: Not found.\nfatal: repository 'http://novox.internal:20000/novox/x.git/' not found",
|
|
"exit status 128\nfatal: unable to access 'http://novox.internal:20000/novox/x.git/': The requested URL returned error: 404",
|
|
} {
|
|
r, workspace := aRepository(t, nothingToBuild, nil)
|
|
f := &failingClones{r: r, n: -1, failure: failure}
|
|
_, err := Build(context.Background(), f.run, r, "http://novox.internal:20000/novox/x.git", "", "", workspace, nil,
|
|
Npmrc{}, GitCredential{}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "cannot clone") {
|
|
t.Errorf("a clone failing with %q said %v", failure, err)
|
|
}
|
|
if f.clones != 1 {
|
|
t.Errorf("a clone failing with %q ran %d times; it is not a lookup and is not tried again", failure, f.clones)
|
|
}
|
|
}
|
|
}
|
|
|
|
// quickLookups shortens the bound and the pauses, so a test waits for milliseconds.
|
|
func quickLookups(t *testing.T) {
|
|
t.Helper()
|
|
wasBound, wasFirst := lookupWait, lookupFirstPause
|
|
lookupWait, lookupFirstPause = 200*time.Millisecond, 10*time.Millisecond
|
|
t.Cleanup(func() { lookupWait, lookupFirstPause = wasBound, wasFirst })
|
|
}
|
|
|
|
// 1773/AC1, 1773/AC3: a check's clones are the same: a failed lookup is tried again, and the failure after it (here
|
|
// an authentication refused) ends the check at once, not tried again.
|
|
func TestACheckCloneTriesAFailedLookupAgain(t *testing.T) {
|
|
quickLookups(t)
|
|
clones := 0
|
|
run := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
if name == "git" && isACloneOf(args) {
|
|
clones++
|
|
if clones <= 2 {
|
|
return "", fmt.Errorf("git clone: %s", lookupFailure)
|
|
}
|
|
return "", fmt.Errorf("git clone: exit status 128\nfatal: Authentication failed")
|
|
}
|
|
return "", nil
|
|
}
|
|
_, err := Check(context.Background(), run, CheckSpec{ID: "build-1", Owner: "novox", Repo: "x", Number: 1,
|
|
Repository: "http://novox.internal:20000/novox/x.git", Ref: "abc"}, t.TempDir(), "registry.invalid",
|
|
GitCredential{}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "cannot clone") || !strings.Contains(err.Error(), "Authentication failed") {
|
|
t.Fatalf("the check said %v", err)
|
|
}
|
|
if clones != 3 {
|
|
t.Errorf("the check's clone ran %d times, want 3: two failed lookups, then the refusal, not tried again", clones)
|
|
}
|
|
}
|