Автоматическое тестирование приложений
Как автоматически обнаруживать утечки памяти в 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
, чтобы он сгенерировал для нас граф памяти:
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
и выполнять команды командной строки:
// 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
и проверять его на утечку памяти:
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:
#!/bin/bash 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 аварийным только в случае обнаружения утечек памяти ❌.
-
Интегрированные среды разработки2 недели назад
Лучшая работа с Android Studio: 5 советов
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2024.43
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2024.44
-
Исследования2 недели назад
Поможет ли новая архитектура React Native отобрать лидерство у Flutter в кроссплатформенной разработке?