chore(workflows): 🔧 Update Swift workflow files in version control
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
commit
6f435cde6a
6 changed files with 541 additions and 0 deletions
66
.forgejo/workflows/publish.yml
Normal file
66
.forgejo/workflows/publish.yml
Normal 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
28
Package.swift
Normal 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"
|
||||
),
|
||||
]
|
||||
)
|
||||
207
Sources/LilithTesting/BaseUITestCase.swift
Executable file
207
Sources/LilithTesting/BaseUITestCase.swift
Executable 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()
|
||||
}
|
||||
}
|
||||
130
Sources/LilithTesting/PerformanceTestCase.swift
Executable file
130
Sources/LilithTesting/PerformanceTestCase.swift
Executable 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
|
||||
}
|
||||
}
|
||||
100
Sources/LilithTesting/TestHelpers.swift
Executable file
100
Sources/LilithTesting/TestHelpers.swift
Executable 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 {}
|
||||
10
Tests/LilithTestingTests/LilithTestingTests.swift
Normal file
10
Tests/LilithTestingTests/LilithTestingTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue