diff --git a/temporal-testing-junit5/src/main/java/io/temporal/testing/TestActivityExtension.java b/temporal-testing-junit5/src/main/java/io/temporal/testing/TestActivityExtension.java index d7bb3d7942..267aad2c62 100644 --- a/temporal-testing-junit5/src/main/java/io/temporal/testing/TestActivityExtension.java +++ b/temporal-testing-junit5/src/main/java/io/temporal/testing/TestActivityExtension.java @@ -19,9 +19,11 @@ package io.temporal.testing; +import io.temporal.activity.DynamicActivity; import io.temporal.common.metadata.POJOActivityImplMetadata; import io.temporal.common.metadata.POJOActivityInterfaceMetadata; import java.lang.reflect.Constructor; +import java.lang.reflect.Parameter; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.extension.AfterEachCallback; @@ -67,6 +69,8 @@ public class TestActivityExtension private final Set> supportedParameterTypes = new HashSet<>(); + private boolean includesDynamicActivity; + private TestActivityExtension(Builder builder) { testEnvironmentOptions = builder.testEnvironmentOptions; activityImplementations = builder.activityImplementations; @@ -74,6 +78,10 @@ private TestActivityExtension(Builder builder) { supportedParameterTypes.add(TestActivityEnvironment.class); for (Object activity : activityImplementations) { + if (DynamicActivity.class.isAssignableFrom(activity.getClass())) { + includesDynamicActivity = true; + continue; + } POJOActivityImplMetadata metadata = POJOActivityImplMetadata.newInstance(activity.getClass()); for (POJOActivityInterfaceMetadata activityInterface : metadata.getActivityInterfaces()) { supportedParameterTypes.add(activityInterface.getInterfaceClass()); @@ -90,13 +98,31 @@ public boolean supportsParameter( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - if (parameterContext.getParameter().getDeclaringExecutable() instanceof Constructor) { + Parameter parameter = parameterContext.getParameter(); + if (parameter.getDeclaringExecutable() instanceof Constructor) { // Constructor injection is not supported return false; } - Class parameterType = parameterContext.getParameter().getType(); - return supportedParameterTypes.contains(parameterType); + Class parameterType = parameter.getType(); + if (supportedParameterTypes.contains(parameterType)) { + return true; + } + + if (!includesDynamicActivity) { + // If no DynamicActivity implementation was registered then supportedParameterTypes are the + // only ones types that can be injected + return false; + } + + try { + // If POJOActivityInterfaceMetadata can be instantiated then parameterType is a proper + // activity interface and can be injected + POJOActivityInterfaceMetadata.newInstance(parameterType); + return true; + } catch (Exception e) { + return false; + } } @Override diff --git a/temporal-testing-junit5/src/main/java/io/temporal/testing/TestWorkflowExtension.java b/temporal-testing-junit5/src/main/java/io/temporal/testing/TestWorkflowExtension.java index fa7293d219..68951891cf 100644 --- a/temporal-testing-junit5/src/main/java/io/temporal/testing/TestWorkflowExtension.java +++ b/temporal-testing-junit5/src/main/java/io/temporal/testing/TestWorkflowExtension.java @@ -28,7 +28,9 @@ import io.temporal.worker.Worker; import io.temporal.worker.WorkerFactoryOptions; import io.temporal.worker.WorkerOptions; +import io.temporal.workflow.DynamicWorkflow; import java.lang.reflect.Constructor; +import java.lang.reflect.Parameter; import java.time.Instant; import java.util.HashSet; import java.util.Set; @@ -40,6 +42,7 @@ import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestWatcher; +import org.junit.platform.commons.support.AnnotationSupport; /** * JUnit Jupiter extension that simplifies testing of Temporal workflows. @@ -77,7 +80,7 @@ public class TestWorkflowExtension private static final String TEST_ENVIRONMENT_KEY = "testEnvironment"; private static final String WORKER_KEY = "worker"; - private static final String TASK_QUEUE_KEY = "taskQueue"; + private static final String WORKFLOW_OPTIONS_KEY = "workflowOptions"; private final WorkerOptions workerOptions; private final WorkflowClientOptions workflowClientOptions; @@ -90,6 +93,7 @@ public class TestWorkflowExtension private final long initialTimeMillis; private final Set> supportedParameterTypes = new HashSet<>(); + private boolean includesDynamicWorkflow; private TestWorkflowExtension(Builder builder) { workerOptions = builder.workerOptions; @@ -113,6 +117,10 @@ private TestWorkflowExtension(Builder builder) { supportedParameterTypes.add(Worker.class); for (Class workflowType : workflowTypes) { + if (DynamicWorkflow.class.isAssignableFrom(workflowType)) { + includesDynamicWorkflow = true; + continue; + } POJOWorkflowImplMetadata metadata = POJOWorkflowImplMetadata.newInstance(workflowType); for (POJOWorkflowInterfaceMetadata workflowInterface : metadata.getWorkflowInterfaces()) { supportedParameterTypes.add(workflowInterface.getInterfaceClass()); @@ -129,13 +137,31 @@ public boolean supportsParameter( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - if (parameterContext.getParameter().getDeclaringExecutable() instanceof Constructor) { + Parameter parameter = parameterContext.getParameter(); + if (parameter.getDeclaringExecutable() instanceof Constructor) { // Constructor injection is not supported return false; } - Class parameterType = parameterContext.getParameter().getType(); - return supportedParameterTypes.contains(parameterType); + Class parameterType = parameter.getType(); + if (supportedParameterTypes.contains(parameterType)) { + return true; + } + + if (!includesDynamicWorkflow) { + // If no DynamicWorkflow implementation was registered then supportedParameterTypes are the + // only ones types that can be injected + return false; + } + + try { + // If POJOWorkflowInterfaceMetadata can be instantiated then parameterType is a proper + // workflow interface and can be injected + POJOWorkflowInterfaceMetadata.newInstance(parameterType); + return true; + } catch (Exception e) { + return false; + } } @Override @@ -143,36 +169,36 @@ public Object resolveParameter( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - TestWorkflowEnvironment testEnvironment = getTestEnvironment(extensionContext); - Class parameterType = parameterContext.getParameter().getType(); if (parameterType == TestWorkflowEnvironment.class) { - return testEnvironment; + return getTestEnvironment(extensionContext); } else if (parameterType == WorkflowClient.class) { - return testEnvironment.getWorkflowClient(); + return getTestEnvironment(extensionContext).getWorkflowClient(); } else if (parameterType == WorkflowOptions.class) { - String taskQueue = getTaskQueue(extensionContext); - return WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build(); + return getWorkflowOptions(extensionContext); } else if (parameterType == Worker.class) { return getWorker(extensionContext); } else { // Workflow stub - String taskQueue = getTaskQueue(extensionContext); - WorkflowOptions workflowOptions = - WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build(); - return testEnvironment.getWorkflowClient().newWorkflowStub(parameterType, workflowOptions); + return getTestEnvironment(extensionContext) + .getWorkflowClient() + .newWorkflowStub(parameterType, getWorkflowOptions(extensionContext)); } } @Override public void beforeEach(ExtensionContext context) throws Exception { + long currentInitialTimeMillis = + AnnotationSupport.findAnnotation(context.getElement(), WorkflowInitialTime.class) + .map(annotation -> Instant.parse(annotation.value()).toEpochMilli()) + .orElse(initialTimeMillis); TestEnvironmentOptions testOptions = TestEnvironmentOptions.newBuilder() .setWorkflowClientOptions(workflowClientOptions) .setWorkerFactoryOptions(workerFactoryOptions) .setUseExternalService(useExternalService) .setTarget(target) - .setInitialTimeMillis(initialTimeMillis) + .setInitialTimeMillis(currentInitialTimeMillis) .build(); TestWorkflowEnvironment testEnvironment = TestWorkflowEnvironment.newInstance(testOptions); String taskQueue = @@ -187,7 +213,7 @@ public void beforeEach(ExtensionContext context) throws Exception { setTestEnvironment(context, testEnvironment); setWorker(context, worker); - setTaskQueue(context, taskQueue); + setWorkflowOptions(context, WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build()); } @Override @@ -219,12 +245,12 @@ private void setWorker(ExtensionContext context, Worker worker) { getStore(context).put(WORKER_KEY, worker); } - private String getTaskQueue(ExtensionContext context) { - return getStore(context).get(TASK_QUEUE_KEY, String.class); + private WorkflowOptions getWorkflowOptions(ExtensionContext context) { + return getStore(context).get(WORKFLOW_OPTIONS_KEY, WorkflowOptions.class); } - private void setTaskQueue(ExtensionContext context, String taskQueue) { - getStore(context).put(TASK_QUEUE_KEY, taskQueue); + private void setWorkflowOptions(ExtensionContext context, WorkflowOptions taskQueue) { + getStore(context).put(WORKFLOW_OPTIONS_KEY, taskQueue); } private ExtensionContext.Store getStore(ExtensionContext context) { diff --git a/temporal-testing-junit5/src/main/java/io/temporal/testing/WorkflowInitialTime.java b/temporal-testing-junit5/src/main/java/io/temporal/testing/WorkflowInitialTime.java new file mode 100644 index 0000000000..60aaf51e6d --- /dev/null +++ b/temporal-testing-junit5/src/main/java/io/temporal/testing/WorkflowInitialTime.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Overrides the initial timestamp used by the {@link TestWorkflowExtension} */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface WorkflowInitialTime { + + String value(); +} diff --git a/temporal-testing-junit5/src/test/java/io/temporal/testing/TestActivityExtensionDynamicTest.java b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestActivityExtensionDynamicTest.java new file mode 100644 index 0000000000..685d4e1f3f --- /dev/null +++ b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestActivityExtensionDynamicTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.temporal.activity.Activity; +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; +import io.temporal.activity.DynamicActivity; +import io.temporal.common.converter.EncodedValues; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class TestActivityExtensionDynamicTest { + + @RegisterExtension + public static final TestActivityExtension activityExtension = + TestActivityExtension.newBuilder() + .setActivityImplementations(new MyDynamicActivityImpl()) + .build(); + + @ActivityInterface + public interface MyActivity { + + @ActivityMethod(name = "OverriddenActivityMethod") + String activity1(String input); + } + + private static class MyDynamicActivityImpl implements DynamicActivity { + + @Override + public Object execute(EncodedValues args) { + return Activity.getExecutionContext().getInfo().getActivityType() + + "-" + + args.get(0, String.class); + } + } + + @Test + public void extensionShouldResolveDynamicActivitiesParameters(MyActivity activity) { + assertEquals("OverriddenActivityMethod-input1", activity.activity1("input1")); + } +} diff --git a/temporal-testing-junit5/src/test/java/io/temporal/testing/ActivityExtensionTest.java b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestActivityExtensionTest.java similarity index 98% rename from temporal-testing-junit5/src/test/java/io/temporal/testing/ActivityExtensionTest.java rename to temporal-testing-junit5/src/test/java/io/temporal/testing/TestActivityExtensionTest.java index a7a97085cf..0f13425c58 100644 --- a/temporal-testing-junit5/src/test/java/io/temporal/testing/ActivityExtensionTest.java +++ b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestActivityExtensionTest.java @@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -public class ActivityExtensionTest { +public class TestActivityExtensionTest { @RegisterExtension public static final TestActivityExtension activityExtension = diff --git a/temporal-testing-junit5/src/test/java/io/temporal/testing/TestWorkflowExtensionDynamicTest.java b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestWorkflowExtensionDynamicTest.java new file mode 100644 index 0000000000..b2b70e6341 --- /dev/null +++ b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestWorkflowExtensionDynamicTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not + * use this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.temporal.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.temporal.activity.Activity; +import io.temporal.activity.ActivityInfo; +import io.temporal.activity.ActivityOptions; +import io.temporal.activity.DynamicActivity; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.client.WorkflowStub; +import io.temporal.common.converter.EncodedValues; +import io.temporal.workflow.ActivityStub; +import io.temporal.workflow.DynamicWorkflow; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; + +@Timeout(value = 30, unit = TimeUnit.SECONDS) +public class TestWorkflowExtensionDynamicTest { + + @RegisterExtension + public static final TestWorkflowExtension testWorkflow = + TestWorkflowExtension.newBuilder() + .setWorkflowTypes(HelloDynamicWorkflowImpl.class) + .setActivityImplementations(new HelloDynamicActivityImpl()) + .build(); + + public static class HelloDynamicActivityImpl implements DynamicActivity { + + @Override + public Object execute(EncodedValues args) { + String name = args.get(0, String.class); + ActivityInfo activityInfo = Activity.getExecutionContext().getInfo(); + return String.format( + "Hello %s from activity %s and workflow %s", + name, activityInfo.getActivityType(), activityInfo.getWorkflowType()); + } + } + + @WorkflowInterface + public interface HelloWorkflow { + + @WorkflowMethod + String sayHello(String name); + } + + public static class HelloDynamicWorkflowImpl implements DynamicWorkflow { + + private static final Logger logger = Workflow.getLogger(HelloDynamicWorkflowImpl.class); + + private final ActivityStub activity = + Workflow.newUntypedActivityStub( + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofMinutes(1)).build()); + + @Override + public Object execute(EncodedValues args) { + String name = args.get(0, String.class); + logger.info("Hello, {}", name); + Workflow.sleep(Duration.ofHours(1)); + return activity.execute("BuildGreeting", String.class, name); + } + } + + @Test + public void extensionShouldResolveDynamicWorkflowParameters(HelloWorkflow workflow) { + assertEquals( + "Hello World from activity BuildGreeting and workflow HelloWorkflow", + workflow.sayHello("World")); + } + + @Test + public void extensionShouldSupportLaunchingViaUntypedWorkflowStubs( + WorkflowClient workflowClient, WorkflowOptions workflowOptions) { + WorkflowStub workflow = + workflowClient.newUntypedWorkflowStub("AnotherHelloWorkflow", workflowOptions); + workflow.start("World"); + assertEquals( + "Hello World from activity BuildGreeting and workflow AnotherHelloWorkflow", + workflow.getResult(String.class)); + } +} diff --git a/temporal-testing-junit5/src/test/java/io/temporal/testing/TestWorkflowExtensionTest.java b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestWorkflowExtensionTest.java index 62a38edb6f..2a25e6f281 100644 --- a/temporal-testing-junit5/src/test/java/io/temporal/testing/TestWorkflowExtensionTest.java +++ b/temporal-testing-junit5/src/test/java/io/temporal/testing/TestWorkflowExtensionTest.java @@ -35,12 +35,15 @@ import io.temporal.workflow.WorkflowInterface; import io.temporal.workflow.WorkflowMethod; import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; +@Timeout(value = 30, unit = TimeUnit.SECONDS) public class TestWorkflowExtensionTest { @RegisterExtension @@ -48,6 +51,7 @@ public class TestWorkflowExtensionTest { TestWorkflowExtension.newBuilder() .setWorkflowTypes(HelloWorkflowImpl.class) .setActivityImplementations(new HelloActivityImpl()) + .setInitialTime(Instant.parse("2021-10-10T10:01:00Z")) .build(); @ActivityInterface @@ -89,7 +93,7 @@ public String sayHello(String name) { } @Test - @Timeout(value = 30, unit = TimeUnit.SECONDS) + @WorkflowInitialTime("2020-01-01T01:00:00Z") public void extensionShouldLaunchTestEnvironmentAndResolveParameters( TestWorkflowEnvironment testEnv, WorkflowClient workflowClient, @@ -102,6 +106,10 @@ public void extensionShouldLaunchTestEnvironmentAndResolveParameters( () -> assertNotNull(workflowClient), () -> assertNotNull(workflowOptions.getTaskQueue()), () -> assertNotNull(worker), + () -> + assertEquals( + Instant.parse("2020-01-01T01:00:00Z"), + Instant.ofEpochMilli(testEnv.currentTimeMillis()).truncatedTo(ChronoUnit.HOURS)), () -> assertEquals( "Hello World from activity BuildGreeting and workflow HelloWorkflow",