Connect with us

Автоматическое тестирование приложений

Как автоматически обнаруживать утечки памяти в 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, мы увидим, что тест завершился неудачей и что был сгенерирован граф памяти:

Как автоматически обнаруживать утечки памяти в CI/CD с помощью UI-тестов

Посмотрев на граф памяти, мы увидим, что приложение имеет многочисленные утечки памяти:

Как автоматически обнаруживать утечки памяти в CI/CD с помощью UI-тестов

Парсинг пакета результатов и графа памяти

Теперь, когда у нас есть способ генерировать графы памяти для наших 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)")
            }
        }
    }
}

В приведенном выше коде происходит много всего, поэтому давайте разберем его на части:

  1. Мы определяем аргумент командной строки, который позволит пользователям передавать путь к пакету .xcresult.
  2. Мы создаем экземпляр XCResultFile с URL-адресом пакета .xcresult и извлекаем список обращений, в которых мы найдем неудачные тесты.
  3. Мы извлекаем неудачные тесты из вызовов.
  4. Извлекаем объекты вложений графа памяти из неудачных тестов.
  5. Выполняем итерации по вложениям с графами памяти.
  6. Экспортируем вложения графа памяти во временный каталог.
  7. Распаковываем вложение с графом памяти. Мы используем ShellOut для вызова исполняемогоtar файла из командной строки и распаковки файла. Мы распаковываем файл в тот же каталог, что и вложение.
  8. Мы извлекаем имя разархивированного файла, экранируя все специальные символы, которые могут присутствовать в имени.
  9. Запускаем инструмент командной строки 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 аварийным только в случае обнаружения утечек памяти ❌.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: