diff --git a/apple/testing/default_runner/ios_xctestrun_runner.bzl b/apple/testing/default_runner/ios_xctestrun_runner.bzl index f866570dc8..62090506a4 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_capture_format, 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_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", @@ -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_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, @@ -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_capture_format": 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 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. """, ), "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..5bb4f37dbe 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_capture_format="%(screen_capture_format)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_capture_format=*) + screen_capture_format="${arg##*=}" + ;; *) echo "error: Unsupported argument '${arg}'" >&2 exit 1 @@ -538,6 +542,16 @@ if (( ${#custom_xcodebuild_args[@]} )); then echo "note: Using 'xcodebuild' because '--xcodebuild_args' was provided" should_use_xcodebuild=true fi +if [[ -n "$screen_capture_format" ]]; then + echo "note: Using 'xcodebuild' because a screen capture format was requested" + should_use_xcodebuild=true + # The capture is only observable in the XCResult bundle, so force one. + create_xcresult_bundle=true + if [[ "$attachment_lifetime" == "keepNever" ]]; then + 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 # Run a pre-action binary, if provided. pre_action_binary=%(pre_action_binary)s @@ -556,6 +570,14 @@ if [[ "$should_use_xcodebuild" == true ]]; then 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_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" /usr/bin/sed \ -e "s${sed_delim}BAZEL_INSERT_LIBRARIES${sed_delim}$xctestrun_libraries${sed_delim}g" \ @@ -573,6 +595,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_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 4cca6158df..3b3c7270f3 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_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 5928e666d0..53dd50192b 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_capture_format, 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_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 6ba8505ef7..31bfe2ce4e 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_capture_format_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_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 + create_ios_ui_tests + # 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_capture_format=screenRecording \ + --test_arg=--xctestrun_attachment_lifetime=keepAlways \ + //ios:PassingUITest || fail "should pass" + + expect_log "note: Using 'xcodebuild' because a screen capture format was requested" + expect_log "PreferredScreenCaptureFormat" + expect_log "screenRecording" + expect_log "SystemAttachmentLifetime" + expect_log "keepAlways" +} + run_suite "ios_ui_test with iOS xctestrun runner bundling tests"