* feat(suite): Implement Suites Fixes #1230 * Update docs * Fix variable alignment * Prevent always-run endpoint from running if a context placeholder fails to resolve in the URL * Return errors when a context placeholder path fails to resolve * Add a couple of unit tests * Add a couple of unit tests * fix(ui): Update group count properly Fixes #1233 * refactor: Pass down entire config instead of several sub-configs * fix: Change default suite interval and timeout * fix: Deprecate disable-monitoring-lock in favor of concurrency * fix: Make sure there are no duplicate keys * Refactor some code * Update watchdog/watchdog.go * Update web/app/src/components/StepDetailsModal.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: Remove useless log * fix: Set default concurrency to 3 instead of 5 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1095 lines
37 KiB
Go
1095 lines
37 KiB
Go
package memory
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
|
"github.com/TwiN/gatus/v5/config/suite"
|
|
"github.com/TwiN/gatus/v5/storage"
|
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
|
)
|
|
|
|
var (
|
|
firstCondition = endpoint.Condition("[STATUS] == 200")
|
|
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
|
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
|
|
|
now = time.Now()
|
|
|
|
testEndpoint = endpoint.Endpoint{
|
|
Name: "name",
|
|
Group: "group",
|
|
URL: "https://example.org/what/ever",
|
|
Method: "GET",
|
|
Body: "body",
|
|
Interval: 30 * time.Second,
|
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
|
Alerts: nil,
|
|
NumberOfFailuresInARow: 0,
|
|
NumberOfSuccessesInARow: 0,
|
|
}
|
|
testSuccessfulResult = endpoint.Result{
|
|
Hostname: "example.org",
|
|
IP: "127.0.0.1",
|
|
HTTPStatus: 200,
|
|
Errors: nil,
|
|
Connected: true,
|
|
Success: true,
|
|
Timestamp: now,
|
|
Duration: 150 * time.Millisecond,
|
|
CertificateExpiration: 10 * time.Hour,
|
|
ConditionResults: []*endpoint.ConditionResult{
|
|
{
|
|
Condition: "[STATUS] == 200",
|
|
Success: true,
|
|
},
|
|
{
|
|
Condition: "[RESPONSE_TIME] < 500",
|
|
Success: true,
|
|
},
|
|
{
|
|
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
|
Success: true,
|
|
},
|
|
},
|
|
}
|
|
testUnsuccessfulResult = endpoint.Result{
|
|
Hostname: "example.org",
|
|
IP: "127.0.0.1",
|
|
HTTPStatus: 200,
|
|
Errors: []string{"error-1", "error-2"},
|
|
Connected: true,
|
|
Success: false,
|
|
Timestamp: now,
|
|
Duration: 750 * time.Millisecond,
|
|
CertificateExpiration: 10 * time.Hour,
|
|
ConditionResults: []*endpoint.ConditionResult{
|
|
{
|
|
Condition: "[STATUS] == 200",
|
|
Success: true,
|
|
},
|
|
{
|
|
Condition: "[RESPONSE_TIME] < 500",
|
|
Success: false,
|
|
},
|
|
{
|
|
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
|
Success: false,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// Note that are much more extensive tests in /storage/store/store_test.go.
|
|
// This test is simply an extra sanity check
|
|
func TestStore_SanityCheck(t *testing.T) {
|
|
store, _ := NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
|
defer store.Clear()
|
|
defer store.Close()
|
|
store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult)
|
|
endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams())
|
|
if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
|
|
t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
|
|
}
|
|
store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult)
|
|
// Both results inserted are for the same endpoint, therefore, the count shouldn't have increased
|
|
endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams())
|
|
if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 {
|
|
t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses)
|
|
}
|
|
if hourlyAverageResponseTime, err := store.GetHourlyAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-24*time.Hour), time.Now()); err != nil {
|
|
t.Errorf("expected no error, got %v", err)
|
|
} else if len(hourlyAverageResponseTime) != 1 {
|
|
t.Errorf("expected 1 hour to have had a result in the past 24 hours, got %d", len(hourlyAverageResponseTime))
|
|
}
|
|
if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-24*time.Hour), time.Now()); uptime != 0.5 {
|
|
t.Errorf("expected uptime of last 24h to be 0.5, got %f", uptime)
|
|
}
|
|
if averageResponseTime, _ := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-24*time.Hour), time.Now()); averageResponseTime != 450 {
|
|
t.Errorf("expected average response time of last 24h to be 450, got %d", averageResponseTime)
|
|
}
|
|
ss, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, 20).WithEvents(1, 20))
|
|
if ss == nil {
|
|
t.Fatalf("Store should've had key '%s', but didn't", testEndpoint.Key())
|
|
}
|
|
if len(ss.Events) != 3 {
|
|
t.Errorf("Endpoint '%s' should've had 3 events, got %d", ss.Name, len(ss.Events))
|
|
}
|
|
if len(ss.Results) != 2 {
|
|
t.Errorf("Endpoint '%s' should've had 2 results, got %d", ss.Name, len(ss.Results))
|
|
}
|
|
if deleted := store.DeleteAllEndpointStatusesNotInKeys([]string{}); deleted != 1 {
|
|
t.Errorf("%d entries should've been deleted, got %d", 1, deleted)
|
|
}
|
|
}
|
|
|
|
func TestStore_Save(t *testing.T) {
|
|
store, err := NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err.Error())
|
|
}
|
|
err = store.Save()
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err.Error())
|
|
}
|
|
store.Clear()
|
|
store.Close()
|
|
}
|
|
|
|
func TestStore_HasEndpointStatusNewerThan(t *testing.T) {
|
|
store, _ := NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
|
defer store.Clear()
|
|
defer store.Close()
|
|
// InsertEndpointResult a result
|
|
err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult)
|
|
if err != nil {
|
|
t.Fatalf("expected no error while inserting result, got %v", err)
|
|
}
|
|
// Check with a timestamp in the past
|
|
hasNewerStatus, err := store.HasEndpointStatusNewerThan(testEndpoint.Key(), time.Now().Add(-time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if !hasNewerStatus {
|
|
t.Fatal("expected to have a newer status, but didn't")
|
|
}
|
|
// Check with a timestamp in the future
|
|
hasNewerStatus, err = store.HasEndpointStatusNewerThan(testEndpoint.Key(), time.Now().Add(time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if hasNewerStatus {
|
|
t.Fatal("expected not to have a newer status, but did")
|
|
}
|
|
}
|
|
|
|
// TestStore_MixedEndpointsAndSuites tests that having both endpoints and suites in the cache
|
|
// doesn't cause issues with core operations
|
|
func TestStore_MixedEndpointsAndSuites(t *testing.T) {
|
|
// Helper function to create and populate a store with test data
|
|
setupStore := func(t *testing.T) (*Store, *endpoint.Endpoint, *endpoint.Endpoint, *endpoint.Endpoint, *endpoint.Endpoint, *suite.Suite) {
|
|
store, err := NewStore(100, 50)
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err)
|
|
}
|
|
|
|
// Create regular endpoints
|
|
endpoint1 := &endpoint.Endpoint{
|
|
Name: "endpoint1",
|
|
Group: "group1",
|
|
URL: "https://example.com/1",
|
|
}
|
|
endpoint2 := &endpoint.Endpoint{
|
|
Name: "endpoint2",
|
|
Group: "group2",
|
|
URL: "https://example.com/2",
|
|
}
|
|
|
|
// Create suite endpoints (these would be part of a suite)
|
|
suiteEndpoint1 := &endpoint.Endpoint{
|
|
Name: "suite-endpoint1",
|
|
Group: "suite-group",
|
|
URL: "https://example.com/suite1",
|
|
}
|
|
suiteEndpoint2 := &endpoint.Endpoint{
|
|
Name: "suite-endpoint2",
|
|
Group: "suite-group",
|
|
URL: "https://example.com/suite2",
|
|
}
|
|
|
|
// Create a suite
|
|
testSuite := &suite.Suite{
|
|
Name: "test-suite",
|
|
Group: "suite-group",
|
|
Endpoints: []*endpoint.Endpoint{
|
|
suiteEndpoint1,
|
|
suiteEndpoint2,
|
|
},
|
|
}
|
|
|
|
return store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, testSuite
|
|
}
|
|
|
|
// Test 1: InsertEndpointResult endpoint results
|
|
t.Run("InsertEndpointResults", func(t *testing.T) {
|
|
store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, _ := setupStore(t)
|
|
// InsertEndpointResult regular endpoint results
|
|
result1 := &endpoint.Result{
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: 100 * time.Millisecond,
|
|
}
|
|
if err := store.InsertEndpointResult(endpoint1, result1); err != nil {
|
|
t.Fatalf("failed to insert endpoint1 result: %v", err)
|
|
}
|
|
|
|
result2 := &endpoint.Result{
|
|
Success: false,
|
|
Timestamp: time.Now(),
|
|
Duration: 200 * time.Millisecond,
|
|
Errors: []string{"error"},
|
|
}
|
|
if err := store.InsertEndpointResult(endpoint2, result2); err != nil {
|
|
t.Fatalf("failed to insert endpoint2 result: %v", err)
|
|
}
|
|
|
|
// InsertEndpointResult suite endpoint results
|
|
suiteResult1 := &endpoint.Result{
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: 50 * time.Millisecond,
|
|
}
|
|
if err := store.InsertEndpointResult(suiteEndpoint1, suiteResult1); err != nil {
|
|
t.Fatalf("failed to insert suite endpoint1 result: %v", err)
|
|
}
|
|
|
|
suiteResult2 := &endpoint.Result{
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: 75 * time.Millisecond,
|
|
}
|
|
if err := store.InsertEndpointResult(suiteEndpoint2, suiteResult2); err != nil {
|
|
t.Fatalf("failed to insert suite endpoint2 result: %v", err)
|
|
}
|
|
})
|
|
|
|
// Test 2: InsertEndpointResult suite result
|
|
t.Run("InsertSuiteResult", func(t *testing.T) {
|
|
store, _, _, _, _, testSuite := setupStore(t)
|
|
timestamp := time.Now()
|
|
suiteResult := &suite.Result{
|
|
Name: testSuite.Name,
|
|
Group: testSuite.Group,
|
|
Success: true,
|
|
Timestamp: timestamp,
|
|
Duration: 125 * time.Millisecond,
|
|
EndpointResults: []*endpoint.Result{
|
|
{Success: true, Duration: 50 * time.Millisecond},
|
|
{Success: true, Duration: 75 * time.Millisecond},
|
|
},
|
|
}
|
|
if err := store.InsertSuiteResult(testSuite, suiteResult); err != nil {
|
|
t.Fatalf("failed to insert suite result: %v", err)
|
|
}
|
|
|
|
// Verify the suite result was stored correctly
|
|
status, err := store.GetSuiteStatusByKey(testSuite.Key(), nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to get suite status: %v", err)
|
|
}
|
|
if len(status.Results) != 1 {
|
|
t.Errorf("expected 1 suite result, got %d", len(status.Results))
|
|
}
|
|
|
|
stored := status.Results[0]
|
|
if stored.Name != testSuite.Name {
|
|
t.Errorf("expected result name %s, got %s", testSuite.Name, stored.Name)
|
|
}
|
|
if stored.Group != testSuite.Group {
|
|
t.Errorf("expected result group %s, got %s", testSuite.Group, stored.Group)
|
|
}
|
|
if !stored.Success {
|
|
t.Error("expected result to be successful")
|
|
}
|
|
if stored.Duration != 125*time.Millisecond {
|
|
t.Errorf("expected duration 125ms, got %v", stored.Duration)
|
|
}
|
|
if len(stored.EndpointResults) != 2 {
|
|
t.Errorf("expected 2 endpoint results, got %d", len(stored.EndpointResults))
|
|
}
|
|
})
|
|
|
|
// Test 3: GetAllEndpointStatuses should only return endpoints, not suites
|
|
t.Run("GetAllEndpointStatuses", func(t *testing.T) {
|
|
store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, testSuite := setupStore(t)
|
|
|
|
// InsertEndpointResult all test data
|
|
store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond})
|
|
store.InsertEndpointResult(endpoint2, &endpoint.Result{Success: false, Timestamp: time.Now(), Duration: 200 * time.Millisecond})
|
|
store.InsertEndpointResult(suiteEndpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 50 * time.Millisecond})
|
|
store.InsertEndpointResult(suiteEndpoint2, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 75 * time.Millisecond})
|
|
store.InsertSuiteResult(testSuite, &suite.Result{
|
|
Name: testSuite.Name, Group: testSuite.Group, Success: true,
|
|
Timestamp: time.Now(), Duration: 125 * time.Millisecond,
|
|
})
|
|
statuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get all endpoint statuses: %v", err)
|
|
}
|
|
|
|
// Should have 4 endpoints (2 regular + 2 suite endpoints)
|
|
if len(statuses) != 4 {
|
|
t.Errorf("expected 4 endpoint statuses, got %d", len(statuses))
|
|
}
|
|
|
|
// Verify all are endpoint statuses with correct data, not suite statuses
|
|
expectedEndpoints := map[string]struct {
|
|
success bool
|
|
duration time.Duration
|
|
}{
|
|
"endpoint1": {success: true, duration: 100 * time.Millisecond},
|
|
"endpoint2": {success: false, duration: 200 * time.Millisecond},
|
|
"suite-endpoint1": {success: true, duration: 50 * time.Millisecond},
|
|
"suite-endpoint2": {success: true, duration: 75 * time.Millisecond},
|
|
}
|
|
|
|
for _, status := range statuses {
|
|
if status.Name == "" {
|
|
t.Error("endpoint status should have a name")
|
|
}
|
|
// Make sure none of them are the suite itself
|
|
if status.Name == "test-suite" {
|
|
t.Error("suite should not appear in endpoint statuses")
|
|
}
|
|
|
|
// Verify detailed endpoint data
|
|
expected, exists := expectedEndpoints[status.Name]
|
|
if !exists {
|
|
t.Errorf("unexpected endpoint name: %s", status.Name)
|
|
continue
|
|
}
|
|
|
|
// Check that endpoint has results and verify the data
|
|
if len(status.Results) != 1 {
|
|
t.Errorf("endpoint %s should have 1 result, got %d", status.Name, len(status.Results))
|
|
continue
|
|
}
|
|
|
|
result := status.Results[0]
|
|
if result.Success != expected.success {
|
|
t.Errorf("endpoint %s result success should be %v, got %v", status.Name, expected.success, result.Success)
|
|
}
|
|
if result.Duration != expected.duration {
|
|
t.Errorf("endpoint %s result duration should be %v, got %v", status.Name, expected.duration, result.Duration)
|
|
}
|
|
|
|
delete(expectedEndpoints, status.Name)
|
|
}
|
|
if len(expectedEndpoints) > 0 {
|
|
t.Errorf("missing expected endpoints: %v", expectedEndpoints)
|
|
}
|
|
})
|
|
|
|
// Test 4: GetAllSuiteStatuses should only return suites, not endpoints
|
|
t.Run("GetAllSuiteStatuses", func(t *testing.T) {
|
|
store, endpoint1, _, _, _, testSuite := setupStore(t)
|
|
|
|
// InsertEndpointResult test data
|
|
store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond})
|
|
timestamp := time.Now()
|
|
store.InsertSuiteResult(testSuite, &suite.Result{
|
|
Name: testSuite.Name, Group: testSuite.Group, Success: true,
|
|
Timestamp: timestamp, Duration: 125 * time.Millisecond,
|
|
})
|
|
statuses, err := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get all suite statuses: %v", err)
|
|
}
|
|
|
|
// Should have 1 suite
|
|
if len(statuses) != 1 {
|
|
t.Errorf("expected 1 suite status, got %d", len(statuses))
|
|
}
|
|
|
|
if len(statuses) > 0 {
|
|
suiteStatus := statuses[0]
|
|
if suiteStatus.Name != "test-suite" {
|
|
t.Errorf("expected suite name 'test-suite', got '%s'", suiteStatus.Name)
|
|
}
|
|
if suiteStatus.Group != "suite-group" {
|
|
t.Errorf("expected suite group 'suite-group', got '%s'", suiteStatus.Group)
|
|
}
|
|
if len(suiteStatus.Results) != 1 {
|
|
t.Errorf("expected 1 suite result, got %d", len(suiteStatus.Results))
|
|
}
|
|
if len(suiteStatus.Results) > 0 {
|
|
result := suiteStatus.Results[0]
|
|
if !result.Success {
|
|
t.Error("expected suite result to be successful")
|
|
}
|
|
if result.Duration != 125*time.Millisecond {
|
|
t.Errorf("expected suite result duration 125ms, got %v", result.Duration)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test 5: GetEndpointStatusByKey should work for all endpoints
|
|
t.Run("GetEndpointStatusByKey", func(t *testing.T) {
|
|
store, endpoint1, _, suiteEndpoint1, _, _ := setupStore(t)
|
|
|
|
// InsertEndpointResult test data with specific timestamps and durations
|
|
timestamp1 := time.Now()
|
|
timestamp2 := time.Now().Add(1 * time.Hour)
|
|
store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: timestamp1, Duration: 100 * time.Millisecond})
|
|
store.InsertEndpointResult(suiteEndpoint1, &endpoint.Result{Success: false, Timestamp: timestamp2, Duration: 50 * time.Millisecond, Errors: []string{"suite error"}})
|
|
|
|
// Test regular endpoints
|
|
status1, err := store.GetEndpointStatusByKey(endpoint1.Key(), &paging.EndpointStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get endpoint1 status: %v", err)
|
|
}
|
|
if status1.Name != "endpoint1" {
|
|
t.Errorf("expected endpoint1, got %s", status1.Name)
|
|
}
|
|
if status1.Group != "group1" {
|
|
t.Errorf("expected group1, got %s", status1.Group)
|
|
}
|
|
if len(status1.Results) != 1 {
|
|
t.Errorf("expected 1 result for endpoint1, got %d", len(status1.Results))
|
|
}
|
|
if len(status1.Results) > 0 {
|
|
result := status1.Results[0]
|
|
if !result.Success {
|
|
t.Error("expected endpoint1 result to be successful")
|
|
}
|
|
if result.Duration != 100*time.Millisecond {
|
|
t.Errorf("expected endpoint1 result duration 100ms, got %v", result.Duration)
|
|
}
|
|
}
|
|
|
|
// Test suite endpoints
|
|
suiteStatus1, err := store.GetEndpointStatusByKey(suiteEndpoint1.Key(), &paging.EndpointStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get suite endpoint1 status: %v", err)
|
|
}
|
|
if suiteStatus1.Name != "suite-endpoint1" {
|
|
t.Errorf("expected suite-endpoint1, got %s", suiteStatus1.Name)
|
|
}
|
|
if suiteStatus1.Group != "suite-group" {
|
|
t.Errorf("expected suite-group, got %s", suiteStatus1.Group)
|
|
}
|
|
if len(suiteStatus1.Results) != 1 {
|
|
t.Errorf("expected 1 result for suite-endpoint1, got %d", len(suiteStatus1.Results))
|
|
}
|
|
if len(suiteStatus1.Results) > 0 {
|
|
result := suiteStatus1.Results[0]
|
|
if result.Success {
|
|
t.Error("expected suite-endpoint1 result to be unsuccessful")
|
|
}
|
|
if result.Duration != 50*time.Millisecond {
|
|
t.Errorf("expected suite-endpoint1 result duration 50ms, got %v", result.Duration)
|
|
}
|
|
if len(result.Errors) != 1 || result.Errors[0] != "suite error" {
|
|
t.Errorf("expected suite-endpoint1 to have error 'suite error', got %v", result.Errors)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test 6: GetSuiteStatusByKey should work for suites
|
|
t.Run("GetSuiteStatusByKey", func(t *testing.T) {
|
|
store, _, _, _, _, testSuite := setupStore(t)
|
|
|
|
// InsertEndpointResult suite result with endpoint results
|
|
timestamp := time.Now()
|
|
store.InsertSuiteResult(testSuite, &suite.Result{
|
|
Name: testSuite.Name, Group: testSuite.Group, Success: false,
|
|
Timestamp: timestamp, Duration: 125 * time.Millisecond,
|
|
EndpointResults: []*endpoint.Result{
|
|
{Success: true, Duration: 50 * time.Millisecond},
|
|
{Success: false, Duration: 75 * time.Millisecond, Errors: []string{"endpoint failed"}},
|
|
},
|
|
})
|
|
suiteStatus, err := store.GetSuiteStatusByKey(testSuite.Key(), &paging.SuiteStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get suite status: %v", err)
|
|
}
|
|
if suiteStatus.Name != "test-suite" {
|
|
t.Errorf("expected test-suite, got %s", suiteStatus.Name)
|
|
}
|
|
if suiteStatus.Group != "suite-group" {
|
|
t.Errorf("expected suite-group, got %s", suiteStatus.Group)
|
|
}
|
|
if len(suiteStatus.Results) != 1 {
|
|
t.Errorf("expected 1 suite result, got %d", len(suiteStatus.Results))
|
|
}
|
|
|
|
if len(suiteStatus.Results) > 0 {
|
|
result := suiteStatus.Results[0]
|
|
if result.Success {
|
|
t.Error("expected suite result to be unsuccessful")
|
|
}
|
|
if result.Duration != 125*time.Millisecond {
|
|
t.Errorf("expected suite result duration 125ms, got %v", result.Duration)
|
|
}
|
|
if len(result.EndpointResults) != 2 {
|
|
t.Errorf("expected 2 endpoint results, got %d", len(result.EndpointResults))
|
|
}
|
|
if len(result.EndpointResults) >= 2 {
|
|
if !result.EndpointResults[0].Success {
|
|
t.Error("expected first endpoint result to be successful")
|
|
}
|
|
if result.EndpointResults[1].Success {
|
|
t.Error("expected second endpoint result to be unsuccessful")
|
|
}
|
|
if len(result.EndpointResults[1].Errors) != 1 || result.EndpointResults[1].Errors[0] != "endpoint failed" {
|
|
t.Errorf("expected second endpoint to have error 'endpoint failed', got %v", result.EndpointResults[1].Errors)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test 7: DeleteAllEndpointStatusesNotInKeys should not affect suites
|
|
t.Run("DeleteEndpointsNotInKeys", func(t *testing.T) {
|
|
store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, testSuite := setupStore(t)
|
|
|
|
// InsertEndpointResult all test data
|
|
store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond})
|
|
store.InsertEndpointResult(endpoint2, &endpoint.Result{Success: false, Timestamp: time.Now(), Duration: 200 * time.Millisecond})
|
|
store.InsertEndpointResult(suiteEndpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 50 * time.Millisecond})
|
|
store.InsertEndpointResult(suiteEndpoint2, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 75 * time.Millisecond})
|
|
store.InsertSuiteResult(testSuite, &suite.Result{
|
|
Name: testSuite.Name, Group: testSuite.Group, Success: true,
|
|
Timestamp: time.Now(), Duration: 125 * time.Millisecond,
|
|
})
|
|
// Keep only endpoint1 and suite-endpoint1
|
|
keysToKeep := []string{endpoint1.Key(), suiteEndpoint1.Key()}
|
|
deleted := store.DeleteAllEndpointStatusesNotInKeys(keysToKeep)
|
|
|
|
// Should have deleted 2 endpoints (endpoint2 and suite-endpoint2)
|
|
if deleted != 2 {
|
|
t.Errorf("expected to delete 2 endpoints, deleted %d", deleted)
|
|
}
|
|
|
|
// Verify remaining endpoints
|
|
statuses, _ := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
if len(statuses) != 2 {
|
|
t.Errorf("expected 2 remaining endpoint statuses, got %d", len(statuses))
|
|
}
|
|
|
|
// Suite should still exist
|
|
suiteStatuses, _ := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
|
if len(suiteStatuses) != 1 {
|
|
t.Errorf("suite should not be affected by DeleteAllEndpointStatusesNotInKeys")
|
|
}
|
|
})
|
|
|
|
// Test 8: DeleteAllSuiteStatusesNotInKeys should not affect endpoints
|
|
t.Run("DeleteSuitesNotInKeys", func(t *testing.T) {
|
|
store, endpoint1, _, _, _, testSuite := setupStore(t)
|
|
|
|
// InsertEndpointResult test data
|
|
store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond})
|
|
store.InsertSuiteResult(testSuite, &suite.Result{
|
|
Name: testSuite.Name, Group: testSuite.Group, Success: true,
|
|
Timestamp: time.Now(), Duration: 125 * time.Millisecond,
|
|
})
|
|
// First, add another suite to test deletion
|
|
anotherSuite := &suite.Suite{
|
|
Name: "another-suite",
|
|
Group: "another-group",
|
|
}
|
|
anotherSuiteResult := &suite.Result{
|
|
Name: anotherSuite.Name,
|
|
Group: anotherSuite.Group,
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: 100 * time.Millisecond,
|
|
}
|
|
store.InsertSuiteResult(anotherSuite, anotherSuiteResult)
|
|
|
|
// Keep only the original test-suite
|
|
deleted := store.DeleteAllSuiteStatusesNotInKeys([]string{testSuite.Key()})
|
|
|
|
// Should have deleted 1 suite (another-suite)
|
|
if deleted != 1 {
|
|
t.Errorf("expected to delete 1 suite, deleted %d", deleted)
|
|
}
|
|
|
|
// Endpoints should still exist
|
|
endpointStatuses, _ := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
if len(endpointStatuses) != 1 {
|
|
t.Errorf("endpoints should not be affected by DeleteAllSuiteStatusesNotInKeys")
|
|
}
|
|
|
|
// Only one suite should remain
|
|
suiteStatuses, _ := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
|
if len(suiteStatuses) != 1 {
|
|
t.Errorf("expected 1 remaining suite, got %d", len(suiteStatuses))
|
|
}
|
|
})
|
|
|
|
// Test 9: Clear should remove everything
|
|
t.Run("Clear", func(t *testing.T) {
|
|
store, endpoint1, _, _, _, testSuite := setupStore(t)
|
|
|
|
// InsertEndpointResult test data
|
|
store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond})
|
|
store.InsertSuiteResult(testSuite, &suite.Result{
|
|
Name: testSuite.Name, Group: testSuite.Group, Success: true,
|
|
Timestamp: time.Now(), Duration: 125 * time.Millisecond,
|
|
})
|
|
store.Clear()
|
|
|
|
// No endpoints should remain
|
|
endpointStatuses, _ := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
if len(endpointStatuses) != 0 {
|
|
t.Errorf("expected 0 endpoints after clear, got %d", len(endpointStatuses))
|
|
}
|
|
|
|
// No suites should remain
|
|
suiteStatuses, _ := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
|
if len(suiteStatuses) != 0 {
|
|
t.Errorf("expected 0 suites after clear, got %d", len(suiteStatuses))
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestStore_EndpointStatusCastingSafety tests that type assertions are safe
|
|
func TestStore_EndpointStatusCastingSafety(t *testing.T) {
|
|
store, err := NewStore(100, 50)
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err)
|
|
}
|
|
|
|
// InsertEndpointResult an endpoint
|
|
ep := &endpoint.Endpoint{
|
|
Name: "test-endpoint",
|
|
Group: "test",
|
|
URL: "https://example.com",
|
|
}
|
|
result := &endpoint.Result{
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: 100 * time.Millisecond,
|
|
}
|
|
store.InsertEndpointResult(ep, result)
|
|
|
|
// InsertEndpointResult a suite
|
|
testSuite := &suite.Suite{
|
|
Name: "test-suite",
|
|
Group: "test",
|
|
}
|
|
suiteResult := &suite.Result{
|
|
Name: testSuite.Name,
|
|
Group: testSuite.Group,
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: 200 * time.Millisecond,
|
|
}
|
|
store.InsertSuiteResult(testSuite, suiteResult)
|
|
|
|
// This should not panic even with mixed types in cache
|
|
statuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get all endpoint statuses: %v", err)
|
|
}
|
|
|
|
// Should only have the endpoint, not the suite
|
|
if len(statuses) != 1 {
|
|
t.Errorf("expected 1 endpoint status, got %d", len(statuses))
|
|
}
|
|
if statuses[0].Name != "test-endpoint" {
|
|
t.Errorf("expected test-endpoint, got %s", statuses[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestStore_MaximumLimits(t *testing.T) {
|
|
// Use small limits to test trimming behavior
|
|
maxResults := 5
|
|
maxEvents := 3
|
|
store, err := NewStore(maxResults, maxEvents)
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err)
|
|
}
|
|
defer store.Clear()
|
|
|
|
t.Run("endpoint-result-limits", func(t *testing.T) {
|
|
ep := &endpoint.Endpoint{Name: "test-endpoint", Group: "test", URL: "https://example.com"}
|
|
|
|
// Insert more results than the maximum
|
|
baseTime := time.Now().Add(-10 * time.Hour)
|
|
for i := 0; i < maxResults*2; i++ {
|
|
result := &endpoint.Result{
|
|
Success: i%2 == 0,
|
|
Timestamp: baseTime.Add(time.Duration(i) * time.Hour),
|
|
Duration: time.Duration(i*10) * time.Millisecond,
|
|
}
|
|
err := store.InsertEndpointResult(ep, result)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert result %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Verify only maxResults are kept
|
|
status, err := store.GetEndpointStatusByKey(ep.Key(), nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to get endpoint status: %v", err)
|
|
}
|
|
if len(status.Results) != maxResults {
|
|
t.Errorf("expected %d results after trimming, got %d", maxResults, len(status.Results))
|
|
}
|
|
|
|
// Verify the newest results are kept (should be results 5-9, not 0-4)
|
|
if len(status.Results) > 0 {
|
|
firstResult := status.Results[0]
|
|
lastResult := status.Results[len(status.Results)-1]
|
|
// First result should be older than last result due to append order
|
|
if !lastResult.Timestamp.After(firstResult.Timestamp) {
|
|
t.Error("expected results to be in chronological order")
|
|
}
|
|
// The last result should be the most recent one we inserted
|
|
expectedLastDuration := time.Duration((maxResults*2-1)*10) * time.Millisecond
|
|
if lastResult.Duration != expectedLastDuration {
|
|
t.Errorf("expected last result duration %v, got %v", expectedLastDuration, lastResult.Duration)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("suite-result-limits", func(t *testing.T) {
|
|
testSuite := &suite.Suite{Name: "test-suite", Group: "test"}
|
|
|
|
// Insert more results than the maximum
|
|
baseTime := time.Now().Add(-10 * time.Hour)
|
|
for i := 0; i < maxResults*2; i++ {
|
|
result := &suite.Result{
|
|
Name: testSuite.Name,
|
|
Group: testSuite.Group,
|
|
Success: i%2 == 0,
|
|
Timestamp: baseTime.Add(time.Duration(i) * time.Hour),
|
|
Duration: time.Duration(i*10) * time.Millisecond,
|
|
}
|
|
err := store.InsertSuiteResult(testSuite, result)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert suite result %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Verify only maxResults are kept
|
|
status, err := store.GetSuiteStatusByKey(testSuite.Key(), &paging.SuiteStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get suite status: %v", err)
|
|
}
|
|
if len(status.Results) != maxResults {
|
|
t.Errorf("expected %d results after trimming, got %d", maxResults, len(status.Results))
|
|
}
|
|
|
|
// Verify the newest results are kept (should be results 5-9, not 0-4)
|
|
if len(status.Results) > 0 {
|
|
firstResult := status.Results[0]
|
|
lastResult := status.Results[len(status.Results)-1]
|
|
// First result should be older than last result due to append order
|
|
if !lastResult.Timestamp.After(firstResult.Timestamp) {
|
|
t.Error("expected results to be in chronological order")
|
|
}
|
|
// The last result should be the most recent one we inserted
|
|
expectedLastDuration := time.Duration((maxResults*2-1)*10) * time.Millisecond
|
|
if lastResult.Duration != expectedLastDuration {
|
|
t.Errorf("expected last result duration %v, got %v", expectedLastDuration, lastResult.Duration)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSuiteResultOrdering(t *testing.T) {
|
|
store, err := NewStore(10, 5)
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err)
|
|
}
|
|
defer store.Clear()
|
|
|
|
testSuite := &suite.Suite{Name: "ordering-suite", Group: "test"}
|
|
|
|
// Insert results with distinct timestamps
|
|
baseTime := time.Now().Add(-5 * time.Hour)
|
|
timestamps := make([]time.Time, 5)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
timestamp := baseTime.Add(time.Duration(i) * time.Hour)
|
|
timestamps[i] = timestamp
|
|
result := &suite.Result{
|
|
Name: testSuite.Name,
|
|
Group: testSuite.Group,
|
|
Success: true,
|
|
Timestamp: timestamp,
|
|
Duration: time.Duration(i*100) * time.Millisecond,
|
|
}
|
|
err := store.InsertSuiteResult(testSuite, result)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert result %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
t.Run("chronological-append-order", func(t *testing.T) {
|
|
status, err := store.GetSuiteStatusByKey(testSuite.Key(), nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to get suite status: %v", err)
|
|
}
|
|
|
|
// Verify results are in chronological order (oldest first due to append)
|
|
for i := 0; i < len(status.Results)-1; i++ {
|
|
current := status.Results[i]
|
|
next := status.Results[i+1]
|
|
if !next.Timestamp.After(current.Timestamp) {
|
|
t.Errorf("result %d timestamp %v should be before result %d timestamp %v",
|
|
i, current.Timestamp, i+1, next.Timestamp)
|
|
}
|
|
}
|
|
|
|
// Verify specific timestamp order
|
|
if !status.Results[0].Timestamp.Equal(timestamps[0]) {
|
|
t.Errorf("first result timestamp should be %v, got %v", timestamps[0], status.Results[0].Timestamp)
|
|
}
|
|
if !status.Results[len(status.Results)-1].Timestamp.Equal(timestamps[len(timestamps)-1]) {
|
|
t.Errorf("last result timestamp should be %v, got %v", timestamps[len(timestamps)-1], status.Results[len(status.Results)-1].Timestamp)
|
|
}
|
|
})
|
|
|
|
t.Run("pagination-newest-first", func(t *testing.T) {
|
|
// Test reverse pagination (newest first in paginated results)
|
|
page1 := ShallowCopySuiteStatus(
|
|
&suite.Status{
|
|
Name: testSuite.Name, Group: testSuite.Group, Key: testSuite.Key(),
|
|
Results: []*suite.Result{
|
|
{Timestamp: timestamps[0], Duration: 0 * time.Millisecond},
|
|
{Timestamp: timestamps[1], Duration: 100 * time.Millisecond},
|
|
{Timestamp: timestamps[2], Duration: 200 * time.Millisecond},
|
|
{Timestamp: timestamps[3], Duration: 300 * time.Millisecond},
|
|
{Timestamp: timestamps[4], Duration: 400 * time.Millisecond},
|
|
},
|
|
},
|
|
paging.NewSuiteStatusParams().WithPagination(1, 3),
|
|
)
|
|
|
|
if len(page1.Results) != 3 {
|
|
t.Errorf("expected 3 results in page 1, got %d", len(page1.Results))
|
|
}
|
|
|
|
// With reverse pagination, page 1 should have the 3 newest results
|
|
// That means results[2], results[3], results[4] from original array
|
|
if page1.Results[0].Duration != 200*time.Millisecond {
|
|
t.Errorf("expected first result in page to have 200ms duration, got %v", page1.Results[0].Duration)
|
|
}
|
|
if page1.Results[2].Duration != 400*time.Millisecond {
|
|
t.Errorf("expected last result in page to have 400ms duration, got %v", page1.Results[2].Duration)
|
|
}
|
|
})
|
|
|
|
t.Run("trimming-preserves-newest", func(t *testing.T) {
|
|
limitedStore, err := NewStore(3, 2) // Very small limits
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err)
|
|
}
|
|
defer limitedStore.Clear()
|
|
|
|
smallSuite := &suite.Suite{Name: "small-suite", Group: "test"}
|
|
|
|
// Insert 6 results, should keep only the newest 3
|
|
for i := 0; i < 6; i++ {
|
|
result := &suite.Result{
|
|
Name: smallSuite.Name,
|
|
Group: smallSuite.Group,
|
|
Success: true,
|
|
Timestamp: baseTime.Add(time.Duration(i) * time.Hour),
|
|
Duration: time.Duration(i*50) * time.Millisecond,
|
|
}
|
|
err := limitedStore.InsertSuiteResult(smallSuite, result)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert result %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
status, err := limitedStore.GetSuiteStatusByKey(smallSuite.Key(), nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to get suite status: %v", err)
|
|
}
|
|
|
|
if len(status.Results) != 3 {
|
|
t.Errorf("expected 3 results after trimming, got %d", len(status.Results))
|
|
}
|
|
|
|
// Should have results 3, 4, 5 (the newest ones)
|
|
expectedDurations := []time.Duration{150 * time.Millisecond, 200 * time.Millisecond, 250 * time.Millisecond}
|
|
for i, expectedDuration := range expectedDurations {
|
|
if status.Results[i].Duration != expectedDuration {
|
|
t.Errorf("result %d should have duration %v, got %v", i, expectedDuration, status.Results[i].Duration)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestStore_ConcurrentAccess(t *testing.T) {
|
|
store, err := NewStore(100, 50)
|
|
if err != nil {
|
|
t.Fatal("expected no error, got", err)
|
|
}
|
|
defer store.Clear()
|
|
|
|
t.Run("concurrent-endpoint-insertions", func(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 10
|
|
resultsPerGoroutine := 5
|
|
|
|
// Create endpoints for concurrent testing
|
|
endpoints := make([]*endpoint.Endpoint, numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
endpoints[i] = &endpoint.Endpoint{
|
|
Name: "endpoint-" + string(rune('A'+i)),
|
|
Group: "concurrent",
|
|
URL: "https://example.com/" + string(rune('A'+i)),
|
|
}
|
|
}
|
|
|
|
// Concurrently insert results for different endpoints
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(endpointIndex int) {
|
|
defer wg.Done()
|
|
ep := endpoints[endpointIndex]
|
|
for j := 0; j < resultsPerGoroutine; j++ {
|
|
result := &endpoint.Result{
|
|
Success: j%2 == 0,
|
|
Timestamp: time.Now().Add(time.Duration(j) * time.Minute),
|
|
Duration: time.Duration(j*10) * time.Millisecond,
|
|
}
|
|
if err := store.InsertEndpointResult(ep, result); err != nil {
|
|
t.Errorf("failed to insert result for endpoint %d: %v", endpointIndex, err)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all endpoints were created and have correct result counts
|
|
statuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get all endpoint statuses: %v", err)
|
|
}
|
|
if len(statuses) != numGoroutines {
|
|
t.Errorf("expected %d endpoint statuses, got %d", numGoroutines, len(statuses))
|
|
}
|
|
|
|
// Verify each endpoint has the correct number of results
|
|
for _, status := range statuses {
|
|
if len(status.Results) != resultsPerGoroutine {
|
|
t.Errorf("endpoint %s should have %d results, got %d", status.Name, resultsPerGoroutine, len(status.Results))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("concurrent-suite-insertions", func(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 5
|
|
resultsPerGoroutine := 3
|
|
|
|
// Create suites for concurrent testing
|
|
suites := make([]*suite.Suite, numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
suites[i] = &suite.Suite{
|
|
Name: "suite-" + string(rune('A'+i)),
|
|
Group: "concurrent",
|
|
}
|
|
}
|
|
|
|
// Concurrently insert results for different suites
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(suiteIndex int) {
|
|
defer wg.Done()
|
|
su := suites[suiteIndex]
|
|
for j := 0; j < resultsPerGoroutine; j++ {
|
|
result := &suite.Result{
|
|
Name: su.Name,
|
|
Group: su.Group,
|
|
Success: j%2 == 0,
|
|
Timestamp: time.Now().Add(time.Duration(j) * time.Minute),
|
|
Duration: time.Duration(j*50) * time.Millisecond,
|
|
}
|
|
if err := store.InsertSuiteResult(su, result); err != nil {
|
|
t.Errorf("failed to insert result for suite %d: %v", suiteIndex, err)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all suites were created and have correct result counts
|
|
statuses, err := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get all suite statuses: %v", err)
|
|
}
|
|
if len(statuses) != numGoroutines {
|
|
t.Errorf("expected %d suite statuses, got %d", numGoroutines, len(statuses))
|
|
}
|
|
|
|
// Verify each suite has the correct number of results
|
|
for _, status := range statuses {
|
|
if len(status.Results) != resultsPerGoroutine {
|
|
t.Errorf("suite %s should have %d results, got %d", status.Name, resultsPerGoroutine, len(status.Results))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("concurrent-mixed-operations", func(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
|
|
// Setup test data
|
|
ep := &endpoint.Endpoint{Name: "mixed-endpoint", Group: "test", URL: "https://example.com"}
|
|
testSuite := &suite.Suite{Name: "mixed-suite", Group: "test"}
|
|
|
|
// Concurrent endpoint insertions
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 5; i++ {
|
|
result := &endpoint.Result{
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: time.Duration(i*10) * time.Millisecond,
|
|
}
|
|
store.InsertEndpointResult(ep, result)
|
|
}
|
|
}()
|
|
|
|
// Concurrent suite insertions
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 5; i++ {
|
|
result := &suite.Result{
|
|
Name: testSuite.Name,
|
|
Group: testSuite.Group,
|
|
Success: true,
|
|
Timestamp: time.Now(),
|
|
Duration: time.Duration(i*20) * time.Millisecond,
|
|
}
|
|
store.InsertSuiteResult(testSuite, result)
|
|
}
|
|
}()
|
|
|
|
// Concurrent reads
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 10; i++ {
|
|
store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
// Verify final state is consistent
|
|
endpointStatuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get endpoint statuses after concurrent operations: %v", err)
|
|
}
|
|
if len(endpointStatuses) == 0 {
|
|
t.Error("expected at least one endpoint status after concurrent operations")
|
|
}
|
|
|
|
suiteStatuses, err := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get suite statuses after concurrent operations: %v", err)
|
|
}
|
|
if len(suiteStatuses) == 0 {
|
|
t.Error("expected at least one suite status after concurrent operations")
|
|
}
|
|
})
|
|
}
|