From 3a861d288123b4bc0f5157a13fedf7db11b41235 Mon Sep 17 00:00:00 2001 From: Ernesto Cambuston Date: Thu, 16 Jul 2026 13:25:58 -0700 Subject: [PATCH 1/3] Add screen_recording attribute to ios_xctestrun_runner Threads a `PreferredScreenCaptureFormat` value (added in Xcode 15) into the generated xctestrun file so UI tests can capture a full screen recording instead of just screenshots. Accepted values are `""` (default; keep Xcode's platform default), `"screenshots"`, and `"screenRecording"`. Setting a non-empty value implies use of `xcodebuild test-without-building` (the direct `xctest` path does not read the xctestrun file) and implies `create_xcresult_bundle = True` (the recording is only observable in the `.xcresult`). Screen recordings are system attachments, so `attachment_lifetime` must be `"keepAlways"` or `"deleteOnSuccess"` for the recording to survive into the `.xcresult`; this is documented on the attribute. --- .../default_runner/ios_xctestrun_runner.bzl | 18 +++++++ .../ios_xctestrun_runner.template.sh | 20 +++++++- .../ios_xctestrun_runner.template.xctestrun | 1 + doc/rules-ios.md | 4 +- test/ios_xctestrun_runner_ui_test.sh | 49 +++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/apple/testing/default_runner/ios_xctestrun_runner.bzl b/apple/testing/default_runner/ios_xctestrun_runner.bzl index f866570dc8..0e3357cd00 100644 --- a/apple/testing/default_runner/ios_xctestrun_runner.bzl +++ b/apple/testing/default_runner/ios_xctestrun_runner.bzl @@ -30,6 +30,7 @@ def _get_template_substitutions( pre_action_binary, random, reuse_simulator, + screen_recording, sdk_version, xcodebuild_args, xctestrun_template, @@ -47,6 +48,7 @@ def _get_template_substitutions( "post_action_determines_exit_code": post_action_determines_exit_code, "pre_action_binary": pre_action_binary, "reuse_simulator": reuse_simulator, + "screen_recording": screen_recording, "sdk_version": sdk_version, # "ordered" isn't a special string, but anything besides "random" for this field runs in order "test_order": "random" if random else "ordered", @@ -120,6 +122,7 @@ def _ios_xctestrun_runner_impl(ctx): pre_action_binary = pre_action_binary, random = ctx.attr.random, reuse_simulator = "true" if ctx.attr.reuse_simulator else "false", + screen_recording = ctx.attr.screen_recording, sdk_version = sdk_version, xcodebuild_args = " ".join(ctx.attr.xcodebuild_args) if ctx.attr.xcodebuild_args else "", xctestrun_template = ctx.file._xctestrun_template.short_path, @@ -248,6 +251,21 @@ dependencies. default = True, doc = """ Toggle simulator reuse. The default behavior is to reuse an existing device of the same type and OS version. When disabled, a new simulator is created before testing starts and shutdown when the runner completes. +""", + ), + "screen_recording": attr.string( + default = "", + values = ["", "screenshots", "screenRecording"], + doc = """ +Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of: + +* `""` (default): do not set the key, use Xcode's platform default. +* `"screenshots"`: capture screenshots only. +* `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle. + +Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot record video) and forces an `.xcresult` bundle to be produced (the recording is only observable there). This attribute is only meaningful for UI tests. + +Screen recordings are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the recording to survive into the `.xcresult` — the default `"keepNever"` discards it. """, ), "test_environment": attr.string_dict( diff --git a/apple/testing/default_runner/ios_xctestrun_runner.template.sh b/apple/testing/default_runner/ios_xctestrun_runner.template.sh index 5b94efe71b..91c2f6e076 100755 --- a/apple/testing/default_runner/ios_xctestrun_runner.template.sh +++ b/apple/testing/default_runner/ios_xctestrun_runner.template.sh @@ -21,6 +21,7 @@ custom_xcodebuild_args=(%(xcodebuild_args)s) device_id="" command_line_args=(%(command_line_args)s) attachment_lifetime="%(attachment_lifetime)s" +screen_recording="%(screen_recording)s" destination_timeout="%(destination_timeout)s" while [[ $# -gt 0 ]]; do arg="$1" @@ -38,6 +39,9 @@ while [[ $# -gt 0 ]]; do --xctestrun_attachment_lifetime=*) attachment_lifetime="${arg##*=}" ;; + --xctestrun_screen_recording=*) + screen_recording="${arg##*=}" + ;; *) echo "error: Unsupported argument '${arg}'" >&2 exit 1 @@ -538,6 +542,12 @@ if (( ${#custom_xcodebuild_args[@]} )); then echo "note: Using 'xcodebuild' because '--xcodebuild_args' was provided" should_use_xcodebuild=true fi +if [[ -n "$screen_recording" ]]; then + echo "note: Using 'xcodebuild' because screen recording was requested" + should_use_xcodebuild=true + # The recording is only observable in the XCResult bundle, so force one. + create_xcresult_bundle=true +fi # Run a pre-action binary, if provided. pre_action_binary=%(pre_action_binary)s @@ -550,12 +560,19 @@ if [[ "$should_use_xcodebuild" == true ]]; then exit 1 fi - # Set xctest attachment liftime xctestrun_attachment_lifetime_section+=" SystemAttachmentLifetime\n" xctestrun_attachment_lifetime_section+=" $attachment_lifetime\n" xctestrun_attachment_lifetime_section+=" UserAttachmentLifetime\n" xctestrun_attachment_lifetime_section+=" $attachment_lifetime" + # Set the preferred screen capture format (Xcode 15+). Left empty when the + # attribute is unset so we don't override Xcode's platform default. + xctestrun_screen_recording_section="" + if [[ -n "$screen_recording" ]]; then + xctestrun_screen_recording_section+=" PreferredScreenCaptureFormat\n" + xctestrun_screen_recording_section+=" $screen_recording" + fi + readonly xctestrun_file="$test_tmp_dir/tests.xctestrun" /usr/bin/sed \ -e "s${sed_delim}BAZEL_INSERT_LIBRARIES${sed_delim}$xctestrun_libraries${sed_delim}g" \ @@ -573,6 +590,7 @@ if [[ "$should_use_xcodebuild" == true ]]; then -e "s${sed_delim}BAZEL_COVERAGE_OUTPUT_DIR${sed_delim}$test_tmp_dir${sed_delim}g" \ -e "s${sed_delim}BAZEL_COMMAND_LINE_ARGS_SECTION${sed_delim}$xctestrun_cmd_line_args_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_ATTACHMENT_LIFETIME_SECTION${sed_delim}$xctestrun_attachment_lifetime_section${sed_delim}g" \ + -e "s${sed_delim}BAZEL_SCREEN_RECORDING_SECTION${sed_delim}$xctestrun_screen_recording_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_SKIP_TEST_SECTION${sed_delim}$xctestrun_skip_test_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_ONLY_TEST_SECTION${sed_delim}$xctestrun_only_test_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_ARCHITECTURE${sed_delim}$architecture${sed_delim}g" \ diff --git a/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun b/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun index 4cca6158df..d1d4f80691 100644 --- a/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun +++ b/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun @@ -39,6 +39,7 @@ BAZEL_TEST_ENVIRONMENT BAZEL_ATTACHMENT_LIFETIME_SECTION +BAZEL_SCREEN_RECORDING_SECTION ClangProfileDataDirectoryPath BAZEL_COVERAGE_OUTPUT_DIR BAZEL_COMMAND_LINE_ARGS_SECTION diff --git a/doc/rules-ios.md b/doc/rules-ios.md index 5928e666d0..898b43fc5b 100644 --- a/doc/rules-ios.md +++ b/doc/rules-ios.md @@ -644,7 +644,8 @@ load("@rules_apple//apple:ios.bzl", "ios_xctestrun_runner") ios_xctestrun_runner(name, attachment_lifetime, clean_up_simulator_action, command_line_args, create_simulator_action, create_xcresult_bundle, destination_timeout, device_type, os_version, post_action, post_action_determines_exit_code, - pre_action, random, reuse_simulator, test_environment, xcodebuild_args) + pre_action, random, reuse_simulator, screen_recording, + test_environment, xcodebuild_args) This rule creates a test runner for iOS tests that uses xctestrun files to run @@ -703,6 +704,7 @@ in Xcode. | pre_action | A binary to run prior to test execution. Runs after simulator creation. Sets the `$SIMULATOR_UDID` environment variable, in addition to any other variables available to the test runner. | Label | optional | `None` | | random | Whether to run the tests in random order to identify unintended state dependencies. | Boolean | optional | `False` | | reuse_simulator | Toggle simulator reuse. The default behavior is to reuse an existing device of the same type and OS version. When disabled, a new simulator is created before testing starts and shutdown when the runner completes. | Boolean | optional | `True` | +| screen_recording | Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of:

* `""` (default): do not set the key, use Xcode's platform default. * `"screenshots"`: capture screenshots only. * `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle.

Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot record video) and forces an `.xcresult` bundle to be produced (the recording is only observable there). This attribute is only meaningful for UI tests.

Screen recordings are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the recording to survive into the `.xcresult` — the default `"keepNever"` discards it. | String | optional | `""` | | test_environment | Optional dictionary with the environment variables that are to be propagated into the XCTest invocation. | Dictionary: String -> String | optional | `{}` | | xcodebuild_args | Arguments to pass to `xcodebuild` when running the test bundle. This means it will always use `xcodebuild test-without-building` to run the test bundle. | List of strings | optional | `[]` | diff --git a/test/ios_xctestrun_runner_ui_test.sh b/test/ios_xctestrun_runner_ui_test.sh index 6ba8505ef7..b273c34179 100644 --- a/test/ios_xctestrun_runner_ui_test.sh +++ b/test/ios_xctestrun_runner_ui_test.sh @@ -480,4 +480,53 @@ function test_ios_ui_test_attachment_lifetime_arg() { expect_log "deleteOnSuccess" } +function test_ios_ui_test_default_screen_recording_arg() { + create_sim_runners + create_ios_app + create_ios_ui_tests + do_ios_test \ + --test_env=DEBUG_XCTESTRUNNER=1 \ + --test_filter=PassingUITest/testPass2 \ + //ios:PassingUITest || fail "should pass" + + # When unset, PreferredScreenCaptureFormat must not be emitted so we don't + # override Xcode's platform default. + expect_not_log "PreferredScreenCaptureFormat" +} + +function test_ios_ui_test_screen_recording_arg() { + create_sim_runners + create_ios_app + create_ios_ui_tests + # Screen recordings are system attachments, so attachment_lifetime must not + # be "keepNever" or the recording is discarded before landing in .xcresult. + do_ios_test \ + --test_env=DEBUG_XCTESTRUNNER=1 \ + --test_filter=PassingUITest/testPass2 \ + --test_arg=--xctestrun_screen_recording=screenRecording \ + --test_arg=--xctestrun_attachment_lifetime=keepAlways \ + //ios:PassingUITest || fail "should pass" + + expect_log "note: Using 'xcodebuild' because screen recording was requested" + expect_log "PreferredScreenCaptureFormat" + expect_log "screenRecording" + expect_log "SystemAttachmentLifetime" + expect_log "keepAlways" +} + +function test_ios_ui_test_screen_recording_screenshots_arg() { + create_sim_runners + create_ios_app + create_ios_ui_tests + do_ios_test \ + --test_env=DEBUG_XCTESTRUNNER=1 \ + --test_filter=PassingUITest/testPass2 \ + --test_arg=--xctestrun_screen_recording=screenshots \ + //ios:PassingUITest || fail "should pass" + + expect_log "note: Using 'xcodebuild' because screen recording was requested" + expect_log "PreferredScreenCaptureFormat" + expect_log "screenshots" +} + run_suite "ios_ui_test with iOS xctestrun runner bundling tests" From cfc10ddb5742ccd6e7d3a188d4547790eb0dd917 Mon Sep 17 00:00:00 2001 From: Ernesto Cambuston Date: Sat, 18 Jul 2026 21:57:19 -0700 Subject: [PATCH 2/3] Rename screen_recording to screen_capture_format; warn on keepNever Review feedback: - Rename the attribute (and --xctestrun_* flag) to screen_capture_format: it is a passthrough for Xcode's PreferredScreenCaptureFormat key, and screen_recording = "screenshots" read as a contradiction. - Emit a runtime warning when a capture format is requested while attachment_lifetime is "keepNever", since the capture would be silently discarded before reaching the .xcresult bundle. - Drop the screenshots-value test: it exercised the identical code path as the screenRecording test and the attr values list already guarantees the value threads through. - Restore the pre-existing attachment-lifetime comment so the diff stays purely additive. --- .../default_runner/ios_xctestrun_runner.bzl | 12 ++++----- .../ios_xctestrun_runner.template.sh | 26 ++++++++++-------- .../ios_xctestrun_runner.template.xctestrun | 2 +- doc/rules-ios.md | 4 +-- test/ios_xctestrun_runner_ui_test.sh | 27 +++++-------------- 5 files changed, 30 insertions(+), 41 deletions(-) diff --git a/apple/testing/default_runner/ios_xctestrun_runner.bzl b/apple/testing/default_runner/ios_xctestrun_runner.bzl index 0e3357cd00..978a95ae6f 100644 --- a/apple/testing/default_runner/ios_xctestrun_runner.bzl +++ b/apple/testing/default_runner/ios_xctestrun_runner.bzl @@ -30,7 +30,7 @@ def _get_template_substitutions( pre_action_binary, random, reuse_simulator, - screen_recording, + screen_capture_format, sdk_version, xcodebuild_args, xctestrun_template, @@ -48,7 +48,7 @@ def _get_template_substitutions( "post_action_determines_exit_code": post_action_determines_exit_code, "pre_action_binary": pre_action_binary, "reuse_simulator": reuse_simulator, - "screen_recording": screen_recording, + "screen_capture_format": screen_capture_format, "sdk_version": sdk_version, # "ordered" isn't a special string, but anything besides "random" for this field runs in order "test_order": "random" if random else "ordered", @@ -122,7 +122,7 @@ def _ios_xctestrun_runner_impl(ctx): pre_action_binary = pre_action_binary, random = ctx.attr.random, reuse_simulator = "true" if ctx.attr.reuse_simulator else "false", - screen_recording = ctx.attr.screen_recording, + screen_capture_format = ctx.attr.screen_capture_format, sdk_version = sdk_version, xcodebuild_args = " ".join(ctx.attr.xcodebuild_args) if ctx.attr.xcodebuild_args else "", xctestrun_template = ctx.file._xctestrun_template.short_path, @@ -253,7 +253,7 @@ dependencies. Toggle simulator reuse. The default behavior is to reuse an existing device of the same type and OS version. When disabled, a new simulator is created before testing starts and shutdown when the runner completes. """, ), - "screen_recording": attr.string( + "screen_capture_format": attr.string( default = "", values = ["", "screenshots", "screenRecording"], doc = """ @@ -263,9 +263,9 @@ Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun * `"screenshots"`: capture screenshots only. * `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle. -Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot record video) and forces an `.xcresult` bundle to be produced (the recording is only observable there). This attribute is only meaningful for UI tests. +Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot capture the screen) and forces an `.xcresult` bundle to be produced (the capture is only observable there). This attribute is only meaningful for UI tests. -Screen recordings are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the recording to survive into the `.xcresult` — the default `"keepNever"` discards it. +Screen captures are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the capture to survive into the `.xcresult` — the default `"keepNever"` discards it. """, ), "test_environment": attr.string_dict( diff --git a/apple/testing/default_runner/ios_xctestrun_runner.template.sh b/apple/testing/default_runner/ios_xctestrun_runner.template.sh index 91c2f6e076..1946c4fe92 100755 --- a/apple/testing/default_runner/ios_xctestrun_runner.template.sh +++ b/apple/testing/default_runner/ios_xctestrun_runner.template.sh @@ -21,7 +21,7 @@ custom_xcodebuild_args=(%(xcodebuild_args)s) device_id="" command_line_args=(%(command_line_args)s) attachment_lifetime="%(attachment_lifetime)s" -screen_recording="%(screen_recording)s" +screen_capture_format="%(screen_capture_format)s" destination_timeout="%(destination_timeout)s" while [[ $# -gt 0 ]]; do arg="$1" @@ -39,8 +39,8 @@ while [[ $# -gt 0 ]]; do --xctestrun_attachment_lifetime=*) attachment_lifetime="${arg##*=}" ;; - --xctestrun_screen_recording=*) - screen_recording="${arg##*=}" + --xctestrun_screen_capture_format=*) + screen_capture_format="${arg##*=}" ;; *) echo "error: Unsupported argument '${arg}'" >&2 @@ -542,11 +542,14 @@ if (( ${#custom_xcodebuild_args[@]} )); then echo "note: Using 'xcodebuild' because '--xcodebuild_args' was provided" should_use_xcodebuild=true fi -if [[ -n "$screen_recording" ]]; then - echo "note: Using 'xcodebuild' because screen recording was requested" +if [[ -n "$screen_capture_format" ]]; then + echo "note: Using 'xcodebuild' because a screen capture format was requested" should_use_xcodebuild=true - # The recording is only observable in the XCResult bundle, so force one. + # The capture is only observable in the XCResult bundle, so force one. create_xcresult_bundle=true + if [[ "$attachment_lifetime" == "keepNever" ]]; then + echo "warning: 'screen_capture_format' is set but 'attachment_lifetime' is 'keepNever'; the capture will be discarded before it reaches the .xcresult bundle" >&2 + fi fi # Run a pre-action binary, if provided. @@ -560,6 +563,7 @@ if [[ "$should_use_xcodebuild" == true ]]; then exit 1 fi + # Set xctest attachment liftime xctestrun_attachment_lifetime_section+=" SystemAttachmentLifetime\n" xctestrun_attachment_lifetime_section+=" $attachment_lifetime\n" xctestrun_attachment_lifetime_section+=" UserAttachmentLifetime\n" @@ -567,10 +571,10 @@ if [[ "$should_use_xcodebuild" == true ]]; then # Set the preferred screen capture format (Xcode 15+). Left empty when the # attribute is unset so we don't override Xcode's platform default. - xctestrun_screen_recording_section="" - if [[ -n "$screen_recording" ]]; then - xctestrun_screen_recording_section+=" PreferredScreenCaptureFormat\n" - xctestrun_screen_recording_section+=" $screen_recording" + xctestrun_screen_capture_format_section="" + if [[ -n "$screen_capture_format" ]]; then + xctestrun_screen_capture_format_section+=" PreferredScreenCaptureFormat\n" + xctestrun_screen_capture_format_section+=" $screen_capture_format" fi readonly xctestrun_file="$test_tmp_dir/tests.xctestrun" @@ -590,7 +594,7 @@ if [[ "$should_use_xcodebuild" == true ]]; then -e "s${sed_delim}BAZEL_COVERAGE_OUTPUT_DIR${sed_delim}$test_tmp_dir${sed_delim}g" \ -e "s${sed_delim}BAZEL_COMMAND_LINE_ARGS_SECTION${sed_delim}$xctestrun_cmd_line_args_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_ATTACHMENT_LIFETIME_SECTION${sed_delim}$xctestrun_attachment_lifetime_section${sed_delim}g" \ - -e "s${sed_delim}BAZEL_SCREEN_RECORDING_SECTION${sed_delim}$xctestrun_screen_recording_section${sed_delim}g" \ + -e "s${sed_delim}BAZEL_SCREEN_CAPTURE_FORMAT_SECTION${sed_delim}$xctestrun_screen_capture_format_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_SKIP_TEST_SECTION${sed_delim}$xctestrun_skip_test_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_ONLY_TEST_SECTION${sed_delim}$xctestrun_only_test_section${sed_delim}g" \ -e "s${sed_delim}BAZEL_ARCHITECTURE${sed_delim}$architecture${sed_delim}g" \ diff --git a/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun b/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun index d1d4f80691..3b3c7270f3 100644 --- a/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun +++ b/apple/testing/default_runner/ios_xctestrun_runner.template.xctestrun @@ -39,7 +39,7 @@ BAZEL_TEST_ENVIRONMENT BAZEL_ATTACHMENT_LIFETIME_SECTION -BAZEL_SCREEN_RECORDING_SECTION +BAZEL_SCREEN_CAPTURE_FORMAT_SECTION ClangProfileDataDirectoryPath BAZEL_COVERAGE_OUTPUT_DIR BAZEL_COMMAND_LINE_ARGS_SECTION diff --git a/doc/rules-ios.md b/doc/rules-ios.md index 898b43fc5b..8c3f46f317 100644 --- a/doc/rules-ios.md +++ b/doc/rules-ios.md @@ -644,7 +644,7 @@ load("@rules_apple//apple:ios.bzl", "ios_xctestrun_runner") ios_xctestrun_runner(name, attachment_lifetime, clean_up_simulator_action, command_line_args, create_simulator_action, create_xcresult_bundle, destination_timeout, device_type, os_version, post_action, post_action_determines_exit_code, - pre_action, random, reuse_simulator, screen_recording, + pre_action, random, reuse_simulator, screen_capture_format, test_environment, xcodebuild_args) @@ -704,7 +704,7 @@ in Xcode. | pre_action | A binary to run prior to test execution. Runs after simulator creation. Sets the `$SIMULATOR_UDID` environment variable, in addition to any other variables available to the test runner. | Label | optional | `None` | | random | Whether to run the tests in random order to identify unintended state dependencies. | Boolean | optional | `False` | | reuse_simulator | Toggle simulator reuse. The default behavior is to reuse an existing device of the same type and OS version. When disabled, a new simulator is created before testing starts and shutdown when the runner completes. | Boolean | optional | `True` | -| screen_recording | Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of:

* `""` (default): do not set the key, use Xcode's platform default. * `"screenshots"`: capture screenshots only. * `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle.

Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot record video) and forces an `.xcresult` bundle to be produced (the recording is only observable there). This attribute is only meaningful for UI tests.

Screen recordings are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the recording to survive into the `.xcresult` — the default `"keepNever"` discards it. | String | optional | `""` | +| screen_capture_format | Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of:

* `""` (default): do not set the key, use Xcode's platform default. * `"screenshots"`: capture screenshots only. * `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle.

Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot capture the screen) and forces an `.xcresult` bundle to be produced (the capture is only observable there). This attribute is only meaningful for UI tests.

Screen captures are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the capture to survive into the `.xcresult` — the default `"keepNever"` discards it. | String | optional | `""` | | test_environment | Optional dictionary with the environment variables that are to be propagated into the XCTest invocation. | Dictionary: String -> String | optional | `{}` | | xcodebuild_args | Arguments to pass to `xcodebuild` when running the test bundle. This means it will always use `xcodebuild test-without-building` to run the test bundle. | List of strings | optional | `[]` | diff --git a/test/ios_xctestrun_runner_ui_test.sh b/test/ios_xctestrun_runner_ui_test.sh index b273c34179..fc85a549a9 100644 --- a/test/ios_xctestrun_runner_ui_test.sh +++ b/test/ios_xctestrun_runner_ui_test.sh @@ -480,7 +480,7 @@ function test_ios_ui_test_attachment_lifetime_arg() { expect_log "deleteOnSuccess" } -function test_ios_ui_test_default_screen_recording_arg() { +function test_ios_ui_test_default_screen_capture_format_arg() { create_sim_runners create_ios_app create_ios_ui_tests @@ -494,39 +494,24 @@ function test_ios_ui_test_default_screen_recording_arg() { expect_not_log "PreferredScreenCaptureFormat" } -function test_ios_ui_test_screen_recording_arg() { +function test_ios_ui_test_screen_capture_format_arg() { create_sim_runners create_ios_app create_ios_ui_tests - # Screen recordings are system attachments, so attachment_lifetime must not - # be "keepNever" or the recording is discarded before landing in .xcresult. + # Screen captures are system attachments, so attachment_lifetime must not + # be "keepNever" or the capture is discarded before landing in .xcresult. do_ios_test \ --test_env=DEBUG_XCTESTRUNNER=1 \ --test_filter=PassingUITest/testPass2 \ - --test_arg=--xctestrun_screen_recording=screenRecording \ + --test_arg=--xctestrun_screen_capture_format=screenRecording \ --test_arg=--xctestrun_attachment_lifetime=keepAlways \ //ios:PassingUITest || fail "should pass" - expect_log "note: Using 'xcodebuild' because screen recording was requested" + expect_log "note: Using 'xcodebuild' because a screen capture format was requested" expect_log "PreferredScreenCaptureFormat" expect_log "screenRecording" expect_log "SystemAttachmentLifetime" expect_log "keepAlways" } -function test_ios_ui_test_screen_recording_screenshots_arg() { - create_sim_runners - create_ios_app - create_ios_ui_tests - do_ios_test \ - --test_env=DEBUG_XCTESTRUNNER=1 \ - --test_filter=PassingUITest/testPass2 \ - --test_arg=--xctestrun_screen_recording=screenshots \ - //ios:PassingUITest || fail "should pass" - - expect_log "note: Using 'xcodebuild' because screen recording was requested" - expect_log "PreferredScreenCaptureFormat" - expect_log "screenshots" -} - run_suite "ios_ui_test with iOS xctestrun runner bundling tests" From bdd8b97dc57cc745df05bb2fed2a705062d700c5 Mon Sep 17 00:00:00 2001 From: Ernesto Cambuston Date: Wed, 22 Jul 2026 10:29:09 -0700 Subject: [PATCH 3/3] Error when screen_capture_format is combined with keepNever Review feedback: screen captures are system attachments, so with the default attachment_lifetime of "keepNever" the capture would be silently discarded after paying for the xcodebuild path and an xcresult bundle. Fail fast with a clear error instead of warning, and cover the failure in the runner test suite. The simulator serialization commits moved to their own PR (simulator pool), also per review feedback. --- .../default_runner/ios_xctestrun_runner.bzl | 2 +- .../ios_xctestrun_runner.template.sh | 3 ++- doc/rules-ios.md | 6 +++--- test/ios_xctestrun_runner_ui_test.sh | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apple/testing/default_runner/ios_xctestrun_runner.bzl b/apple/testing/default_runner/ios_xctestrun_runner.bzl index 978a95ae6f..62090506a4 100644 --- a/apple/testing/default_runner/ios_xctestrun_runner.bzl +++ b/apple/testing/default_runner/ios_xctestrun_runner.bzl @@ -265,7 +265,7 @@ Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot capture the screen) and forces an `.xcresult` bundle to be produced (the capture is only observable there). This attribute is only meaningful for UI tests. -Screen captures are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the capture to survive into the `.xcresult` — the default `"keepNever"` discards it. +Screen captures are system attachments, so setting this attribute requires `attachment_lifetime` to be `"keepAlways"` or `"deleteOnSuccess"` — with the default `"keepNever"` the capture would be discarded before it reaches the `.xcresult`, so the runner fails with an error. """, ), "test_environment": attr.string_dict( diff --git a/apple/testing/default_runner/ios_xctestrun_runner.template.sh b/apple/testing/default_runner/ios_xctestrun_runner.template.sh index 1946c4fe92..5bb4f37dbe 100755 --- a/apple/testing/default_runner/ios_xctestrun_runner.template.sh +++ b/apple/testing/default_runner/ios_xctestrun_runner.template.sh @@ -548,7 +548,8 @@ if [[ -n "$screen_capture_format" ]]; then # The capture is only observable in the XCResult bundle, so force one. create_xcresult_bundle=true if [[ "$attachment_lifetime" == "keepNever" ]]; then - echo "warning: 'screen_capture_format' is set but 'attachment_lifetime' is 'keepNever'; the capture will be discarded before it reaches the .xcresult bundle" >&2 + echo "error: 'screen_capture_format' requires 'attachment_lifetime' to be 'keepAlways' or 'deleteOnSuccess'; with 'keepNever' the capture would be discarded before it reaches the .xcresult bundle" >&2 + exit 1 fi fi diff --git a/doc/rules-ios.md b/doc/rules-ios.md index 8c3f46f317..53dd50192b 100644 --- a/doc/rules-ios.md +++ b/doc/rules-ios.md @@ -644,8 +644,8 @@ load("@rules_apple//apple:ios.bzl", "ios_xctestrun_runner") ios_xctestrun_runner(name, attachment_lifetime, clean_up_simulator_action, command_line_args, create_simulator_action, create_xcresult_bundle, destination_timeout, device_type, os_version, post_action, post_action_determines_exit_code, - pre_action, random, reuse_simulator, screen_capture_format, - test_environment, xcodebuild_args) + pre_action, random, reuse_simulator, screen_capture_format, test_environment, + xcodebuild_args) This rule creates a test runner for iOS tests that uses xctestrun files to run @@ -704,7 +704,7 @@ in Xcode. | pre_action | A binary to run prior to test execution. Runs after simulator creation. Sets the `$SIMULATOR_UDID` environment variable, in addition to any other variables available to the test runner. | Label | optional | `None` | | random | Whether to run the tests in random order to identify unintended state dependencies. | Boolean | optional | `False` | | reuse_simulator | Toggle simulator reuse. The default behavior is to reuse an existing device of the same type and OS version. When disabled, a new simulator is created before testing starts and shutdown when the runner completes. | Boolean | optional | `True` | -| screen_capture_format | Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of:

* `""` (default): do not set the key, use Xcode's platform default. * `"screenshots"`: capture screenshots only. * `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle.

Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot capture the screen) and forces an `.xcresult` bundle to be produced (the capture is only observable there). This attribute is only meaningful for UI tests.

Screen captures are system attachments, so `attachment_lifetime` must be `"keepAlways"` (or `"deleteOnSuccess"`) for the capture to survive into the `.xcresult` — the default `"keepNever"` discards it. | String | optional | `""` | +| screen_capture_format | Controls the `PreferredScreenCaptureFormat` written into the generated xctestrun file (added in Xcode 15). One of:

* `""` (default): do not set the key, use Xcode's platform default. * `"screenshots"`: capture screenshots only. * `"screenRecording"`: capture a full screen recording of each UI test run, attached to the resulting `.xcresult` bundle.

Setting a non-empty value forces the runner onto the `xcodebuild test-without-building` path (the direct `xctest` path does not read the xctestrun file and cannot capture the screen) and forces an `.xcresult` bundle to be produced (the capture is only observable there). This attribute is only meaningful for UI tests.

Screen captures are system attachments, so setting this attribute requires `attachment_lifetime` to be `"keepAlways"` or `"deleteOnSuccess"` — with the default `"keepNever"` the capture would be discarded before it reaches the `.xcresult`, so the runner fails with an error. | String | optional | `""` | | test_environment | Optional dictionary with the environment variables that are to be propagated into the XCTest invocation. | Dictionary: String -> String | optional | `{}` | | xcodebuild_args | Arguments to pass to `xcodebuild` when running the test bundle. This means it will always use `xcodebuild test-without-building` to run the test bundle. | List of strings | optional | `[]` | diff --git a/test/ios_xctestrun_runner_ui_test.sh b/test/ios_xctestrun_runner_ui_test.sh index fc85a549a9..31bfe2ce4e 100644 --- a/test/ios_xctestrun_runner_ui_test.sh +++ b/test/ios_xctestrun_runner_ui_test.sh @@ -494,6 +494,21 @@ function test_ios_ui_test_default_screen_capture_format_arg() { expect_not_log "PreferredScreenCaptureFormat" } +function test_ios_ui_test_screen_capture_format_keep_never_fails() { + create_sim_runners + create_ios_app + create_ios_ui_tests + # Screen captures are system attachments; with the default + # attachment_lifetime of "keepNever" the capture would be silently + # discarded, so the runner must fail instead. + ! do_ios_test \ + --test_filter=PassingUITest/testPass2 \ + --test_arg=--xctestrun_screen_capture_format=screenRecording \ + //ios:PassingUITest || fail "should fail" + + expect_log "error: 'screen_capture_format' requires 'attachment_lifetime' to be 'keepAlways' or 'deleteOnSuccess'" +} + function test_ios_ui_test_screen_capture_format_arg() { create_sim_runners create_ios_app