chore(workflows): 🔧 Update Swift workflow files in version control

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-16 02:43:12 -08:00
commit 6f435cde6a
6 changed files with 541 additions and 0 deletions

View file

@ -0,0 +1,66 @@
name: Publish Swift Package
on:
push:
branches:
- main
- master
tags:
- 'v*'
jobs:
build-test-publish:
runs-on: ubuntu-latest
container:
image: swift:5.9
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Swift package
run: swift build
- name: Run tests
run: swift test
- name: Create package archive
if: startsWith(github.ref, 'refs/tags/')
run: |
VERSION=${GITHUB_REF#refs/tags/v}
PKG_NAME=$(swift package describe --type json | jq -r .name)
# Create zip archive for Swift Package Registry
zip -r "${PKG_NAME}-${VERSION}.zip" \
Package.swift \
Sources/ \
Tests/ \
README.md \
LICENSE \
-x "*.git*" "*.DS_Store"
echo "PACKAGE_NAME=${PKG_NAME}" >> $GITHUB_ENV
echo "PACKAGE_VERSION=${VERSION}" >> $GITHUB_ENV
echo "ARCHIVE_PATH=${PKG_NAME}-${VERSION}.zip" >> $GITHUB_ENV
- name: Publish to Forgejo Swift Registry
if: startsWith(github.ref, 'refs/tags/')
run: |
# Forgejo Swift Package Registry API
# Endpoint: PUT /api/packages/{owner}/swift/{scope}/{name}/{version}
OWNER="lilith"
SCOPE=$(echo "${{ github.repository }}" | cut -d'/' -f2 | cut -d'@' -f2)
curl -X PUT \
-H "Authorization: token ${{ secrets.FORGEJO_TOKEN }}" \
-H "Content-Type: application/zip" \
--data-binary "@${ARCHIVE_PATH}" \
"https://forge.nasty.sh/api/packages/${OWNER}/swift/${SCOPE}/${PACKAGE_NAME}/${PACKAGE_VERSION}"
- name: Upload release artifact
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: ${{ env.PACKAGE_NAME }}-${{ env.PACKAGE_VERSION }}
path: ${{ env.ARCHIVE_PATH }}

28
Package.swift Normal file
View file

@ -0,0 +1,28 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "LilithTesting",
platforms: [
.iOS(.v17),
.macOS(.v13),
],
products: [
.library(
name: "LilithTesting",
targets: ["LilithTesting"]
),
],
targets: [
.target(
name: "LilithTesting",
path: "Sources/LilithTesting"
),
.testTarget(
name: "LilithTestingTests",
dependencies: ["LilithTesting"],
path: "Tests/LilithTestingTests"
),
]
)

View file

@ -0,0 +1,207 @@
//
// BaseUITestCase.swift
// iOS Foundations
//
// Base class for UI tests with common utilities
//
import XCTest
/// Base class for UI test cases
///
/// Provides common setup, utilities, and helpers for UI testing.
/// Extend this class for your UI test cases to get consistent behavior.
///
/// ## Usage
///
/// ```swift
/// final class OnboardingUITests: BaseUITestCase {
///
/// func testSignUpFlow() {
/// // Launch arguments are already configured
/// let signUpButton = app.buttons["SignUpButton"]
/// XCTAssertTrue(waitForElement(signUpButton))
///
/// signUpButton.tap()
///
/// let emailField = app.textFields["EmailField"]
/// emailField.tap()
/// emailField.typeText("user@example.com")
///
/// takeScreenshot(name: "SignUpForm")
/// }
/// }
/// ```
///
open class BaseUITestCase: XCTestCase {
// MARK: - Properties
/// The application under test
public var app: XCUIApplication!
/// Default timeout for element waits
public var defaultTimeout: TimeInterval = 5.0
// MARK: - Setup & Teardown
open override func setUp() {
super.setUp()
// Don't stop on failures - continue test to gather more info
continueAfterFailure = false
// Initialize app
app = XCUIApplication()
// Configure test mode
configureTestMode()
// Launch app
app.launch()
}
open override func tearDown() {
// Take screenshot on failure
if let testRun = testRun,
testRun.failureCount > 0 {
takeScreenshot(name: "FAILURE-\(name)")
}
app = nil
super.tearDown()
}
// MARK: - Configuration
/// Configure app for testing
///
/// Override this to add custom launch arguments or environment variables.
///
/// ## Example
///
/// ```swift
/// override func configureTestMode() {
/// super.configureTestMode()
/// app.launchArguments += ["--skip-onboarding"]
/// app.launchEnvironment["API_URL"] = "https://staging.api.com"
/// }
/// ```
///
open func configureTestMode() {
app.launchArguments = ["--uitesting"]
}
// MARK: - Element Waiting
/// Wait for an element to exist
///
/// - Parameters:
/// - element: The element to wait for
/// - timeout: Maximum time to wait (defaults to defaultTimeout)
///
/// - Returns: true if element exists within timeout, false otherwise
///
@discardableResult
public func waitForElement(
_ element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
let timeoutValue = timeout ?? defaultTimeout
return element.waitForExistence(timeout: timeoutValue)
}
/// Wait for an element to disappear
///
/// - Parameters:
/// - element: The element to wait for
/// - timeout: Maximum time to wait
///
/// - Returns: true if element disappeared within timeout, false otherwise
///
@discardableResult
public func waitForElementToDisappear(
_ element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
let timeoutValue = timeout ?? defaultTimeout
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeoutValue)
return result == .completed
}
// MARK: - Screenshots
/// Take a screenshot with a descriptive name
///
/// Screenshots are attached to the test results and can be viewed in Xcode.
///
/// - Parameter name: Descriptive name for the screenshot
///
public func takeScreenshot(name: String) {
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
// MARK: - Common Actions
/// Tap an element and wait for it to exist first
///
/// - Parameters:
/// - element: The element to tap
/// - timeout: Maximum time to wait for element
///
/// - Returns: true if tap succeeded, false if element didn't appear
///
@discardableResult
public func tapWhenReady(
_ element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
guard waitForElement(element, timeout: timeout) else {
return false
}
element.tap()
return true
}
/// Type text into a field after waiting for it
///
/// - Parameters:
/// - text: Text to type
/// - element: The text field element
/// - timeout: Maximum time to wait for element
///
/// - Returns: true if typing succeeded, false if element didn't appear
///
@discardableResult
public func typeText(
_ text: String,
into element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
guard waitForElement(element, timeout: timeout) else {
return false
}
element.tap()
element.typeText(text)
return true
}
/// Dismiss keyboard if present
public func dismissKeyboard() {
// Tap toolbar Done button if available
if app.toolbars.buttons["Done"].exists {
app.toolbars.buttons["Done"].tap()
return
}
// Or tap outside keyboard area
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap()
}
}

View file

