Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions apple/testing/default_runner/ios_xctestrun_runner.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def _get_template_substitutions(
create_xcresult_bundle,
device_type,
os_version,
simulator_creator,
simulator_manager_start,
random,
xcodebuild_args,
command_line_args,
Expand All @@ -27,12 +27,12 @@ def _get_template_substitutions(
post_action_binary,
post_action_determines_exit_code):
substitutions = {
"device_type": device_type,
"url_encoded_device_type": device_type.replace(" ", "%20"),
"os_version": os_version,
"create_xcresult_bundle": create_xcresult_bundle,
"xcodebuild_args": xcodebuild_args,
"command_line_args": command_line_args,
"simulator_creator.py": simulator_creator,
"simulator_manager_start": simulator_manager_start,
# "ordered" isn't a special string, but anything besides "random" for this field runs in order
"test_order": "random" if random else "ordered",
"xctestrun_template": xctestrun_template,
Expand Down Expand Up @@ -69,7 +69,7 @@ def _ios_xctestrun_runner_impl(ctx):
runfiles = ctx.runfiles(files = [
ctx.file._xctestrun_template,
ctx.file._xctrunner_entitlements_template,
]).merge(ctx.attr._simulator_creator[DefaultInfo].default_runfiles)
]).merge(ctx.attr._simulator_manager_start[DefaultInfo].default_runfiles)

default_action_binary = "/usr/bin/true"

