Skip to content

Commit

Permalink
Conform to the XCTRuntime protocol
Browse files Browse the repository at this point in the history
This ensures all tests are run from inside a live Godot engine.

Simplify GodotTestCase

Avoids the need to use platform-specific XCTest details to arrange
for a GodotEngine to be running during tests.
  • Loading branch information
pcbeard committed Feb 16, 2024
1 parent 44c6147 commit 27ba30c
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 54 deletions.
10 changes: 10 additions & 0 deletions Sources/SwiftGodot/Extensions/RefCountedExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ public extension RefCounted {
}
}

#if !canImport(Darwin)
///
/// Installs a callback into Godot engine on non-Darwin platforms, which don't typically have a run-loop.
/// Creates a 10 Hz timer that gives time back to the Godot main loop, so that XCTest can use blocking sub-runloops
/// to wait for expectations to be fulfilled, which is the basis for the async versions of tests.
/// This mechanism avoids hangs using this bi-directional callback approach.
/// This code is currently in the SwiftGodot package because it needs to make calls into
/// GodotInterface functions.
///
public extension RunLoop {
static var in_runloop_count: Int = 0

Expand All @@ -33,3 +42,4 @@ public extension RunLoop {
RunLoop.main.add(timer, forMode: .default)
}
}
#endif
18 changes: 14 additions & 4 deletions Sources/SwiftGodotTestability/GodotRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

import libgodot
import Foundation
import XCTRuntime
@_implementationOnly import GDExtension
@testable import SwiftGodot

public final class GodotRuntime {
public final class GodotRuntime: XCTRuntime {
enum State {
case begin
case running
Expand All @@ -26,11 +27,20 @@ public final class GodotRuntime {
public static func run (completion: @escaping () -> Void) {
guard state == .begin else { return }
state = .running
runGodot (loadScene: { scene in
runGodot { scene in
#if !canImport(Darwin)
// non-Darwin OS doesn't have implicit runloop.
RunLoop.install()
#endif
self.scene = scene
completion ()
})
Task { @MainActor in
// Calling the completion block from a main actor task guarantees it will be called when the Godot engine has
// finished starting up, and calling the runloop callback installed above.
completion ()
// after the tests complete, signal the engine to shut down.
stop()
}
}
}

public static func stop () {
Expand Down
54 changes: 4 additions & 50 deletions Sources/SwiftGodotTestability/GodotTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,11 @@
import XCTest
import SwiftGodot

open class GodotTestCase: XCTestCase, XCTestObservation {
private static var testSuites: [XCTestSuite] = []
private static weak var observation: GodotTestCase?
///
/// All the heavy lifting is now done in GodotRuntime.
///
open class GodotTestCase: XCTestCase {

public func testSuiteWillStart(_ testSuite: XCTestSuite) {
Self.testSuites.append(testSuite)
XCTestObservationCenter.shared.removeTestObserver(self)
}

public required init(name: String, testClosure: @escaping XCTestCaseClosure) {
super.init(name: name, testClosure: testClosure)
if Self.observation == nil {
Self.observation = self
XCTestObservationCenter.shared.addTestObserver(self)
}
}

#if os(macOS)
override open class var defaultTestSuite: XCTestSuite {
let testSuite = super.defaultTestSuite
testSuites.append (testSuite)
return testSuite
}
#endif

override open func run () {
if GodotRuntime.isRunning {
super.run ()
} else {
guard GodotRuntime.state == .begin else { return }
GodotRuntime.run {
let testSuites = Self.testSuites
Task { @MainActor in
if !testSuites.isEmpty {
// Executing all test suites from the context
for testSuite in Self.testSuites {
testSuite.perform (XCTestSuiteRun (test: testSuite))
}
} else {
Self.godotSetUp ()
// Executing single test method
super.run ()
Self.godotTearDown ()
}

GodotRuntime.stop ()
}
}
}
}

open class var godotSubclasses: [Wrapped.Type] {
return []
}
Expand Down

0 comments on commit 27ba30c

Please sign in to comment.