Как проверить контракты кеша iOS в памяти на облачном Mac

Как проверить контракты кеша iOS в памяти на облачном Mac

Как проверить контракты кеша iOS в памяти на облачном Mac

Если после смены аккаунта карточка по-прежнему показывает старые данные, причина не обязательно в сетевом кеше. Возможно, объекты в памяти сохраняются только по идентификатору ресурса, без учёта аккаунта. Для регрессионной проверки такого поведения на облачном Mac ArmShell не нужно создавать дефицит памяти. Сначала закрепите в тестах идентичность ключей, снимок данных при записи, явную очистку и поведение при промахе, а вытеснение объектов системой исследуйте отдельно.

Сначала определите контракт кеша

В этой статье мы тестируем только внутрипроцессный NSCache. Дисковый кеш и кеш сетевых ответов здесь не рассматриваются. Подготовьте таргет модульных тестов iOS с подключённым XCTest и поместите приведённые ниже типы и тесты в один тестовый файл. При каждом запуске тестового процесса создавайте кеш заново, не используя общий глобальный синглтон.

Договоримся, что ключ состоит из аккаунта, идентификатора ресурса и версии содержимого. Одинаковые идентификаторы в разных аккаунтах не должны приводить к попаданию в одну и ту же запись. После смены версии старые данные не должны возвращаться как результат для новой версии. Значения сохраняются в виде Data: если после записи вызывающий код изменит свою переменную, снимок в кеше должен остаться прежним.

Кеш — это ускоряющий слой, содержимое которого можно потерять, а не источник истины. Любое чтение должно допускать промах. Корректность бизнес-логики не должна зависеть от того, остаётся ли объект в кеше.

Пароли, токены и полные профили пользователей в этих тестах не используются. Для фикстур берём только короткие тексты, а в журнал ошибок записываем лишь имя тестового случая и наличие попадания в кеш.

Включите в ключ все составляющие идентичности

Не стоит просто соединять три поля дефисами: если разделитель встречается внутри самих полей, результат может оказаться неоднозначным. Ниже идентичность ключа задаётся через равенство объектов. Коллизии хешей допустимы, но проверка равенства должна сравнивать каждое поле. NSCache не копирует ключи за вас, поэтому их поля должны быть неизменяемыми.

import Foundation
import XCTest

final class MemoryKey: NSObject {
    let account: String
    let resource: String
    let revision: String

    init(_ account: String, _ resource: String, _ revision: String) {
        self.account = account
        self.resource = resource
        self.revision = revision
    }

    override var hash: Int {
        var hasher = Hasher()
        hasher.combine(account)
        hasher.combine(resource)
        hasher.combine(revision)
        return hasher.finalize()
    }

    override func isEqual(_ object: Any?) -> Bool {
        guard let other = object as? MemoryKey else { return false }
        return account == other.account
            && resource == other.resource
            && revision == other.revision
    }
}

@MainActor
final class SessionMemoryCache {
    private let storage = NSCache<MemoryKey, NSData>()

    init() {
        storage.totalCostLimit = 4 * 1024 * 1024
    }

    func store(_ data: Data, for key: MemoryKey) {
        storage.setObject(
            NSData(data: data),
            forKey: key,
            cost: data.count
        )
    }

    func read(_ key: MemoryKey) -> Data? {
        guard let value = storage.object(forKey: key) else { return nil }
        return Data(referencing: value)
    }

    func clear() {
        storage.removeAllObjects()
    }
}

В примере обёртка изолирована главным актором, чтобы её было удобнее использовать согласованно с состоянием интерфейса. Это не означает, что к самому NSCache можно обращаться только из главного потока. Если нужен фоновый доступ, отдельно определите модель изоляции, а не просто снимайте ограничение.

Проверьте изоляцию, версии и снимки данных

В первом тесте мы намеренно используем один идентификатор ресурса для разных аккаунтов, а читаем данные с помощью новых, равных исходным ключей. Так тест проверяет не только то, что значение можно получить обратно с тем же объектом ключа. Во втором тесте изменяем исходный Data, затем проверяем значение в кеше и явную очистку.

final class SessionMemoryCacheTests: XCTestCase {
    @MainActor
    func testIdentityIsolation() {
        let cache = SessionMemoryCache()
        cache.store(Data([1]), for: MemoryKey("a", "card", "v1"))
        cache.store(Data([2]), for: MemoryKey("b", "card", "v1"))

        XCTAssertEqual(
            cache.read(MemoryKey("a", "card", "v1")), Data([1])
        )
        XCTAssertEqual(
            cache.read(MemoryKey("b", "card", "v1")), Data([2])
        )
        XCTAssertNil(cache.read(MemoryKey("a", "card", "v2")))
    }

