Merge upstream/main into main
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
88
Tools/Sources/Commands/CI/PreviewTests.swift
Normal file
88
Tools/Sources/Commands/CI/PreviewTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user