Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .github/dule-test.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# OpenSwiftUI Copilot Test Authoring Rules

This file documents precise rules and templates for writing Tests and DualTests used across the OpenSwiftUI repository. Follow these rules exactly when generating new test cases.

## General principles
- Use the `swift-testing` framework and the `#expect` macro for all assertions.
- Do NOT use XCTest unless explicitly required for compatibility.
- Keep test function bodies free of comments.
- Use descriptive test function names (do not prefix with `test`).
- Use `@Test` for each test function.
- Group related tests with `// MARK: -`.
- Keep tests small and focused.
- Use `package` / access-level rules only inside implementation files — tests import the target.

## File layout and naming
- Test filenames should end with `Tests.swift`.
- Put project-only tests in `Tests/*Tests`.
- Put dual tests that compare against SwiftUI in `Tests/*SymbolDualTests`.
- Use `struct <FeatureName>Tests { ... }` to group tests.

## Test file headers and conditions

Project-only tests (call OpenSwiftUI APIs):
- Use compile condition matching the implementation (example: `#if os(iOS) && canImport(QuartzCore)`).
- Required imports:
- `import Testing`
- `@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore` (if testing internal SPI)
- platform framework imports (e.g., `import QuartzCore`)

DualTests (call into SwiftUI symbol via stub):
- Use a condition that ensures SwiftUI is available and matches the reference SDK:
- Example: `#if os(iOS) && canImport(SwiftUI, _underlyingVersion: 6.5.4)`
- Required imports:
- `import QuartzCore`
- `import Testing`
- Provide a `@_silgen_name` initializer (or function) declaration on the type to call the SwiftUI symbol:
- Example:
extension CAFrameRateRange {
@_silgen_name("OpenSwiftUITestStub_CAFrameRateRangeInitInterval")
init(swiftUI_interval: Double)
}
- Provide a C stub that publishes the SwiftUI symbol name for the test bundle (placed in SymbolDualTestsSupport target). The C file must define the same symbol name used in `@_silgen_name`.

## Test implementation rules
- Use `@Test(arguments: [...])` to run parameterized cases when appropriate.
- Argument arrays must be typed where necessary (e.g., `as [(Double, CAFrameRateRange)]`).
- In each `@Test` function:
- Instantiate or call the API under test.
- Use `#expect(...)` for every assertion.
- Do not include comments inside the test function body.
- Use exact equality checks where types are Equatable (e.g., `#expect(range == expected)`).

## Small templates

Project test template:
```swift
// ... file header and #if condition ...
import Testing
@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore
import QuartzCore

struct <FeatureName>Tests {
@Test(arguments: [
// example tuples
] as [(/* types */)])
func <scenario>(/* params */) {
let result = /* call OpenSwiftUI API */
#expect(result == expected)
}
}
```

Dual test template:
```swift
// ... file header and #if condition ...
import QuartzCore
import Testing

extension <TypeUnderTest> {
@_silgen_name("<C stub symbol name>")
init(swiftUI_<param>: /* type */)
}

struct <FeatureName>DualTests {
@Test(arguments: [
// example tuples
] as [(/* types */)])
func <scenario>(/* params */) {
let result = <TypeUnderTest>(swiftUI_<param>: input)
#expect(result == expected)
}
}
```

C stub template (SymbolDualTestsSupport target):
```c
// Provide a C stub that resolves to the SwiftUI initializer/function
// Use the same symbol name used in @_silgen_name in the DualTest
#include "OpenSwiftUIBase.h"

#if OPENSWIFTUI_TARGET_OS_IOS

#import <SymbolLocator.h>

DEFINE_SL_STUB_SLF(<C stub symbol name>, SwiftUICore, <mangled SwiftUI symbol>);

#endif
```

## Examples and important notes
- Do not include test-comments in function bodies.
- Keep parameterized arguments typed explicitly when the compiler cannot infer them.
- Match the platform compile conditions to the implementation file being tested.
- When comparing framework-provided types (e.g., `CAFrameRateRange`) rely on their Equatable conformance using `==`.
- Ensure the C stub symbol name and the `@_silgen_name` string are identical.

