Swift 6.4 migration and exhancements (#1)
This PR contains the work done to update the library and the attached example project to use the Swift 6.4 computer as a minimum supported version and also, to use the latest features introduced in it. Reviewed-on: #1 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -24,8 +24,10 @@ enum Mock {
|
||||
nonisolated(unsafe) static let soundEffectAPI = UnsafeMutablePointer<playdate_sound_effect>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let lfoAPI = UnsafeMutablePointer<playdate_sound_lfo>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let delayLineAPI = UnsafeMutablePointer<playdate_sound_effect_delayline>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let tilemapAPI = UnsafeMutablePointer<playdate_tilemap>.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 networkAPI = UnsafeMutablePointer<playdate_network>.allocate(capacity: 1)
|
||||
nonisolated(unsafe) static let apiStruct = UnsafeMutablePointer<PlaydateAPI>.allocate(capacity: 1)
|
||||
|
||||
// MARK: - Recordings
|
||||
@@ -35,6 +37,18 @@ enum Mock {
|
||||
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] = []
|
||||
/// The 8 rows last handed to `setStencilPattern`.
|
||||
nonisolated(unsafe) static var stencilRows: [UInt8] = []
|
||||
/// Caps the bytes a file read returns (0 = end of file, negative =
|
||||
/// error); `nil` fills the whole request.
|
||||
nonisolated(unsafe) static var fileReadLimit: Int32?
|
||||
/// Bytes left to read before 0 (end of file); `nil` never ends.
|
||||
nonisolated(unsafe) static var fileBytesRemaining: Int32?
|
||||
/// The last `network->setEnabled` callback.
|
||||
nonisolated(unsafe) static var networkEnabledCallback: (@convention(c) (PDNetErr) -> Void)?
|
||||
/// The index buffer and count last handed to `setTiles`.
|
||||
nonisolated(unsafe) static var tilesPointer: UnsafeMutablePointer<UInt16>?
|
||||
nonisolated(unsafe) static var tilesCount: Int32 = 0
|
||||
/// 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] = [:]
|
||||
@@ -83,6 +97,12 @@ enum Mock {
|
||||
events = []
|
||||
buttonState = (0, 0, 0)
|
||||
patternBytes = []
|
||||
stencilRows = []
|
||||
fileReadLimit = nil
|
||||
fileBytesRemaining = nil
|
||||
networkEnabledCallback = nil
|
||||
tilesPointer = nil
|
||||
tilesCount = 0
|
||||
spriteUserdata = [:]
|
||||
menuUserdata = [:]
|
||||
menuCallback = nil
|
||||
@@ -102,6 +122,7 @@ enum Mock {
|
||||
installSound()
|
||||
installFile()
|
||||
installJSON()
|
||||
installNetwork()
|
||||
apiStruct.initialize(to: PlaydateAPI(
|
||||
system: UnsafePointer(sysAPI),
|
||||
file: UnsafePointer(fileAPI),
|
||||
@@ -112,7 +133,7 @@ enum Mock {
|
||||
lua: nil,
|
||||
json: UnsafePointer(jsonAPI),
|
||||
scoreboards: nil,
|
||||
network: nil))
|
||||
network: UnsafePointer(networkAPI)))
|
||||
Playdate.initialize(with: UnsafeMutableRawPointer(apiStruct))
|
||||
}
|
||||
|
||||
@@ -170,6 +191,19 @@ enum Mock {
|
||||
|
||||
private static func installGraphics() {
|
||||
gfxAPI.initialize(to: playdate_graphics())
|
||||
tilemapAPI.initialize(to: playdate_tilemap())
|
||||
gfxAPI.pointee.tilemap = UnsafePointer(tilemapAPI)
|
||||
|
||||
tilemapAPI.pointee.newTilemap = {
|
||||
Mock.record("newTilemap")
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
tilemapAPI.pointee.freeTilemap = { _ in Mock.record("freeTilemap") }
|
||||
tilemapAPI.pointee.setTiles = { _, indexes, count, rowWidth in
|
||||
Mock.record("setTiles(\(count),\(rowWidth))")
|
||||
Mock.tilesPointer = indexes
|
||||
Mock.tilesCount = count
|
||||
}
|
||||
|
||||
gfxAPI.pointee.fillRect = { x, y, width, height, color in
|
||||
if color > 3, let pattern = UnsafeRawPointer(bitPattern: color) {
|
||||
@@ -194,6 +228,10 @@ enum Mock {
|
||||
gfxAPI.pointee.freeBitmap = { _ in
|
||||
Mock.record("freeBitmap")
|
||||
}
|
||||
gfxAPI.pointee.getBitmapMask = { _ in
|
||||
Mock.record("getBitmapMask")
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
|
||||
gfxAPI.pointee.newBitmapTable = { count, width, height in
|
||||
Mock.record("newBitmapTable(\(count))")
|
||||
@@ -223,6 +261,9 @@ enum Mock {
|
||||
Mock.record("newSprite")
|
||||
return Mock.fakePointer()
|
||||
}
|
||||
spriteAPI.pointee.setStencilPattern = { _, pattern in
|
||||
Mock.stencilRows = Array(UnsafeBufferPointer(start: pattern, count: 8))
|
||||
}
|
||||
spriteAPI.pointee.freeSprite = { _ in
|
||||
Mock.record("freeSprite")
|
||||
}
|
||||
@@ -400,9 +441,14 @@ enum Mock {
|
||||
return 0
|
||||
}
|
||||
fileAPI.pointee.read = { _, buffer, length in
|
||||
memset(buffer, 0xAB, Int(length))
|
||||
Mock.record("read(\(length))")
|
||||
return Int32(length)
|
||||
var count = min(Int32(length), Mock.fileReadLimit ?? Int32(length))
|
||||
if let remaining = Mock.fileBytesRemaining {
|
||||
count = min(count, remaining)
|
||||
Mock.fileBytesRemaining = remaining - max(count, 0)
|
||||
}
|
||||
if count > 0 { memset(buffer, 0xAB, Int(count)) }
|
||||
return count
|
||||
}
|
||||
fileAPI.pointee.write = { _, _, length in
|
||||
Mock.record("write(\(length))")
|
||||
@@ -410,6 +456,16 @@ enum Mock {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Network
|
||||
|
||||
private static func installNetwork() {
|
||||
networkAPI.initialize(to: playdate_network())
|
||||
networkAPI.pointee.setEnabled = { flag, callback in
|
||||
Mock.record("setEnabled(\(flag),\(callback == nil ? "nil" : "callback"))")
|
||||
Mock.networkEnabledCallback = callback
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JSON
|
||||
|
||||
/// Simulates the OS parser's callback sequence for the document
|
||||
@@ -417,6 +473,23 @@ enum Mock {
|
||||
private static func installJSON() {
|
||||
jsonAPI.initialize(to: playdate_json())
|
||||
|
||||
// Reads until the reader returns 0 or less, then decodes `null`.
|
||||
jsonAPI.pointee.decode = { _, reader, outval in
|
||||
var buffer = [UInt8](repeating: 0, count: 16)
|
||||
var total: Int32 = 0, last: Int32 = 0
|
||||
for _ in 0..<64 {
|
||||
last = buffer.withUnsafeMutableBufferPointer { buffer in
|
||||
reader.read?(reader.userdata, buffer.baseAddress, Int32(buffer.count)) ?? -1
|
||||
}
|
||||
guard last > 0 else { break }
|
||||
total += last
|
||||
}
|
||||
Mock.record("decode(total:\(total),end:\(last))")
|
||||
outval?.pointee = json_value()
|
||||
outval?.pointee.type = CChar(kJSONNull.rawValue)
|
||||
return 1
|
||||
}
|
||||
|
||||
jsonAPI.pointee.decodeString = { decoder, _, outval in
|
||||
guard let decoder else { return 0 }
|
||||
|
||||
|
||||
@@ -69,8 +69,7 @@ import Testing
|
||||
defer { copy.deallocate() }
|
||||
#expect(String(playdateCString: copy) == original)
|
||||
|
||||
let viaClosure = original.withPlaydateCString { String(playdateCString: $0) }
|
||||
#expect(viaClosure == original)
|
||||
#expect(String(playdateCString: nil) == nil)
|
||||
}
|
||||
|
||||
@Test func dateTimeMirrorsCStruct() {
|
||||
|
||||
@@ -64,6 +64,24 @@ struct WrapperTests {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
|
||||
}
|
||||
|
||||
@Test func inlineArrayPatternMatchesTheTupleOne() throws {
|
||||
guard #available(macOS 26, *) else { return }
|
||||
// An array literal picks the InlineArray overload; a tuple literal
|
||||
// still picks the tuple one.
|
||||
let inline = Graphics.Pattern(rows: [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
let tuple = Graphics.Pattern(rows: (1, 2, 3, 4, 5, 6, 7, 8))
|
||||
#expect(withUnsafeBytes(of: inline.bytes, Array.init)
|
||||
== withUnsafeBytes(of: tuple.bytes, Array.init))
|
||||
|
||||
Graphics.fillRect(x: 0, y: 0, width: 8, height: 8, color: .pattern(inline))
|
||||
#expect(Mock.patternBytes == [1, 2, 3, 4, 5, 6, 7, 8,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
|
||||
|
||||
var pattern = inline
|
||||
pattern.inlineBytes[8] = 0x0f
|
||||
#expect(pattern.bytes.8 == 0x0f)
|
||||
}
|
||||
|
||||
@Test func drawTextSendsUTF8BytesAndLength() {
|
||||
let width = Graphics.drawText("Hëllo", x: 4, y: 6)
|
||||
#expect(Mock.events == ["drawText(Hëllo,enc:\(kUTF8Encoding.rawValue),4,6)"])
|
||||
@@ -99,6 +117,33 @@ struct WrapperTests {
|
||||
#expect(visited == 1)
|
||||
}
|
||||
|
||||
@Test func tileMapSetTilesPassesTheCallersStorageWithoutCopying() {
|
||||
let tileMap = Graphics.TileMap()
|
||||
let indexes: [UInt16] = [1, 2, 3, 4, 5, 6]
|
||||
tileMap.setTiles(indexes, rowWidth: 3)
|
||||
|
||||
#expect(Mock.events.contains("setTiles(6,3)"))
|
||||
let storage = indexes.withUnsafeBufferPointer { $0.baseAddress }
|
||||
#expect(UnsafePointer(Mock.tilesPointer) == storage)
|
||||
}
|
||||
|
||||
@Test func bitmapMaskIsFreedAndKeepsItsBitmapAlive() {
|
||||
weak var weakBitmap: Graphics.Bitmap?
|
||||
var mask: Graphics.Bitmap?
|
||||
do {
|
||||
let bitmap = Graphics.Bitmap(width: 8, height: 8)
|
||||
weakBitmap = bitmap
|
||||
mask = bitmap.mask
|
||||
}
|
||||
#expect(mask != nil)
|
||||
#expect(weakBitmap != nil) // the mask keeps it alive
|
||||
#expect(Mock.eventCount("freeBitmap") == 0)
|
||||
|
||||
mask = nil
|
||||
#expect(weakBitmap == nil)
|
||||
#expect(Mock.eventCount("freeBitmap") == 2) // the mask, then the bitmap
|
||||
}
|
||||
|
||||
// MARK: Sprite
|
||||
|
||||
@Test func spriteUserdataRecoversWrapperInCallbacks() {
|
||||
@@ -111,6 +156,17 @@ struct WrapperTests {
|
||||
#expect(updated == [ObjectIdentifier(sprite)])
|
||||
}
|
||||
|
||||
@Test func spriteStencilPatternOverloadsSendTheSameRows() {
|
||||
let sprite = Sprite()
|
||||
sprite.setStencilPattern((1, 2, 3, 4, 5, 6, 7, 8))
|
||||
#expect(Mock.stencilRows == [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
|
||||
guard #available(macOS 26, *) else { return }
|
||||
Mock.stencilRows = []
|
||||
sprite.setStencilPattern([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
#expect(Mock.stencilRows == [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
}
|
||||
|
||||
@Test func spriteIsFreedOnDeinitAndNotWhileReferenced() {
|
||||
var sprite: Sprite? = Sprite()
|
||||
_ = sprite
|
||||
@@ -146,7 +202,8 @@ struct WrapperTests {
|
||||
var produced = 0
|
||||
let source = Sound.addSource(stereo: false) { left, right in
|
||||
produced += left.count
|
||||
#expect(right == nil)
|
||||
let isMono = right.isEmpty // #expect cannot capture a span
|
||||
#expect(isMono)
|
||||
return true
|
||||
}
|
||||
#expect(Sound.CallbackSource.live.count == baseline + 1)
|
||||
@@ -280,7 +337,8 @@ struct WrapperTests {
|
||||
let effect = Sound.Effect(processor: { left, right, _ in
|
||||
_ = token
|
||||
processed += left.count
|
||||
#expect(right == nil)
|
||||
let isMono = right.isEmpty // #expect cannot capture a span
|
||||
#expect(isMono)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -343,6 +401,54 @@ struct WrapperTests {
|
||||
#expect(Mock.eventCount("close") == 1)
|
||||
}
|
||||
|
||||
@Test func jsonDecodeFileReadsThroughTheHandleAndClosesIt() throws {
|
||||
Mock.fileBytesRemaining = 20
|
||||
let value = try JSON.decodeFile(path: "save.json")
|
||||
guard case .null = value else {
|
||||
Issue.record("expected .null, got \(value)")
|
||||
return
|
||||
}
|
||||
// End of file reaches the decoder as 0, not -1.
|
||||
#expect(Mock.events.contains("decode(total:20,end:0)"))
|
||||
#expect(Mock.eventCount("close") == 1)
|
||||
}
|
||||
|
||||
@Test func fileHandleClosesWhenItGoesOutOfScope() throws {
|
||||
do {
|
||||
let handle = try File.Handle(path: "save.dat", mode: .write)
|
||||
_ = try handle.write([1])
|
||||
#expect(Mock.eventCount("close") == 0)
|
||||
}
|
||||
#expect(Mock.eventCount("close") == 1)
|
||||
}
|
||||
|
||||
@Test func fileHandleReadLengthReturnsOnlyTheBytesRead() throws {
|
||||
let handle = try File.Handle(path: "save.dat", mode: [.read, .readData])
|
||||
|
||||
Mock.fileReadLimit = 3
|
||||
#expect(try handle.read(length: 8) == [0xAB, 0xAB, 0xAB])
|
||||
|
||||
Mock.fileReadLimit = 0
|
||||
#expect(try handle.read(length: 8).isEmpty)
|
||||
|
||||
Mock.fileReadLimit = -1
|
||||
#expect(throws: PlaydateError.self) { try handle.read(length: 8) }
|
||||
}
|
||||
|
||||
// MARK: Network
|
||||
|
||||
@Test func networkDisableRegistersNoCallbackSoEnableGetsItsOwnResult() {
|
||||
var results: [String] = []
|
||||
Network.disable()
|
||||
#expect(Mock.events.last == "setEnabled(false,nil)")
|
||||
|
||||
Network.enable { error in results.append(error == nil ? "ok" : "failed") }
|
||||
#expect(Mock.events.last == "setEnabled(true,callback)")
|
||||
|
||||
Mock.networkEnabledCallback?(NET_OK)
|
||||
#expect(results == ["ok"])
|
||||
}
|
||||
|
||||
// MARK: JSON
|
||||
|
||||
@Test func jsonDecodeBuildsTheValueTree() throws {
|
||||
|
||||
Reference in New Issue
Block a user