Files

215 lines
8.3 KiB
Go

package githubmirror
import (
"context"
"errors"
"reflect"
"strings"
"sync"
"testing"
"time"
)
const testSource = "https://github.com/owner/repo.git"
func testConfig() *Config {
return &Config{Enabled: true, Mirrors: []Mirror{
{Type: "ghProxy", URL: "https://first.example"},
{Type: "ghProxy", URL: "https://second.example"},
}}
}
func TestRaceWinsAndDrainsOwnedResults(t *testing.T) {
config := testConfig()
candidates := Candidates(config, testSource, Resource)
var started sync.WaitGroup
started.Add(len(candidates))
discarded := []string{}
winner, err := Race(context.Background(), config, testSource, Options[string]{
Discard: func(value string) { discarded = append(discarded, value) },
}, func(ctx context.Context, address string) (string, error) {
started.Done()
started.Wait()
switch address {
case candidates[0]:
return address, errors.New("official offline")
case candidates[1]:
return address, nil
default:
<-ctx.Done()
return address, ctx.Err()
}
})
if err != nil || winner != candidates[1] {
t.Fatalf("winner=%s err=%v", winner, err)
}
if len(discarded) != 2 || !contains(discarded, candidates[0]) || !contains(discarded, candidates[2]) {
t.Fatalf("discarded %v", discarded)
}
}
func contains(values []string, value string) bool {
for _, item := range values {
if item == value {
return true
}
}
return false
}
func TestRaceCleansSimultaneousSuccessfulLosers(t *testing.T) {
config := testConfig()
var started sync.WaitGroup
started.Add(3)
cleaned := []string{}
winner, err := Race(context.Background(), config, testSource, Options[string]{Discard: func(value string) { cleaned = append(cleaned, value) }}, func(_ context.Context, address string) (string, error) {
started.Done()
started.Wait()
return address, nil
})
if err != nil || winner == "" || len(cleaned) != 2 || contains(cleaned, winner) {
t.Fatalf("winner=%s discarded=%v err=%v", winner, cleaned, err)
}
}
func TestRaceParentCancellationCleansWinnerToo(t *testing.T) {
config := testConfig()
config.Mirrors = config.Mirrors[:1]
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cleaned := []string{}
winner, err := Race(ctx, config, testSource, Options[string]{Discard: func(value string) { cleaned = append(cleaned, value) }}, func(ctx context.Context, address string) (string, error) {
if address == testSource {
return address, nil
}
<-ctx.Done()
cancel()
return address, ctx.Err()
})
if !errors.Is(err, context.Canceled) || winner != "" || len(cleaned) != 2 || !contains(cleaned, testSource) {
t.Fatalf("winner=%s cleanup=%v err=%v", winner, cleaned, err)
}
}
func TestRaceTimeoutAndErrorAggregation(t *testing.T) {
expected := errors.New("offline")
config := testConfig()
config.Mirrors = config.Mirrors[:1]
discarded := 0
_, err := Race(context.Background(), config, testSource, Options[string]{Timeout: 10 * time.Millisecond, Discard: func(string) { discarded++ }}, func(ctx context.Context, address string) (string, error) {
if address == testSource {
return address, expected
}
<-ctx.Done()
return address, ctx.Err()
})
if !errors.Is(err, expected) || !errors.Is(err, context.DeadlineExceeded) || discarded != 2 {
t.Fatalf("discarded=%d err=%v", discarded, err)
}
}
func TestFallbackPreservesOrderAndStopsOnSuccess(t *testing.T) {
config := testConfig()
candidates := Candidates(config, testSource, Repository)
tried := []string{}
cleaned := []string{}
winner, err := Fallback(context.Background(), config, testSource, Options[string]{Kind: Repository, Discard: func(value string) { cleaned = append(cleaned, value) }}, func(_ context.Context, address string) (string, error) {
tried = append(tried, address)
if address == candidates[1] {
return address, nil
}
return address, errors.New("offline")
})
if err != nil || winner != candidates[1] || !reflect.DeepEqual(tried, candidates[:2]) || !reflect.DeepEqual(cleaned, candidates[:1]) {
t.Fatalf("winner=%s tried=%v discarded=%v err=%v", winner, tried, cleaned, err)
}
tried = nil
_, err = Fallback(context.Background(), config, testSource, Options[string]{Kind: Repository}, func(_ context.Context, address string) (string, error) {
tried = append(tried, address)
return address, nil
})
if err != nil || !reflect.DeepEqual(tried, candidates[:1]) {
t.Fatalf("tried=%v err=%v", tried, err)
}
}
func TestFallbackTimeoutContinuesWithFreshContext(t *testing.T) {
discarded := []string{}
winner, err := Fallback(context.Background(), testConfig(), testSource, Options[string]{Kind: Repository, Timeout: 10 * time.Millisecond, Discard: func(value string) { discarded = append(discarded, value) }}, func(ctx context.Context, address string) (string, error) {
if address == testSource {
<-ctx.Done()
return address, ctx.Err()
}
if err := ctx.Err(); err != nil {
return address, err
}
return address, nil
})
if err != nil || !strings.HasPrefix(winner, "https://first.example/") || !reflect.DeepEqual(discarded, []string{testSource}) {
t.Fatalf("winner=%s cleanup=%v err=%v", winner, discarded, err)
}
}
func TestFallbackCancellationStopsBeforeNextMirror(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
calls := 0
cleaned := 0
_, err := Fallback(ctx, testConfig(), testSource, Options[string]{Kind: Repository, Discard: func(string) { cleaned++ }}, func(ctx context.Context, address string) (string, error) {
calls++
cancel()
return address, ctx.Err()
})
if !errors.Is(err, context.Canceled) || calls != 1 || cleaned != 1 {
t.Fatalf("calls=%d discarded=%d err=%v", calls, cleaned, err)
}
}
func TestExecutorsDoNotStartInvalidOrCanceledRequests(t *testing.T) {
canceled, cancel := context.WithCancel(context.Background())
cancel()
for _, test := range []struct {
ctx context.Context
config *Config
options Options[string]
attempt Attempt[string]
}{
{ctx: canceled, options: Options[string]{}, attempt: func(context.Context, string) (string, error) { t.Fatal("called after cancellation"); return "", nil }},
{ctx: context.Background(), options: Options[string]{Kind: 99}, attempt: func(context.Context, string) (string, error) { t.Fatal("invalid kind"); return "", nil }},
{ctx: context.Background(), options: Options[string]{Timeout: -time.Second}, attempt: func(context.Context, string) (string, error) { t.Fatal("invalid timeout"); return "", nil }},
{ctx: context.Background(), options: Options[string]{}, attempt: nil},
{ctx: context.Background(), config: &Config{Mirrors: []Mirror{{Type: "invalid", URL: "https://mirror.example"}}}, attempt: func(context.Context, string) (string, error) { t.Fatal("invalid config"); return "", nil }},
} {
if _, err := Race(test.ctx, test.config, testSource, test.options, test.attempt); err == nil {
t.Fatal("invalid race accepted")
}
if _, err := Fallback(test.ctx, test.config, testSource, test.options, test.attempt); err == nil {
t.Fatal("invalid fallback accepted")
}
}
}
func TestAttemptPanicBecomesError(t *testing.T) {
attempt := func(context.Context, string) (string, error) { panic("broken operation") }
if _, err := Race(context.Background(), nil, testSource, Options[string]{}, attempt); err == nil || !strings.Contains(err.Error(), "broken operation") {
t.Fatal(err)
}
if _, err := Fallback(context.Background(), nil, testSource, Options[string]{}, attempt); err == nil || !strings.Contains(err.Error(), "broken operation") {
t.Fatal(err)
}
}
func TestNonGithubResourcesAndDisabledMirrorsUseOnlyOriginal(t *testing.T) {
for _, test := range []struct {
config *Config
raw string
}{{testConfig(), "https://example.com/resource"}, {&Config{Enabled: false, Mirrors: testConfig().Mirrors}, testSource}} {
calls := 0
got, err := Race(context.Background(), test.config, test.raw, Options[string]{}, func(_ context.Context, address string) (string, error) { calls++; return address, nil })
if err != nil || got != test.raw || calls != 1 {
t.Fatalf("got=%s calls=%d err=%v", got, calls, err)
}
}
}
func TestRepositoryAttemptsHaveNoDefaultDeadline(t *testing.T) {
value, err := Fallback(context.Background(), nil, testSource, Options[string]{Kind: Repository}, func(ctx context.Context, raw string) (string, error) {
if _, ok := ctx.Deadline(); ok {
t.Fatal("repository attempt has a total deadline")
}
return raw, nil
})
if err != nil || value != testSource {
t.Fatalf("value=%s error=%v", value, err)
}
}