package writers import ( "context" "errors" "path/filepath" "strings" "testing" ) func TestExecutionOutcomes(t *testing.T) { for _, atomic := range []bool{false, true} { for _, continuation := range []bool{false, true} { t.Run(strings.Join([]string{map[bool]string{false: "independent", true: "atomic"}[atomic], map[bool]string{false: "stop", true: "continue"}[continuation]}, "/"), func(t *testing.T) { attempted, rollbacks := 0, 0 cause := errors.New("injected failure") backend := ExecutionBackend{ Exec: func(_ context.Context, i int, _ string) error { attempted++ if i == 1 { return cause } return nil }, Begin: func(context.Context) error { return nil }, Commit: func(context.Context) error { t.Fatal("must not commit"); return nil }, Rollback: func(context.Context) error { rollbacks++; return nil }, } opts := &WriterOptions{ContinueOnError: continuation, AtomicExecution: atomic} report := &ExecutionReport{Dialect: "test"} err := RunExecution(context.Background(), opts, report, StatementOperations([]string{"one", "bad", "three"}), backend) if atomic && continuation { if err == nil || attempted != 0 || report.SkippedStatements != 3 { t.Fatalf("invalid options must not execute: %+v %v", report, err) } return } var failure *ExecutionFailure if !errors.As(err, &failure) || !errors.Is(err, cause) { t.Fatalf("must preserve typed report and cause: %v", err) } if report.FailedStatements != 1 || report.TotalStatements != 3 || report.Errors[0].StatementNumber != 2 { t.Fatalf("failure report: %+v", report) } if atomic { if rollbacks != 1 || report.ExecutedStatements != 0 || report.RolledBackStatements != 1 || report.Operations[0].Status != "rolled_back" || report.Outcome != "rolled_back" { t.Fatalf("rollback report: %+v", report) } } else if continuation { if attempted != 3 || report.ExecutedStatements != 2 || report.SkippedStatements != 0 { t.Fatalf("continuation report: %+v", report) } } else if attempted != 2 || report.ExecutedStatements != 1 || report.SkippedStatements != 1 { t.Fatalf("stop report: %+v", report) } if opts.Metadata["execution_report"] != report { t.Fatal("typed results missing") } }) } } } func TestExecutionUncertainFinalization(t *testing.T) { for _, failStatement := range []bool{false, true} { t.Run(map[bool]string{false: "commit", true: "rollback"}[failStatement], func(t *testing.T) { finalErr := errors.New("connection lost during finalization") backend := ExecutionBackend{ Begin: func(context.Context) error { return nil }, Exec: func(_ context.Context, i int, _ string) error { if failStatement && i == 1 { return errors.New("statement failed") } return nil }, Commit: func(context.Context) error { return finalErr }, Rollback: func(context.Context) error { return finalErr }, } report := &ExecutionReport{} err := RunExecution(context.Background(), &WriterOptions{AtomicExecution: true}, report, StatementOperations([]string{"one", "two"}), backend) if !errors.Is(err, finalErr) || report.Outcome != "unknown" || report.Operations[0].Status != "unknown" || report.RolledBackStatements != 0 || report.ExecutedStatements != 0 { t.Fatalf("uncertain outcome misreported: %+v %v", report, err) } }) } } func TestExecutionReportFailureAndFatalPreflight(t *testing.T) { opts := &WriterOptions{ContinueOnError: true, Metadata: map[string]interface{}{"report_path": filepath.Join(t.TempDir(), "missing", "report.json")}} report := &ExecutionReport{} calls := 0 ops := []ExecutionOperation{{Statement: "preflight", StopOnFailure: true}, {Statement: "must not run"}} err := RunExecution(context.Background(), opts, report, ops, ExecutionBackend{Exec: func(context.Context, int, string) error { calls++; return errors.New("invalid data") }}) var failure *ExecutionFailure if !errors.As(err, &failure) || !strings.Contains(err.Error(), "requested execution report") || calls != 1 || report.SkippedStatements != 1 { t.Fatalf("must retain both failures and stop: %+v %v", report, err) } }