Skip to content

Commit 617f77b

Browse files
authored
mock-tasks: allow matching mocks by meta attributes or step name (#1024)
1 parent 7a2fa7e commit 617f77b

File tree

4 files changed

+348
-5
lines changed

4 files changed

+348
-5
lines changed

plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockDefinition.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ public MockDefinition(Map<String, Object> definition) {
3535
this.definition = definition;
3636
}
3737

38+
public String stepName() {
39+
return MapUtils.getString(definition, "stepName");
40+
}
41+
42+
public Map<String, Object> stepMeta() {
43+
return MapUtils.getMap(definition, "stepMeta", Map.of());
44+
}
45+
3846
public String task() {
3947
try {
4048
return MapUtils.assertString(definition, "task");
@@ -66,6 +74,4 @@ public Serializable result() {
6674
public String throwError() {
6775
return MapUtils.getString(definition, "throwError");
6876
}
69-
70-
7177
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.walmartlabs.concord.plugins.mock;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2024 Walmart Inc.
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.walmartlabs.concord.runtime.v2.model.Step;
24+
import com.walmartlabs.concord.runtime.v2.sdk.Variables;
25+
26+
import java.util.Objects;
27+
28+
public record MockDefinitionContext(Step currentStep, String taskName, Variables input, String method, Object[] params) {
29+
30+
public static MockDefinitionContext task(Step currentStep, String taskName, Variables input) {
31+
Objects.requireNonNull(taskName);
32+
Objects.requireNonNull(input);
33+
34+
return new MockDefinitionContext(currentStep, taskName, input, null, null);
35+
}
36+
37+
public static MockDefinitionContext method(Step currentStep, String taskName, String methodName, Object[] params) {
38+
Objects.requireNonNull(taskName);
39+
Objects.requireNonNull(methodName);
40+
Objects.requireNonNull(params);
41+
return new MockDefinitionContext(currentStep, taskName, null, methodName, params);
42+
}
43+
}

plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockDefinitionProvider.java

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
*/
2222

2323
import com.walmartlabs.concord.plugins.mock.matcher.ArgsMatcher;
24+
import com.walmartlabs.concord.runtime.v2.model.AbstractStep;
25+
import com.walmartlabs.concord.runtime.v2.runner.logging.LogUtils;
2426
import com.walmartlabs.concord.runtime.v2.sdk.Context;
2527
import com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;
2628
import com.walmartlabs.concord.runtime.v2.sdk.Variables;
@@ -34,18 +36,20 @@
3436
@Singleton
3537
public class MockDefinitionProvider {
3638

39+
private final MockDefinitionMatcher mockDefinitionMatcher = new MockDefinitionMatcher();
40+
3741
public MockDefinition find(Context ctx, String taskName, Variables input) {
3842
return findMockDefinitions(ctx, mock ->
39-
taskName.equals(mock.task()) && ArgsMatcher.match(input.toMap(), mock.input()));
43+
mockDefinitionMatcher.matches(MockDefinitionContext.task(ctx.execution().currentStep(), taskName, input), mock));
4044
}
4145

4246
public MockDefinition find(Context ctx, String taskName, String method, Object[] params) {
4347
return findMockDefinitions(ctx, mock ->
44-
taskName.equals(mock.task()) && method.equals(mock.method()) && ArgsMatcher.match(params, mock.args()));
48+
mockDefinitionMatcher.matches(MockDefinitionContext.method(ctx.execution().currentStep(), taskName, method, params), mock));
4549
}
4650

4751
public boolean isTaskMocked(Context ctx, String taskName) {
48-
return mocks(ctx).anyMatch(mock -> taskName.equals(mock.task()));
52+
return mocks(ctx).anyMatch(mock -> ArgsMatcher.match(taskName, mock.task()));
4953
}
5054

5155
private static MockDefinition findMockDefinitions(Context ctx, Predicate<MockDefinition> predicate) {
@@ -63,4 +67,108 @@ private static Stream<MockDefinition> mocks(Context ctx) {
6367
return ctx.variables().getList("mocks", List.of()).stream()
6468
.map(m -> new MockDefinition((Map<String, Object>) m));
6569
}
70+
71+
public static class MockDefinitionMatcher {
72+
73+
private final List<Matcher> matchers = List.of(
74+
new TaskNameMatcher(),
75+
new MethodNameMatcher(),
76+
new StepNameMatcher(),
77+
new StepMetaMatcher(),
78+
new TaskInputMatcher(),
79+
new TaskArgsMatcher()
80+
);
81+
82+
public boolean matches(MockDefinitionContext context, MockDefinition mock) {
83+
for (Matcher matcher : matchers) {
84+
if (!matcher.matches(context, mock)) {
85+
return false;
86+
}
87+
}
88+
return true;
89+
}
90+
}
91+
92+
public interface Matcher {
93+
94+
boolean matches(MockDefinitionContext context, MockDefinition mock);
95+
}
96+
97+
public static class TaskNameMatcher implements Matcher {
98+
99+
@Override
100+
public boolean matches(MockDefinitionContext context, MockDefinition mock) {
101+
return ArgsMatcher.match(context.taskName(), mock.task());
102+
}
103+
}
104+
105+
public static class MethodNameMatcher implements Matcher {
106+
107+
@Override
108+
public boolean matches(MockDefinitionContext context, MockDefinition mock) {
109+
if (context.method() == null) {
110+
return true;
111+
}
112+
113+
return ArgsMatcher.match(context.method(), mock.method());
114+
}
115+
}
116+
117+
public static class StepNameMatcher implements Matcher {
118+
119+
@Override
120+
public boolean matches(MockDefinitionContext context, MockDefinition mock) {
121+
if (mock.stepName() == null) {
122+
return true;
123+
}
124+
125+
var logContext = LogUtils.getContext();
126+
return logContext != null && ArgsMatcher.match(logContext.segmentName(), mock.stepName());
127+
}
128+
}
129+
130+
public static class StepMetaMatcher implements Matcher {
131+
132+
@Override
133+
public boolean matches(MockDefinitionContext context, MockDefinition mock) {
134+
if (mock.stepMeta().isEmpty()) {
135+
return true;
136+
}
137+
138+
if (!(context.currentStep() instanceof AbstractStep<?> step)) {
139+
return false;
140+
}
141+
142+
var stepOptions = step.getOptions();
143+
if (stepOptions == null) {
144+
return false;
145+
}
146+
147+
return ArgsMatcher.match(stepOptions.meta(), mock.stepMeta());
148+
}
149+
}
150+
151+
public static class TaskInputMatcher implements Matcher {
152+
153+
@Override
154+
public boolean matches(MockDefinitionContext context, MockDefinition mock) {
155+
if (context.input() == null) {
156+
return true;
157+
}
158+
159+
return ArgsMatcher.match(context.input().toMap(), mock.input());
160+
}
161+
}
162+
163+
public static class TaskArgsMatcher implements Matcher {
164+
165+
@Override
166+
public boolean matches(MockDefinitionContext context, MockDefinition mock) {
167+
if (context.params() == null) {
168+
return true;
169+
}
170+
171+
return ArgsMatcher.match(context.params(), mock.args());
172+
}
173+
}
66174
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package com.walmartlabs.concord.plugins.mock;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2024 Walmart Inc.
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.walmartlabs.concord.runtime.v2.model.Location;
24+
import com.walmartlabs.concord.runtime.v2.model.Step;
25+
import com.walmartlabs.concord.runtime.v2.model.TaskCall;
26+
import com.walmartlabs.concord.runtime.v2.model.TaskCallOptions;
27+
import com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
31+
import java.util.List;
32+
import java.util.Map;
33+
34+
import static com.walmartlabs.concord.plugins.mock.MockDefinitionProvider.*;
35+
import static org.junit.jupiter.api.Assertions.assertFalse;
36+
import static org.junit.jupiter.api.Assertions.assertTrue;
37+
import static org.mockito.Mockito.mock;
38+
39+
public class MockDefinitionMatcherTest {
40+
41+
private MockDefinitionMatcher mockDefinitionMatcher;
42+
43+
@BeforeEach
44+
public void setUp() {
45+
mockDefinitionMatcher = new MockDefinitionMatcher();
46+
}
47+
48+
@Test
49+
public void testTaskMatch() {
50+
/*
51+
task: myTask
52+
in:
53+
param1: value1
54+
param2: value2
55+
*/
56+
var context = MockDefinitionContext.task(mock(Step.class), "myTask", new MapBackedVariables(Map.of("param1", "value1", "param2", "value2")));
57+
var mock = new MockDefinition(Map.of(
58+
"task", "myTask",
59+
"in", Map.of("param1", "value1")
60+
));
61+
62+
assertTrue(mockDefinitionMatcher.matches(context, mock));
63+
}
64+
65+
@Test
66+
public void testMatchOnlyByMeta() {
67+
/*
68+
task: myTask
69+
in:
70+
param1: value1
71+
param2: value2
72+
*/
73+
var currentStep = new TaskCall(Location.builder().build(), "myTask", TaskCallOptions.builder().meta(Map.of("taskId", "BOO")).build());
74+
var context = MockDefinitionContext.task(currentStep, "myTask", new MapBackedVariables(Map.of("param1", "value1", "param2", "value2")));
75+
var mock = new MockDefinition(Map.of(
76+
"task", "myTask",
77+
"stepMeta", Map.of("taskId", "BO.*")
78+
));
79+
80+
assertTrue(mockDefinitionMatcher.matches(context, mock));
81+
}
82+
83+
@Test
84+
public void testTaskMatchByMeta() {
85+
/*
86+
task: myTask
87+
in:
88+
param1: value1
89+
meta:
90+
taskId: "BOO"
91+
*/
92+
var currentStep = new TaskCall(Location.builder().build(), "myTask", TaskCallOptions.builder().meta(Map.of("taskId", "BOO")).build());
93+
var context = MockDefinitionContext.task(currentStep, "myTask", new MapBackedVariables(Map.of("param1", "value1", "param2", "value2")));
94+
var mock = new MockDefinition(Map.of(
95+
"task", "myTask",
96+
"in", Map.of("param1", "value1"),
97+
"stepMeta", Map.of("taskId", "BO.*")
98+
));
99+
100+
assertTrue(mockDefinitionMatcher.matches(context, mock));
101+
}
102+
103+
@Test
104+
public void testTaskMethodMatch() {
105+
// expr: ${myTask.myMethod(1, 2)}
106+
var context = MockDefinitionContext.method(mock(Step.class), "myTask", "myMethod", new Object[] {1, 2});
107+
108+
var mock = new MockDefinition(Map.of(
109+
"task", "myTask",
110+
"method", "myMethod",
111+
"args", List.of(1, 2)
112+
));
113+
114+
assertTrue(mockDefinitionMatcher.matches(context, mock));
115+
}
116+
117+
@Test
118+
public void testTaskMethodMatchByMeta() {
119+
// expr: ${myTask.myMethod(1, 2)}
120+
// meta:
121+
// taskId: "BOO"
122+
var currentStep = new TaskCall(Location.builder().build(), "myTask", TaskCallOptions.builder().meta(Map.of("taskId", "BOO")).build());
123+
var context = MockDefinitionContext.method(currentStep, "myTask", "myMethod", new Object[] {1, 2});
124+
125+
var mock = new MockDefinition(Map.of(
126+
"task", "myTask",
127+
"method", "myMethod",
128+
"args", List.of(1, 2),
129+
"stepMeta", Map.of("taskId", "BO.*")
130+
));
131+
132+
assertTrue(mockDefinitionMatcher.matches(context, mock));
133+
}
134+
135+
@Test
136+
public void testNotMatch_taskName() {
137+
/*
138+
task: myTask
139+
in:
140+
param1: value1
141+
param2: value2
142+
*/
143+
var context = MockDefinitionContext.task(mock(Step.class), "myTask", new MapBackedVariables(Map.of("param1", "value1", "param2", "value2")));
144+
var mock = new MockDefinition(Map.of(
145+
"task", "myTask2",
146+
"in", Map.of("param1", "value1")
147+
));
148+
149+
// real taskName=myTask, mocked: "myTask2"
150+
assertFalse(mockDefinitionMatcher.matches(context, mock));
151+
}
152+
153+
@Test
154+
public void testNotMatch_InputParams() {
155+
/*
156+
task: myTask
157+
in:
158+
param1: value1
159+
param2: value2
160+
*/
161+
var context = MockDefinitionContext.task(mock(Step.class), "myTask", new MapBackedVariables(Map.of("param1", "value1", "param2", "value2")));
162+
var mock = new MockDefinition(Map.of(
163+
"task", "myTask2",
164+
"in", Map.of("param3", "value3")
165+
));
166+
167+
assertFalse(mockDefinitionMatcher.matches(context, mock));
168+
}
169+
170+
@Test
171+
public void testNotMatch_Meta() {
172+
/*
173+
task: myTask
174+
in:
175+
param1: value1
176+
param2: value2
177+
*/
178+
var context = MockDefinitionContext.task(mock(Step.class), "myTask", new MapBackedVariables(Map.of("param1", "value1", "param2", "value2")));
179+
var mock = new MockDefinition(Map.of(
180+
"task", "myTask2",
181+
"stepMeta", Map.of("taskId", "BO.*")
182+
));
183+
184+
assertFalse(mockDefinitionMatcher.matches(context, mock));
185+
}
186+
}

0 commit comments

Comments
 (0)