Expand All @@ -93,7 +93,7 @@ def _ios_xctestrun_runner_impl(ctx):
create_xcresult_bundle = "true" if ctx.attr.create_xcresult_bundle else "false",
device_type = device_type,
os_version = os_version,
simulator_creator = ctx.executable._simulator_creator.short_path,
simulator_manager_start = ctx.executable._simulator_manager_start.short_path,
random = ctx.attr.random,
xcodebuild_args = " ".join(ctx.attr.xcodebuild_args) if ctx.attr.xcodebuild_args else "",
command_line_args = " ".join(ctx.attr.command_line_args) if ctx.attr.command_line_args else "",
Expand Down Expand Up @@ -204,9 +204,9 @@ A binary to run following test execution. Runs after testing but before test res
When true, the exit code of the test run will be set to the exit code of the post action. This is useful for tests that need to fail the test run based on their own criteria.
""",
),
"_simulator_creator": attr.label(
"_simulator_manager_start": attr.label(
default = Label(
"//apple/testing/default_runner:simulator_creator",
"//apple/testing/simulator_manager:start",
),
executable = True,
cfg = "exec",
Expand Down
91 changes: 69 additions & 22 deletions apple/testing/default_runner/ios_xctestrun_runner.template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,13 @@ if [[ -n "${CREATE_XCRESULT_BUNDLE:-}" ]]; then
fi

custom_xcodebuild_args=(%(xcodebuild_args)s)
simulator_name=""
device_id=""
command_line_args=(%(command_line_args)s)
attachment_lifetime="%(attachment_lifetime)s"
destination_timeout="%(destination_timeout)s"
while [[ $# -gt 0 ]]; do
arg="$1"
case $arg in
--simulator_name=*)
simulator_name="${arg##*=}"
;;
--xcodebuild_args=*)
xcodebuild_arg="${arg#--xcodebuild_args=}" # Strip "--xcodebuild_args=" prefix
custom_xcodebuild_args+=("$xcodebuild_arg")
Expand Down Expand Up @@ -391,24 +387,71 @@ fi

readonly profraw="$test_tmp_dir/coverage.profraw"

simulator_creator_args=(
"%(os_version)s" \
"%(device_type)s" \
--name "$simulator_name"
)
simulator_id=""
if [[ "$build_for_device" == false ]]; then
simulator_manager_command() {
local _http_method="$1"
local _command_path="$2"

# Retry up to 10 times with a 1 second delay between each attempt, to allow
# for the server to be down during upgrades
for i in {1..10}; do
set +e
local _simulator_response
_simulator_response=$(
curl \
"http:/-/$_command_path" \
--request "$_http_method" \
--silent \
--fail-with-body \
--unix-socket "/tmp/simulator_manager.sock"
)
local _curl_exit_code=$?
set -e

if [[ $_curl_exit_code -eq 0 ]]; then
echo "$_simulator_response"
return 0
fi

reuse_simulator=%(reuse_simulator)s
if [[ "$reuse_simulator" == true ]]; then
simulator_creator_args+=(--reuse-simulator)
else
simulator_creator_args+=(--no-reuse-simulator)
fi
# If the error was a connection issue (e.g., couldn't connect), then retry
# 6: couldn't resolve host
# 7: failed to connect
# 28: operation timeout
if [[
$_curl_exit_code -eq 6 ||
$_curl_exit_code -eq 7 ||
$_curl_exit_code -eq 28
]]; then
echo >&2 "$(date '+[%H:%M:%S]') warning: simulator manager command" \
"\"$_command_path\" failed with exit code $_curl_exit_code:"
echo >&2 "$(date '+[%H:%M:%S]') $_simulator_response"
echo >&2 "$(date '+[%H:%M:%S]') retrying in 1 second"
sleep 1
else
echo >&2 "$(date '+[%H:%M:%S]') error: simulator manager command" \
"\"$_command_path\" failed:"
echo >&2 "$(date '+[%H:%M:%S]') $_simulator_response"
return $_curl_exit_code
fi
done
}

simulator_id="unused"
if [[ "$build_for_device" == false ]]; then
simulator_id="$("./%(simulator_creator.py)s" \
"${simulator_creator_args[@]}"
reuse_simulator=%(reuse_simulator)s
if [[ "$reuse_simulator" == true ]]; then
exclusive_simulator=0
else
exclusive_simulator=1
fi

"./%(simulator_manager_start)s"

echo "$(date '+[%H:%M:%S]') Attempting to lease simulator"
simulator_id="$(
simulator_manager_command POST "simulator/$$?exclusive=$exclusive_simulator&deviceType=%(url_encoded_device_type)s&os=iOS&version=%(os_version)s"
)"

echo "$(date '+[%H:%M:%S]') ✅ Leased simulator $simulator_id"
fi

test_exit_code=0
Expand Down Expand Up @@ -585,9 +628,13 @@ if [[
rm -r "$result_bundle_path"
fi

if [[ "$reuse_simulator" == false ]]; then
# Delete will shutdown down the simulator if it's still currently running.
xcrun simctl delete "$simulator_id"
if [[ -n "$simulator_id" ]]; then
echo "$(date '+[%H:%M:%S]') Releasing simulator $simulator_id"
if response=$(simulator_manager_command DELETE "simulator/$$"); then
echo "$(date '+[%H:%M:%S]') ✅ Released simulator $simulator_id"
else
echo "$(date '+[%H:%M:%S]') ❌ Failed to release simulator $simulator_id: $response" >&2
fi
fi

profdata="$test_tmp_dir/$simulator_id/Coverage.profdata"
Expand Down
31 changes: 31 additions & 0 deletions apple/testing/simulator_manager/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")

# This is a macro in our repo, so this won't compile as is. But it's basically `macos_application` and `macos_unit_test` combined.
macos_swift_tool(
name = "simulator_manager",
deps = [
"@swiftpkg_shellout//:ShellOut",
"@swiftpkg_swift_argument_parser//:ArgumentParser",
"@swiftpkg_swift_nio//:NIO",
"@swiftpkg_swift_nio//:NIOCore",
"@swiftpkg_swift_nio//:NIOHTTP1",
"@swiftpkg_swift_nio_extras//:NIOExtras",
],
)

sh_binary(
name = "start",
srcs = ["start.sh"],
data = [
":prepare_simulator",
":simulator_manager_opt",
":start_tunnel",
],
visibility = ["//visibility:public"],
deps = ["@bazel_tools//tools/bash/runfiles"],
)

sh_binary(
name = "prepare_simulator",
srcs = ["prepare_simulator.sh"],
)
17 changes: 17 additions & 0 deletions apple/testing/simulator_manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# `simulator_manager`

The `simulator_manager` replaces the `simulator_creator.py` used in
**rules_apple**'s `ios_xctestrun_runner.template.sh`.

It manages simulator "leases". Tests can lease and then release a simulator of a
given configuration (device type and os version), and can also request that it's
an "exclusive" lease. The manager automatically releases a simulator if the
requested process exits without releasing first.

Exclusive leases mean that test has exclusive access to the simulator, which is
needed for App Host and UI tests.

Base simulators are created for a given configuration, leases are on clones of
the base simulators. After a simulator has been released for 10 minutes the
clone is deleted. This allows us to free up disk space on remote executors, but
also allow reuse in a short period of time.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import NIO
import NIOHTTP1
import os.log

extension Logger {
static let accumulatedHTTP = simulatorManager(category: "accumulated_http")
}

struct FullHTTPRequest {
let head: HTTPRequestHead
var body: ByteBuffer
}

struct FullHTTPResponse {
let head: HTTPResponseHead
var body: ByteBuffer
}

final class AccumulatedHTTPHandler: ChannelInboundHandler, ChannelOutboundHandler {
typealias InboundIn = HTTPServerRequestPart
typealias InboundOut = FullHTTPRequest

typealias OutboundIn = FullHTTPResponse
typealias OutboundOut = HTTPServerResponsePart

private var requestHead: HTTPRequestHead?
private var bodyBuffer: ByteBuffer?

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let part = self.unwrapInboundIn(data)

switch part {
case .head(let head):
self.requestHead = head
self.bodyBuffer = context.channel.allocator.buffer(capacity: 0)

case .body(var chunk):
self.bodyBuffer?.writeBuffer(&chunk)

case .end:
if let head = requestHead, let body = bodyBuffer {
Logger.accumulatedHTTP.info(
"""
▶️ Received \(head.method.rawValue, privacy: .public) request for \
\(head.uri, privacy: .public)
"""
)

let fullRequest = FullHTTPRequest(head: head, body: body)
context.fireChannelRead(self.wrapInboundOut(fullRequest))
}

self.requestHead = nil
self.bodyBuffer = nil
}
}

func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
let fullResponse = unwrapOutboundIn(data)

Logger.accumulatedHTTP.info(
"◀️ Sending \(fullResponse.head.status, privacy: .public) response"
)

context.write(wrapOutboundOut(.head(fullResponse.head)), promise: nil)

if fullResponse.body.readableBytes > 0 {
context.write(wrapOutboundOut(.body(.byteBuffer(fullResponse.body))), promise: nil)
}

context.write(wrapOutboundOut(.end(nil)), promise: promise)
}

func errorCaught(context: ChannelHandlerContext, error: Error) {
Logger.accumulatedHTTP.error(
"❌ \(error.localizedDescription, privacy: .public)"
)
context.close(promise: nil)
}
}
Loading