Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

392 lines
11 KiB
Go

// Copyright 2026 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package supervisor
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"
)
// The test binary re-execs itself to act as a fake child process. The mode
// is selected by env so callers do not need a separate helper binary on disk.
const childModeEnv = "SUPERVISOR_TEST_CHILD"
func TestMain(m *testing.M) {
switch os.Getenv(childModeEnv) {
case "":
os.Exit(m.Run())
case "exit0":
os.Exit(0)
case "exit1":
os.Exit(1)
case "crash-after-100ms":
time.Sleep(100 * time.Millisecond)
os.Exit(2)
case "sleep-then-exit0":
dur, _ := time.ParseDuration(os.Getenv("CHILD_SLEEP"))
time.Sleep(dur)
os.Exit(0)
case "hang-until-sigterm":
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
<-c
os.Exit(0)
case "hang-ignore-sigterm":
signal.Ignore(syscall.SIGTERM)
time.Sleep(time.Hour)
os.Exit(99)
case "write-stdout":
fmt.Println("hello from child")
os.Exit(0)
default:
os.Exit(99)
}
}
// childCmd builds the args needed to re-invoke the test binary in a given
// child mode. The supervisor sees this as a normal external command.
func childCmd(mode string, extraEnv ...string) (cmd string, args []string, env []string) {
env = append(os.Environ(), childModeEnv+"="+mode)
env = append(env, extraEnv...)
return os.Args[0], []string{"-test.run=TestMain"}, env
}
// fakeClock implements Clock with controllable time advancement. After is
// implemented as a real-time short sleep so we don't have to build a full
// scheduler; tests use sub-second backoffs.
type fakeClock struct {
mu sync.Mutex
t time.Time
}
func newFakeClock() *fakeClock {
return &fakeClock{t: time.Unix(0, 0)}
}
func (c *fakeClock) Now() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.t
}
func (c *fakeClock) After(d time.Duration) <-chan time.Time {
c.mu.Lock()
c.t = c.t.Add(d)
c.mu.Unlock()
ch := make(chan time.Time, 1)
ch <- time.Now()
return ch
}
func parseEvents(t *testing.T, buf *bytes.Buffer) []Event {
t.Helper()
var out []Event
sc := bufio.NewScanner(strings.NewReader(buf.String()))
for sc.Scan() {
if len(sc.Bytes()) == 0 {
continue
}
var e Event
if err := json.Unmarshal(sc.Bytes(), &e); err != nil {
t.Fatalf("bad event JSON: %v\nline=%q", err, sc.Text())
}
out = append(out, e)
}
return out
}
func filterEvents(events []Event, kind string) []Event {
var out []Event
for _, e := range events {
if e.Event == kind {
out = append(out, e)
}
}
return out
}
// baseSpec returns a Spec with short timeouts suitable for tests.
func baseSpec(buf *bytes.Buffer, clk Clock) Spec {
false_ := false
jitter := 0.01
return Spec{
Name: "test-worker",
BackoffMin: 10 * time.Millisecond,
BackoffMax: 20 * time.Millisecond,
BackoffJitter: &jitter,
StableAfter: 50 * time.Millisecond,
BurstWindow: time.Second,
BurstMax: 1000, // disabled by default for most tests
OnBurstExit: &false_,
GracePeriod: 200 * time.Millisecond,
EventLog: buf,
WorkerStdout: &bytes.Buffer{},
WorkerStderr: &bytes.Buffer{},
Clock: clk,
}
}
func TestRun_CrashRestartsThenShutdown(t *testing.T) {
var buf bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
spec.Cmd, spec.Args, spec.Env = childCmd("exit1")
ctx, cancel := context.WithCancel(context.Background())
// Let it crash a couple of times then cancel.
go func() {
time.Sleep(300 * time.Millisecond)
cancel()
}()
err := Run(ctx, spec)
if !errors.Is(err, context.Canceled) {
t.Fatalf("want context.Canceled, got %v", err)
}
events := parseEvents(t, &buf)
starts := filterEvents(events, EventStart)
exits := filterEvents(events, EventExit)
if len(starts) < 2 {
t.Fatalf("expected ≥2 starts, got %d", len(starts))
}
if len(exits) < 2 {
t.Fatalf("expected ≥2 exits, got %d", len(exits))
}
// The last exit may be a SIGTERM-on-shutdown race where the child gets
// killed before its os.Exit(1) runs. Only assert on completed exits.
for i, e := range exits {
if e.Reason == "shutdown" || e.Reason == "signaled" {
continue
}
if e.ExitCode == nil || *e.ExitCode != 1 {
code := "<nil>"
if e.ExitCode != nil {
code = strconv.Itoa(*e.ExitCode)
}
t.Errorf("exit[%d] code = %s reason=%q, want exit code 1", i, code, e.Reason)
}
}
if len(filterEvents(events, EventBackoff)) < 1 {
t.Errorf("expected backoff events")
}
if len(filterEvents(events, EventShutdown)) != 1 {
t.Errorf("expected one shutdown event")
}
}
func TestRun_GracefulShutdownOnSIGTERM(t *testing.T) {
var buf bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
spec.Cmd, spec.Args, spec.Env = childCmd("hang-until-sigterm")
spec.GracePeriod = 2 * time.Second
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(150 * time.Millisecond)
cancel()
}()
err := Run(ctx, spec)
if !errors.Is(err, context.Canceled) {
t.Fatalf("want context.Canceled, got %v", err)
}
events := parseEvents(t, &buf)
exits := filterEvents(events, EventExit)
if len(exits) != 1 {
t.Fatalf("expected 1 exit, got %d", len(exits))
}
if exits[0].ExitCode == nil || *exits[0].ExitCode != 0 {
t.Errorf("expected clean exit 0 after SIGTERM, got code=%v signal=%q", exits[0].ExitCode, exits[0].Signal)
}
}
func TestRun_SIGKILLAfterGracePeriod(t *testing.T) {
var buf bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
spec.Cmd, spec.Args, spec.Env = childCmd("hang-ignore-sigterm")
spec.GracePeriod = 250 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(150 * time.Millisecond)
cancel()
}()
err := Run(ctx, spec)
if !errors.Is(err, context.Canceled) {
t.Fatalf("want context.Canceled, got %v", err)
}
events := parseEvents(t, &buf)
exits := filterEvents(events, EventExit)
if len(exits) != 1 {
t.Fatalf("expected 1 exit, got %d", len(exits))
}
if exits[0].Signal != syscall.SIGKILL.String() {
t.Errorf("expected SIGKILL, got %q", exits[0].Signal)
}
}
func TestRun_StableResetsBackoff(t *testing.T) {
var buf bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
// Sleep child for longer than StableAfter so we mark stable.
spec.Cmd, spec.Args, spec.Env = childCmd("sleep-then-exit0", "CHILD_SLEEP=100ms")
spec.StableAfter = 50 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond)
defer cancel()
_ = Run(ctx, spec)
events := parseEvents(t, &buf)
if len(filterEvents(events, EventStable)) == 0 {
t.Fatalf("expected at least one stable event\nall events: %+v", events)
}
}
func TestRun_BurstExitTriggers(t *testing.T) {
var buf bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
spec.Cmd, spec.Args, spec.Env = childCmd("exit1")
spec.BurstMax = 3
spec.BurstWindow = 5 * time.Second
true_ := true
spec.OnBurstExit = &true_
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := Run(ctx, spec)
if !errors.Is(err, ErrBurstExceeded) {
t.Fatalf("want ErrBurstExceeded, got %v", err)
}
events := parseEvents(t, &buf)
if len(filterEvents(events, EventBurstExit)) != 1 {
t.Fatalf("expected 1 burst_exit event\nevents: %+v", events)
}
}
func TestRun_PreStartFailureCountsAsBurst(t *testing.T) {
var buf bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
spec.Cmd, spec.Args, spec.Env = childCmd("exit0")
// A pre-start hook that always fails (false on POSIX).
spec.PreStart = []Hook{{Argv: []string{"/bin/false"}}}
spec.BurstMax = 2
spec.BurstWindow = 5 * time.Second
true_ := true
spec.OnBurstExit = &true_
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := Run(ctx, spec)
if !errors.Is(err, ErrBurstExceeded) {
t.Fatalf("want ErrBurstExceeded, got %v", err)
}
events := parseEvents(t, &buf)
ps := filterEvents(events, EventPreStart)
if len(ps) < 2 {
t.Fatalf("expected ≥2 prestart events\nevents: %+v", events)
}
for i, e := range ps {
if e.ExitCode == nil || *e.ExitCode == 0 {
t.Errorf("prestart[%d] expected non-zero exit, got %v", i, e.ExitCode)
}
}
// No worker should have launched.
if got := filterEvents(events, EventStart); len(got) != 0 {
t.Errorf("expected zero start events, got %d", len(got))
}
}
func TestRun_PostExitHookRunsWithWorkerEnv(t *testing.T) {
var buf bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
spec.Cmd, spec.Args, spec.Env = childCmd("exit0")
// Use a hook that writes its env to a file we can inspect.
tmp, err := os.CreateTemp(t.TempDir(), "posthook-*.env")
if err != nil {
t.Fatal(err)
}
tmp.Close()
script := fmt.Sprintf("#!/bin/sh\nenv | grep ^WORKER_ > %s\n", tmp.Name())
hookPath := tmp.Name() + ".sh"
if err := os.WriteFile(hookPath, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
spec.PostExit = []Hook{{Argv: []string{hookPath}}}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
_ = Run(ctx, spec)
got, err := os.ReadFile(tmp.Name())
if err != nil {
t.Fatal(err)
}
// The script truncates the file each invocation, so we see only the
// last hook run's env. Assert the env var keys are present; the value
// may be the shutdown iteration's (e.g. WORKER_EXIT_CODE=-1).
needles := []string{"WORKER_EXIT_CODE=", "WORKER_PID=", "WORKER_ATTEMPT=", "WORKER_DURATION_MS="}
for _, n := range needles {
if !strings.Contains(string(got), n) {
t.Errorf("hook env missing %q\nfile=\n%s", n, string(got))
}
}
}
func TestRun_WorkerStdoutForwarded(t *testing.T) {
var buf bytes.Buffer
var stdout bytes.Buffer
spec := baseSpec(&buf, newFakeClock())
spec.Cmd, spec.Args, spec.Env = childCmd("write-stdout")
spec.WorkerStdout = &stdout
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
_ = Run(ctx, spec)
if !strings.Contains(stdout.String(), "hello from child") {
t.Errorf("expected child stdout, got %q", stdout.String())
}
}
func TestRun_RejectsEmptyCmd(t *testing.T) {
err := Run(context.Background(), Spec{})
if err == nil {
t.Fatal("expected error for empty Cmd")
}
if !strings.Contains(err.Error(), "Cmd") {
t.Errorf("unexpected error: %v", err)
}
}
// Sanity: parse a duration into a string and back to verify the suite runs
// when triggered by the real go test entry (not the re-exec path).
func TestSanity(t *testing.T) {
_, err := strconv.Atoi("1")
if err != nil {
t.Fatal(err)
}
}