Files
ShivamandYichao Yang b864ea6799 Extend versioning's replay tester (#8124)
## What changed?
- Make the replay tester, used by versioning entity workflows, better.
- The versioning entity workflows CAN on every state change. Previously,
the replay tester was simply fetching the last execution of a versioning
entity workflow. Thus, we were skipping over a lot of NDE's that could
happen if one were to make changes in our workflows.

To fix this, the replay tester:
1. Fetches all the workflow ID's present in the CAN chain. Then, it
tests if for each individual run, are there code changes that have
caused NDE's.
2. It is important to remember that while we are fetching all the
workflows in CAN chain, SDK's replay tester does not test the chaining
itself. In other words, it tests the code path from the input that is
given at the start of each workflow execution. Thus, there could be a
chance that a code change introduced doesn't necessarily add new events
into a single workflow run. However, this code change can increase the
overall runs of the entity workflows and now the replay tester will see
this and understand that this as a cause for NDE.
3. Neatly makes a folder for that run and saves the required workflow
histories over there.
4. *Bonus*: I have also made the change to store the raw json file of
each workflow history. This would make debugging a whole lotta simpler.

It shall now look like:
<img width="1064" height="861" alt="image"
src="https://github.com/user-attachments/assets/67daf5c5-04c3-4236-86a0-f81470661d72"
/>



## Why?
- Robustness.

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [ ] added new unit test(s)
- [ ] added new functional test(s)

## Potential risks
- None
2025-08-05 10:43:26 -07:00

187 lines
6.9 KiB
Go

