Автоматическое тестирование приложений
Как автоматически обнаруживать утечки памяти в CI/CD с помощью UI-тестов
Хотя эта функция осталась незамеченной многими разработчиками, при правильном использовании она может стать мощным инструментом для автоматического обнаружения утечек памяти в ваших iOS-приложениях в CI/CD-средах.
Еще на WWDC21 и с запуском Xcode 13 Apple представила новую опцию xcodebuild
, которая генерирует граф памяти каждый раз, когда тест пользовательского интерфейса, измеряющий XCTMemoryMetrics
, терпит неудачу.
Флаг называется enablePerformanceTestsDiagnostics
, доступен только в xcodebuild
, а не в Xcode, и генерирует граф памяти для неудачных UI-тестов только в том случае, если тесты выполняются на физическом устройстве, а не на симуляторе.
Хотя эта функция осталась незамеченной многими разработчиками, при правильном использовании она может стать мощным инструментом для автоматического обнаружения утечек памяти в ваших iOS-приложениях в CI/CD-средах.
Написание UI-теста на использование памяти
Первое, что нам нужно сделать, это написать UI-тест, который измеряет использование памяти нашим приложением с помощью XCTMemoryMetric
:
import XCTest
final class AutomatedTestingUITests: XCTestCase {
func testMemoryLeaks() {
let app = XCUIApplication()
let options = XCTMeasureOptions()
options.invocationOptions = [.manuallyStart]
measure(metrics: [XCTMemoryMetric(application: app)], options: options) {
app.launch()
startMeasuring()
for _ in (0...3) {
let button = app.buttons["Cause a memory leak"].firstMatch
if button.waitForExistence(timeout: 5) {
button.tap()
let backButton = app.navigationBars.buttons.element(boundBy: 0)
if backButton.waitForExistence(timeout: 5) {
backButton.tap()
}
}
}
}
}
}
Чтобы упростить задачу и показать, как обнаружить утечку памяти с помощью UI-тестов, я создал простое приложение с кнопкой, которая переходит на экран и при этом создает утечку памяти.
Если мы запустим UI-тест в Xcode, то увидим серый индикатор рядом с вызовом метода measure
, говорящий о том, что мы еще не установили базовое измерение для теста. Поскольку целью этого теста является создание графа памяти и мы хотим, чтобы измерения всегда были неудачными, мы установим исходный уровень на очень низкое значение, которое всегда будет превышено:
Генерация графа памяти
Теперь, когда у нас есть тест, который всегда терпит неудачу, нам нужно вызвать его из командной строки с помощью xcodebuild
и флага enablePerformanceTestsDiagnostics
, чтобы он сгенерировал для нас граф памяти:
xxxxxxxxxx
xcodebuild test \
-project AutomatedTesting.xcodeproj \
-scheme AutomatedTesting \
-destination "platform=iOS,name=Pol Piella Abadia's iPhone" \
-enablePerformanceTestsDiagnostics YES \
-derivedDataPath ./derived_data \
-resultBundlePath TestResults
Поскольку мы указали кастомный путь вывода для бандла .xcresult
, мы можем просто найти результаты в том же каталоге, из которого мы вызывали команду, с именем TestResults
. Открыв пакет в Xcode, мы увидим, что тест завершился неудачей и что был сгенерирован граф памяти:
Посмотрев на граф памяти, мы увидим, что приложение имеет многочисленные утечки памяти:
Парсинг пакета результатов и графа памяти
Теперь, когда у нас есть способ генерировать графы памяти для наших UI-тестов, мы можем создать небольшой инструмент командной строки, который программно извлекает граф памяти из пакета .xcresult
и проверяет его содержимое на предмет утечек памяти.
Давайте начнем с создания Swift-пакета с одной исполняемой целью и несколькими зависимостями, которые помогут нам обрабатывать пользовательский ввод, анализировать содержимое пакета .xcresult
и выполнять команды командной строки:
xxxxxxxxxx
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "XCLeaks",
platforms: [
.macOS(.v13)
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.5.0"),
.package(url: "https://github.com/davidahouse/XCResultKit.git", exact: "1.2.0"),
.package(url: "https://github.com/JohnSundell/ShellOut.git", exact: "2.3.0")
],
targets: [
.executableTarget(
name: "XCLeaks",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "XCResultKit", package: "XCResultKit"),
.product(name: "ShellOut", package: "ShellOut")
]
),
]
)
Я не буду подробно останавливаться на том, как использовать библиотеку XCResultKit
для экспорта вложений, так как ранее я уже писал статью, в которой эта тема рассматривается очень подробно.
Теперь давайте напишем главный файл нашего исполняемого файла, который будет разбирать содержимое пакета .xcresult
и проверять его на утечку памяти:
xxxxxxxxxx
import Foundation
import ArgumentParser
import XCResultKit
import ShellOut
@main
struct XCLeaks: ParsableCommand {
// 1
@Argument(help: "The path to an `.xcresult` bundle")
var bundle: String
func run() throws {
guard let url = URL(string: bundle) else { return }
// 2
let result = XCResultFile(url: url)
guard let invocationRecord = result.getInvocationRecord() else { return }
// 3
let testBundles = invocationRecord
.actions
.compactMap { action -> ActionTestPlanRunSummaries? in
guard let id = action.actionResult.testsRef?.id, let summaries = result.getTestPlanRunSummaries(id: id) else {
return nil
}
return summaries
}
.flatMap(\.summaries)
.flatMap(\.testableSummaries)
let allFailingTests = testBundles
.flatMap(\.tests)
.flatMap(\.subtestGroups)
.flatMap(\.subtestGroups)
.flatMap(\.subtests)
.filter { $0.testStatus.lowercased() == "failure" }
// 4
let memoryGraphAttachments = allFailingTests
.compactMap { test -> ActionTestSummary? in
guard let id = test.summaryRef?.id else { return nil }
return result.getActionTestSummary(id: id)
}
.flatMap(\.activitySummaries)
.filter { $0.title.contains("Added attachment named") && $0.title.contains(".memgraphset.zip") }
.flatMap(\.attachments)
// 5
for attachment in memoryGraphAttachments {
// 6
let url = URL.temporaryDirectory
let filePath = url.appending(path: attachment.filename ?? "")
result.exportAttachment(attachment: attachment, outputPath: url.path(percentEncoded: false))
// 7
try shellOut(
to: "tar",
arguments: [
"-zxvf",
"\"\(filePath.path(percentEncoded: false))\"",
"-C",
url.path(percentEncoded: false)
]
)
// 8
guard let unzipped = (filePath.path(percentEncoded: false) as NSString)
.deletingPathExtension
.split(separator: "_")
.first else {
return
}
let unzippedAndEscaped = String(unzipped)
.replacingOccurrences(of: "(", with: "\\(")
.replacingOccurrences(of: ")", with: "\\)")
.replacingOccurrences(of: "[", with: "\\[")
.replacingOccurrences(of: "]", with: "\\]")
// 9
do {
try shellOut(to: "leaks", arguments: ["\(unzippedAndEscaped)/post_*"])
print("✅ No leaks found!")
} catch let error as ShellOutError {
let regex = /(?<numberOfLeaks>\d+)\s+leaks for/
if let output = try? regex.firstMatch(in: error.output) {
print("❌ Found \(output.numberOfLeaks) leaks")
exit(1)
} else {
print("✅ No leaks found!")
}
} catch let error {
print("🛑 Something else went wrong: \(error)")
}
}
}
}
В приведенном выше коде происходит много всего, поэтому давайте разберем его на части:
- Мы определяем аргумент командной строки, который позволит пользователям передавать путь к пакету
.xcresult
. - Мы создаем экземпляр
XCResultFile
с URL-адресом пакета.xcresult
и извлекаем список обращений, в которых мы найдем неудачные тесты. - Мы извлекаем неудачные тесты из вызовов.
- Извлекаем объекты вложений графа памяти из неудачных тестов.
- Выполняем итерации по вложениям с графами памяти.
- Экспортируем вложения графа памяти во временный каталог.
- Распаковываем вложение с графом памяти. Мы используем
ShellOut
для вызова исполняемогоtar
файла из командной строки и распаковки файла. Мы распаковываем файл в тот же каталог, что и вложение. - Мы извлекаем имя разархивированного файла, экранируя все специальные символы, которые могут присутствовать в имени.
- Запускаем инструмент командной строки
leaks
, чтобы прочитать содержимое графа памяти. Если граф памяти содержит какие-либо утечки, инструмент не срабатывает, поэтому мы перехватываем ошибку, а затем разбираем вывод с помощью регулярного выражения, чтобы извлечь количество найденных утечек и выйти с ошибкой.
Собираем все вместе
Теперь, когда у нас есть все необходимое, давайте соберем все вместе и посмотрим, как можно обнаружить утечки памяти в среде CI/CD:
xxxxxxxxxx
set -e
function test {
xcodebuild test \
-project AutomatedTesting.xcodeproj \
-scheme AutomatedTesting \
-destination "platform=iOS,name=Pol Piella Abadia’s iPhone" \
-enablePerformanceTestsDiagnostics YES \
-derivedDataPath ./derived_data \
-resultBundlePath TestResults
}
function leaks {
swift run \
--package-path xcleaks/ \
XCLeaks \
$(pwd)/TestResults.xcresult
}
test || leaks
Приведенная выше команда обеспечит проверку графа памяти при каждом сбое UI-теста и сделает конвейер CI/CD аварийным только в случае обнаружения утечек памяти ❌.
-
Программирование3 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков6 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8