// SPDX-FileCopyrightText: 2022 Free Mobile // SPDX-License-Identifier: AGPL-3.0-only //go:build !release // Package helpers contains small functions usable by any other // package, both for testing or not. package helpers import ( "context" "errors" "fmt" "net" "os" "path/filepath" "runtime" "sync" "testing" "time" ) var ( externalServiceSkipped = make(map[string]bool) externalServiceMu sync.RWMutex ) // CheckExternalService checks an external service, available either // as a named service or on a specific port on localhost. This applies // for example for Kafka and ClickHouse. The timeouts are quite short, // but we suppose that either the services are run through // docker compose manually and ready, either through CI and they are // checked for readiness. func CheckExternalService(t *testing.T, name string, candidates []string) string { t.Helper() if testing.Short() { t.Skipf("Skip test with real %s in short mode", name) } mandatory := os.Getenv("CI_AKVORADO_FUNCTIONAL_TESTS") != "" externalServiceMu.RLock() if skipped, cached := externalServiceSkipped[name]; cached && skipped { externalServiceMu.RUnlock() if mandatory { t.Fatalf("%s is not running (CI_AKVORADO_FUNCTIONAL_TESTS is set)", name) } else { t.Skipf("%s is not running (CI_AKVORADO_FUNCTIONAL_TESTS is not set)", name) } } externalServiceMu.RUnlock() server := "" for _, candidate := range candidates { resolv := net.Resolver{PreferGo: true} ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) hostname, _, err := net.SplitHostPort(candidate) if err != nil { t.Fatalf("%s is an invalid candidate", candidate) } _, err = resolv.LookupHost(ctx, hostname) cancel() if err == nil { server = candidate break } } if server == "" { externalServiceMu.Lock() externalServiceSkipped[name] = true externalServiceMu.Unlock() if mandatory { t.Fatalf("%s cannot be resolved (CI_AKVORADO_FUNCTIONAL_TESTS is set)", name) } t.Skipf("%s cannot be resolved (CI_AKVORADO_FUNCTIONAL_TESTS is not set)", name) } var d net.Dialer ctx, cancel := context.WithTimeout(context.Background(), time.Second) for { _, err := d.DialContext(ctx, "tcp", server) if err == nil { break } if mandatory { t.Logf("DialContext() error:\n%+v", err) } if errors.Is(ctx.Err(), context.DeadlineExceeded) { externalServiceMu.Lock() externalServiceSkipped[name] = true externalServiceMu.Unlock() if mandatory { t.Fatalf("%s is not running (CI_AKVORADO_FUNCTIONAL_TESTS is set)", name) } else { t.Skipf("%s is not running (CI_AKVORADO_FUNCTIONAL_TESTS is not set)", name) } } time.Sleep(100 * time.Millisecond) } cancel() return server } // StartStop starts a component and stops it on cleanup. func StartStop(t testing.TB, component any) { t.Helper() if starterC, ok := component.(starter); ok { if err := starterC.Start(); err != nil { t.Fatalf("Start() error:\n%+v", err) } } t.Cleanup(func() { if stopperC, ok := component.(stopper); ok { if err := stopperC.Stop(); err != nil { t.Errorf("Stop() error:\n%+v", err) } } }) } type starter interface { Start() error } type stopper interface { Stop() error } // Pos is a file:line recording a test data position. type Pos struct { file string line int } // Mark reports the file:line position of the source file in which it appears. func Mark() Pos { _, file, line, _ := runtime.Caller(1) return Pos{filepath.Base(file), line} } // String returns a textual representation of a Pos. func (p Pos) String() string { if p.file != "" { return fmt.Sprintf("%s:%d: ", p.file, p.line) } return "" } // Testing reports whether the current code is being run in a test. func Testing() bool { return testing.Testing() }