Команда экспортирует записи из iOS-приложения в CSV и запускает тесты на удалённом сборочном узле. Модульный тест сообщает, что файл успешно записан, но коллега открывает его и видит: примечание разбилось на две строки, а другое значение, введённое пользователем, превратилось в формулу электронной таблицы. Проверять здесь нужно не кнопку «Поделиться», а байты экспортированного файла, границы полей и обработку опасных значений. Облачный Mac позволяет запускать эти проверки при каждой сборке без ручных действий.
Сначала определите контракт экспорта
В этой статье используется чёткий контракт для текстового файла: кодировка UTF-8 без маркера порядка байтов; каждое поле заключено в двойные кавычки, а двойная кавычка внутри поля записывается дважды; записи разделяются CRLF, и файл тоже заканчивается CRLF. Переносы строк внутри поля сохраняются без изменений. Нельзя сначала разбить весь CSV по символам переноса строки, а потом подсчитать записи.
Есть и правило безопасности: если после удаления пробельных и управляющих символов по краям поле со свободным вводом начинается с =, +, - или @, экспорт завершается ошибкой. Такое правило отклонит и некоторые отрицательные числа, которые могли бы быть обычным текстом. Если продукту действительно нужны числовые столбцы, значения следует разбирать и проверять согласно определению каждого столбца, а не ослаблять правило для всех текстовых полей. Заголовки столбцов проходят через ту же функцию кодирования, чтобы не поддерживать две разные реализации экранирования.
Кавычки в CSV определяют границы полей, но не управляют тем, как электронная таблица распознаёт формулы. Добавление апострофа перед текстом не обязательно даст одинаковый результат в разных средствах импорта. Отказ от опасных значений проще определить и протестировать, но поведение целевого средства импорта всё равно нужно проверить.
Генерируйте проверяемые байты одной функцией
Поместите кодировщик в Swift-файл, доступный и приложению, и тестовому таргету. Функция ниже принимает двумерный массив строк; в реальном приложении бизнес-модель можно предварительно преобразовать в столбцы заданного порядка. trimmingCharacters используется только для проверки риска: при выводе исходное значение сохраняется, поэтому пользовательские данные не меняются незаметно.
import Foundation
enum CSVExportError: Error {
case unsafeValue
}
func csvField(_ value: String) throws -> String {
let ignored = CharacterSet.whitespacesAndNewlines
.union(.controlCharacters)
let probe = value.trimmingCharacters(in: ignored)
if let first = probe.first, "=+-@".contains(first) {
throw CSVExportError.unsafeValue
}
let escaped = value.replacingOccurrences(of: "\"", with: "\"\"")
return "\"" + escaped + "\""
}
func csvDocument(_ rows: [[String]]) throws -> Data {
let lines = try rows.map { row in
try row.map(csvField).joined(separator: ",")
}
return Data((lines.joined(separator: "\r\n") + "\r\n").utf8)
}
Вызывающий код также должен проверять, соответствует ли число столбцов в каждой строке заголовку, решать, экспортировать ли для пустого набора данных только заголовок или показать сообщение пользователю, и преобразовывать ошибки в понятные уведомления. Не перехватывайте unsafeValue, чтобы затем записать непроверенный исходный текст: тесты могут пройти, а файл, которым поделится пользователь, обойдёт правило. Если экспорт содержит конфиденциальные данные, перед отправкой проверьте целевой файл и права доступа к нему.
Тестируйте и успешный экспорт, и отказ
Тесты должны сравнивать фактический результат, а не просто проверять, что Data не пуст. Первый пример ниже охватывает запятую, кавычки, китайские символы и перенос строки внутри поля; второй проверяет, что начальный символ табуляции не позволяет обойти защиту.
import XCTest
final class CSVExportTests: XCTestCase {
func testQuotingAndLineEndings() throws {
let data = try csvDocument([
["name", "note"],
["张三", "甲,\"乙\"\n丙"]
])
XCTAssertEqual(
String(decoding: data, as: UTF8.self),
"\"name\",\"note\"\r\n\"张三\",\"甲,\"\"乙\"\"\n丙\"\r\n"
)
}
func testFormulaLikeTextIsRejected() {
for value in ["=1+1", "\t@SUM(A1:A2)", "+1", " -2"] {
XCTAssertThrowsError(try csvDocument([[value]]))
}
}
}
Эти тесты подтверждают согласованный формат вывода и правило отказа, но не доказывают, что каждая электронная таблица импортирует файл как ожидается. Переносы строк внутри кавычек, текст, похожий на дату, и символы вне ASCII всё равно нужно проверять тем способом импорта, которым пользуются получатели файла.
Сохраняйте проверяемые результаты на сборочном узле
Сначала убедитесь, что на облачном Mac доступны инструменты командной строки Xcode, затем запустите тесты через общую для проекта схему. Замените имена в примере на реальные имена из репозитория; перед запуском можно проверить доступные схемы командой xcodebuild -list -project App.xcodeproj. Тестовый таргет должен напрямую обращаться к файлу с кодировщиком — не создавайте для тестов отдельную копию реализации.
xcodebuild test \
-project App.xcodeproj \
-scheme App \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:AppTests/CSVExportTests \
-resultBundlePath "$PWD/CSVExport.xcresult"
Имя симулятора должно соответствовать среде выполнения, установленной на этом узле. Проверить доступные устройства можно командой xcrun simctl list devices available. Путь CSVExport.xcresult не должен существовать до запуска; при повторных запусках задавайте отдельный каталог для каждого задания, чтобы старые результаты не мешали проверке. Этот тест проверяет только кодирование на Swift и не зависит от графического интерфейса. Если нужно также проверить сценарий отправки файла, отдельно подготовьте для UI-тестов сеанс и необходимые разрешения.
От тестовых данных — к реальному файлу
После прохождения модульных тестов создайте фиксированный образец через фактический путь экспорта в приложении. Он должен содержать как минимум пустое поле, запятую, двойную кавычку, перенос строки внутри поля и китайские символы. Убедитесь, что файл создан тем же csvDocument, а не собран повторно из строк в слое отправки. При сохранении в доступное приложению для записи место можно использовать Data.write(to:options: .atomic), чтобы файл не прочитали во время записи. Удаление временного файла после неудачной отправки тоже следует проверить в рамках сценария приложения.
Не судите об успехе только по «числу строк» в терминале: внутри кавычек переносы строк допустимы. Надёжнее импортировать образец и сверить каждое поле, порядок столбцов, последнюю запись и сообщения об ошибках. Если средство импорта автоматически превращает идентификатор в число или текст даты — в дату, это отдельный контракт формата. Его нужно обеспечивать явными типами столбцов или специализированным форматом обмена, а не рассчитывать, что кавычки CSV зафиксируют тип ячейки.
Контрольный список перед слиянием
- Число и порядок столбцов в заголовке и строках данных совпадают; пустые значения записываются согласно контракту.
- Для текста UTF-8, кавычек, запятых и переносов строк внутри полей есть точные проверки.
- Ввод, похожий на формулу, вызывает видимую ошибку; альтернативный файл с непроверенными данными не создаётся.
- CI использует симулятор, который действительно доступен на узле, и сохраняет отдельные результаты каждого запуска.
- Слой отправки читает уже протестированные байты; при ручной проверке в целевом средстве импорта зафиксированы все автоматические преобразования типов.
Самые трудные для обнаружения ошибки экспорта CSV часто скрываются не в типовом примере, а в примечании реального пользователя. Превратить пограничные значения в постоянные тесты и затем проверить итоговый файл гораздо надёжнее, чем искать ошибки на глаз уже после его создания.
Часто задаваемые вопросы
Нужно ли экранировать кавычки внутри уже заключённого в кавычки поля CSV?
Да. Каждую внутреннюю двойную кавычку удваивают, после чего всё поле заключают в двойные кавычки.
Защитит ли начальный апостроф от формул в любом табличном редакторе?
Нет. Поведение импортёров различается. Пример отклоняет подозрительные значения; импорт в целевом приложении проверяют отдельно.
Перенесите следующую сборку Xcode на выделенный физический сервер.
Выберите модель Apple Silicon, регион и срок аренды, а затем запускайте собственный процесс сборки через удалённое подключение. Фактическая доступность отображается в консоли при оформлении заказа.