Merge upstream/main into main

This commit is contained in:
Letro Bot
2026-05-09 14:51:17 +04:00
2683 changed files with 42649 additions and 27674 deletions

View File

@@ -47,12 +47,12 @@ struct TemplateScreen_Previews: PreviewProvider, TestablePreview {
static let incrementedViewModel = makeViewModel(counterValue: 1)
static var previews: some View {
NavigationStack {
ElementNavigationStack {
TemplateScreen(context: viewModel.context)
}
.previewDisplayName("Initial")
NavigationStack {
ElementNavigationStack {
TemplateScreen(context: incrementedViewModel.context)
}
.previewDisplayName("Incremented")

View File

@@ -9,7 +9,7 @@ struct AccessibilityTests: AsyncParsableCommand {
var device = "iPhone 17"
@Option(help: "iOS version for the simulator.")
var osVersion = "26.1"
var osVersion = "26.4.1"
func run() async throws {
var testsFailed = false

View File

@@ -6,6 +6,7 @@ import Yams
struct CI: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "CI workflow commands that can be run both locally and in CI environments.",
subcommands: [
PreviewTests.self,
AccessibilityTests.self,
UnitTests.self,
UITests.self,

View File

@@ -13,7 +13,7 @@ struct IntegrationTests: AsyncParsableCommand {
var device = "iPhone 17"
@Option(help: "iOS version for the simulator.")
var osVersion = "26.1"
var osVersion = "26.4.1"
func run() async throws {
// Delete old log files

View File

@@ -0,0 +1,88 @@
import ArgumentParser
import Foundation
struct PreviewTests: AsyncParsableCommand {
static let configuration = CommandConfiguration(commandName: "preview-tests",
abstract: "Runs the preview test CI workflow, with optional snapshot recording.")
@Flag(help: "Re-record snapshots for tests that fail or are missing a reference image.")
var record = false
private static let scheme = "PreviewTests"
private static let device = "iPhone SE (3rd generation)"
private static let osVersion = "26.4.1"
private static let simulatorType = "com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation"
private static let testPlanPath = "PreviewTests/SupportingFiles/PreviewTests.xctestplan"
func run() async throws {
if record {
try setRecordFailures(enabled: true)
}
var testsFailed = false
do {
logger.info("\n🧪 Running preview tests…\n")
try await RunTests.parse([
"--scheme", Self.scheme,
"--device", Self.device,
"--os-version", Self.osVersion,
"--create-simulator-name", Self.device,
"--create-simulator-type", Self.simulatorType
]).run()
} catch {
if record {
// In recording mode, test failures are expected swift-snapshot-testing marks
// recording runs as failed. Check whether the xcresult bundle was created to
// distinguish genuine failures (compilation error, simulator issue) from the
// expected snapshot-recording "failures".
let resultBundleURL = URL.projectDirectory
.appending(path: "\(CI.testOutputDirectory)/\(Self.scheme).xcresult")
guard FileManager.default.fileExists(atPath: resultBundleURL.path) else {
logger.error("\n❌ Preview tests could not run. Check for compilation or configuration errors.\n")
throw error
}
logger.info("\n📸 Snapshots recorded.\n")
} else {
logger.error("\n❌ Preview tests failed.\n")
testsFailed = true
}
}
// Collect coverage and test results regardless of test outcome (best-effort).
await CI.collectCoverage(resultBundle: "\(Self.scheme).xcresult", outputName: "preview-cobertura.xml")
await CI.collectTestResults(resultBundle: "\(Self.scheme).xcresult", outputName: "preview-junit.xml")
if testsFailed {
throw ExitCode.failure
}
if !record {
logger.info("\n✅ Preview tests passed.\n")
}
}
// MARK: - Test Plan
/// Enables or disables the `RECORD_FAILURES` environment variable entry in the test plan.
private func setRecordFailures(enabled: Bool) throws {
let url = URL.projectDirectory.appendingPathComponent(Self.testPlanPath)
let data = try Data(contentsOf: url)
guard var plan = try JSONSerialization.jsonObject(with: data) as? [String: Any],
var defaultOptions = plan["defaultOptions"] as? [String: Any],
var envVars = defaultOptions["environmentVariableEntries"] as? [[String: Any]] else {
throw ValidationError("Could not parse test plan at \(Self.testPlanPath).")
}
for index in envVars.indices where envVars[index]["key"] as? String == "RECORD_FAILURES" {
envVars[index]["enabled"] = enabled
break
}
defaultOptions["environmentVariableEntries"] = envVars
plan["defaultOptions"] = defaultOptions
let jsonData = try JSONSerialization.data(withJSONObject: plan, options: [.prettyPrinted, .sortedKeys])
try jsonData.write(to: url)
}
}

View File

@@ -9,7 +9,7 @@ struct RunTests: AsyncParsableCommand {
Examples:
swift run tools run-tests --scheme UnitTests
swift run tools run-tests --scheme UITests --device iPhone --os-version 26.1
swift run tools run-tests --scheme UITests --device iPhone --os-version 26.4
swift run tools run-tests --scheme PreviewTests --create-simulator-name "iPhone SE (3rd generation)" \
--create-simulator-type com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation
""")
@@ -20,8 +20,12 @@ struct RunTests: AsyncParsableCommand {
@Option(help: "The simulator device name to run tests on (e.g. 'iPhone 17').")
var device = "iPhone 17"
@Option(help: "The iOS version to use for the simulator runtime (e.g. '26.1').")
var osVersion = "26.1"
@Option(help: "The iOS version to use for the simulator runtime (e.g. '26.4').")
var osVersion = "26.4.1"
var runtime: String {
osVersion.split(separator: ".").prefix(2).joined(separator: ".")
}
@Option(help: "Number of times to retry failed tests. Only the failing tests are re-run, not the entire suite.")
var retries = 0
@@ -48,7 +52,7 @@ struct RunTests: AsyncParsableCommand {
}
private var simulatorRuntime: String {
"com.apple.CoreSimulator.SimRuntime.iOS-\(osVersion.replacingOccurrences(of: ".", with: "-"))"
"com.apple.CoreSimulator.SimRuntime.iOS-\(runtime.replacingOccurrences(of: ".", with: "-"))"
}
func run() async throws {
@@ -81,9 +85,9 @@ struct RunTests: AsyncParsableCommand {
private func createSimulatorIfNecessary(name: String, type: String) async throws {
logger.info("Checking for simulator '\(name)'…")
guard let simulators = try await CI.run(.path("/bin/zsh"), ["-cu", "xcrun simctl list devices \"iOS \(osVersion)\" available"],
guard let simulators = try await CI.run(.path("/bin/zsh"), ["-cu", "xcrun simctl list devices \"iOS \(runtime)\" available"],
output: .string(limit: 4096)).standardOutput else {
logger.info("No simulators found for iOS \(osVersion). Creating '\(name)'…")
logger.info("No simulators found for iOS \(runtime). Creating '\(name)'…")
try await createSimulator(name: name, type: type)
return
}

View File

@@ -20,7 +20,7 @@ struct UITests: AsyncParsableCommand {
var deviceType: DeviceType
@Option(help: "iOS version for the simulator.")
var osVersion = "26.1"
var osVersion = "26.4.1"
@Option(help: "Run only a specific test (format: 'ClassName/testName').")
var testName: String?

View File

@@ -4,16 +4,13 @@ import Foundation
struct UnitTests: AsyncParsableCommand {
static let configuration = CommandConfiguration(commandName: "unit-tests",
abstract: "Runs the unit test CI workflow: lint, unit tests, preview tests, and result collection.")
@Option(help: "Device name for unit tests.")
var device = "iPhone 17"
@Option(help: "iOS version for the simulator.")
var osVersion = "26.1"
@Flag(help: "Skip preview tests")
var skipPreviews = false
private static let osVersion = "26.4.1"
private static let device = "iPhone 17"
func run() async throws {
try await CI.lint()
@@ -24,29 +21,21 @@ struct UnitTests: AsyncParsableCommand {
logger.info("\n🧪 Running unit tests…\n")
try await RunTests.parse([
"--scheme", "UnitTests",
"--device", device,
"--os-version", osVersion,
"--device", Self.device,
"--os-version", Self.osVersion,
"--retries", "3"
]).run()
} catch {
failures.append("Unit tests failed: \(error)")
failures.append("UnitTests")
logger.error("\n❌ Unit tests failed. \(error)\n")
}
if !skipPreviews {
// Run preview tests on a smaller device
do {
logger.info("\n🧪 Running preview tests\n")
try await RunTests.parse([
"--scheme", "PreviewTests",
"--device", "iPhone SE (3rd generation)",
"--os-version", osVersion,
"--create-simulator-name", "iPhone SE (3rd generation)",
"--create-simulator-type", "com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation"
]).run()
try await PreviewTests.parse([]).run()
} catch {
failures.append("Preview tests failed: \(error)")
logger.error("\n❌ Preview tests failed.\n")
failures.append("PreviewTests")
logger.error("\n❌ Preview tests failed. \(error)\n")
}
}
@@ -54,16 +43,13 @@ struct UnitTests: AsyncParsableCommand {
await CI.zipResults(bundles: ["UnitTests.xcresult", "PreviewTests.xcresult"],
outputName: "UnitTests.zip")
// Collect coverage reports
// Collect coverage and JUnit results for unit tests
await CI.collectCoverage(resultBundle: "UnitTests.xcresult", outputName: "unit-cobertura.xml")
await CI.collectCoverage(resultBundle: "PreviewTests.xcresult", outputName: "preview-cobertura.xml")
// Collect JUnit test results
await CI.collectTestResults(resultBundle: "UnitTests.xcresult", outputName: "unit-junit.xml")
await CI.collectTestResults(resultBundle: "PreviewTests.xcresult", outputName: "preview-junit.xml")
if !failures.isEmpty {
logger.error("\n\(failures.count) test suite(s) failed.\n")
let failedSuites = "[\(failures.joined(separator: ","))]"
logger.error("\n\(failures.count) test suite(s) failed \(failedSuites)\n")
throw ExitCode.failure
}

View File

@@ -10,6 +10,7 @@ struct DownloadStrings: ParsableCommand {
func run() throws {
try localazyDownload()
try sortStringsFiles()
try swiftgen()
}
@@ -17,6 +18,63 @@ struct DownloadStrings: ParsableCommand {
let arguments = allLanguages ? " all" : ""
try Zsh.run(command: "localazy download\(arguments)")
}
private func sortStringsFiles() throws {
let localizationsURL = URL(fileURLWithPath: "ElementX/Resources/Localizations")
let fileManager = FileManager.default
guard let enumerator = fileManager.enumerator(at: localizationsURL,
includingPropertiesForKeys: nil) else {
return
}
for case let fileURL as URL in enumerator {
switch fileURL.pathExtension {
case "strings":
try sortStringsFile(at: fileURL)
case "stringsdict":
try sortStringsdictFile(at: fileURL)
default:
break
}
}
}
private func sortStringsFile(at url: URL) throws {
let content = try String(contentsOf: url, encoding: .utf8)
let lines = content.components(separatedBy: .newlines)
let keyValueRegex = /^\s*".+"\s*=\s*".*";\s*$/
let keyValueLines = lines.filter { $0.wholeMatch(of: keyValueRegex) != nil }
guard !keyValueLines.isEmpty else { return }
let sortedLines = keyValueLines.sorted { lhs, rhs in
guard let lhsKey = extractKey(from: lhs),
let rhsKey = extractKey(from: rhs) else {
return lhs < rhs
}
return lhsKey.localizedStandardCompare(rhsKey) == .orderedAscending
}
let sortedContent = sortedLines.joined(separator: "\n") + "\n"
try sortedContent.write(to: url, atomically: true, encoding: .utf8)
}
private func sortStringsdictFile(at url: URL) throws {
let data = try Data(contentsOf: url)
let plist = try PropertyListSerialization.propertyList(from: data, format: nil)
let xmlData = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
try xmlData.write(to: url)
}
private func extractKey(from line: String) -> String? {
guard let openQuote = line.firstIndex(of: "\"") else { return nil }
let afterOpen = line.index(after: openQuote)
guard let closeQuote = line[afterOpen...].firstIndex(of: "\"") else { return nil }
return String(line[afterOpen..<closeQuote])
}
private func swiftgen() throws {
try Zsh.run(command: "swiftgen config run --config Tools/SwiftGen/swiftgen-config.yml")

View File

@@ -17,7 +17,7 @@ struct SetupProject: ParsableCommand {
}
func brewInstall() throws {
try Zsh.run(command: "brew install xcodegen swiftgen swiftformat git-lfs sourcery mint pkl kiliankoe/formulae/swift-outdated localazy/tools/localazy peripheryapp/periphery/periphery FelixHerrmann/tap/swift-package-list")
try Zsh.run(command: "brew install xcodegen swiftgen swiftformat git-lfs sourcery mint pkl kiliankoe/formulae/swift-outdated localazy/tools/localazy peripheryapp/periphery/periphery")
}
func mintPackagesInstall() throws {