Skip to content

Commit

Permalink
Add xctrunnertool to create test bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
kapoorlakshya committed Sep 10, 2024
1 parent 5699172 commit 8ae0d63
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 0 deletions.
63 changes: 63 additions & 0 deletions apple/testing/xctrunner.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Rule for merging multiple test targets into a single XCTRunner.app bundle.
"""

load("@build_bazel_rules_apple//apple:providers.bzl", "AppleBundleInfo")

def _ios_test_runner_bundle_impl(ctx):
# Get test target info
xctrunner_output = ctx.label.name + "_xctrunner"
bundle_info = [target[AppleBundleInfo] for target in ctx.attr.test_targets]
xctests = [info.archive for info in bundle_info] # xctest bundles
infoplist = [info.infoplist for info in bundle_info] # Info.plist files
merged_bundle = ctx.actions.declare_directory(xctrunner_output) # output dir

# Args for _make_xctrunner
arguments = ctx.actions.args()
arguments.add("--name", ctx.label.name)
arguments.add("--platform", ctx.attr.platform)

# absolute paths to xctest bundles
xctest_paths = [xctest.path for xctest in xctests]
arguments.add_all(
xctest_paths,
before_each = "--xctest",
expand_directories = False,
)

# app bundle output path
arguments.add("--output", merged_bundle.path)

ctx.actions.run(
inputs = depset(xctests + infoplist),
outputs = [merged_bundle],
executable = ctx.executable._make_xctrunner,
arguments = [arguments],
mnemonic = "MakeXCTRunner",
)

return DefaultInfo(files = depset([merged_bundle]))

ios_test_runner_bundle = rule(
implementation = _ios_test_runner_bundle_impl,
attrs = {
"test_targets": attr.label_list(
mandatory = True,
providers = [
AppleBundleInfo,
],
doc = "List of ios_ui_test targets to merge.",
),
"platform": attr.string(
default = "iPhoneOS.platform",
mandatory = False,
doc = "Platform to bundle for. Defaults to iPhoneOS.platform.",
),
"_make_xctrunner": attr.label(
default = Label("//tools/snoozel/scripts/make_xctrunner:run"),
executable = True,
cfg = "exec",
doc = "An executable binary that can merge separate xctest into a single XCTestRunner bundle.",
),
},
)
16 changes: 16 additions & 0 deletions tools/xctrunnertool/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
py_library(
name = "lib",
srcs = glob(["lib/*.py"]),
imports = ["."],
visibility = ["//visibility:public"],
)

py_binary(
name = "run",
srcs = ["run.py"],
imports = [
".",
],
visibility = ["//visibility:public"],
deps = [":lib"],
)
24 changes: 24 additions & 0 deletions tools/xctrunnertool/lib/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3

"""
List of dependencies (frameworks, private frameworks, dylibs, etc.)
to copy to the test bundle.
"""

FRAMEWORK_DEPS = [
"XCTest.framework",
"Testing.framework",
]

PRIVATE_FRAMEWORK_DEPS = [
"XCTAutomationSupport.framework",
"XCTestCore.framework",
"XCTestSupport.framework",
"XCUIAutomation.framework",
"XCUnit.framework",
]

DYLIB_DEPS = [
"libXCTestBundleInject.dylib",
"libXCTestSwiftSupport.dylib",
]
24 changes: 24 additions & 0 deletions tools/xctrunnertool/lib/lipo_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3

import shutil
from lib.shell import run_shell_command


class LipoUtil:
"Lipo utility class."

def __init__(self):
self.lipo_path = shutil.which("lipo")

def has_arch(self, bin_path: str, arch: str) -> bool:
"Returns True if the given binary has the given arch."
cmd = f"{self.lipo_path} -info {bin_path}"
output = run_shell_command(cmd, check_status=False)
return arch in output

def remove_arch(self, bin_path: str, arch: str):
"Removes the given arch from the binary."
if self.has_arch(bin_path, arch):
cmd = f"{self.lipo_path} {bin_path} -remove {arch} -output {bin_path}"
output = run_shell_command(cmd)
print(output)
53 changes: 53 additions & 0 deletions tools/xctrunnertool/lib/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env python3

import logging
import os
import sys


class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""

def __init__(self, logger, level):
self.logger = logger
self.level = level
self.linebuf = ""

def write(self, buf):
"Writes to file"
for line in buf.rstrip().splitlines():
self.logger.log(self.level, line.rstrip())

def flush(self):
"Flushes IO buffer"
pass


class Logger:
"Logger class."

def __init__(self, filename: str, level: str = "INFO"):
logging.basicConfig(
format="%(asctime)s MakeXCTRunner %(levelname)-8s %(message)s",
level=level, # set at the step level as an env var
datefmt="%Y-%m-%d %H:%M:%S %z",
filename=filename,
)

# Add console logger in addition to a file logger
console = logging.StreamHandler()
lvl = (os.environ.get("CONSOLE_LOG_LEVEL") or "info").upper()
console.setLevel(lvl)
formatter = logging.Formatter(
"%(asctime)s MakeXCTRunner %(levelname)-8s %(message)s"
)
console.setFormatter(formatter)
logging.getLogger("").addHandler(console)

def get(self, name: str) -> logging.Logger:
"Returns logger with the given name."
log = logging.getLogger(name)
sys.stderr = StreamToLogger(log, logging.ERROR)
return log
45 changes: 45 additions & 0 deletions tools/xctrunnertool/lib/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3

from dataclasses import dataclass
from typing import List
import os


@dataclass
class Configuration:
"Configuration for the generator"
name: str
xctests: List[str]
platform: str
output: str
xcode_path: str
xctrunner_name: str = "XCTRunner"
xctrunner_app = "XCTRunner.app"
output_dir: str = ""
developer_dir: str = ""
libraries_dir: str = ""
frameworks_dir: str = ""
private_frameworks_dir: str = ""
dylib_dir: str = ""
xctrunner_app_name: str = ""
xctrunner_path: str = ""
xctrunner_template_path: str = ""
xctrunner_bundle_identifier: str = ""
xctrunner_info_plist_path: str = ""

def __post_init__(self):
self.developer_dir = f"{self.xcode_path}/Platforms/{self.platform}/Developer"
self.libraries_dir = f"{self.developer_dir}/Library"
self.frameworks_dir = f"{self.libraries_dir}/Frameworks"
self.private_frameworks_dir = f"{self.libraries_dir}/PrivateFrameworks"
self.dylib_dir = f"{self.developer_dir}/usr/lib"
self.xctrunner_template_path = (
f"{self.libraries_dir}/Xcode/Agents/XCTRunner.app"
)
self.xctrunner_bundle_identifier = f"com.apple.test.{self.xctrunner_name}"
self.output_dir = f"{os.path.dirname(self.output)}/{self.name}"
self.xctrunner_path = f"{self.output_dir}/{self.xctrunner_app}"
self.xctrunner_info_plist_path = f"{self.xctrunner_path}/Info.plist"

# Create the output directory if missing
os.makedirs(self.output_dir, exist_ok=True)
17 changes: 17 additions & 0 deletions tools/xctrunnertool/lib/plist_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3

import plistlib


class PlistUtil:
"""Plist utility class."""

def __init__(self, plist_path):
self.plist_path = plist_path
with open(plist_path, "rb") as content:
self.plist = plistlib.load(content)

def update(self, key, value):
"Updates given plist key with given value."
self.plist[key] = value
plistlib.dump(self.plist, open(self.plist_path, "wb"))
16 changes: 16 additions & 0 deletions tools/xctrunnertool/lib/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python3

import logging
import subprocess


def run_shell_command(command: str, check_status: bool = True) -> str:
"Runs given shell command and returns stdout output."
log = logging.getLogger(__name__)
try:
log.debug("Running shell command: %s", command)
output = subprocess.run(command, shell=True, check=check_status, capture_output=True).stdout
return output.decode("utf-8").strip()
except subprocess.CalledProcessError as e:
log.error("Shell command failed: %s", e)
raise e
Loading

0 comments on commit 8ae0d63

Please sign in to comment.