-
Notifications
You must be signed in to change notification settings - Fork 18
Extend test helpers #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend test helpers #189
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ require ( | |
| github.com/spf13/cobra v1.7.0 | ||
| github.com/spf13/pflag v1.0.5 | ||
| github.com/stretchr/testify v1.10.0 | ||
| github.com/temporalio/features v0.0.0-20250714172315-c9d352c46b16 | ||
| github.com/temporalio/features v0.0.0-20250808182149-bb2a99cdf200 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to pull in temporalio/features#662 |
||
| go.temporal.io/api v1.50.0 | ||
| go.temporal.io/sdk v1.35.0 | ||
| go.uber.org/zap v1.27.0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,13 +12,58 @@ import ( | |
| "google.golang.org/protobuf/encoding/protojson" | ||
| ) | ||
|
|
||
| type specLine struct { | ||
| EventID int64 | ||
| Type string | ||
| Fields map[string]any | ||
| type event struct { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some refactorings to make the events a typed entity. |
||
| Type string | ||
| Attributes map[string]any | ||
| } | ||
|
|
||
| // requireHistoryMatches checks that the list of history events match the expected spec. | ||
| func (e event) String(lineNum int) string { | ||
| out := e.Type | ||
| if len(e.Attributes) > 0 { | ||
| b, _ := json.Marshal(e.Attributes) | ||
| out += " " + string(b) | ||
| } | ||
| return fmt.Sprintf("%3d: %s\n", lineNum, out) | ||
| } | ||
|
|
||
| type eventList []event | ||
|
|
||
| func (el eventList) findAllByType(eventType string) []struct { | ||
| Event *event | ||
| Index int | ||
| } { | ||
| var result []struct { | ||
| Event *event | ||
| Index int | ||
| } | ||
| for i, evt := range el { | ||
| if evt.Type == eventType { | ||
| result = append(result, struct { | ||
| Event *event | ||
| Index int | ||
| }{ | ||
| Event: &evt, | ||
| Index: i, | ||
| }) | ||
| } | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| func (el eventList) Strings() []string { | ||
| result := make([]string, len(el)) | ||
| for i, evt := range el { | ||
| result[i] = evt.String(i + 1) | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| // HistoryMatcher defines the interface for history matching strategies | ||
| type HistoryMatcher interface { | ||
| Match(t *testing.T, actualHistoryEvents []*historypb.HistoryEvent) error | ||
| } | ||
|
|
||
| // fullHistoryMatcher checks that the list of history events match the expected spec. | ||
| // The expected spec is a string with each line containing an (1) event type, (2) optional | ||
| // JSON literal matching the event attributes and (3) an optional comment. | ||
| // | ||
|
|
@@ -34,22 +79,26 @@ type specLine struct { | |
| // WorkflowTaskStarted | ||
| // WorkflowTaskCompleted | ||
| // WorkflowExecutionCompleted | ||
| func requireHistoryMatches(t *testing.T, actualEvents []*historypb.HistoryEvent, expectedSpec string) { | ||
| type fullHistoryMatcher string | ||
|
|
||
| func (m fullHistoryMatcher) Match(t *testing.T, actualHistoryEvents []*historypb.HistoryEvent) error { | ||
| t.Helper() | ||
|
|
||
| actualEvents := parseEvents(actualHistoryEvents) | ||
| expectedEvents := parseSpec(t, string(m)) | ||
|
|
||
| var diffs []string | ||
| var actualDump strings.Builder | ||
| var expectedDump strings.Builder | ||
| expectedLines := parseSpec(t, expectedSpec) | ||
| maxLines := max(len(actualEvents), len(expectedLines)) | ||
| maxLines := max(len(actualEvents), len(expectedEvents)) | ||
|
|
||
| for i := 0; i < maxLines; i++ { | ||
| lineNumber := i + 1 | ||
|
|
||
| var expectedLine specLine | ||
| if i < len(expectedLines) { | ||
| expectedLine = expectedLines[i] | ||
| expectedDump.WriteString(formatExpectedLine(lineNumber, expectedLine, expectedLine.Fields)) | ||
| var expectedEvent event | ||
| if i < len(expectedEvents) { | ||
| expectedEvent = expectedEvents[i] | ||
| expectedDump.WriteString(expectedEvent.String(lineNumber)) | ||
| } | ||
|
|
||
| if i >= len(actualEvents) { | ||
|
|
@@ -59,40 +108,92 @@ func requireHistoryMatches(t *testing.T, actualEvents []*historypb.HistoryEvent, | |
| } | ||
| actualEvent := actualEvents[i] | ||
|
|
||
| // Transform event for comparison. | ||
| actualType := strings.TrimPrefix(actualEvent.GetEventType().String(), "EVENT_TYPE_") | ||
| actualFields, _ := flattenEvent(actualEvent) | ||
| actualDump.WriteString(formatActualLine(lineNumber, actualType, expectedLine.Fields, actualFields)) | ||
| var fieldsToShow map[string]any | ||
| if expectedEvent.Attributes != nil && len(actualEvent.Attributes) > 0 { | ||
| fieldsToShow = actualEvent.Attributes | ||
| } | ||
| displayEvent := event{Type: actualEvent.Type, Attributes: fieldsToShow} | ||
| actualDump.WriteString(displayEvent.String(lineNumber)) | ||
|
|
||
| // Compare types and fields. | ||
| if expectedLine.Type == "" { | ||
| if expectedEvent.Type == "" { | ||
| diffs = append(diffs, fmt.Sprintf("line %d: unexpected event", lineNumber)) | ||
| } else if actualType != expectedLine.Type { | ||
| } else if actualEvent.Type != expectedEvent.Type { | ||
| diffs = append(diffs, fmt.Sprintf("line %d: type mismatch, expected %s got %s", | ||
| lineNumber, expectedLine.Type, actualType)) | ||
| } else if !mapIsSuperset(actualFields, expectedLine.Fields) { | ||
| lineNumber, expectedEvent.Type, actualEvent.Type)) | ||
| } else if !mapIsSuperset(actualEvent.Attributes, expectedEvent.Attributes) { | ||
| diffs = append(diffs, fmt.Sprintf("line %d: field(s) mismatch: expected %#v got %#v", | ||
| lineNumber, expectedLine.Fields, actualFields)) | ||
| lineNumber, expectedEvent.Attributes, actualEvent.Attributes)) | ||
| } | ||
| } | ||
|
|
||
| if len(diffs) > 0 { | ||
| t.Log("--- MISMATCH ---\n") | ||
| t.Log("EXPECTED:\n" + expectedDump.String()) | ||
| t.Log("ACTUAL:\n" + actualDump.String()) | ||
| logHistoryMismatch(t, string(m), actualEvents) | ||
|
|
||
| t.Log("--- DIFFS ---") | ||
| t.Log("--- DIFFS ---\n") | ||
| for _, d := range diffs { | ||
| t.Log(d) | ||
| } | ||
| require.Equal(t, 0, len(diffs), "history match failed") | ||
| return fmt.Errorf("history match failed with %d diffs", len(diffs)) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func parseSpec(t *testing.T, input string) []specLine { | ||
| // partialHistoryMatcher checks that the list of history events contains a subsequence | ||
| // that matches the expected spec. The expected spec is a string with each line containing an (1) event type, | ||
| // (2) optional JSON literal matching the event attributes and (3) an optional comment. | ||
| // Use "..." as an event type to skip any number of events. | ||
| // | ||
| // Example: | ||
| // | ||
| // WorkflowExecutionStarted {"taskQueue":"foo-bar"} | ||
| // ... | ||
| // WorkflowExecutionCompleted | ||
| type partialHistoryMatcher string | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needed because some tests are not deterministic enough to run the full history matcher. |
||
|
|
||
| func (m partialHistoryMatcher) Match(t *testing.T, actualHistoryEvents []*historypb.HistoryEvent) error { | ||
| t.Helper() | ||
|
|
||
| var lines []specLine | ||
| actualEvents := parseEvents(actualHistoryEvents) | ||
| expectedEvents := parseSpec(t, string(m)) | ||
|
|
||
| var matchFromPosition func(actualIdx, expectedIdx int) bool | ||
| matchFromPosition = func(actualIdx, expectedIdx int) bool { | ||
| if expectedIdx >= len(expectedEvents) { | ||
| return true | ||
| } | ||
| if actualIdx >= len(actualEvents) { | ||
| return false | ||
| } | ||
|
|
||
| expectedEvent := expectedEvents[expectedIdx] | ||
|
|
||
| if expectedEvent.Type == "..." { | ||
| for i := actualIdx; i <= len(actualEvents); i++ { | ||
| if matchFromPosition(i, expectedIdx+1) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| actualEvent := actualEvents[actualIdx] | ||
| if actualEvent.Type == expectedEvent.Type && mapIsSuperset(actualEvent.Attributes, expectedEvent.Attributes) { | ||
| return matchFromPosition(actualIdx+1, expectedIdx+1) | ||
| } | ||
| return matchFromPosition(actualIdx+1, expectedIdx) | ||
| } | ||
|
|
||
| if matchFromPosition(0, 0) { | ||
| return nil | ||
| } | ||
|
|
||
| logHistoryMismatch(t, string(m), actualEvents) | ||
| return fmt.Errorf("partialHistory matcher failed: expected sequence not found in history") | ||
| } | ||
|
|
||
| func parseSpec(t *testing.T, input string) eventList { | ||
| var events eventList | ||
| for lineNum, raw := range strings.Split(strings.TrimSpace(input), "\n") { | ||
| raw = strings.TrimSpace(raw) | ||
|
|
||
|
|
@@ -103,25 +204,38 @@ func parseSpec(t *testing.T, input string) []specLine { | |
| tokens := strings.SplitN(raw, " ", 2) | ||
| require.NotEmptyf(t, tokens, "line %d: empty line in spec", lineNum+1) | ||
|
|
||
| line := specLine{Type: tokens[0]} | ||
| evt := event{Type: tokens[0]} | ||
| if len(tokens) > 1 { | ||
| err := json.Unmarshal([]byte(tokens[1]), &line.Fields) | ||
| err := json.Unmarshal([]byte(tokens[1]), &evt.Attributes) | ||
| require.NoErrorf(t, err, "line %d: invalid JSON in line: %q", lineNum+1, raw) | ||
| } | ||
| lines = append(lines, line) | ||
| events = append(events, evt) | ||
| } | ||
| return lines | ||
| return events | ||
| } | ||
|
|
||
| func flattenEvent(event *historypb.HistoryEvent) (map[string]any, error) { | ||
| func parseEvents(events []*historypb.HistoryEvent) eventList { | ||
| var result eventList | ||
| for _, histEvent := range events { | ||
| eventType := strings.TrimPrefix(histEvent.GetEventType().String(), "EVENT_TYPE_") | ||
| fields := extractFields(histEvent) | ||
| result = append(result, event{ | ||
| Type: eventType, | ||
| Attributes: fields, | ||
| }) | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| func extractFields(event *historypb.HistoryEvent) map[string]any { | ||
| b, err := protojson.Marshal(event) | ||
| if err != nil { | ||
| return nil, err | ||
| panic(fmt.Sprintf("failed to marshal protobuf event: %v", err)) | ||
| } | ||
|
|
||
| var raw map[string]any | ||
| if err = json.Unmarshal(b, &raw); err != nil { | ||
| return nil, err | ||
| panic(fmt.Sprintf("failed to unmarshal JSON: %v", err)) | ||
| } | ||
| result := make(map[string]any) | ||
|
|
||
|
|
@@ -136,7 +250,7 @@ func flattenEvent(event *historypb.HistoryEvent) (map[string]any, error) { | |
| result[k] = v | ||
| } | ||
| } | ||
| return result, nil | ||
| return result | ||
| } | ||
|
|
||
| func mapIsSuperset(actual, expected map[string]any) bool { | ||
|
|
@@ -160,24 +274,49 @@ func looselyEqual(x, y any) bool { | |
| } | ||
| case string: | ||
| return fmt.Sprint(x) == fmt.Sprint(y) | ||
| case map[string]any: | ||
| if yMap, ok := y.(map[string]any); ok { | ||
| return mapIsSuperset(x, yMap) | ||
| } | ||
| return false | ||
| } | ||
| return reflect.DeepEqual(x, y) | ||
| } | ||
|
|
||
| func formatExpectedLine(lineNum int, line specLine, fields map[string]any) string { | ||
| out := fmt.Sprintf("%3d: %s", lineNum, line.Type) | ||
| if len(fields) > 0 { | ||
| b, _ := json.Marshal(fields) | ||
| out += " " + string(b) | ||
| func formatEventTypeName(event *historypb.HistoryEvent) string { | ||
| return strings.TrimPrefix(event.GetEventType().String(), "EVENT_TYPE_") | ||
| } | ||
|
|
||
| func logHistoryMismatch(t *testing.T, expectedSpec string, actualEvents eventList) { | ||
| t.Helper() | ||
|
|
||
| t.Logf("\n--- MISMATCH ---\n\n") | ||
|
|
||
| if expectedSpec != "" { | ||
| expectedEvents := parseSpec(t, expectedSpec) | ||
| t.Log("EXPECTED:") | ||
| for _, expectedEvent := range expectedEvents { | ||
| out := expectedEvent.Type | ||
| if len(expectedEvent.Attributes) > 0 { | ||
| b, _ := json.Marshal(expectedEvent.Attributes) | ||
| out += " " + string(b) | ||
| } | ||
| t.Logf(" %s\n", out) | ||
| } | ||
| } | ||
| return out + "\n" | ||
|
|
||
| t.Log("ACTUAL:") | ||
| logHistoryEvents(t, actualEvents) | ||
| } | ||
|
|
||
| func formatActualLine(lineNum int, typ string, expectedFields map[string]any, actualFields map[string]any) string { | ||
| out := fmt.Sprintf("%3d: %s", lineNum, typ) | ||
| if expectedFields != nil && len(actualFields) > 0 { | ||
| b, _ := json.Marshal(actualFields) | ||
| out += " " + string(b) | ||
| func logHistoryEvents(t *testing.T, actualEvents eventList) { | ||
| t.Helper() | ||
| for _, actualEvent := range actualEvents { | ||
| out := actualEvent.Type | ||
| if len(actualEvent.Attributes) > 0 { | ||
| b, _ := json.Marshal(actualEvent.Attributes) | ||
| out += " " + string(b) | ||
| } | ||
| t.Logf(" %s\n", out) | ||
| } | ||
| return out + "\n" | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed for consistency.