diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 767ebe23..b5d0c07c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,7 +173,7 @@ jobs: # Secrets are unavailable to workflows from forks. if: ${{ (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-ruby') && github.actor != 'dependabot[bot]' }} runs-on: ubuntu-latest - timeout-minutes: 45 + timeout-minutes: 20 env: TEMPORAL_TEST_ENV_CONFIG_SERVER: "1" TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1 @@ -241,7 +241,9 @@ jobs: env: TEMPORAL_ADDRESS: ${{ steps.create-cloud-namespace.outputs.namespace }}.tmprl.cloud:7233 TEMPORAL_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }} - run: bundle exec rake test TEST=test/worker_workflow_test.rb TESTOPTS="--name=/WorkerWorkflowTest#test_simple/" + run: | + mkdir -p tmp + TESTOPTS="--verbose" bundle exec rake test:cloud 2>&1 | tee tmp/cloud-test.log - name: Delete Cloud namespace id: delete-cloud-namespace @@ -255,3 +257,12 @@ jobs: - name: Report Cloud namespace cleanup failure if: ${{ always() && steps.delete-cloud-namespace.outcome == 'failure' }} run: echo "::warning title=Cloud namespace cleanup failed::Failed to delete Cloud namespace ${{ steps.create-cloud-namespace.outputs.namespace }}" + + - name: Upload Cloud test output + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: cloud-test-${{ github.run_id }}-${{ github.run_attempt }} + path: temporalio/tmp/cloud-test.log + if-no-files-found: warn + retention-days: 14 diff --git a/README.md b/README.md index 03c8e8c5..ba765e59 100644 --- a/README.md +++ b/README.md @@ -1491,6 +1491,14 @@ E.g. show all test names while executing: bundle exec rake test TESTOPTS="--verbose" +To list source tests excluded from Cloud, grouped by reason and note: + + bundle exec rake test:cloud_inventory + +To run the Cloud-eligible suite against an envconfig-defined server: + + bundle exec rake test:cloud + ### Code Formatting and Type Checking This project uses `rubocop`: diff --git a/temporalio/Rakefile b/temporalio/Rakefile index af4cb008..ee1d604c 100644 --- a/temporalio/Rakefile +++ b/temporalio/Rakefile @@ -27,6 +27,25 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList['test/**/*_test.rb', 'extra/release/test/**/*_test.rb'] end +namespace :test do + desc 'List tests excluded from Temporal Cloud by reason' + task :cloud_inventory do + ENV['TESTOPTS'] = [ + ENV.fetch('TESTOPTS', nil), + '--cloud-inventory', + '--name=/CloudTestInventoryTest#test_report/' + ].compact.join(' ') + Rake::Task[:test].invoke + end + + desc 'Run tests eligible for Temporal Cloud using envconfig' + task :cloud do + ENV['TEMPORAL_TEST_ENV_CONFIG_SERVER'] = '1' + ENV['TESTOPTS'] = [ENV.fetch('TESTOPTS', nil), '--cloud'].compact.join(' ') + Rake::Task[:test].invoke + end +end + require 'rubocop/rake_task' RuboCop::RakeTask.new diff --git a/temporalio/test/client_activity_test.rb b/temporalio/test/client_activity_test.rb index af9bb673..d9b99a77 100644 --- a/temporalio/test/client_activity_test.rb +++ b/temporalio/test/client_activity_test.rb @@ -742,6 +742,8 @@ def test_start_activity_rejects_negative_start_delay assert_match(/start_delay must be non-negative/i, err.message) end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Standalone Activity start delay to be enabled in the Cloud namespace.' def test_start_activity_with_start_delay_dispatches_after_delay delay = 2.0 with_activity_worker([SimpleActivity]) do |task_queue| @@ -763,6 +765,8 @@ def test_start_activity_with_start_delay_dispatches_after_delay end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Standalone Activity start delay to be enabled in the Cloud namespace.' def test_start_activity_with_zero_start_delay_dispatches_immediately # start_delay: 0 means no delay (proto-3 default for an unset Duration); activity # behaves exactly like one started without a start_delay kwarg. @@ -783,6 +787,8 @@ def test_start_activity_with_zero_start_delay_dispatches_immediately end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Standalone Activity start delay to be enabled in the Cloud namespace.' def test_start_activity_with_nil_start_delay_dispatches_immediately # Explicit nil should be indistinguishable from omitting the kwarg. with_activity_worker([SimpleActivity]) do |task_queue| @@ -802,6 +808,8 @@ def test_start_activity_with_nil_start_delay_dispatches_immediately end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Standalone Activity start delay to be enabled in the Cloud namespace.' def test_cancel_during_start_delay_transitions_to_canceled_immediately with_activity_worker([SimpleActivity]) do |task_queue| handle = env.client.start_activity( @@ -818,6 +826,8 @@ def test_cancel_during_start_delay_transitions_to_canceled_immediately end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Standalone Activity start delay to be enabled in the Cloud namespace.' def test_terminate_during_start_delay_transitions_to_terminated_immediately with_activity_worker([SimpleActivity]) do |task_queue| handle = env.client.start_activity( @@ -834,6 +844,8 @@ def test_terminate_during_start_delay_transitions_to_terminated_immediately end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Standalone Activity start delay to be enabled in the Cloud namespace.' def test_start_delay_extends_schedule_to_start_timeout # schedule_to_start_timeout (0.5s) is shorter than start_delay (1.0s), and would fire if it # were not properly delayed by start_delay. @@ -850,6 +862,8 @@ def test_start_delay_extends_schedule_to_start_timeout end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Standalone Activity start delay to be enabled in the Cloud namespace.' def test_start_delay_extends_schedule_to_close_timeout # schedule_to_start_timeout (0.5s) is shorter than start_delay (1.0s), and would fire if it # were not properly delayed by start_delay. diff --git a/temporalio/test/client_cloud_test.rb b/temporalio/test/client_cloud_test.rb index 067cd6f4..d9141447 100644 --- a/temporalio/test/client_cloud_test.rb +++ b/temporalio/test/client_cloud_test.rb @@ -6,6 +6,9 @@ require 'test' class ClientCloudTest < Test + exclude_class_from_cloud :requires_cloud_provisioning, + 'Requires separately provisioned Cloud credentials and operations permissions.' + class SimpleWorkflow < Temporalio::Workflow::Definition def execute(name) "Hello, #{name}!" diff --git a/temporalio/test/client_schedule_test.rb b/temporalio/test/client_schedule_test.rb index f6171e1d..b033f784 100644 --- a/temporalio/test/client_schedule_test.rb +++ b/temporalio/test/client_schedule_test.rb @@ -9,6 +9,8 @@ class ClientScheduleTest < Test also_run_all_tests_in_fiber + exclude_from_cloud :needs_cloud_adaptation, + 'Requires custom search attributes and immediately consistent schedule visibility.' def test_basics # rubocop:disable Metrics/AbcSize expected_ids = [] assert_no_schedules @@ -265,6 +267,8 @@ def test_calendar_spec_defaults delete_schedules(handle.id) if handle end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_trigger_immediately assert_no_schedules @@ -296,6 +300,8 @@ def test_trigger_immediately delete_schedules(handle.id) if handle end + exclude_from_cloud :needs_cloud_adaptation, + 'Relies on backfill action counts becoming immediately consistent.' def test_backfill assert_no_schedules diff --git a/temporalio/test/client_test.rb b/temporalio/test/client_test.rb index 41b07f08..76926c65 100644 --- a/temporalio/test/client_test.rb +++ b/temporalio/test/client_test.rb @@ -11,9 +11,11 @@ def test_version_number assert !Temporalio::VERSION.nil? end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_lazy_connection assert env.client.connection.connected? - client = Temporalio::Client.connect(env.client.connection.target_host, env.client.namespace, lazy_connect: true) + client = env.reconnect_client(lazy_connect: true) refute client.connection.connected? env.with_kitchen_sink_worker(client) do |task_queue| result = client.execute_workflow( @@ -140,6 +142,8 @@ def terminate_workflow(input) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_interceptor # Create client with interceptor track = TrackCallsInterceptor.new @@ -285,9 +289,7 @@ def test_fork reader, writer = IO.pipe pid = fork do reader.close - client = Temporalio::Client.connect( - env.client.options.connection.target_host, - env.client.options.namespace, + client = env.reconnect_client( runtime: Temporalio::Runtime.new, logger: Logger.new($stdout) ) diff --git a/temporalio/test/client_workflow_test.rb b/temporalio/test/client_workflow_test.rb index c63a6fc0..e481ae82 100644 --- a/temporalio/test/client_workflow_test.rb +++ b/temporalio/test/client_workflow_test.rb @@ -8,6 +8,8 @@ class ClientWorkflowTest < Test also_run_all_tests_in_fiber + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_start_simple # Create ephemeral test server env.with_kitchen_sink_worker do |task_queue| @@ -26,6 +28,8 @@ def test_start_simple end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_result_preserves_fiber_local_context_for_history_rpc context_key = :temporalio_test_result_context context = :my_context @@ -55,6 +59,8 @@ def test_result_preserves_fiber_local_context_for_history_rpc Thread.current[context_key] = previous_context end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_workflow_exists env.with_kitchen_sink_worker do |task_queue| # Create a workflow that hangs @@ -105,11 +111,7 @@ def test_workflow_exists end def test_lazy_connect - client = Temporalio::Client.connect( - env.client.connection.target_host, - env.client.namespace, - lazy_connect: true - ) + client = env.reconnect_client(lazy_connect: true) # Not connected until we do something refute client.connection.connected? client.start_workflow( @@ -120,6 +122,8 @@ def test_lazy_connect assert client.connection.connected? end + exclude_from_cloud :needs_cloud_adaptation, + 'Requires custom search attributes and a Go worker with Cloud TLS configuration.' def test_describe # Make sure all keys on server env.ensure_common_search_attribute_keys @@ -174,6 +178,8 @@ def test_describe end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_start_delay env.with_kitchen_sink_worker do |task_queue| handle = env.client.start_workflow( @@ -194,6 +200,8 @@ def test_start_delay end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_failure env.with_kitchen_sink_worker do |task_queue| # Simple error @@ -226,6 +234,8 @@ def test_failure end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_retry_policy env.with_kitchen_sink_worker do |task_queue| err = assert_raises(Temporalio::Error::WorkflowFailedError) do @@ -245,6 +255,8 @@ def test_retry_policy end end + exclude_from_cloud :needs_cloud_adaptation, + 'Requires custom search attributes, consistent visibility, and Go worker Cloud TLS.' def test_list_and_count # Make sure all keys on server env.ensure_common_search_attribute_keys @@ -325,6 +337,8 @@ def test_list_and_count end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_continue_as_new env.with_kitchen_sink_worker do |task_queue| handle = env.client.start_workflow( @@ -354,6 +368,8 @@ def test_not_found assert_equal Temporalio::Error::RPCError::Code::NOT_FOUND, err.code end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_config_change env.with_kitchen_sink_worker do |task_queue| # Regular query works @@ -378,6 +394,8 @@ def test_config_change end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_signal env.with_kitchen_sink_worker do |task_queue| handle = env.client.start_workflow( @@ -391,6 +409,8 @@ def test_signal end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_query env.with_kitchen_sink_worker do |task_queue| handle = env.client.start_workflow( @@ -417,6 +437,8 @@ def test_query end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_update env.with_kitchen_sink_worker do |task_queue| handle = env.client.start_workflow( @@ -466,6 +488,8 @@ def test_update end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_cancel env.with_kitchen_sink_worker do |task_queue| handle = env.client.start_workflow( @@ -482,6 +506,8 @@ def test_cancel end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_terminate env.with_kitchen_sink_worker do |task_queue| handle = env.client.start_workflow( @@ -500,6 +526,8 @@ def test_terminate end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_rpc_cancellation # Start a workflow, create cancellation, cancel after 500ms in background, # wait on complete diff --git a/temporalio/test/cloud_test_exclusion_test.rb b/temporalio/test/cloud_test_exclusion_test.rb new file mode 100644 index 00000000..cc059bd8 --- /dev/null +++ b/temporalio/test/cloud_test_exclusion_test.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'test' + +class CloudTestExclusionTest < Test + EXCLUSION_NOTE = 'Requires test-specific Cloud setup.' + + def setup + super + @test_classes = [] + end + + def teardown + @test_classes.each { |test_class| Minitest::Runnable.runnables.delete(test_class) } + super + end + + def test_method_exclusion_filters_cloud_runs_and_fiber_variant + test_class = Class.new(Test) #: singleton(Test) + @test_classes << test_class + test_class.also_run_all_tests_in_fiber + + # The annotation is consumed when the next test method is defined. + test_class.exclude_from_cloud(:needs_cloud_adaptation, EXCLUSION_NOTE) + test_class.define_method(:test_excluded) { nil } + test_class.define_method(:test_eligible) { nil } + + expected = Test::CloudTestExclusion.new(:needs_cloud_adaptation, EXCLUSION_NOTE) + assert_equal expected, test_class.cloud_test_exclusion(:test_excluded) + eligible_methods = ['test_eligible'] + if Temporalio::Internal::Bridge.fibers_supported + # The harness-generated fiber copy must carry the source test's exclusion. + assert_equal expected, test_class.cloud_test_exclusion(:test_excluded_in_fiber) + eligible_methods << 'test_eligible_in_fiber' + end + TemporalioTestMode.stub(:cloud?, true) do # steep:ignore NoMethod + assert_equal eligible_methods.sort, test_class.runnable_methods.sort + end + end + + def test_class_exclusion_filters_inherited_tests + parent = Class.new(Test) #: singleton(Test) + parent.exclude_class_from_cloud(:requires_local_server, EXCLUSION_NOTE) + parent.define_method(:test_parent) { nil } + child = Class.new(parent) #: singleton(Test) + child.define_method(:test_child) { nil } + @test_classes.push(parent, child) + + expected = Test::CloudTestExclusion.new(:requires_local_server, EXCLUSION_NOTE) + assert_equal expected, child.cloud_test_exclusion(:test_child) + TemporalioTestMode.stub(:cloud?, true) { assert_empty child.runnable_methods } # steep:ignore NoMethod + end + + def test_invalid_exclusions_fail_fast + invalid = Class.new(Test) #: singleton(Test) + @test_classes << invalid + assert_raises(ArgumentError) do + invalid.exclude_from_cloud(:unknown, EXCLUSION_NOTE) # steep:ignore ArgumentTypeMismatch + end + assert_raises(ArgumentError) { invalid.exclude_from_cloud(:needs_cloud_adaptation, ' ') } + invalid.exclude_from_cloud(:needs_cloud_adaptation, EXCLUSION_NOTE) + assert_raises(RuntimeError) { invalid.exclude_from_cloud(:requires_local_server, EXCLUSION_NOTE) } + assert_raises(RuntimeError) { invalid.define_method(:helper) { nil } } + + dangling = Class.new(Test) #: singleton(Test) + @test_classes << dangling + dangling.exclude_from_cloud(:needs_cloud_adaptation, EXCLUSION_NOTE) + assert_raises(RuntimeError) { dangling.runnable_methods } + end +end diff --git a/temporalio/test/cloud_test_inventory_test.rb b/temporalio/test/cloud_test_inventory_test.rb new file mode 100644 index 00000000..0bd7f5a3 --- /dev/null +++ b/temporalio/test/cloud_test_inventory_test.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'test' + +class CloudTestInventoryTest < Test + def test_report + return unless TemporalioTestMode.cloud_inventory? + + exclusions = Test::CLOUD_TEST_EXCLUSION_REASONS.to_h { |reason, _| [reason, []] } + test_classes = Minitest::Runnable.runnables.select do |runnable| + runnable.is_a?(Class) && runnable < Test + end + test_classes.uniq.sort_by { |test_class| test_class.name.to_s }.each do |test_class| + test_class.runnable_methods.sort.each do |method_name| + # Fiber variants are generated copies with the same exclusion, so report the source annotation once. + next if method_name.end_with?('_in_fiber') + + exclusion = test_class.cloud_test_exclusion(method_name) + next unless exclusion + + exclusions.fetch(exclusion.reason) << ["#{test_class}##{method_name}", exclusion.note] + end + end + + puts 'Cloud test exclusions' + Test::CLOUD_TEST_EXCLUSION_REASONS.each do |reason, description| + tests = exclusions.fetch(reason).sort_by(&:first) + puts + puts "#{reason} (#{tests.length}) — #{description}" + tests.each { |name, note| puts " #{name} — #{note}" } + end + end +end diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index 7fb3c258..133817bd 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -63,6 +63,18 @@ class EnvConfigTest < Test "mixed_Case-header" = "mixed-value" TOML + def setup + super + @original_temporal_env = ENV.to_h.select { |key, _| key.start_with?('TEMPORAL_') } + @original_temporal_env.each_key { |key| ENV.delete(key) } + end + + def teardown + ENV.keys.select { |key| key.start_with?('TEMPORAL_') }.each { |key| ENV.delete(key) } + @original_temporal_env.each { |key, value| ENV[key] = value } + super + end + # ============================================================================= # PROFILE LOADING TESTS (7 tests) # ============================================================================= diff --git a/temporalio/test/plugin_test.rb b/temporalio/test/plugin_test.rb index aa394a54..49867367 100644 --- a/temporalio/test/plugin_test.rb +++ b/temporalio/test/plugin_test.rb @@ -55,9 +55,8 @@ def execute(name) def test_client_plugin # Connect with a plugin that fails on connect err = assert_raises do - Temporalio::Client.connect( - 'bad', - env.client.namespace, + env.reconnect_client( + target_host: 'bad', plugins: [ClientPluginForTest.new(target_host: env.client.connection.target_host, fail_connect: true)] ) end @@ -65,7 +64,7 @@ def test_client_plugin # Connect with a plugin that sets address, run a workflow, confirm plugin properly configured interceptor plugin = ClientPluginForTest.new(target_host: env.client.connection.target_host) - client = Temporalio::Client.connect('bad-address', env.client.namespace, plugins: [plugin]) + client = env.reconnect_client(target_host: 'bad-address', plugins: [plugin]) refute plugin.start_workflow_called client.start_workflow(SimpleWorkflow, 'some-name', id: "wf-#{SecureRandom.uuid}", task_queue: "tq-#{SecureRandom.uuid}") @@ -228,7 +227,7 @@ def test_simple_plugin ) # Create a client and worker with the plugin, run workflow, confirm success - client = Temporalio::Client.connect(env.client.connection.target_host, env.client.namespace, plugins: [plugin]) + client = env.reconnect_client(plugins: [plugin]) worker = Temporalio::Worker.new(client:, task_queue: "tq-#{SecureRandom.uuid}") handle = worker.run do client.start_workflow(SimpleWorkflow, 'some-name', diff --git a/temporalio/test/sig/test.rbs b/temporalio/test/sig/test.rbs index 4dd80243..3a6c6339 100644 --- a/temporalio/test/sig/test.rbs +++ b/temporalio/test/sig/test.rbs @@ -1,7 +1,22 @@ +module TemporalioTestMode + def self.cloud?: -> bool + def self.cloud_inventory?: -> bool +end + class Test < Minitest::Test include ExtraAssertions include WorkflowUtils + type cloud_test_exclusion_reason = :requires_cloud_provisioning | :needs_cloud_adaptation | :requires_local_server + + CLOUD_TEST_EXCLUSION_REASONS: Hash[cloud_test_exclusion_reason, String] + + class CloudTestExclusion + attr_reader reason: cloud_test_exclusion_reason + attr_reader note: String + def initialize: (cloud_test_exclusion_reason reason, String note) -> void + end + ATTR_KEY_TEXT: Temporalio::SearchAttributes::Key ATTR_KEY_KEYWORD: Temporalio::SearchAttributes::Key ATTR_KEY_INTEGER: Temporalio::SearchAttributes::Key @@ -11,6 +26,15 @@ class Test < Minitest::Test ATTR_KEY_KEYWORD_LIST: Temporalio::SearchAttributes::Key def self.also_run_all_tests_in_fiber: -> void + def self.exclude_from_cloud: (cloud_test_exclusion_reason reason, String note) -> void + def self.exclude_class_from_cloud: (cloud_test_exclusion_reason reason, String note) -> void + def self.cloud_test_exclusion: (String | Symbol method_name) -> CloudTestExclusion? + + private + + def self.build_cloud_test_exclusion: (cloud_test_exclusion_reason reason, String note) -> CloudTestExclusion + + public def skip_if_fibers_not_supported!: -> void def skip_if_not_x86!: -> void @@ -37,6 +61,12 @@ class Test < Minitest::Test def client: -> Temporalio::Client + def reconnect_client: ( + ?target_host: String, + ?namespace: String, + **untyped overrides + ) -> Temporalio::Client + def with_kitchen_sink_worker: [T] ( ?Temporalio::Client worker_client, ?task_queue: String, diff --git a/temporalio/test/test.rb b/temporalio/test/test.rb index 77c283f8..cf7f34ee 100644 --- a/temporalio/test/test.rb +++ b/temporalio/test/test.rb @@ -37,10 +37,34 @@ # report.pretty_print # end +# Rake passes these internal modes through TESTOPTS; consume them before Minitest parses ARGV. +module TemporalioTestMode + @cloud = !ARGV.delete('--cloud').nil? + @cloud_inventory = !ARGV.delete('--cloud-inventory').nil? + + def self.cloud? + @cloud + end + + def self.cloud_inventory? + @cloud_inventory + end +end + class Test < Minitest::Test include ExtraAssertions include WorkflowUtils + CloudTestExclusion = Data.define(:reason, :note) + + CLOUD_TEST_EXCLUSION_REASONS = { + requires_cloud_provisioning: 'Requires a Cloud capability that the test harness cannot provision or a feature ' \ + 'that is not yet available in Temporal Cloud', + needs_cloud_adaptation: 'Requires setup or assertions adapted for the Temporal Cloud environment', + requires_local_server: 'Inherently requires one or more local Temporal Server instances, such as a dev or ' \ + 'time-skipping server or custom server configuration' + }.freeze + ATTR_KEY_TEXT = Temporalio::SearchAttributes::Key.new('ruby-key-text', Temporalio::SearchAttributes::IndexedValueType::TEXT) ATTR_KEY_KEYWORD = Temporalio::SearchAttributes::Key.new('ruby-key-keyword', @@ -58,6 +82,38 @@ class Test < Minitest::Test Temporalio::SearchAttributes::IndexedValueType::KEYWORD_LIST ) + def self.exclude_from_cloud(reason, note) + raise 'A Cloud test exclusion is already waiting for the next test method' if @next_cloud_test_exclusion + + @next_cloud_test_exclusion = build_cloud_test_exclusion(reason, note) + end + + def self.exclude_class_from_cloud(reason, note) + @cloud_test_exclusion = build_cloud_test_exclusion(reason, note) + end + + def self.cloud_test_exclusion(method_name) + method_name = method_name.to_s + ancestors.each do |ancestor| + break unless ancestor <= Test + + exclusion = ancestor.instance_variable_get(:@cloud_test_exclusion) + return exclusion if exclusion + + exclusions = ancestor.instance_variable_get(:@cloud_test_exclusions) + return exclusions[method_name] if exclusions&.key?(method_name) # steep:ignore + end + nil + end + + def self.runnable_methods + methods = super + raise 'Cloud test exclusion is not followed by a test method' if @next_cloud_test_exclusion + return methods unless TemporalioTestMode.cloud? + + methods.reject { |method_name| cloud_test_exclusion(method_name) } + end + def self.also_run_all_tests_in_fiber @also_run_all_tests_in_fiber = true # We have to tell Minitest the diff executable to use because "async" has an @@ -69,6 +125,13 @@ def self.also_run_all_tests_in_fiber def self.method_added(method_name) super + if (exclusion = @next_cloud_test_exclusion) + @next_cloud_test_exclusion = nil + raise 'Cloud test exclusions must decorate test methods' unless method_name.start_with?('test_') + + (@cloud_test_exclusions ||= {})[method_name.to_s] = exclusion + end + # If we are also running all tests in fiber, define `_in_fiber` equivalent, # unless we are < 3.3 unless @also_run_all_tests_in_fiber && @@ -79,13 +142,25 @@ def self.method_added(method_name) end original_method = instance_method(method_name) - define_method("#{method_name}_in_fiber") do + fiber_method_name = "#{method_name}_in_fiber" + (@cloud_test_exclusions ||= {})[fiber_method_name] = exclusion if exclusion + define_method(fiber_method_name) do Kernel.Async do |_task| original_method.bind(self).call end end end + def self.build_cloud_test_exclusion(reason, note) + unless CLOUD_TEST_EXCLUSION_REASONS.key?(reason) + raise ArgumentError, "Unknown Cloud test exclusion reason: #{reason.inspect}" + end + raise ArgumentError, 'Cloud test exclusion note cannot be blank' unless note.is_a?(String) && !note.strip.empty? + + CloudTestExclusion.new(reason, note.dup.freeze) + end + private_class_method :build_cloud_test_exclusion + def skip_if_fibers_not_supported! return if Temporalio::Internal::Bridge.fibers_supported @@ -205,6 +280,20 @@ def client @server.client end + def reconnect_client(target_host: client.connection.target_host, namespace: client.namespace, **overrides) + # Reconnect through Client.connect so overrides and connection plugins apply. Host and namespace are positional, + # the connection is rebuilt, and Client.connect cannot accept payload limits. + connection_options = client.connection.options.to_h.except(:target_host, :payload_limits) + client_options = client.options.to_h.except(:connection, :namespace) + Temporalio::Client.connect( + target_host, + namespace, + **connection_options, + **client_options, + **overrides + ) + end + def with_kitchen_sink_worker(worker_client = client, task_queue: "tq-#{SecureRandom.uuid}", nexus: false) # Create Nexus endpoint for the task queue if requested endpoint = nil diff --git a/temporalio/test/test_environment_test.rb b/temporalio/test/test_environment_test.rb index 0000c0b5..11617ef1 100644 --- a/temporalio/test/test_environment_test.rb +++ b/temporalio/test/test_environment_test.rb @@ -70,6 +70,32 @@ def test_legacy_and_local_client_connect_options end end + def test_reconnect_client_preserves_base_configuration + tls = Temporalio::Client::Connection::TLSOptions.new(domain: 'cloud.example') + base_client = Temporalio::Client.connect( + 'cloud.example:7233', + 'base-namespace', + api_key: 'api-key', + tls:, + rpc_metadata: { 'x-test-source' => 'base' }, + logger: Logger.new(File::NULL), + lazy_connect: true + ) + test_env = TestEnvironment.send(:allocate) + test_env.instance_variable_set(:@server, Temporalio::Testing::WorkflowEnvironment.new(base_client)) + + new_client = test_env.reconnect_client(namespace: 'override-namespace') + + refute_same base_client.connection, new_client.connection + assert_equal 'override-namespace', new_client.namespace + assert_equal 'api-key', new_client.connection.options.api_key + assert_equal tls, new_client.connection.options.tls + assert_equal({ 'x-test-source' => 'base' }, new_client.connection.options.rpc_metadata) + assert_same base_client.connection.options.runtime, new_client.connection.options.runtime + assert_same base_client.options.data_converter, new_client.options.data_converter + assert_equal 'base-namespace', base_client.namespace + end + private def with_test_env(values) diff --git a/temporalio/test/testing/workflow_environment_test.rb b/temporalio/test/testing/workflow_environment_test.rb index 49bf1bf6..cbe4487a 100644 --- a/temporalio/test/testing/workflow_environment_test.rb +++ b/temporalio/test/testing/workflow_environment_test.rb @@ -11,6 +11,8 @@ module Testing class WorkflowEnvironmentTest < Test + exclude_class_from_cloud :requires_local_server, 'Covers local and time-skipping test-server APIs.' + include WorkflowUtils class SlowWorkflow < Temporalio::Workflow::Definition diff --git a/temporalio/test/worker_activity_test.rb b/temporalio/test/worker_activity_test.rb index 5ff743ea..b4ad7800 100644 --- a/temporalio/test/worker_activity_test.rb +++ b/temporalio/test/worker_activity_test.rb @@ -18,6 +18,8 @@ def execute(name) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_class assert_equal 'Hello, Class!', execute_activity(ClassActivity, 'Class') end @@ -32,10 +34,14 @@ def execute(name) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_instance assert_equal 'Howdy, Instance!', execute_activity(InstanceActivity.new('Howdy'), 'Instance') end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_block activity = Temporalio::Activity::Definition::Info.new(name: 'BlockActivity') { |name| "Greetings, #{name}!" } assert_equal 'Greetings, Block!', execute_activity(activity, 'Block') @@ -50,6 +56,8 @@ def execute(name) end # Unconditionally assert fiber executor works for all supported Ruby versions support fibers + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_simple_fiber_activity Async do assert_equal 'Hello, Fiber!', execute_activity(SimpleFiberActivity, 'Fiber') @@ -73,6 +81,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_fiber # Tests are doubly executed in threaded and fiber, so we start a new Async block just in case Async do |_task| @@ -103,6 +113,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_logging out, = safe_capture_io do # New logger each time since stdout is replaced @@ -121,6 +133,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_custom_name execute_activity(CustomNameActivity) do |handle| assert_equal 'done', handle.result @@ -212,6 +226,8 @@ def execute(form) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_failure # Check basic error error = assert_raises(Temporalio::Error::WorkflowFailedError) { execute_activity(FailureActivity, 'simple') } @@ -246,11 +262,15 @@ def test_failure class UnimplementedExecuteActivity < Temporalio::Activity::Definition end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_unimplemented_execute error = assert_raises(Temporalio::Error::WorkflowFailedError) { execute_activity(UnimplementedExecuteActivity) } assert_equal 'Activity did not implement "execute"', error.cause.cause.message end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_not_found error = assert_raises(Temporalio::Error::WorkflowFailedError) do execute_activity(UnimplementedExecuteActivity, override_name: 'not-found') @@ -266,6 +286,8 @@ def execute(arg1, arg2, arg3) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_multi_param assert_equal "Args: #{{ 'foo' => 'bar' }}, 123, baz", # rubocop:disable Lint/LiteralInInterpolation execute_activity(MultiParamActivity, { foo: 'bar' }, 123, 'baz') @@ -280,6 +302,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_info # The hash is round-tripped through the data converter so non-primitive # values (Time, RetryPolicy) are serialized to strings. Use string keys @@ -330,6 +354,8 @@ def wait_started end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_cancellation_simple act = CancellationActivity.new execute_activity( @@ -350,6 +376,8 @@ def test_cancellation_simple end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_cancellation_swallowed act = CancellationActivity.new(swallow: true) execute_activity( @@ -382,6 +410,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_heartbeat_details assert_equal 'details: ["detail1", "detail2"]', execute_activity(HeartbeatDetailsActivity, retry_max_attempts: 2, heartbeat_timeout: 0.8) @@ -415,6 +445,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_background_heartbeat assert_equal 'details: ["some detail"]', execute_activity(BackgroundHeartbeatActivity, retry_max_attempts: 2, heartbeat_timeout: 0.8) @@ -454,6 +486,8 @@ def wait_until_waiting end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_activity_shielding act = ShieldingActivity.new execute_activity( @@ -479,6 +513,8 @@ class FiberShieldingActivity < ShieldingActivity activity_executor :fiber # steep:ignore end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_activity_shielding_fiber skip_if_fibers_not_supported! @@ -532,6 +568,8 @@ def wait_started end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_no_raise_cancellation act = NoRaiseCancellationActivity.new execute_activity( @@ -576,6 +614,8 @@ def wait_started end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_fiber_cancellation skip_if_fibers_not_supported! # Tests are doubly executed in threaded and fiber, so we start a new Async block just in case @@ -637,6 +677,8 @@ def reraise_cancel end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_worker_shutdown act = WorkerShutdownActivity.new # Start the activity, then cancel worker but let block complete @@ -691,6 +733,8 @@ def wait_id_ref end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_async_completion_success act = AsyncCompletionActivity.new execute_activity(act) do |handle| @@ -703,6 +747,8 @@ def test_async_completion_success end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_async_completion_heartbeat_and_fail act = AsyncCompletionActivity.new execute_activity(act) do |handle| @@ -725,6 +771,8 @@ def test_async_completion_heartbeat_and_fail end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_async_completion_cancel act = AsyncCompletionActivity.new execute_activity(act, wait_for_cancellation: true) do |handle| @@ -746,6 +794,8 @@ def test_async_completion_cancel end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Activity pause and reset APIs that are not enabled in Cloud CI.' def test_async_completion_cancel_details # Cancel act = AsyncCompletionActivity.new @@ -810,6 +860,8 @@ def test_async_completion_cancel_details end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_async_completion_timeout act = AsyncCompletionActivity.new execute_activity(act, start_to_close_timeout: 0.5, wait_for_cancellation: true) do @@ -851,6 +903,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_custom_executor assert_equal 'local val: foo', execute_activity(CustomExecutorActivity, activity_executors: { my_executor: CustomExecutor.new }) @@ -919,10 +973,14 @@ def assert_multi_worker_activities(activities) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_concurrent_multi_worker_threaded_activities assert_multi_worker_activities(50.times.map { ConcurrentActivity.new }) end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_concurrent_multi_worker_fiber_activities skip 'Must be fiber-based worker to do fiber-based activities' if Fiber.current_scheduler.nil? assert_multi_worker_activities(50.times.map { ConcurrentFiberActivity.new }) @@ -962,10 +1020,14 @@ def assert_single_worker_activities(activity, count) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_concurrent_single_worker_threaded_activities assert_single_worker_activities(ConcurrentActivity.new, 50) end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_concurrent_single_worker_fiber_activities skip 'Must be fiber-based worker to do fiber-based activities' if Fiber.current_scheduler.nil? assert_single_worker_activities(ConcurrentFiberActivity.new, 50) @@ -1024,6 +1086,8 @@ def execute(name) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_interceptor interceptor = TrackCallsInterceptor.new assert_equal 'Hello, Temporal!', execute_activity(InterceptorActivity, 'Temporal', interceptors: [interceptor]) @@ -1035,6 +1099,8 @@ def test_interceptor assert_equal ['heartbeat-val'], interceptor.calls[2][1].details end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_interceptor_from_client interceptor = TrackCallsInterceptor.new # Create new client with the interceptor set @@ -1057,6 +1123,8 @@ def execute(*args) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_dynamic_activity assert_equal 'Activity does-not-exist called with ["arg1", 123]', execute_activity(DynamicActivity, 'arg1', 123, override_name: 'does-not-exist') @@ -1078,6 +1146,8 @@ def execute(*args) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_dynamic_activity_raw_args assert_equal 'Activity does-not-exist called with ' \ '["arg1", nil, 123] that have encodings ["json/plain", "binary/null", "json/plain"]', @@ -1114,6 +1184,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_context_instance # Instance-per-attempt (twice) assert_equal %w[interceptor-init interceptor-execute execute], @@ -1137,6 +1209,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_client_access assert_equal 'ClientAccessActivity', execute_activity(ClientAccessActivity) end @@ -1156,6 +1230,8 @@ class KeywordArgumentActivity < Temporalio::Activity::Definition def execute(foo, bar: 'baz'); end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_keyword_arguments err = assert_raises { Temporalio::Activity::Definition::Info.from_activity(KeywordArgumentActivity) } assert_includes err.message, 'Activity execute cannot have keyword arguments' diff --git a/temporalio/test/worker_payload_size_limits_test.rb b/temporalio/test/worker_payload_size_limits_test.rb index 19f75e2f..1b549b6f 100644 --- a/temporalio/test/worker_payload_size_limits_test.rb +++ b/temporalio/test/worker_payload_size_limits_test.rb @@ -14,6 +14,9 @@ # disable_payload_error_limit opt-out lets the oversized payload reach (and be rejected by) the # server. The warning channel is asserted via the default log filter. class WorkerPayloadSizeLimitsTest < Test + exclude_class_from_cloud :requires_local_server, + 'Starts a server with custom payload-size dynamic configuration.' + PAYLOAD_ERROR_LIMIT = 10 * 1024 class LargePayloadActivity < Temporalio::Activity::Definition diff --git a/temporalio/test/worker_test.rb b/temporalio/test/worker_test.rb index 40ba7544..078917af 100644 --- a/temporalio/test/worker_test.rb +++ b/temporalio/test/worker_test.rb @@ -121,6 +121,8 @@ def test_block_failure_causes_shutdown assert_equal 'Intentional error', err.message end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_can_run_with_resource_tuner worker = Temporalio::Worker.new( client: env.client, @@ -143,6 +145,8 @@ def test_can_run_with_resource_tuner end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_can_run_with_composite_tuner resource_tuner_options = Temporalio::Worker::Tuner::ResourceBasedTunerOptions.new( target_memory_usage: 0.5, diff --git a/temporalio/test/worker_workflow_activity_test.rb b/temporalio/test/worker_workflow_activity_test.rb index 0e5e202f..609fe6aa 100644 --- a/temporalio/test/worker_workflow_activity_test.rb +++ b/temporalio/test/worker_workflow_activity_test.rb @@ -67,6 +67,8 @@ def execute(activity_task_queue) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_activity_without_result_from_go_sdk env.with_kitchen_sink_worker do |activity_task_queue| execute_workflow(GoNoResultActivityWorkflow, activity_task_queue) do |handle| @@ -315,6 +317,8 @@ def execute(swallow) end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Activity pause and reset APIs that are not enabled in Cloud CI.' def test_cancellation_pause # Swallow queue = Queue.new @@ -370,6 +374,8 @@ def test_cancellation_pause end end + exclude_from_cloud :requires_cloud_provisioning, + 'Requires Activity pause and reset APIs that are not enabled in Cloud CI.' def test_cancellation_reset queue = Queue.new execute_workflow( diff --git a/temporalio/test/worker_workflow_child_test.rb b/temporalio/test/worker_workflow_child_test.rb index ad6cc3a3..05317724 100644 --- a/temporalio/test/worker_workflow_child_test.rb +++ b/temporalio/test/worker_workflow_child_test.rb @@ -263,6 +263,8 @@ def execute end end + exclude_from_cloud :needs_cloud_adaptation, + 'Requires custom search attributes that the Cloud harness does not provision.' def test_search_attributes env.ensure_common_search_attribute_keys @@ -283,6 +285,8 @@ def execute(child_task_queue) end end + exclude_from_cloud :needs_cloud_adaptation, + 'The Go kitchen-sink worker does not receive Cloud TLS configuration.' def test_child_workflow_without_result_from_go_sdk env.with_kitchen_sink_worker do |child_task_queue| execute_workflow(GoNoResultParentWorkflow, child_task_queue) do |handle| diff --git a/temporalio/test/worker_workflow_handler_test.rb b/temporalio/test/worker_workflow_handler_test.rb index 634ac760..6b2a42a6 100644 --- a/temporalio/test/worker_workflow_handler_test.rb +++ b/temporalio/test/worker_workflow_handler_test.rb @@ -630,6 +630,8 @@ def some_update end end + exclude_from_cloud :requires_local_server, + 'Requires local server metrics and update-related dynamic configuration.' def test_retry_start_update # This test confirms that the UpdateWorkflowExecution call occurs multiple times if it hasn't reached accepted. We # check this via metrics. diff --git a/temporalio/test/worker_workflow_nexus_test.rb b/temporalio/test/worker_workflow_nexus_test.rb index 484d4efd..f04a294c 100644 --- a/temporalio/test/worker_workflow_nexus_test.rb +++ b/temporalio/test/worker_workflow_nexus_test.rb @@ -10,6 +10,9 @@ require 'test' class WorkerWorkflowNexusTest < Test + exclude_class_from_cloud :needs_cloud_adaptation, + 'Requires Cloud Nexus endpoint setup and a Go worker with Cloud TLS configuration.' + # Test basic sync operation success class NexusSyncOperationSuccessWorkflow < Temporalio::Workflow::Definition def execute(endpoint) diff --git a/temporalio/test/worker_workflow_test.rb b/temporalio/test/worker_workflow_test.rb index 3f0219bd..38d11324 100644 --- a/temporalio/test/worker_workflow_test.rb +++ b/temporalio/test/worker_workflow_test.rb @@ -493,6 +493,8 @@ def execute(scenario) end end + exclude_from_cloud :needs_cloud_adaptation, + 'Requires custom search attributes that the Cloud harness does not provision.' def test_search_attributes_memo env.ensure_common_search_attribute_keys @@ -1103,6 +1105,8 @@ def some_update(input) end end + exclude_from_cloud :needs_cloud_adaptation, + 'Requires custom search attributes that the Cloud harness does not provision.' def test_payload_codec env.ensure_common_search_attribute_keys @@ -1993,21 +1997,22 @@ def test_custom_metrics line = lines.find { |l| l.start_with?('temporal_activity_task_received{') } assert_includes line, 'activity_type="CustomMetricsActivity"' assert_includes line, 'task_queue="' - assert_includes line, 'namespace="default"' + namespace_label = %(namespace="#{client.namespace}") + assert_includes line, namespace_label assert line.end_with?(' 1') # Confirm we have the regular workflow metrics line = lines.find { |l| l.start_with?('temporal_workflow_completed{') } assert_includes line, 'workflow_type="CustomMetricsWorkflow"' assert_includes line, 'task_queue="' - assert_includes line, 'namespace="default"' + assert_includes line, namespace_label assert line.end_with?(' 1') # Confirm custom activity metric has the tags we expect line = lines.find { |l| l.start_with?('my_activity_counter{') } assert_includes line, 'activity_type="CustomMetricsActivity"' assert_includes line, 'task_queue="' - assert_includes line, 'namespace="default"' + assert_includes line, namespace_label assert_includes line, 'someattr="someval1"' assert_includes line, 'anotherattr="anotherval1"' assert line.end_with?(' 123') @@ -2016,7 +2021,7 @@ def test_custom_metrics line = lines.find { |l| l.start_with?('my_workflow_histogram_sum{') } assert_includes line, 'workflow_type="CustomMetricsWorkflow"' assert_includes line, 'task_queue="' - assert_includes line, 'namespace="default"' + assert_includes line, namespace_label assert_includes line, 'someattr="someval2"' assert_includes line, 'anotherattr="anotherval2"' assert line.end_with?(' 4560') @@ -2122,7 +2127,7 @@ def test_workflow_buffered_metrics value: 4560, attributes: { 'service_name' => 'temporal-core-sdk', - 'namespace' => 'default', + 'namespace' => client.namespace, 'task_queue' => task_queue, 'workflow_type' => 'CustomMetricsWorkflow', 'someattr' => 'someval2', @@ -2137,7 +2142,7 @@ def test_workflow_buffered_metrics value: 123, attributes: { 'service_name' => 'temporal-core-sdk', - 'namespace' => 'default', + 'namespace' => client.namespace, 'task_queue' => task_queue, 'activity_type' => 'CustomMetricsActivity', 'someattr' => 'someval1', @@ -2337,6 +2342,8 @@ def complete(value) end end + exclude_from_cloud :requires_local_server, + 'Starts a second local server to verify worker client replacement.' def test_worker_client_replacement # Create a second ephemeral server and start workflow on both servers Temporalio::Testing::WorkflowEnvironment.start_local do |env2|