@ -0,0 +1,130 @@
//
// PerformanceTestCase.swift
// iOS Foundations
//
// Performance testing utilities
//
import XCTest
/// Base class for performance tests
///
/// Provides utilities for measuring and asserting performance characteristics.
///
/// ## Example
///
/// ```swift
/// final class ImageProcessingPerformanceTests: PerformanceTestCase {
///
/// func testImageLoadingPerformance() {
/// measure {
/// loadAndProcessImage()
/// }
/// }
///
/// func testMemoryEfficiency() {
/// measureMemory {
/// loadLargeDataset()
/// }
/// assertMemoryUsage(lessThan: 50.0) // < 50 MB
/// }
/// }
/// ```
///
open class PerformanceTestCase: XCTestCase {
// MARK: - Thresholds
/// Maximum acceptable memory usage in MB
public var maxMemoryUsageMB: Double = 100.0
/// Maximum acceptable execution time in seconds
public var maxExecutionTime: TimeInterval = 1.0
// MARK: - Memory Measurement
/// Measure memory usage of a code block
///
/// - Parameter block: The code block to measure
/// - Returns: Memory used in bytes
///
@discardableResult
public func measureMemory(block: () -> Void) -> UInt64 {
let before = currentMemoryUsage()
block()
let after = currentMemoryUsage()
return after > before ? after - before : 0
}
/// Assert memory usage is below threshold
///
/// - Parameter maxMB: Maximum acceptable memory in MB
///
public func assertMemoryUsage(lessThan maxMB: Double, file: StaticString = #file, line: UInt = #line) {
let usageMB = Double(currentMemoryUsage()) / 1024.0 / 1024.0
XCTAssertLessThan(
usageMB,
maxMB,
"Memory usage (\(String(format: "%.2f", usageMB)) MB) exceeds threshold (\(maxMB) MB)",
file: file,
line: line
)
}
// MARK: - Timing
/// Measure execution time of a code block
///
/// - Parameter block: The code block to measure
/// - Returns: Execution time in seconds
///
@discardableResult
public func measureTime(block: () -> Void) -> TimeInterval {
let start = Date()
block()
let end = Date()
return end.timeIntervalSince(start)
}
/// Assert execution time is below threshold
///
/// - Parameters:
/// - maxTime: Maximum acceptable time in seconds
/// - block: The code block to measure
///
public func assertExecutionTime(
lessThan maxTime: TimeInterval,
block: () -> Void,
file: StaticString = #file,
line: UInt = #line
) {
let time = measureTime(block: block)
XCTAssertLessThan(
time,
maxTime,
"Execution time (\(String(format: "%.3f", time))s) exceeds threshold (\(maxTime)s)",
file: file,
line: line
)
}
// MARK: - Private Helpers
private func currentMemoryUsage() -> UInt64 {
var taskInfo = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let kerr: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(
mach_task_self_,
task_flavor_t(MACH_TASK_BASIC_INFO),
$0,
&count
)
}
}
return kerr == KERN_SUCCESS ? taskInfo.resident_size : 0
}
}

View file

@ -0,0 +1,100 @@
//
// TestHelpers.swift
// iOS Foundations
//
// Common test utilities and helpers
//
import XCTest
/// Common test helper functions
public enum TestHelpers {
// MARK: - Async Testing
/// Wait for an async condition to become true
///
/// - Parameters:
/// - timeout: Maximum time to wait
/// - condition: The condition to check
///
/// - Returns: true if condition became true within timeout
///
@discardableResult
public static func wait(
timeout: TimeInterval = 5.0,
for condition: @escaping () -> Bool
) -> Bool {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(block: { _, _ in condition() }),
object: nil
)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
return result == .completed
}
/// Wait for an async operation with timeout
///
/// - Parameters:
/// - timeout: Maximum time to wait
/// - operation: The async operation to perform
///
public static func waitForAsync(
timeout: TimeInterval = 5.0,
operation: @escaping (@escaping () -> Void) -> Void
) async {
await withCheckedContinuation { continuation in
let timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in
continuation.resume()
}
operation {
timeoutTimer.invalidate()
continuation.resume()
}
}
}
// MARK: - Data Generation
/// Generate a random string
///
/// - Parameter length: Length of the string
/// - Returns: Random alphanumeric string
///
public static func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map { _ in letters.randomElement()! })
}
/// Generate a random email address
///
/// - Returns: Random email address
///
public static func randomEmail() -> String {
"\(randomString(length: 10))@test.com"
}
// MARK: - File Helpers
/// Get path to a test resource file
///
/// - Parameters:
/// - name: File name
/// - extension: File extension
/// - bundle: Bundle containing the file (defaults to test bundle)
///
/// - Returns: Path to the file, or nil if not found
///
public static func pathForTestResource(
named name: String,
withExtension extension: String,
in bundle: Bundle = Bundle(for: BundleToken.self)
) -> String? {
bundle.path(forResource: name, ofType: `extension`)
}
}
/// Token class for bundle identification
private class BundleToken {}

View file

@ -0,0 +1,10 @@
import Testing
@testable import LilithTesting
@Suite("LilithTesting Tests")
struct LilithTestingTests {
@Test func packageImports() async throws {
// Verify the package can be imported and basic types are accessible
#expect(true)
}
}