Add a mock PlaydateAPI test harness for the wrapper layer
The mock allocates real playdate_* structs whose function pointers are Swift stubs that record their arguments, then hands the fake PlaydateAPI* to Playdate.initialize(with:). This makes the wrapper plumbing testable on the host: argument forwarding and type conversion (buttons, colors, patterns, UTF-8 text), callback trampolines (menu items, sprite update, audio sources), ownership (bitmaps freed exactly once, table-vended bitmaps not freed, file handles closed once), collision-info parsing with system-allocator balance, the JSON tree builder driven by a simulated parser callback sequence, and regression tests for the CallbackSource lifecycle fix. Tests run serialized because the recording state is global. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
//
|
||||
// MockPlaydate.swift
|
||||
// A fake PlaydateAPI for host testing: real C structs whose function
|
||||
// pointers are Swift stubs that record their arguments. Only the fields
|
||||
// the tests exercise are populated; calling an unpopulated field traps.
|
||||
//
|
||||
// Recording state is global (C function pointers cannot capture), so all
|
||||
// tests using this harness must run in a `.serialized` suite.
|
||||
//
|
||||
|
||||
import CPlaydate
|
||||
@testable import PlayDate
|
||||
|
||||
enum Mock {
|
||||
// MARK: - Stable API allocations
|
||||
|
||||
nonisolated(unsafe) static let sysAPI = UnsafeMutablePointer<playdate_sys>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let displayAPI = UnsafeMutablePointer<playdate_display>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let gfxAPI = UnsafeMutablePointer<playdate_graphics>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let spriteAPI = UnsafeMutablePointer<playdate_sprite>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let soundAPI = UnsafeMutablePointer<playdate_sound>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let channelAPI = UnsafeMutablePointer<playdate_sound_channel>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let fileAPI = UnsafeMutablePointer<playdate_file>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let jsonAPI = UnsafeMutablePointer<playdate_json>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let apiStruct = UnsafeMutablePointer<PlaydateAPI>.allocate(capacity: 1)
|
||||
|
||||
// MARK: - Recordings
|
||||
|
||||
/// Chronological log of stub invocations, formatted per stub.
|
||||
nonisolated(unsafe) static var events: [String] = []
|
||||
nonisolated(unsafe) static var buttonState: (current: UInt32, pushed: UInt32, released: UInt32) = (0, 0, 0)
|
||||
/// The 16 bytes behind the last pattern `LCDColor` seen by a stub.
|
||||
nonisolated(unsafe) static var patternBytes: [UInt8] = []
|
||||
/// Userdata stored per sprite / menu item, as the OS would keep it.
|
||||
nonisolated(unsafe) static var spriteUserdata: [OpaquePointer: UnsafeMutableRawPointer] = [:]
|
||||
nonisolated(unsafe) static var menuUserdata: [OpaquePointer: UnsafeMutableRawPointer] = [:]
|
||||
/// Callbacks handed to the C API, for re-invocation by tests.
|
||||
nonisolated(unsafe) static var menuCallback: (@convention(c) (UnsafeMutableRawPointer?) -> Void)?
|
||||
nonisolated(unsafe) static var spriteUpdateCallback: (@convention(c) (OpaquePointer?) -> Void)?
|
||||
nonisolated(unsafe) static var audioCallback: (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutablePointer<Int16>?, UnsafeMutablePointer<Int16>?, Int32) -> Int32)?
|
||||
nonisolated(unsafe) static var audioContext: UnsafeMutableRawPointer?
|
||||
|
||||
static func record(_ event: String) {
|
||||
events.append(event)
|
||||
}
|
||||
|
||||
static func eventCount(_ event: String) -> Int {
|
||||
events.filter { $0 == event }.count
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Distinct fake object pointers, so identity-keyed recordings work.
|
||||
nonisolated(unsafe) private static var pointerSeed: UInt = 0x1000
|
||||
|
||||
static func fakePointer() -> OpaquePointer {
|
||||
pointerSeed += 16
|
||||
return OpaquePointer(bitPattern: pointerSeed)!
|
||||
}
|
||||
|
||||
/// Installs the mock API exactly once per process.
|
||||
static let ready: Void = install()
|
||||
|
||||
static func resetRecordings() {
|
||||
_ = ready
|
||||
events = []
|
||||
buttonState = (0, 0, 0)
|
||||
patternBytes = []
|
||||
spriteUserdata = [:]
|
||||
menuUserdata = [:]
|
||||
menuCallback = nil
|
||||
spriteUpdateCallback = nil
|
||||
audioCallback = nil
|
||||
audioContext = nil
|
||||
}
|
||||
|
||||
private static func install() {
|
||||
installSystem()
|
||||
installDisplay()
|
||||
installGraphics()
|
||||
installSprite()
|
||||
installSound()
|
||||
installFile()
|
||||
installJSON()
|
||||
apiStruct.initialize(to: PlaydateAPI(
|
||||
system: UnsafePointer(sysAPI),
|
||||
file: UnsafePointer(fileAPI),
|
||||
graphics: UnsafePointer(gfxAPI),
|
||||
sprite: UnsafePointer(spriteAPI),
|
||||
display: UnsafePointer(displayAPI),
|
||||
sound: UnsafePointer(soundAPI),
|
||||
lua: nil,
|
||||
json: UnsafePointer(jsonAPI),
|
||||
scoreboards: nil,
|
||||
network: nil))
|
||||
Playdate.initialize(with: UnsafeMutableRawPointer(apiStruct))
|
||||
}
|
||||
|
||||
// MARK: - System
|
||||
|
||||
private static func installSystem() {
|
||||
sysAPI.initialize(to: playdate_sys())
|
||||
|
||||
// Backed by the host allocator, so wrapper-freed OS memory balances.
|
||||
sysAPI.pointee.realloc = { pointer, size in
|
||||
if size == 0 {
|
||||
if let pointer {
|
||||
Mock.record("sysFree")
|
||||
free(pointer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return realloc(pointer, size)
|
||||
}
|
||||
|
||||
sysAPI.pointee.getButtonState = { current, pushed, released in
|
||||
current?.pointee = PDButtons(Mock.buttonState.current)
|
||||
pushed?.pointee = PDButtons(Mock.buttonState.pushed)
|
||||
released?.pointee = PDButtons(Mock.buttonState.released)
|
||||
}
|
||||
|
||||
sysAPI.pointee.addMenuItem = { title, callback, _ in
|
||||
Mock.record("addMenuItem(\(String(cString: title!)))")
|
||||
Mock.menuCallback = callback
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
sysAPI.pointee.setMenuItemUserdata = { item, userdata in
|
||||
Mock.menuUserdata[item!] = userdata
|
||||
}
|
||||
sysAPI.pointee.getMenuItemUserdata = { item in
|
||||
Mock.menuUserdata[item!]
|
||||
}
|
||||
sysAPI.pointee.removeMenuItem = { _ in
|
||||
Mock.record("removeMenuItem")
|
||||
}
|
||||
sysAPI.pointee.removeAllMenuItems = {
|
||||
Mock.record("removeAllMenuItems")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Display
|
||||
|
||||
private static func installDisplay() {
|
||||
displayAPI.initialize(to: playdate_display())
|
||||
displayAPI.pointee.getWidth = { 400 }
|
||||
displayAPI.pointee.getHeight = { 240 }
|
||||
}
|
||||
|
||||
// MARK: - Graphics
|
||||
|
||||
private static func installGraphics() {
|
||||
gfxAPI.initialize(to: playdate_graphics())
|
||||
|
||||
gfxAPI.pointee.fillRect = { x, y, width, height, color in
|
||||
if color > 3, let pattern = UnsafeRawPointer(bitPattern: color) {
|
||||
Mock.patternBytes = Array(UnsafeRawBufferPointer(start: pattern, count: 16))
|
||||
Mock.record("fillRect(\(x),\(y),\(width),\(height),pattern)")
|
||||
} else {
|
||||
Mock.record("fillRect(\(x),\(y),\(width),\(height),\(color))")
|
||||
}
|
||||
}
|
||||
|
||||
gfxAPI.pointee.drawText = { text, length, encoding, x, y in
|
||||
let bytes = UnsafeRawBufferPointer(start: text, count: length)
|
||||
let string = String(decoding: bytes, as: UTF8.self)
|
||||
Mock.record("drawText(\(string),enc:\(encoding.rawValue),\(x),\(y))")
|
||||
return Int32(length)
|
||||
}
|
||||
|
||||
gfxAPI.pointee.newBitmap = { width, height, _ in
|
||||
Mock.record("newBitmap(\(width)x\(height))")
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
gfxAPI.pointee.freeBitmap = { _ in
|
||||
Mock.record("freeBitmap")
|
||||
}
|
||||
|
||||
gfxAPI.pointee.newBitmapTable = { count, width, height in
|
||||
Mock.record("newBitmapTable(\(count))")
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
gfxAPI.pointee.freeBitmapTable = { _ in
|
||||
Mock.record("freeBitmapTable")
|
||||
}
|
||||
gfxAPI.pointee.getTableBitmap = { _, index in
|
||||
Mock.record("getTableBitmap(\(index))")
|
||||
return index == 0 ? Mock.fakePointer() : nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sprite
|
||||
|
||||
private static func installSprite() {
|
||||
spriteAPI.initialize(to: playdate_sprite())
|
||||
|
||||
spriteAPI.pointee.newSprite = {
|
||||
Mock.record("newSprite")
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
spriteAPI.pointee.freeSprite = { _ in
|
||||
Mock.record("freeSprite")
|
||||
}
|
||||
spriteAPI.pointee.setUserdata = { sprite, userdata in
|
||||
Mock.spriteUserdata[sprite!] = userdata
|
||||
}
|
||||
spriteAPI.pointee.getUserdata = { sprite in
|
||||
Mock.spriteUserdata[sprite!]
|
||||
}
|
||||
spriteAPI.pointee.moveTo = { _, x, y in
|
||||
Mock.record("moveTo(\(x),\(y))")
|
||||
}
|
||||
spriteAPI.pointee.addSprite = { _ in
|
||||
Mock.record("addSprite")
|
||||
}
|
||||
spriteAPI.pointee.removeSprite = { _ in
|
||||
Mock.record("removeSprite")
|
||||
}
|
||||
spriteAPI.pointee.setUpdateFunction = { _, function in
|
||||
Mock.spriteUpdateCallback = function
|
||||
}
|
||||
|
||||
spriteAPI.pointee.moveWithCollisions = { sprite, goalX, goalY, actualX, actualY, length in
|
||||
actualX?.pointee = goalX - 1
|
||||
actualY?.pointee = goalY - 2
|
||||
length?.pointee = 1
|
||||
// The wrapper frees this with the system allocator.
|
||||
let info = malloc(MemoryLayout<SpriteCollisionInfo>.stride)!
|
||||
.assumingMemoryBound(to: SpriteCollisionInfo.self)
|
||||
info.pointee = SpriteCollisionInfo(
|
||||
sprite: sprite,
|
||||
other: sprite,
|
||||
responseType: kCollisionTypeBounce,
|
||||
overlaps: 1,
|
||||
ti: 0.5,
|
||||
move: CollisionPoint(x: 1, y: 2),
|
||||
normal: CollisionVector(x: 0, y: -1),
|
||||
touch: CollisionPoint(x: 3, y: 4),
|
||||
spriteRect: PDRect(x: 0, y: 0, width: 8, height: 8),
|
||||
otherRect: PDRect(x: 8, y: 8, width: 8, height: 8))
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sound
|
||||
|
||||
private static func installSound() {
|
||||
soundAPI.initialize(to: playdate_sound())
|
||||
soundAPI.pointee.channel = UnsafePointer(channelAPI)
|
||||
|
||||
soundAPI.pointee.addSource = { callback, context, _ in
|
||||
Mock.record("addSource")
|
||||
Mock.audioCallback = callback
|
||||
Mock.audioContext = context
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
soundAPI.pointee.removeSource = { _ in
|
||||
Mock.record("removeSource")
|
||||
return 1
|
||||
}
|
||||
|
||||
channelAPI.initialize(to: playdate_sound_channel())
|
||||
channelAPI.pointee.newChannel = {
|
||||
Mock.record("newChannel")
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
channelAPI.pointee.freeChannel = { _ in
|
||||
Mock.record("freeChannel")
|
||||
}
|
||||
channelAPI.pointee.addCallbackSource = { _, callback, context, _ in
|
||||
Mock.record("addCallbackSource")
|
||||
Mock.audioCallback = callback
|
||||
Mock.audioContext = context
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
channelAPI.pointee.removeSource = { _, _ in
|
||||
Mock.record("channelRemoveSource")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File
|
||||
|
||||
private static func installFile() {
|
||||
fileAPI.initialize(to: playdate_file())
|
||||
|
||||
fileAPI.pointee.geterr = { nil }
|
||||
fileAPI.pointee.open = { path, mode in
|
||||
Mock.record("open(\(String(cString: path!)),\(mode.rawValue))")
|
||||
return malloc(1)
|
||||
}
|
||||
fileAPI.pointee.close = { file in
|
||||
Mock.record("close")
|
||||
free(file)
|
||||
return 0
|
||||
}
|
||||
fileAPI.pointee.read = { _, buffer, length in
|
||||
memset(buffer, 0xAB, Int(length))
|
||||
Mock.record("read(\(length))")
|
||||
return Int32(length)
|
||||
}
|
||||
fileAPI.pointee.write = { _, _, length in
|
||||
Mock.record("write(\(length))")
|
||||
return Int32(length)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JSON
|
||||
|
||||
/// Simulates the OS parser's callback sequence for the document
|
||||
/// `{"level": 3, "name": "up", "list": [1, true]}` regardless of input.
|
||||
private static func installJSON() {
|
||||
jsonAPI.initialize(to: playdate_json())
|
||||
|
||||
jsonAPI.pointee.decodeString = { decoder, _, outval in
|
||||
guard let decoder else { return 0 }
|
||||
|
||||
func intValue(_ value: Int32) -> json_value {
|
||||
json_value(type: CChar(kJSONInteger.rawValue), data: .init(intval: value))
|
||||
}
|
||||
|
||||
decoder.pointee.willDecodeSublist?(decoder, "_root", kJSONTable)
|
||||
decoder.pointee.didDecodeTableValue?(decoder, "level", intValue(3))
|
||||
|
||||
"up".withCString { name in
|
||||
decoder.pointee.didDecodeTableValue?(
|
||||
decoder, "name",
|
||||
json_value(type: CChar(kJSONString.rawValue),
|
||||
data: .init(stringval: UnsafeMutablePointer(mutating: name))))
|
||||
}
|
||||
|
||||
decoder.pointee.willDecodeSublist?(decoder, "list", kJSONArray)
|
||||
decoder.pointee.didDecodeArrayValue?(decoder, 1, intValue(1))
|
||||
decoder.pointee.didDecodeArrayValue?(
|
||||
decoder, 2, json_value(type: CChar(kJSONTrue.rawValue), data: .init(intval: 0)))
|
||||
let list = decoder.pointee.didDecodeSublist?(decoder, "list", kJSONArray)
|
||||
decoder.pointee.didDecodeTableValue?(
|
||||
decoder, "list",
|
||||
json_value(type: CChar(kJSONArray.rawValue), data: .init(arrayval: list)))
|
||||
|
||||
let root = decoder.pointee.didDecodeSublist?(decoder, "_root", kJSONTable)
|
||||
outval?.pointee = json_value(type: CChar(kJSONTable.rawValue),
|
||||
data: .init(tableval: root))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// WrapperTests.swift
|
||||
// Exercises the wrapper plumbing against the mock PlaydateAPI: argument
|
||||
// forwarding, type conversions, callback trampolines, ownership, and
|
||||
// registry lifecycles.
|
||||
//
|
||||
// Serialized because the mock's recording state is global (C function
|
||||
// pointers cannot capture context).
|
||||
//
|
||||
|
||||
import CPlaydate
|
||||
import Testing
|
||||
@testable import PlayDate
|
||||
|
||||
@Suite(.serialized)
|
||||
struct WrapperTests {
|
||||
init() {
|
||||
Mock.resetRecordings()
|
||||
}
|
||||
|
||||
// MARK: System
|
||||
|
||||
@Test func buttonStateConvertsMasks() {
|
||||
Mock.buttonState = (current: kButtonA.rawValue | kButtonUp.rawValue,
|
||||
pushed: kButtonB.rawValue,
|
||||
released: kButtonLeft.rawValue)
|
||||
|
||||
let (current, pushed, released) = System.buttonState
|
||||
#expect(current == [.a, .up])
|
||||
#expect(pushed == .b)
|
||||
#expect(released == .left)
|
||||
}
|
||||
|
||||
@Test func menuItemCallbackDispatchesToClosure() {
|
||||
var selections = 0
|
||||
let item = System.addMenuItem(title: "reset") { _ in selections += 1 }
|
||||
#expect(item != nil)
|
||||
#expect(Mock.events.contains("addMenuItem(reset)"))
|
||||
|
||||
// Simulate the user selecting the item: the OS invokes the recorded
|
||||
// trampoline with the item's stored userdata.
|
||||
let userdata = Mock.menuUserdata[item!.pointer]
|
||||
#expect(userdata != nil)
|
||||
Mock.menuCallback?(userdata)
|
||||
Mock.menuCallback?(userdata)
|
||||
#expect(selections == 2)
|
||||
|
||||
System.removeAllMenuItems()
|
||||
#expect(Mock.events.contains("removeAllMenuItems"))
|
||||
}
|
||||
|
||||
// MARK: Graphics
|
||||
|
||||
@Test func fillRectForwardsCoordinatesAndSolidColor() {
|
||||
Graphics.fillRect(x: 10, y: 20, width: 30, height: 40, color: .black)
|
||||
#expect(Mock.events == ["fillRect(10,20,30,40,\(kColorBlack.rawValue))"])
|
||||
}
|
||||
|
||||
@Test func fillRectMaterializesPatternBytes() {
|
||||
let pattern = Graphics.Pattern(rows: (1, 2, 3, 4, 5, 6, 7, 8))
|
||||
Graphics.fillRect(x: 0, y: 0, width: 8, height: 8, color: .pattern(pattern))
|
||||
#expect(Mock.events == ["fillRect(0,0,8,8,pattern)"])
|
||||
#expect(Mock.patternBytes == [1, 2, 3, 4, 5, 6, 7, 8,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
|
||||
}
|
||||
|
||||
@Test func drawTextSendsUTF8BytesAndLength() {
|
||||
let width = Graphics.drawText("Hëllo", x: 4, y: 6)
|
||||
#expect(Mock.events == ["drawText(Hëllo,enc:\(kUTF8Encoding.rawValue),4,6)"])
|
||||
#expect(width == "Hëllo".utf8.count)
|
||||
}
|
||||
|
||||
@Test func ownedBitmapIsFreedExactlyOnceOnDeinit() {
|
||||
do {
|
||||
let bitmap = Graphics.Bitmap(width: 32, height: 16)
|
||||
_ = bitmap
|
||||
}
|
||||
#expect(Mock.eventCount("freeBitmap") == 1)
|
||||
}
|
||||
|
||||
@Test func tableVendedBitmapIsNotFreedButTableIs() {
|
||||
do {
|
||||
let table = Graphics.BitmapTable(count: 4, width: 8, height: 8)
|
||||
let bitmap = table.bitmap(at: 0)
|
||||
#expect(bitmap != nil)
|
||||
#expect(table.bitmap(at: 3) == nil) // mock returns nil past index 0
|
||||
}
|
||||
#expect(Mock.eventCount("freeBitmap") == 0)
|
||||
#expect(Mock.eventCount("freeBitmapTable") == 1)
|
||||
}
|
||||
|
||||
// MARK: Sprite
|
||||
|
||||
@Test func spriteUserdataRecoversWrapperInCallbacks() {
|
||||
var updated: [ObjectIdentifier] = []
|
||||
let sprite = Sprite()
|
||||
sprite.setUpdateFunction { updated.append(ObjectIdentifier($0)) }
|
||||
|
||||
// Simulate the OS driving the sprite's update.
|
||||
Mock.spriteUpdateCallback?(sprite.pointer)
|
||||
#expect(updated == [ObjectIdentifier(sprite)])
|
||||
}
|
||||
|
||||
@Test func spriteIsFreedOnDeinitAndNotWhileReferenced() {
|
||||
var sprite: Sprite? = Sprite()
|
||||
_ = sprite
|
||||
#expect(Mock.eventCount("freeSprite") == 0)
|
||||
sprite = nil
|
||||
#expect(Mock.eventCount("freeSprite") == 1)
|
||||
}
|
||||
|
||||
@Test func moveWithCollisionsParsesInfoAndFreesTheCArray() {
|
||||
let sprite = Sprite()
|
||||
let (actual, collisions) = sprite.moveWithCollisions(goalX: 10, goalY: 20)
|
||||
|
||||
#expect(actual.x == 9 && actual.y == 18)
|
||||
#expect(collisions.count == 1)
|
||||
let collision = try! #require(collisions.first)
|
||||
#expect(collision.sprite === sprite) // recovered through userdata
|
||||
#expect(collision.other === sprite)
|
||||
#expect(collision.response == .bounce)
|
||||
#expect(collision.overlaps)
|
||||
#expect(collision.ti == 0.5)
|
||||
#expect(collision.normal == (0, -1))
|
||||
#expect(collision.spriteRect.width == 8)
|
||||
|
||||
// The C info array must be returned to the system allocator.
|
||||
#expect(Mock.eventCount("sysFree") == 1)
|
||||
}
|
||||
|
||||
// MARK: Sound (regression tests for the CallbackSource lifecycle)
|
||||
|
||||
@Test func callbackSourceIsRetainedUntilRemovedAndDispatches() {
|
||||
let baseline = Sound.CallbackSource.live.count
|
||||
|
||||
var produced = 0
|
||||
let source = Sound.addSource(stereo: false) { left, right in
|
||||
produced += left.count
|
||||
#expect(right == nil)
|
||||
return true
|
||||
}
|
||||
#expect(Sound.CallbackSource.live.count == baseline + 1)
|
||||
|
||||
// Simulate the audio engine pulling samples through the trampoline.
|
||||
var samples = [Int16](repeating: 0, count: 64)
|
||||
let result = samples.withUnsafeMutableBufferPointer { buffer in
|
||||
Mock.audioCallback?(Mock.audioContext, buffer.baseAddress, nil, Int32(buffer.count))
|
||||
}
|
||||
#expect(result == 1)
|
||||
#expect(produced == 64)
|
||||
|
||||
Sound.removeSource(source)
|
||||
#expect(Sound.CallbackSource.live.count == baseline)
|
||||
}
|
||||
|
||||
@Test func channelCallbackSourceIsReleasedWhenChannelIsFreed() {
|
||||
let baseline = Sound.CallbackSource.live.count
|
||||
do {
|
||||
let channel = Sound.Channel()
|
||||
_ = channel.addCallbackSource(stereo: true) { _, _ in false }
|
||||
#expect(Sound.CallbackSource.live.count == baseline + 1)
|
||||
}
|
||||
#expect(Mock.eventCount("freeChannel") == 1)
|
||||
#expect(Sound.CallbackSource.live.count == baseline)
|
||||
}
|
||||
|
||||
@Test func channelRemoveSourceReleasesCallbackSource() {
|
||||
let baseline = Sound.CallbackSource.live.count
|
||||
let channel = Sound.Channel()
|
||||
let source = channel.addCallbackSource(stereo: false) { _, _ in false }
|
||||
channel.removeSource(source)
|
||||
#expect(Sound.CallbackSource.live.count == baseline)
|
||||
}
|
||||
|
||||
// MARK: File
|
||||
|
||||
@Test func fileHandleReadsWritesAndClosesExactlyOnce() throws {
|
||||
do {
|
||||
let handle = try File.Handle(path: "save.dat", mode: [.read, .readData])
|
||||
#expect(Mock.events.first == "open(save.dat,\(kFileRead.rawValue | kFileReadData.rawValue))")
|
||||
|
||||
let bytes = try handle.read(length: 8)
|
||||
#expect(bytes == [UInt8](repeating: 0xAB, count: 8))
|
||||
|
||||
let written = try handle.write([1, 2, 3])
|
||||
#expect(written == 3)
|
||||
|
||||
try handle.close()
|
||||
}
|
||||
// deinit after an explicit close must not close again.
|
||||
#expect(Mock.eventCount("close") == 1)
|
||||
}
|
||||
|
||||
// MARK: JSON
|
||||
|
||||
@Test func jsonDecodeBuildsTheValueTree() throws {
|
||||
let value = try JSON.decode("(input is ignored by the mock)")
|
||||
|
||||
guard case .table(let entries) = value else {
|
||||
Issue.record("expected a table at the root, got \(value)")
|
||||
return
|
||||
}
|
||||
guard case .int(3)? = entries["level"] else {
|
||||
Issue.record("expected level == 3")
|
||||
return
|
||||
}
|
||||
guard case .string("up")? = entries["name"] else {
|
||||
Issue.record("expected name == up")
|
||||
return
|
||||
}
|
||||
guard case .array(let list)? = entries["list"], list.count == 2,
|
||||
case .int(1) = list[0], case .bool(true) = list[1] else {
|
||||
Issue.record("expected list == [1, true]")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user