## Checklist before committing test code
- [ ] Uses `Testing` and `#expect` exclusively.
- [ ] No comments inside `@Test` function bodies.
- [ ] Proper compile-time conditions are present.
- [ ] DualTests have matching C stubs in the SymbolDualTestsSupport target.
- [ ] Test file and struct names follow repository conventions.
2 changes: 1 addition & 1 deletion Example/HostingExample/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ class ViewController: NSViewController {

struct ContentView: View {
var body: some View {
ColorAnimationExample()
GeometryEffectExample()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Rotation3DEffectUITests.swift
// OpenSwiftUIUITests

import Testing
import SnapshotTesting

@MainActor
@Suite(.snapshots(record: .never, diffTool: diffTool))
struct Rotation3DEffectUITests {
@Test
func rotation3DEffect() {
struct ContentView: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.rotation3DEffect(
.degrees(45),
axis: (x: 0, y: 1, z: 0),
anchor: .center,
anchorZ: 0,
perspective: 1
)
.background(Color.red)
.overlay(
Color.green
.frame(width: 40, height: 30)
.rotation3DEffect(
.degrees(-45),
axis: (x: 1, y: 0, z: 0),
anchor: .center,
anchorZ: 0,
perspective: 1
)
)
}
}
openSwiftUIAssertSnapshot(of: ContentView())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// RotationEffectUITests.swift
// OpenSwiftUIUITests

import Testing
import SnapshotTesting

@MainActor
@Suite(.snapshots(record: .never, diffTool: diffTool))
struct RotationEffectUITests {
@Test
func rotationEffect() {
struct ContentView: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.rotationEffect(.degrees(30))
.background(Color.red)
.overlay(
Color.green
.frame(width: 40, height: 30)
.rotationEffect(.degrees(-30))
)
}
}
openSwiftUIAssertSnapshot(of: ContentView())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// GeometryEffectExample.swift
// SharedExample

#if OPENSWIFTUI
import OpenSwiftUI
#else
import SwiftUI
#endif

struct GeometryEffectExample: View {
var body: some View {
VStack(spacing: 50) {
OffsetEffectExample()
RotationEffectExample()
Rotation3DEffectExample()
}
}
}

struct OffsetEffectExample: View {
var body: some View {
Color.blue
.offset(x: 20, y: 15)
.frame(width: 80, height: 60)
.background(Color.red)
.overlay(Color.green.offset(x: 40, y: 30))
}
}

struct RotationEffectExample: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.rotationEffect(.degrees(30))
.background(Color.red)
.overlay(
Color.green
.frame(width: 40, height: 30)
.rotationEffect(.degrees(-30))
)
}
}

struct Rotation3DEffectExample: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.rotation3DEffect(
.degrees(45),
axis: (x: 0, y: 1, z: 0),
anchor: .center,
anchorZ: 0,
perspective: 1
)
.background(Color.red)
.overlay(
Color.green
.frame(width: 40, height: 30)
.rotation3DEffect(
.degrees(-45),
axis: (x: 1, y: 0, z: 0),
anchor: .center,
anchorZ: 0,
perspective: 1
)
)
}
}
94 changes: 89 additions & 5 deletions Sources/CoreGraphicsShims/CATransform3D.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ public func CATransform3DMakeAffineTransform(_ m: CGAffineTransform) -> CATransf
)
}

public func CATransform3DMakeTranslation(_ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D {
return CATransform3D(
m11: 1, m12: 0, m13: 0, m14: 0,
m21: 0, m22: 1, m23: 0, m24: 0,
m31: 0, m32: 0, m33: 1, m34: 0,
m41: tx, m42: ty, m43: tz, m44: 1
)
}

public func CATransform3DMakeScale(_ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D {
return CATransform3D(
m11: sx, m12: 0, m13: 0, m14: 0,
Expand All @@ -112,15 +121,90 @@ public func CATransform3DMakeScale(_ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat)
)
}

public func CATransform3DMakeTranslation(_ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D {
/// Returns a transform that rotates by `angle` radians about the vector
/// `(x, y, z)`. If the vector has length zero the identity transform is
/// returned.
public func CATransform3DMakeRotation(_ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D {
let ax = Double(x)
let ay = Double(y)
let az = Double(z)
let len = sqrt(ax * ax + ay * ay + az * az)
guard len > 0 else {
return CATransform3DIdentity
}

let ux = CGFloat(ax / len)
let uy = CGFloat(ay / len)
let uz = CGFloat(az / len)

let a = Double(angle)
let c = CGFloat(cos(a))
let s = CGFloat(sin(a))
let omc = 1 - c

// Compute standard rotation matrix components (Rodrigues' formula)
let r11 = c + ux * ux * omc
let r12 = ux * uy * omc - uz * s
let r13 = ux * uz * omc + uy * s

let r21 = uy * ux * omc + uz * s
let r22 = c + uy * uy * omc
let r23 = uy * uz * omc - ux * s

let r31 = uz * ux * omc - uy * s
let r32 = uz * uy * omc + ux * s
let r33 = c + uz * uz * omc

// Store the transpose of the standard rotation matrix into CATransform3D
// so that the effective transform used by this implementation matches the
// expected row/column layout when applied to points.
let m11 = r11
let m12 = r21
let m13 = r31
let m14: CGFloat = 0.0

let m21 = r12
let m22 = r22
let m23 = r32
let m24: CGFloat = 0.0

let m31 = r13
let m32 = r23
let m33 = r33
let m34: CGFloat = 0.0

let m41: CGFloat = 0.0
let m42: CGFloat = 0.0
let m43: CGFloat = 0.0
let m44: CGFloat = 1.0

return CATransform3D(
m11: 1, m12: 0, m13: 0, m14: 0,
m21: 0, m22: 1, m23: 0, m24: 0,
m31: 0, m32: 0, m33: 1, m34: 0,
m41: tx, m42: ty, m43: tz, m44: 1
m11: m11, m12: m12, m13: m13, m14: m14,
m21: m21, m22: m22, m23: m23, m24: m24,
m31: m31, m32: m32, m33: m33, m34: m34,
m41: m41, m42: m42, m43: m43, m44: m44
)
}

/// Translate `t` by `(tx, ty, tz)` and return the result:
/// t' = translate(tx, ty, tz) * t.
public func CATransform3DTranslate(_ t: CATransform3D, _ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D {
return CATransform3DConcat(CATransform3DMakeTranslation(tx, ty, tz), t)
}

/// Scale `t` by `(sx, sy, sz)` and return the result:
/// t' = scale(sx, sy, sz) * t.
public func CATransform3DScale(_ t: CATransform3D, _ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D {
return CATransform3DConcat(CATransform3DMakeScale(sx, sy, sz), t)
}

/// Rotate `t` by `angle` radians about the vector `(x, y, z)` and return
/// the result. If the vector has zero length the behavior is undefined:
/// t' = rotation(angle, x, y, z) * t.
public func CATransform3DRotate(_ t: CATransform3D, _ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D {
return CATransform3DConcat(CATransform3DMakeRotation(angle, x, y, z), t)
}

public func CATransform3DConcat(_ a: CATransform3D, _ b: CATransform3D) -> CATransform3D {
if a == CATransform3DIdentity { return b }
if b == CATransform3DIdentity { return a }
Expand Down
Loading