package replaytester
import (
"bufio"
"compress/gzip"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
deploymentspb "go.temporal.io/server/api/deployment/v1"
"go.temporal.io/server/common/log"
"go.temporal.io/server/service/worker/workerdeployment"
)
// TestReplays tests workflow logic backwards compatibility from previous versions.
func TestReplays(t *testing.T) {
replayer := worker.NewWorkflowReplayer()
// Create version workflow wrapper to match production registration
versionWorkflow := func(ctx workflow.Context, args *deploymentspb.WorkerDeploymentVersionWorkflowArgs) error {
refreshIntervalGetter := func() any {
return 5 * time.Minute // default value for testing
}
visibilityGracePeriodGetter := func() any {
return 3 * time.Minute // default value for testing
}
return workerdeployment.VersionWorkflow(ctx, refreshIntervalGetter, visibilityGracePeriodGetter, args)
}
// Create deployment workflow wrapper to match production registration
deploymentWorkflow := func(ctx workflow.Context, args *deploymentspb.WorkerDeploymentWorkflowArgs) error {
maxVersionsGetter := func() int {
return 100
}
return workerdeployment.Workflow(ctx, maxVersionsGetter, args)
}
replayer.RegisterWorkflowWithOptions(versionWorkflow, workflow.RegisterOptions{Name: workerdeployment.WorkerDeploymentVersionWorkflowType})
replayer.RegisterWorkflowWithOptions(deploymentWorkflow, workflow.RegisterOptions{Name: workerdeployment.WorkerDeploymentWorkflowType})
logger := log.NewSdkLogger(log.NewTestLogger())
// Test all run directories (default behavior for comprehensive replay testing)
testAllRunDirectories(t, replayer, logger)
}
// testAllRunDirectories tests all directories prepended with "run_" since they contain replay test data
func testAllRunDirectories(t *testing.T, replayer worker.WorkflowReplayer, logger *log.SdkLogger) {
runDirs, err := filepath.Glob("testdata/run_*")
require.NoError(t, err)
if len(runDirs) == 0 {
t.Skip("No run directories found. Run generate_history.sh first.")
}
fmt.Printf("Testing %d run directories\n", len(runDirs))
for _, runDir := range runDirs {
t.Run(filepath.Base(runDir), func(t *testing.T) {
fmt.Printf("Testing run: %s\n", runDir)
testRunDirectory(t, replayer, logger, runDir)
})
}
}
// testRunDirectory tests all workflow histories in a specific run directory
func testRunDirectory(t *testing.T, replayer worker.WorkflowReplayer, logger *log.SdkLogger, runDir string) {
files, err := filepath.Glob(filepath.Join(runDir, "replay_*.json.gz"))
require.NoError(t, err)
fmt.Printf(" Found %d workflow histories to replay\n", len(files))
// Validate that workflow counts match expected values
validateWorkflowCounts(t, files, runDir)
// Validate that histories replay successfully
for _, filename := range files {
t.Run(filepath.Base(filename), func(t *testing.T) {
replayWorkflowHistory(t, replayer, logger, filename)
})
}
}
// replayWorkflowHistory replays a single workflow history file and validates it
func replayWorkflowHistory(t *testing.T, replayer worker.WorkflowReplayer, logger *log.SdkLogger, filename string) {
logger.Info("Replaying", "file", filename)
f, err := os.Open(filename)
require.NoError(t, err)
r, err := gzip.NewReader(f)
require.NoError(t, err)
history, err := client.HistoryFromJSON(r, client.HistoryJSONOptions{})
require.NoError(t, err)
err = replayer.ReplayWorkflowHistory(logger, history)
require.NoError(t, err)
_ = r.Close()
_ = f.Close()
}
// readExpectedCounts reads expected workflow counts from the expected_counts.txt file
func readExpectedCounts(runDir string) (deploymentCount, versionCount int, err error) {
expectedCountsFile := filepath.Join(runDir, "expected_counts.txt")
file, err := os.Open(expectedCountsFile)
if err != nil {
// File doesn't exist - this might be an older test data directory
return 0, 0, fmt.Errorf("expected_counts.txt not found in %s: %w", runDir, err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#") || line == "" {
continue // Skip comments and empty lines
}
if strings.HasPrefix(line, "EXPECTED_DEPLOYMENT_WORKFLOWS=") {
value := strings.TrimPrefix(line, "EXPECTED_DEPLOYMENT_WORKFLOWS=")
deploymentCount, err = strconv.Atoi(value)
if err != nil {
return 0, 0, fmt.Errorf("invalid deployment count: %w", err)
}
} else if strings.HasPrefix(line, "EXPECTED_VERSION_WORKFLOWS=") {
value := strings.TrimPrefix(line, "EXPECTED_VERSION_WORKFLOWS=")
versionCount, err = strconv.Atoi(value)
if err != nil {
return 0, 0, fmt.Errorf("invalid version count: %w", err)
}
}
}
return deploymentCount, versionCount, scanner.Err()
}
// validateWorkflowCounts ensures the number of deployment and version workflows matches expectations
func validateWorkflowCounts(t *testing.T, files []string, runDir string) {
// Read expected counts from file
expectedDeploymentCount, expectedVersionCount, err := readExpectedCounts(runDir)
if err != nil {
// For backwards compatibility, skip validation if expected_counts.txt doesn't exist
fmt.Printf(" ⚠️ Skipping workflow count validation since expected_counts.txt doesn't exist for this test data directory; this is expected for older test data directories: %v\n", err)
return
}
// Count actual workflows
actualDeploymentCount := 0
actualVersionCount := 0
for _, file := range files {
filename := filepath.Base(file)
// Only count .gz files since those are the ones used for replay testing
if !strings.HasSuffix(filename, ".json.gz") {
continue
}
if strings.Contains(filename, "replay_worker_deployment_wf_run_") {
actualDeploymentCount++
} else if strings.Contains(filename, "replay_worker_deployment_version_wf_run_") {
actualVersionCount++
}
}
fmt.Printf(" Workflow counts - Expected: Deployment=%d, Version=%d | Actual: Deployment=%d, Version=%d\n",
expectedDeploymentCount, expectedVersionCount, actualDeploymentCount, actualVersionCount)
require.Equal(t, expectedDeploymentCount, actualDeploymentCount,
"Deployment workflow count mismatch in %s. Expected %d, got %d. "+
"This could mean your changes caused additional workflow executions. "+
"If this is expected, regenerate test data with: EXPECTED_DEPLOYMENT_WORKFLOWS=%d ./generate_history.sh",
runDir, expectedDeploymentCount, actualDeploymentCount, actualDeploymentCount)
require.Equal(t, expectedVersionCount, actualVersionCount,
"Version workflow count mismatch in %s. Expected %d, got %d. "+
"This could mean your changes caused additional workflow executions. "+
"If this is expected, regenerate test data with: EXPECTED_VERSION_WORKFLOWS=%d ./generate_history.sh",
runDir, expectedVersionCount, actualVersionCount, actualVersionCount)
}