Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RefCounted.unref() to manage lifetime of RefCounted objects #375

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
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
30 changes: 21 additions & 9 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,10 @@ products.append(
#endif

// libgodot is only available for macOS and testability runtime depends on it
#if os(macOS)
products.append(
.library(
name: "SwiftGodotTestability",
targets: ["SwiftGodotTestability"]))
#endif

var targets: [Target] = [
// This contains GDExtension's JSON API data models
Expand Down Expand Up @@ -112,21 +110,35 @@ swiftGodotPlugins.append("SwiftGodotMacroLibrary")

// libgodot is only available for macOS
#if os(macOS)
targets.append(contentsOf: [
// Godot runtime as a library
targets.append(
// on macOS, libgodot is distributed as a binary .xcframework.
.binaryTarget(
name: "libgodot_tests",
url: "https://github.com/migueldeicaza/SwiftGodotKit/releases/download/v4.1.99/libgodot.xcframework.zip",
checksum: "c8ddf62be6c00eacc36bd2dafe8d424c0b374833efe80546f6ee76bd27cee84e"
),

//path: "../libgodot.xcframework.zip"
)
)
let libgodot_dependency: Target.Dependency = "libgodot_tests"
#else
targets.append(
// on non-macOS platforms, link directly to libgodot, which must be available
// already in the .build/config directory, or in /usr/lib.
.systemLibrary(
name: "libgodot_system"
)
)
let libgodot_dependency: Target.Dependency = "libgodot_system"
#endif

targets.append(contentsOf: [
// Base functionality for Godot runtime dependant tests
.target(
name: "SwiftGodotTestability",
dependencies: [
"SwiftGodot",
"libgodot_tests",
"GDExtension"
libgodot_dependency,
"GDExtension", "XCTRuntime"
]),

// General purpose runtime dependant tests
Expand All @@ -145,7 +157,6 @@ targets.append(contentsOf: [
]
),
])
#endif

targets.append(contentsOf: [
// This is the binding itself, it is made up of our generated code for the
Expand Down Expand Up @@ -178,6 +189,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
.package(path: "../XCTRuntime"),
],
targets: targets
)
11 changes: 10 additions & 1 deletion Sources/SwiftGodot/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ struct GodotInterface {
let packed_vector3_array_operator_index: GDExtensionInterfacePackedVector3ArrayOperatorIndex

let callable_custom_create: GDExtensionInterfaceCallableCustomCreate

typealias runloop_function_t = @convention(c) () -> Void
typealias GDExtensionInterfaceDisplayServerSetRunloop = @convention(c) (runloop_function_t) -> Void
let displayserver_set_runloop: GDExtensionInterfaceDisplayServerSetRunloop

typealias GDExtensionInterfaceMainIteration = @convention(c) () -> Void
let main_iteration: GDExtensionInterfaceMainIteration
}

var gi: GodotInterface!
Expand Down Expand Up @@ -289,7 +296,9 @@ func loadGodotInterface (_ godotGetProcAddrPtr: GDExtensionInterfaceGetProcAddre
packed_vector2_array_operator_index: load ("packed_vector2_array_operator_index"),
packed_vector3_array_operator_index: load ("packed_vector3_array_operator_index"),

callable_custom_create: load ("callable_custom_create")
callable_custom_create: load ("callable_custom_create"),
displayserver_set_runloop: load ("displayserver_set_runloop"),
main_iteration: load ("main_iteration")
)
}

Expand Down
28 changes: 28 additions & 0 deletions Sources/SwiftGodot/Extensions/RefCountedExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,31 @@ 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

static func install() {
gi.displayserver_set_runloop {
RunLoop.in_runloop_count += 1
RunLoop.main.run(until: .now)
RunLoop.in_runloop_count -= 1
}
let timer = Foundation.Timer(timeInterval: 0.1, repeats: true) { _ in
if RunLoop.in_runloop_count > 0 {
gi.main_iteration()
}
}
RunLoop.main.add(timer, forMode: .default)
}
}
#endif
47 changes: 32 additions & 15 deletions Sources/SwiftGodotTestability/GodotRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,45 @@
//

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

@MainActor
public final class GodotRuntime {

static var isInitialized: Bool = false
static var isRunning: Bool = false
public final class GodotRuntime: XCTRuntime {
enum State {
case begin
case running
case stopping
case end
}

static var state: State = .begin
static var isRunning: Bool { state == .running }

static var scene: SceneTree?

static func run (completion: @escaping () -> Void) {
guard !isRunning else { return }
isInitialized = true
isRunning = true
runGodot (loadScene: { scene in
public static func run (completion: @escaping () -> Void) {
guard state == .begin else { return }
state = .running
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()
}
}
}

static func stop () {
isRunning = false
public static func stop () {
state = .stopping
scene?.quit ()
}

Expand Down Expand Up @@ -78,7 +95,7 @@ private extension GodotRuntime {
}
)

let args = ["SwiftGodotKit", "--headless"]
let args = ["SwiftGodotKit", "--headless", "--verbose"]
withUnsafePtr (strings: args) { ptr in
godot_main (Int32 (args.count), ptr)
}
Expand Down
37 changes: 4 additions & 33 deletions Sources/SwiftGodotTestability/GodotTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,11 @@
import XCTest
import SwiftGodot

@MainActor
///
/// All the heavy lifting is now done in GodotRuntime.
///
open class GodotTestCase: XCTestCase {

private static var testSuites: [XCTestSuite] = []

override open class var defaultTestSuite: XCTestSuite {
let testSuite = super.defaultTestSuite
testSuites.append (testSuite)
return testSuite
}

override open func run () {
if GodotRuntime.isRunning {
super.run ()
} else {
guard !GodotRuntime.isInitialized else { return }
GodotRuntime.run {
if !Self.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
Loading
Loading