diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5b8881133..1dc11851ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,24 @@ Normal Gradle test filtering works, so a single dev-server-backed test can be ru Java 11 must be available to Gradle for these commands. +To run an SDK test against an externally managed server using the standard Temporal client +environment configuration, set `TEMPORAL_TEST_ENV_CONFIG_SERVER`. For example, the following runs +one Cloud-safe workflow test: + +```bash +TEMPORAL_TEST_ENV_CONFIG_SERVER=true \ +TEMPORAL_ADDRESS=your-namespace.tmprl.cloud:7233 \ +TEMPORAL_NAMESPACE=your-namespace \ +TEMPORAL_API_KEY=your-api-key \ +./gradlew :temporal-sdk:test \ + --tests 'io.temporal.client.functional.SignalTest.signalCompletedWorkflow' +``` + +The harness also supports the standard `TEMPORAL_CONFIG_FILE` and `TEMPORAL_PROFILE` variables. +Values from `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `TEMPORAL_API_KEY`, `TEMPORAL_TLS_*`, and +`TEMPORAL_GRPC_META_*` override the selected profile. Envconfig mode connects to an existing server +and namespace; it does not create or register either one. + ## Things to Avoid Avoid changes that make review harder without improving the contribution: diff --git a/temporal-sdk/build.gradle b/temporal-sdk/build.gradle index d7b090e5b5..7fe5441252 100644 --- a/temporal-sdk/build.gradle +++ b/temporal-sdk/build.gradle @@ -25,6 +25,8 @@ dependencies { } testImplementation project(':temporal-testing') + // The optional envconfig-backed test harness is loaded only while SDK tests are running. + testRuntimeOnly project(':temporal-envconfig') testImplementation "junit:junit:${junitVersion}" testImplementation "org.mockito:mockito-core:${mockitoVersion}" testImplementation 'pl.pragmatists:JUnitParams:1.1.1' @@ -287,4 +289,4 @@ testing { tasks.named('check') { dependsOn(testing.suites.jackson3Tests) dependsOn(testing.suites.virtualThreadTests) -} \ No newline at end of file +} diff --git a/temporal-testing/build.gradle b/temporal-testing/build.gradle index f9ca013456..3715605b8c 100644 --- a/temporal-testing/build.gradle +++ b/temporal-testing/build.gradle @@ -16,6 +16,8 @@ java { dependencies { api project(':temporal-sdk') api project(':temporal-test-server') + // Envconfig is optional for consumers of temporal-testing. + compileOnly project(':temporal-envconfig') implementation 'org.apache.commons:commons-compress:1.28.0' @@ -32,6 +34,8 @@ dependencies { junit5Api 'org.junit.jupiter:junit-jupiter-api' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter' + // Tests construct envconfig profiles directly. + testImplementation project(':temporal-envconfig') testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}" } diff --git a/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java b/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java index c840adf55c..cc45b8b614 100644 --- a/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java +++ b/temporal-testing/src/main/java/io/temporal/internal/docker/RegisterTestNamespace.java @@ -7,6 +7,7 @@ import io.temporal.api.workflowservice.v1.ListNamespacesRequest; import io.temporal.api.workflowservice.v1.ListNamespacesResponse; import io.temporal.api.workflowservice.v1.RegisterNamespaceRequest; +import io.temporal.internal.common.env.EnvironmentVariableUtils; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.serviceclient.WorkflowServiceStubsOptions; @@ -15,10 +16,13 @@ public class RegisterTestNamespace { public static final String NAMESPACE = "UnitTest"; private static final boolean useExternalService = Boolean.parseBoolean(System.getenv("USE_EXTERNAL_SERVICE")); + private static final boolean useEnvConfig = + EnvironmentVariableUtils.readBooleanFlag("TEMPORAL_TEST_ENV_CONFIG_SERVER"); private static final String serviceAddress = System.getenv("TEMPORAL_SERVICE_ADDRESS"); public static void main(String[] args) throws InterruptedException { - if (!useExternalService) { + // Envconfig mode connects to an existing namespace and must not register UnitTest. + if (useEnvConfig || !useExternalService) { return; } diff --git a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java index 6c68d5f2b5..01ca6b2fb4 100644 --- a/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java +++ b/temporal-testing/src/main/java/io/temporal/testing/internal/ExternalServiceTestConfigurator.java @@ -1,12 +1,16 @@ package io.temporal.testing.internal; +import io.temporal.envconfig.ClientConfigProfile; import io.temporal.internal.common.env.EnvironmentVariableUtils; import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowRule; import io.temporal.testing.internal.devserver.SdkJavaTestServerProfile; +import java.io.IOException; import javax.annotation.Nonnull; public class ExternalServiceTestConfigurator { + private static boolean USE_ENV_CONFIG = + EnvironmentVariableUtils.readBooleanFlag("TEMPORAL_TEST_ENV_CONFIG_SERVER"); private static boolean USE_EXTERNAL_SERVICE = EnvironmentVariableUtils.readBooleanFlag("USE_EXTERNAL_SERVICE"); private static String TEMPORAL_SERVICE_ADDRESS = @@ -15,7 +19,7 @@ public class ExternalServiceTestConfigurator { EnvironmentVariableUtils.readBooleanFlag("USE_VIRTUAL_THREADS"); public static boolean isUseExternalService() { - return USE_EXTERNAL_SERVICE || SdkJavaTestServerProfile.isActive(); + return USE_ENV_CONFIG || USE_EXTERNAL_SERVICE || SdkJavaTestServerProfile.isActive(); } public static boolean isUseVirtualThreads() { @@ -23,6 +27,9 @@ public static boolean isUseVirtualThreads() { } public static String getTemporalServiceAddress() { + if (USE_ENV_CONFIG) { + return loadEnvConfigProfile().getAddress(); + } if (SdkJavaTestServerProfile.isActive()) { return SdkJavaTestServerProfile.getTarget(); } @@ -33,6 +40,9 @@ public static String getTemporalServiceAddress() { public static TestWorkflowRule.Builder configure( @Nonnull TestWorkflowRule.Builder testWorkflowRule) { + if (USE_ENV_CONFIG) { + return configureFromEnvConfig(testWorkflowRule, loadEnvConfigProfile()); + } if (isUseExternalService()) { testWorkflowRule.setUseExternalService(true); String target = getTemporalServiceAddress(); @@ -45,6 +55,9 @@ public static TestWorkflowRule.Builder configure( public static TestEnvironmentOptions.Builder configure( @Nonnull TestEnvironmentOptions.Builder testEnvironmentOptions) { + if (USE_ENV_CONFIG) { + return configureFromEnvConfig(testEnvironmentOptions, loadEnvConfigProfile()); + } if (isUseExternalService()) { testEnvironmentOptions.setUseExternalService(true); String target = getTemporalServiceAddress(); @@ -58,4 +71,46 @@ public static TestEnvironmentOptions.Builder configure( public static TestEnvironmentOptions.Builder configuredTestEnvironmentOptions() { return configure(TestEnvironmentOptions.newBuilder()); } + + static TestWorkflowRule.Builder configureFromEnvConfig( + TestWorkflowRule.Builder testWorkflowRule, ClientConfigProfile profile) { + validateEnvConfigProfile(profile); + testWorkflowRule.setUseExternalService(true); + testWorkflowRule.setTarget(profile.getAddress()); + testWorkflowRule.setNamespace(profile.getNamespace()); + testWorkflowRule.setWorkflowServiceStubsOptions(profile.toWorkflowServiceStubsOptions()); + testWorkflowRule.setWorkflowClientOptions(profile.toWorkflowClientOptions()); + return testWorkflowRule; + } + + static TestEnvironmentOptions.Builder configureFromEnvConfig( + TestEnvironmentOptions.Builder testEnvironmentOptions, ClientConfigProfile profile) { + validateEnvConfigProfile(profile); + testEnvironmentOptions.setUseExternalService(true); + testEnvironmentOptions.setTarget(profile.getAddress()); + testEnvironmentOptions.setWorkflowServiceStubsOptions(profile.toWorkflowServiceStubsOptions()); + testEnvironmentOptions.setWorkflowClientOptions(profile.toWorkflowClientOptions()); + return testEnvironmentOptions; + } + + private static ClientConfigProfile loadEnvConfigProfile() { + ClientConfigProfile profile; + try { + profile = ClientConfigProfile.load(); + } catch (IOException e) { + throw new IllegalStateException( + "Unable to load client configuration for the Temporal test harness.", e); + } + validateEnvConfigProfile(profile); + return profile; + } + + private static void validateEnvConfigProfile(ClientConfigProfile profile) { + if (profile.getAddress() == null || profile.getAddress().isEmpty()) { + throw new IllegalStateException("Envconfig test harness requires a Temporal server address."); + } + if (profile.getNamespace() == null || profile.getNamespace().isEmpty()) { + throw new IllegalStateException("Envconfig test harness requires a Temporal namespace."); + } + } } diff --git a/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java new file mode 100644 index 0000000000..6dd808a18d --- /dev/null +++ b/temporal-testing/src/test/java/io/temporal/testing/internal/ExternalServiceTestConfiguratorTest.java @@ -0,0 +1,97 @@ +package io.temporal.testing.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.grpc.Metadata; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.testing.TestEnvironmentOptions; +import io.temporal.testing.TestWorkflowRule; +import org.junit.jupiter.api.Test; + +public class ExternalServiceTestConfiguratorTest { + + @Test + public void configureTestWorkflowRuleFromEnvConfig() { + TestWorkflowRule rule = + ExternalServiceTestConfigurator.configureFromEnvConfig( + TestWorkflowRule.newBuilder(), newProfile()) + .build(); + try { + assertEquals( + "envconfig-address:7233", rule.getWorkflowServiceStubs().getOptions().getTarget()); + assertEquals("envconfig-namespace", rule.getWorkflowClient().getOptions().getNamespace()); + } finally { + rule.getTestEnvironment().close(); + } + } + + @Test + public void configureTestEnvironmentFromEnvConfig() { + TestEnvironmentOptions options = + ExternalServiceTestConfigurator.configureFromEnvConfig( + TestEnvironmentOptions.newBuilder(), newProfile()) + .build(); + + assertTrue(options.isUseExternalService()); + assertEquals("envconfig-address:7233", options.getTarget()); + assertEquals("envconfig-address:7233", options.getWorkflowServiceStubsOptions().getTarget()); + assertEquals("envconfig-namespace", options.getWorkflowClientOptions().getNamespace()); + assertTrue(options.getWorkflowServiceStubsOptions().getEnableHttps()); + + Metadata metadata = metadata(options.getWorkflowServiceStubsOptions()); + assertEquals( + "metadata-value", + metadata.get(Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER))); + assertEquals( + "Bearer api-key", + metadata.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER))); + } + + @Test + public void requireAddressAndNamespaceInEnvConfigMode() { + ClientConfigProfile missingAddress = + ClientConfigProfile.newBuilder().setNamespace("envconfig-namespace").build(); + IllegalStateException missingAddressException = + assertThrows( + IllegalStateException.class, + () -> + ExternalServiceTestConfigurator.configureFromEnvConfig( + TestEnvironmentOptions.newBuilder(), missingAddress)); + assertEquals( + "Envconfig test harness requires a Temporal server address.", + missingAddressException.getMessage()); + + ClientConfigProfile missingNamespace = + ClientConfigProfile.newBuilder().setAddress("envconfig-address:7233").build(); + IllegalStateException missingNamespaceException = + assertThrows( + IllegalStateException.class, + () -> + ExternalServiceTestConfigurator.configureFromEnvConfig( + TestEnvironmentOptions.newBuilder(), missingNamespace)); + assertEquals( + "Envconfig test harness requires a Temporal namespace.", + missingNamespaceException.getMessage()); + } + + private static ClientConfigProfile newProfile() { + Metadata metadata = new Metadata(); + metadata.put( + Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER), "metadata-value"); + return ClientConfigProfile.newBuilder() + .setAddress("envconfig-address:7233") + .setNamespace("envconfig-namespace") + .setApiKey("api-key") + .setMetadata(metadata) + .build(); + } + + private static Metadata metadata(WorkflowServiceStubsOptions options) { + Metadata metadata = new Metadata(); + options.getGrpcMetadataProviders().forEach(provider -> metadata.merge(provider.getMetadata())); + return metadata; + } +}