    @MainActor
    func testSnapshotAndClear() {
        let cache = SessionMemoryCache()
        let key = MemoryKey("a", "card", "v1")
        var source = Data([1, 2])
        cache.store(source, for: key)
        source.append(3)

        XCTAssertEqual(cache.read(key), Data([1, 2]))
        cache.clear()
        XCTAssertNil(cache.read(key))
    }
}

Эти небольшие объекты предназначены для функциональных регрессионных проверок при нормальной доступности ресурсов. Если система всё же вытеснит объект, проверка ожидаемого попадания может завершиться неудачей. Для строго детерминированных тестов бизнес-логики следует внедрять тестовый двойник кеша на основе словаря. Не ставьте весь набор тестов в зависимость от того, сохранит ли система объекты.

Также добавьте сценарий перезаписи по тому же ключу: после последовательной записи двух значений чтение должно вернуть данные второй записи. Пустой Data и промах тоже нужно различать: первый — допустимое значение, второй — nil.

Не считайте настройки ёмкости алгоритмом вытеснения

В примере стоимость измеряется в байтах, поскольку при каждой записи передаётся data.count. Единицу измерения totalCostLimit задаёт вызывающий код; это не автоматически подсчитанный объём памяти процесса. Сами объекты, ключи и контейнер создают дополнительные накладные расходы. Если изображение хранится в декодированном виде, размер сжатого файла также нельзя использовать как его фактическую стоимость.

Ни countLimit, ни totalCostLimit не являются жёсткими ограничениями. Нельзя добавить несколько объектов и затем утверждать, что самый ранний из них обязательно исчезнет. Нельзя и предполагать, что NSCache реализует детерминированную стратегию LRU — вытеснение объекта, к которому дольше всего не обращались. В экспериментах под нагрузкой фиксируйте только долю попаданий и изменение потребления памяти процессом. Не превращайте их результаты в неизменное условие, которое должно выполняться перед каждым слиянием изменений.

Также не пытайтесь вызвать очистку системой, бесконечно выделяя большие массивы. Скорее всего, раньше будет завершён тестовый процесс. Такой результат свидетельствует об исчерпании ресурсов, а не о соблюдении контракта кеша.

Включите промахи и выход из аккаунта в приёмочные проверки

Внедрите в слой чтения бизнес-логики тестовый двойник кеша, который всегда возвращает nil, и убедитесь, что загрузчик всё равно получает данные из авторитетного источника. Затем внедрите двойник с уже сохранённым значением и проверьте, что ветка, в которой разрешено использовать кеш, не выполняет повторную загрузку. Так можно проверить оба пути напрямую, не дожидаясь реального вытеснения.

При выходе из аккаунта сначала остановите запросы старого аккаунта и обновите идентичность сеанса, а затем очистите кеш. Одной очистки недостаточно: если старый запрос завершится позже, он может снова записать данные. Перед записью нужно сравнить поколение сеанса, зафиксированное запросом, с текущим поколением. Если они не совпадают, от записи следует отказаться. Это правило относится к слою координации загрузки и требует отдельного теста.

В завершение проверьте четыре пункта: берётся ли аккаунт для ключа из доверенного сеанса, меняется ли поле версии при обновлении содержимого, может ли старый запрос записать данные после выхода и завершается ли загрузка при промахе кеша. Стабильные проверки бизнес-логики включайте в повседневные регрессионные тесты, а нестабильные наблюдения под нагрузкой оставляйте для диагностики. Тогда наличие данных в кеше не станет скрытым условием, из-за которого возникают редкие сбои.

Часто задаваемые вопросы

Гарантирует ли countLimit точное число объектов?

Нет. countLimit и totalCostLimit не являются строгими пределами для точных проверок. Тестируйте восстановление при промахе кеша; строгая ёмкость и порядок удаления требуют отдельной детерминированной реализации.

Как проверить изоляцию кеша разных аккаунтов?

Включите пространство имён аккаунта в ключ и явно очищайте кеш при выходе. Запишите разные данные для двух аккаунтов с одинаковым идентификатором ресурса и проверьте раздельное чтение.

Подготовьте облачный Mac к следующей сборке

Выберите модель и срок аренды под свои задачи. Выделенный физический узел Apple Silicon, не виртуальная машина.

Арендовать облачный Mac

От чтения — к первой сборке

Подготовьте рабочую нагрузку и выберите подходящий выделенный физический узел.