3f14554fae
- rename internal/pdbmux to internal/puppetdb - fetch facts with one GET /pdb/query/v4/facts AST query per node - drop the per-node fallback route and its two response shapes - rename --pdb to --puppetdb, CATALOG_DIFF_PDB to CATALOG_DIFF_PUPPETDB and pdb_url to puppetdb_url - describe the fact source generically in the README and AGENTS.md
586 lines
18 KiB
Go
586 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const factsBody = `[
|
|
{"certname":"host.example.net","environment":"develop","name":"os","value":{"family":"RedHat"}}
|
|
]`
|
|
|
|
func catalogFor(environment, content string) string {
|
|
return `{"name":"host.example.net","environment":"` + environment + `","version":1,"classes":["settings"],
|
|
"resources":[
|
|
{"type":"File","title":"/etc/motd","file":"site.pp","line":9,"parameters":{"ensure":"file","content":"` + content + `"}}
|
|
]}`
|
|
}
|
|
|
|
// testServers stands up a fake puppetserver (TLS) and PuppetDB (plain), and
|
|
// returns options wired to them with the server's own cert as the CA bundle.
|
|
func testServers(t *testing.T, compile http.HandlerFunc) *options {
|
|
t.Helper()
|
|
|
|
puppet := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/status/v1/simple" {
|
|
_, _ = w.Write([]byte("running"))
|
|
return
|
|
}
|
|
compile(w, r)
|
|
}))
|
|
t.Cleanup(puppet.Close)
|
|
|
|
pdb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(factsBody))
|
|
}))
|
|
t.Cleanup(pdb.Close)
|
|
|
|
caPath := filepath.Join(t.TempDir(), "ca.pem")
|
|
block := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: puppet.Certificate().Raw})
|
|
if err := os.WriteFile(caPath, block, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return &options{
|
|
base: "develop",
|
|
server: puppet.URL,
|
|
puppetdb: pdb.URL,
|
|
cacert: caPath,
|
|
timeout: 10 * time.Second,
|
|
}
|
|
}
|
|
|
|
func exitCode(t *testing.T, err error) int {
|
|
t.Helper()
|
|
if err == nil {
|
|
return exitSame
|
|
}
|
|
var ee *exitError
|
|
if !errors.As(err, &ee) {
|
|
t.Fatalf("error carries no exit code: %v", err)
|
|
}
|
|
return ee.code
|
|
}
|
|
|
|
func TestResolveEnvironment(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts options
|
|
want string
|
|
wantCode int
|
|
}{
|
|
{name: "branch is sanitised", opts: options{branch: "benvin/foo-bar"}, want: "benvin_foo_bar"},
|
|
{name: "explicit environment wins", opts: options{environment: "develop"}, want: "develop"},
|
|
{name: "neither given", opts: options{}, wantCode: exitTransport},
|
|
{name: "both given", opts: options{branch: "a", environment: "b"}, wantCode: exitTransport},
|
|
{name: "branch with capitals is rejected", opts: options{branch: "benvin/PUP-1"}, wantCode: exitTransport},
|
|
{name: "invalid explicit environment", opts: options{environment: "Not-Valid"}, wantCode: exitTransport},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := resolveEnvironment(&tc.opts)
|
|
if tc.wantCode != 0 {
|
|
if code := exitCode(t, err); code != tc.wantCode {
|
|
t.Fatalf("exit code %d, want %d (err %v)", code, tc.wantCode, err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("resolveEnvironment: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("= %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveEnvironmentCapitalsMessage(t *testing.T) {
|
|
_, err := resolveEnvironment(&options{branch: "benvin/PUP-1"})
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range []string{"benvin/PUP-1", "benvin_PUP_1", "--environment"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("message missing %q: %s", want, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunDiff(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
body := readEnvironment(t, r)
|
|
if body == "develop" {
|
|
_, _ = w.Write([]byte(catalogFor("develop", "old")))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(catalogFor("benvin_test", "new")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
|
|
var out bytes.Buffer
|
|
err := run(context.Background(), &out, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitDifferent {
|
|
t.Fatalf("exit code %d, want %d (err %v)", code, exitDifferent, err)
|
|
}
|
|
if !strings.Contains(out.String(), "File[/etc/motd] content: old -> new") {
|
|
t.Errorf("diff not rendered:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestRunJSON(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if readEnvironment(t, r) == "develop" {
|
|
_, _ = w.Write([]byte(catalogFor("develop", "old")))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(catalogFor("benvin_test", "new")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.jsonOut = true
|
|
|
|
var out bytes.Buffer
|
|
err := run(context.Background(), &out, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitDifferent {
|
|
t.Fatalf("exit code %d, want %d (err %v)", code, exitDifferent, err)
|
|
}
|
|
if !strings.Contains(out.String(), `"base_environment": "develop"`) {
|
|
t.Errorf("unexpected json:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestRunTestonlyIsSilent(t *testing.T) {
|
|
var compiled []string
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
env := readEnvironment(t, r)
|
|
compiled = append(compiled, env)
|
|
_, _ = w.Write([]byte(catalogFor(env, "new")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.testonly = true
|
|
|
|
var out bytes.Buffer
|
|
if err := run(context.Background(), &out, "host.example.net", opts); err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
if out.Len() != 0 {
|
|
t.Errorf("--testonly printed output: %q", out.String())
|
|
}
|
|
if len(compiled) != 1 || compiled[0] != "benvin_test" {
|
|
t.Errorf("--testonly must compile the test environment only, compiled %v", compiled)
|
|
}
|
|
}
|
|
|
|
func TestRunCompilationFailureExit1(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"kind":"puppet.server/compilation-error","message":"Server Error: Syntax error at '}'","issue_kind":"PARSE_ERROR"}`))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
|
|
err := run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitCompile {
|
|
t.Fatalf("exit code %d, want %d (err %v)", code, exitCompile, err)
|
|
}
|
|
}
|
|
|
|
func TestRunEnvironmentMismatchExit3(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
// The ENC overrides the requested environment.
|
|
_, _ = w.Write([]byte(catalogFor("develop", "new")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
|
|
err := run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitTransport {
|
|
t.Fatalf("exit code %d, want %d (err %v)", code, exitTransport, err)
|
|
}
|
|
if !strings.Contains(err.Error(), "environment mismatch") {
|
|
t.Errorf("unhelpful mismatch error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunUsageErrors(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(catalogFor("develop", "old")))
|
|
})
|
|
|
|
if code := exitCode(t, run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)); code != exitTransport {
|
|
t.Errorf("missing --branch/--environment: exit %d, want %d", code, exitTransport)
|
|
}
|
|
|
|
same := *opts
|
|
same.environment = "develop"
|
|
if code := exitCode(t, run(context.Background(), &bytes.Buffer{}, "host.example.net", &same)); code != exitTransport {
|
|
t.Errorf("base == test: exit %d, want %d", code, exitTransport)
|
|
}
|
|
|
|
badCA := *opts
|
|
badCA.environment = "benvin_test"
|
|
badCA.cacert = "/nonexistent/ca.pem"
|
|
if code := exitCode(t, run(context.Background(), &bytes.Buffer{}, "host.example.net", &badCA)); code != exitTransport {
|
|
t.Errorf("unreadable CA bundle: exit %d, want %d", code, exitTransport)
|
|
}
|
|
}
|
|
|
|
func TestRunUnreachableServerExit3(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(catalogFor("benvin_test", "new")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.server = "https://127.0.0.1:1"
|
|
|
|
if code := exitCode(t, run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)); code != exitTransport {
|
|
t.Errorf("exit %d, want %d", code, exitTransport)
|
|
}
|
|
}
|
|
|
|
// readEnvironment returns the environment a compile request asked for.
|
|
func readEnvironment(t *testing.T, r *http.Request) string {
|
|
t.Helper()
|
|
var body struct {
|
|
Environment string `json:"environment"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decoding compile request: %v", err)
|
|
}
|
|
return body.Environment
|
|
}
|
|
|
|
func TestRunIdenticalCatalogsExit0(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(catalogFor(readEnvironment(t, r), "same")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
|
|
var out bytes.Buffer
|
|
if err := run(context.Background(), &out, "host.example.net", opts); err != nil {
|
|
t.Fatalf("identical catalogs must exit 0: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "No differences between environments.") {
|
|
t.Errorf("unexpected output:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestRunRetriesTransientFailures(t *testing.T) {
|
|
var attempts int
|
|
var mu sync.Mutex
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
attempts++
|
|
first := attempts == 1
|
|
mu.Unlock()
|
|
if first {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_, _ = w.Write([]byte("bad gateway"))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(catalogFor(readEnvironment(t, r), "same")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.retry = 2
|
|
|
|
if err := run(context.Background(), &bytes.Buffer{}, "host.example.net", opts); err != nil {
|
|
t.Fatalf("a 502 should have been retried: %v", err)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if attempts < 3 {
|
|
t.Errorf("expected a retry, saw %d compile attempts", attempts)
|
|
}
|
|
}
|
|
|
|
func TestRunDoesNotRetryCompilationErrors(t *testing.T) {
|
|
var attempts int
|
|
var mu sync.Mutex
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
mu.Lock()
|
|
attempts++
|
|
mu.Unlock()
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"kind":"puppet.server/compilation-error","message":"Syntax error"}`))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.retry = 3
|
|
opts.testonly = true
|
|
|
|
if code := exitCode(t, run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)); code != exitCompile {
|
|
t.Fatalf("exit %d, want %d", code, exitCompile)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if attempts != 1 {
|
|
t.Errorf("a manifest error must not be retried, saw %d attempts", attempts)
|
|
}
|
|
}
|
|
|
|
func TestRunIgnoreFlags(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if readEnvironment(t, r) == "develop" {
|
|
_, _ = w.Write([]byte(catalogFor("develop", "old")))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(catalogFor("benvin_test", "new")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.ignoreResources = []string{"File[/etc/*]"}
|
|
|
|
var out bytes.Buffer
|
|
if err := run(context.Background(), &out, "host.example.net", opts); err != nil {
|
|
t.Fatalf("ignored resource should leave no differences: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "No differences") {
|
|
t.Errorf("--ignore not applied:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
// wrappedCatalogFor is the shape OpenVoxServer 8.8 actually returns: the
|
|
// catalog under a single "catalog" key.
|
|
func wrappedCatalogFor(environment, content string) string {
|
|
return `{"catalog":` + catalogFor(environment, content) + `}`
|
|
}
|
|
|
|
func TestRunAgainstWrappedResponses(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if readEnvironment(t, r) == "develop" {
|
|
_, _ = w.Write([]byte(wrappedCatalogFor("develop", "old")))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(wrappedCatalogFor("benvin_test", "new")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
|
|
var out bytes.Buffer
|
|
err := run(context.Background(), &out, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitDifferent {
|
|
t.Fatalf("exit code %d, want %d (err %v)", code, exitDifferent, err)
|
|
}
|
|
if !strings.Contains(out.String(), "File[/etc/motd] content: old -> new") {
|
|
t.Errorf("wrapped response not diffed:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestRunUnknownResponseShapeDoesNotBlameTheENC(t *testing.T) {
|
|
var attempts int
|
|
var mu sync.Mutex
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
mu.Lock()
|
|
attempts++
|
|
mu.Unlock()
|
|
_, _ = w.Write([]byte(`{"unexpected":{"environment":"benvin_test"}}`))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.testonly = true
|
|
opts.retry = 3
|
|
|
|
err := run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitTransport {
|
|
t.Fatalf("exit %d, want %d (err %v)", code, exitTransport, err)
|
|
}
|
|
if strings.Contains(err.Error(), "encapi") || strings.Contains(err.Error(), "environment mismatch") {
|
|
t.Errorf("a decode failure must not be reported as an ENC problem: %v", err)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if attempts != 1 {
|
|
t.Errorf("an unusable response shape must not be retried, saw %d attempts", attempts)
|
|
}
|
|
}
|
|
|
|
const pdbFailureBody = "Internal Server Error: org.jruby.exceptions.RuntimeError: (PreformattedError) Evaluation Error: " +
|
|
"Error while evaluating a Resource Statement, Evaluation Error: Error while evaluating a Function Call, " +
|
|
"Failed to execute '/pdb/query/v4' on at least 1 of the following 'server_urls': https://puppetdb.example.net " +
|
|
"(file: /etc/puppetlabs/code/environments/develop/site/profiles/manifests/app/client.pp, line: 31, column: 29) " +
|
|
"on node host.example.net"
|
|
|
|
func TestRunRetriesPuppetDBQueryFailures(t *testing.T) {
|
|
var attempts int
|
|
var mu sync.Mutex
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
attempts++
|
|
first := attempts == 1
|
|
mu.Unlock()
|
|
if first {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(pdbFailureBody))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(wrappedCatalogFor(readEnvironment(t, r), "same")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.testonly = true
|
|
opts.retry = 2
|
|
|
|
if err := run(context.Background(), &bytes.Buffer{}, "host.example.net", opts); err != nil {
|
|
t.Fatalf("a failed PuppetDB query should have been retried: %v", err)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if attempts != 2 {
|
|
t.Errorf("expected one retry, saw %d compile attempts", attempts)
|
|
}
|
|
}
|
|
|
|
func TestRunDoesNotRetryManifestErrorsReturnedAs500(t *testing.T) {
|
|
var attempts int
|
|
var mu sync.Mutex
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
mu.Lock()
|
|
attempts++
|
|
mu.Unlock()
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("Internal Server Error: org.jruby.exceptions.RuntimeError: (Error) " +
|
|
"Could not find class roles::app::demo::node for host.example.net on node host.example.net"))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.testonly = true
|
|
opts.retry = 3
|
|
|
|
if code := exitCode(t, run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)); code != exitCompile {
|
|
t.Fatalf("exit %d, want %d", code, exitCompile)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if attempts != 1 {
|
|
t.Errorf("a manifest error must not be retried even at HTTP 500, saw %d attempts", attempts)
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsOutOfRangeFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts options
|
|
want string
|
|
}{
|
|
{name: "negative retry", opts: options{retry: -1, timeout: time.Minute}, want: "--retry"},
|
|
{name: "retry above the cap", opts: options{retry: maxRetry + 1, timeout: time.Minute}, want: "--retry"},
|
|
{name: "zero timeout", opts: options{}, want: "--timeout"},
|
|
{name: "negative timeout", opts: options{timeout: -time.Second}, want: "--timeout"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := validate(&tc.opts)
|
|
if code := exitCode(t, err); code != exitTransport {
|
|
t.Fatalf("exit %d, want %d", code, exitTransport)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.want) {
|
|
t.Errorf("message must name %s: %v", tc.want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateAcceptsBoundaryValues(t *testing.T) {
|
|
for _, opts := range []options{
|
|
{retry: 0, timeout: time.Nanosecond},
|
|
{retry: maxRetry, timeout: time.Hour},
|
|
} {
|
|
if err := validate(&opts); err != nil {
|
|
t.Errorf("validate(%+v) = %v, want nil", opts, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunRejectsANegativeRetryWithoutPanicking(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(catalogFor(readEnvironment(t, r), "same")))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.retry = -1
|
|
|
|
err := run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitTransport {
|
|
t.Fatalf("exit %d, want %d (err %v)", code, exitTransport, err)
|
|
}
|
|
// main prints this before exiting; a nil inner error used to panic here.
|
|
if msg := err.Error(); !strings.Contains(msg, "--retry") {
|
|
t.Errorf("unhelpful usage error: %s", msg)
|
|
}
|
|
}
|
|
|
|
func TestExitErrorWithNoInnerErrorIsPrintable(t *testing.T) {
|
|
e := &exitError{code: exitTransport}
|
|
if msg := e.Error(); !strings.Contains(msg, "3") {
|
|
t.Errorf("Error() = %q, want the exit code", msg)
|
|
}
|
|
if e.Unwrap() != nil {
|
|
t.Error("Unwrap of an empty exitError must be nil")
|
|
}
|
|
var nilErr *exitError
|
|
if msg := nilErr.Error(); msg == "" {
|
|
t.Error("a nil *exitError must still print something")
|
|
}
|
|
}
|
|
|
|
func TestBackoffStaysWithinBounds(t *testing.T) {
|
|
for _, attempt := range []int{0, 1, 2, 5, 31, 32, 64, 1 << 30} {
|
|
d := backoff(attempt)
|
|
if d <= 0 || d > 45*time.Second {
|
|
t.Errorf("backoff(%d) = %s, out of range", attempt, d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunCancelsTheSiblingCompileOnFirstFailure(t *testing.T) {
|
|
const slow = 4 * time.Second
|
|
opts := testServers(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if readEnvironment(t, r) == "develop" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"kind":"puppet.server/compilation-error","message":"Syntax error"}`))
|
|
return
|
|
}
|
|
select {
|
|
case <-r.Context().Done():
|
|
case <-time.After(slow):
|
|
_, _ = w.Write([]byte(catalogFor("benvin_test", "new")))
|
|
}
|
|
})
|
|
opts.environment = "benvin_test"
|
|
|
|
start := time.Now()
|
|
err := run(context.Background(), &bytes.Buffer{}, "host.example.net", opts)
|
|
elapsed := time.Since(start)
|
|
|
|
// The base compile failed for real; the cancelled sibling is not the cause.
|
|
if code := exitCode(t, err); code != exitCompile {
|
|
t.Fatalf("exit %d, want %d (err %v)", code, exitCompile, err)
|
|
}
|
|
if elapsed > slow/2 {
|
|
t.Errorf("the sibling compile was not cancelled: run took %s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestRunReportsCancellationDuringRetryBackoff(t *testing.T) {
|
|
opts := testServers(t, func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_, _ = w.Write([]byte("bad gateway"))
|
|
})
|
|
opts.environment = "benvin_test"
|
|
opts.testonly = true
|
|
opts.retry = 5
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
|
defer cancel()
|
|
|
|
err := run(ctx, &bytes.Buffer{}, "host.example.net", opts)
|
|
if code := exitCode(t, err); code != exitTransport {
|
|
t.Fatalf("exit %d, want %d (err %v)", code, exitTransport, err)
|
|
}
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Errorf("the cancellation must be the reported cause, got %v", err)
|
|
}
|
|
}
|