Files
go/maintnotifications/e2e/scenario_template.go.example
1e6ee06740 test(e2e): testing framework upgrade (#3541)
* update e2e test, change script

* update script and tests

* fixed bdbid parsing

* disabled majority of tests, swapped event order

* change the config tag

* revert test order

* fix typo

* reenable all e2e tests

* change the clonfig flag key for all e2e tests

* improve logging for debug purposes of tests

* longer deadline for FI in CI

* increase waiting for notifications

* extend tests

* dont fail on flaky third client

* fi new params

* fix test build

* more time for migrating

* first wait for FI action, then assert notification

* fix test build

* fix tests

* fix tests

* change output

* global print logs for tests

* better output

* fix error format

* maybe the notification is already received

* second and third client fix

* print output if failed

* better second and third client checks

* output action data if notification is not received

* stop command runner

* database create / delete actions

* database create / delete actions used in tests

* fix import

* remove example

* remove unused var

* use different port than the one in env

* wait for action to get the response

* fix output

* fix create db config

* fix create db config

* use new database for client

* fix create db config

* db per scenario

* less logs, correct check

* Add CTRF to the scenario tests (#3545)

* add some json ctrf improvements

* fix -v

* attempt to separate the output

---------

Co-authored-by: Nedyalko Dyakov <[email protected]>

---------

Co-authored-by: Nedyalko Dyakov <[email protected]>
Co-authored-by: kiryazovi-redis <[email protected]>
Co-authored-by: Nedyalko Dyakov <[email protected]>
2025-10-17 17:23:10 +03:00

246 lines
6.3 KiB
Plaintext

package e2e
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9/hitless"
)
// TestScenarioTemplate is a template for writing scenario tests
// Copy this file and rename it to scenario_your_test_name.go
func TestScenarioTemplate(t *testing.T) {
if os.Getenv("E2E_SCENARIO_TESTS") != "true" {
t.Skip("Scenario tests require E2E_SCENARIO_TESTS=true")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
// Step 1: Create client factory from configuration
factory, err := CreateTestClientFactory("enterprise-cluster") // or "standalone0"
if err != nil {
t.Fatalf("Failed to create client factory: %v", err)
}
defer factory.DestroyAll()
// Step 2: Create fault injector
faultInjector, err := CreateTestFaultInjector()
if err != nil {
t.Fatalf("Failed to create fault injector: %v", err)
}
// Step 3: Create Redis client with hitless upgrades
client, err := factory.Create("scenario-client", &CreateClientOptions{
Protocol: 3,
HitlessUpgradeConfig: &hitless.Config{
Mode: hitless.MaintNotificationsEnabled,
HandoffTimeout: 30000, // 30 seconds
RelaxedTimeout: 10000, // 10 seconds
MaxWorkers: 20,
},
ClientName: "scenario-test-client",
})
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
// Step 4: Verify initial connectivity
err = client.Ping(ctx).Err()
if err != nil {
t.Fatalf("Failed to ping Redis: %v", err)
}
t.Log("Initial setup completed successfully")
// Step 5: Start background operations (optional)
stopCh := make(chan struct{})
defer close(stopCh)
go func() {
counter := 0
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-stopCh:
return
case <-ticker.C:
key := fmt.Sprintf("test-key-%d", counter)
value := fmt.Sprintf("test-value-%d", counter)
err := client.Set(ctx, key, value, time.Minute).Err()
if err != nil {
t.Logf("Background operation failed: %v", err)
}
counter++
}
}
}()
// Step 6: Wait for baseline operations
time.Sleep(5 * time.Second)
// Step 7: Trigger fault injection scenario
t.Log("Triggering fault injection scenario...")
// Example: Cluster failover
// resp, err := faultInjector.TriggerClusterFailover(ctx, "node-1", false)
// if err != nil {
// t.Fatalf("Failed to trigger failover: %v", err)
// }
// Example: Network latency
// nodes := []string{"localhost:7001", "localhost:7002"}
// resp, err := faultInjector.SimulateNetworkLatency(ctx, nodes, 100*time.Millisecond, 20*time.Millisecond)
// if err != nil {
// t.Fatalf("Failed to simulate latency: %v", err)
// }
// Example: Complex sequence
// sequence := []SequenceAction{
// {
// Type: ActionNetworkLatency,
// Parameters: map[string]interface{}{
// "nodes": []string{"localhost:7001"},
// "latency": "50ms",
// },
// },
// {
// Type: ActionClusterFailover,
// Parameters: map[string]interface{}{
// "node_id": "node-1",
// "force": false,
// },
// Delay: 10 * time.Second,
// },
// }
// resp, err := faultInjector.ExecuteSequence(ctx, sequence)
// if err != nil {
// t.Fatalf("Failed to execute sequence: %v", err)
// }
// Step 8: Wait for fault injection to complete
// status, err := faultInjector.WaitForAction(ctx, resp.ActionID,
// WithMaxWaitTime(240*time.Second),
// WithPollInterval(2*time.Second))
// if err != nil {
// t.Fatalf("Fault injection failed: %v", err)
// }
// t.Logf("Fault injection completed: %s", status.Status)
// Step 9: Verify client remains operational during and after fault injection
time.Sleep(10 * time.Second)
err = client.Ping(ctx).Err()
if err != nil {
t.Errorf("Client not responsive after fault injection: %v", err)
}
// Step 10: Perform additional validation
testKey := "validation-key"
testValue := "validation-value"
err = client.Set(ctx, testKey, testValue, time.Minute).Err()
if err != nil {
t.Errorf("Failed to set validation key: %v", err)
}
retrievedValue, err := client.Get(ctx, testKey).Result()
if err != nil {
t.Errorf("Failed to get validation key: %v", err)
} else if retrievedValue != testValue {
t.Errorf("Validation failed: expected %s, got %s", testValue, retrievedValue)
}
t.Log("Scenario test completed successfully")
}
// Helper functions for common scenario patterns
func performContinuousOperations(ctx context.Context, client redis.UniversalClient, workerID int, stopCh <-chan struct{}, errorCh chan<- error) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
counter := 0
for {
select {
case <-stopCh:
return
case <-ticker.C:
key := fmt.Sprintf("worker_%d_key_%d", workerID, counter)
value := fmt.Sprintf("value_%d", counter)
// Perform operation with timeout
opCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
err := client.Set(opCtx, key, value, time.Minute).Err()
cancel()
if err != nil {
select {
case errorCh <- err:
default:
}
}
counter++
}
}
}
func validateClusterHealth(ctx context.Context, client redis.UniversalClient) error {
// Basic connectivity test
if err := client.Ping(ctx).Err(); err != nil {
return fmt.Errorf("ping failed: %w", err)
}
// Test basic operations
testKey := "health-check-key"
testValue := "health-check-value"
if err := client.Set(ctx, testKey, testValue, time.Minute).Err(); err != nil {
return fmt.Errorf("set operation failed: %w", err)
}
retrievedValue, err := client.Get(ctx, testKey).Result()
if err != nil {
return fmt.Errorf("get operation failed: %w", err)
}
if retrievedValue != testValue {
return fmt.Errorf("value mismatch: expected %s, got %s", testValue, retrievedValue)
}
// Clean up
client.Del(ctx, testKey)
return nil
}
func waitForStableOperations(ctx context.Context, client redis.UniversalClient, duration time.Duration) error {
deadline := time.Now().Add(duration)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if err := validateClusterHealth(ctx, client); err != nil {
return fmt.Errorf("cluster health check failed: %w", err)
}
}
}
return nil
}