@reekon-tools/react-native-pdf-canvas 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +68 -39
- package/README.md +213 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/index.native.d.ts +5 -0
- package/dist/index.native.js +5 -0
- package/dist/index.web.d.ts +10 -0
- package/dist/index.web.js +10 -0
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/skia/index.native.d.ts +7 -3
- package/dist/skia/index.native.js +7 -3
- package/dist/skia/index.web.d.ts +4 -2
- package/dist/skia/index.web.js +4 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +38 -8
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +487 -0
- package/scripts/pdfium-manifest.json +46 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import XCTest
|
|
3
|
-
|
|
4
|
-
@testable import PdfCanvasCore
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
A recording sink, standing in for the JSI slot store.
|
|
8
|
-
|
|
9
|
-
The real one lives in `ios/Sources/PdfCanvasBridge/PdfCanvasSlots.mm`, which
|
|
10
|
-
SwiftPM cannot build — it needs `<jsi/jsi.h>` from a pod. What CAN be proven here
|
|
11
|
-
is the CONTRACT: reserve once, before the draw; publish last; abandon on every path
|
|
12
|
-
that drops a raster. Those are the three properties whose violation is a leak or a
|
|
13
|
-
use-after-free rather than a wrong pixel, so they are worth pinning without a
|
|
14
|
-
device.
|
|
15
|
-
*/
|
|
16
|
-
class RecordingSink: NSObject, PdfCanvasPixelSink {
|
|
17
|
-
/// Set to refuse the reservation, which is the fallback-to-heap path.
|
|
18
|
-
var refuse = false
|
|
19
|
-
var reserveCount = 0
|
|
20
|
-
var publishCount = 0
|
|
21
|
-
var abandonCount = 0
|
|
22
|
-
/// The identifier `publish` hands back. Non-zero means "in a native slot".
|
|
23
|
-
var slot: Int64 = 4242
|
|
24
|
-
|
|
25
|
-
private var storage: UnsafeMutableRawPointer?
|
|
26
|
-
private var length = 0
|
|
27
|
-
private(set) var published = false
|
|
28
|
-
|
|
29
|
-
/// The reserved bytes, so a test can check what CoreGraphics wrote into them.
|
|
30
|
-
func bytes() -> [UInt8] {
|
|
31
|
-
guard let storage else { return [] }
|
|
32
|
-
return Array(UnsafeRawBufferPointer(start: storage, count: length))
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@objc dynamic func pdfCanvasReserveBytes(_ length: UInt) -> UnsafeMutableRawPointer? {
|
|
36
|
-
reserveCount += 1
|
|
37
|
-
if refuse { return nil }
|
|
38
|
-
self.length = Int(length)
|
|
39
|
-
// `calloc`, because ZEROED IS PART OF THE SINK CONTRACT — it is what makes
|
|
40
|
-
// `background: transparent` free and what stops a published buffer ever
|
|
41
|
-
// handing out memory nobody wrote.
|
|
42
|
-
storage = calloc(Int(length), 1)
|
|
43
|
-
return storage
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
func pdfCanvasPublish() -> Int64 {
|
|
47
|
-
publishCount += 1
|
|
48
|
-
published = true
|
|
49
|
-
return slot
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
func pdfCanvasAbandon() {
|
|
53
|
-
abandonCount += 1
|
|
54
|
-
if let storage {
|
|
55
|
-
free(storage)
|
|
56
|
-
self.storage = nil
|
|
57
|
-
length = 0
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
deinit {
|
|
62
|
-
if let storage { free(storage) }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
final class SinkTests: XCTestCase {
|
|
67
|
-
|
|
68
|
-
private func rasterizer() throws -> PdfCanvasRasterizer {
|
|
69
|
-
try openFixture(TestPdfs.quadrants(width: 200, height: 100), name: "sink.pdf")
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
A sink gets exactly one reservation, one publish, and no abandon.
|
|
74
|
-
|
|
75
|
-
The counts matter more than they look: a second reservation would mean the
|
|
76
|
-
rasterizer had drawn twice, and an abandon on the happy path would free the
|
|
77
|
-
buffer the caller is about to redeem.
|
|
78
|
-
*/
|
|
79
|
-
func testTheHappyPathReservesOnceAndPublishesOnce() throws {
|
|
80
|
-
let r = try rasterizer()
|
|
81
|
-
let sink = RecordingSink()
|
|
82
|
-
let pixels = try r.render(
|
|
83
|
-
request(width: 200, height: 100, scale: 2), cancellation: nil, sink: sink)
|
|
84
|
-
|
|
85
|
-
XCTAssertEqual(sink.reserveCount, 1)
|
|
86
|
-
XCTAssertEqual(sink.publishCount, 1)
|
|
87
|
-
XCTAssertEqual(sink.abandonCount, 0)
|
|
88
|
-
XCTAssertTrue(pixels.isInNativeSlot)
|
|
89
|
-
XCTAssertEqual(pixels.slot, 4242)
|
|
90
|
-
XCTAssertNil(pixels.bytes, "a slot-backed raster has no heap bytes")
|
|
91
|
-
r.close()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
The bytes CoreGraphics wrote are the sink's own buffer — no copy anywhere.
|
|
96
|
-
|
|
97
|
-
This is the whole point of the sink protocol: `CGBitmapContextCreate` takes the
|
|
98
|
-
destination pointer, so the raster is written once, by the rasterizer, into
|
|
99
|
-
memory the transport already owns. Android's fast path costs one full-raster
|
|
100
|
-
memcpy and cannot do better, because `PdfRenderer` owns its `Bitmap`'s pixels.
|
|
101
|
-
*/
|
|
102
|
-
func testCoreGraphicsWritesStraightIntoTheSinksBuffer() throws {
|
|
103
|
-
let r = try rasterizer()
|
|
104
|
-
let sink = RecordingSink()
|
|
105
|
-
let pixels = try r.render(
|
|
106
|
-
request(width: 200, height: 100, scale: 1), cancellation: nil, sink: sink)
|
|
107
|
-
XCTAssertTrue(pixels.isInNativeSlot)
|
|
108
|
-
|
|
109
|
-
let bytes = sink.bytes()
|
|
110
|
-
XCTAssertEqual(bytes.count, 200 * 100 * 4)
|
|
111
|
-
// Doc top-left is the PDF's top-left, which the fixture paints blue.
|
|
112
|
-
XCTAssertEqual([bytes[0], bytes[1], bytes[2], bytes[3]], [0, 0, 255, 255])
|
|
113
|
-
// And the same raster rendered to the heap agrees byte for byte.
|
|
114
|
-
let heap = try r.render(
|
|
115
|
-
request(width: 200, height: 100, scale: 1), cancellation: nil, sink: nil)
|
|
116
|
-
XCTAssertEqual(bytes, Array(heap.bytes!))
|
|
117
|
-
r.close()
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
A REFUSED reservation falls through to the heap, and the tile still goes out.
|
|
122
|
-
|
|
123
|
-
The refusal happens before anything is drawn, so it costs nothing — which is the
|
|
124
|
-
structural advantage over Android, where `nativeStoreFromBitmap` can only refuse
|
|
125
|
-
after the raster exists.
|
|
126
|
-
*/
|
|
127
|
-
func testARefusedReservationFallsBackToTheHeap() throws {
|
|
128
|
-
let r = try rasterizer()
|
|
129
|
-
let sink = RecordingSink()
|
|
130
|
-
sink.refuse = true
|
|
131
|
-
let pixels = try r.render(
|
|
132
|
-
request(width: 200, height: 100, scale: 1), cancellation: nil, sink: sink)
|
|
133
|
-
|
|
134
|
-
XCTAssertEqual(sink.reserveCount, 1, "asked once")
|
|
135
|
-
XCTAssertEqual(sink.publishCount, 0, "and never published to")
|
|
136
|
-
XCTAssertFalse(pixels.isInNativeSlot, "the result must be heap-backed")
|
|
137
|
-
XCTAssertEqual(pixels.slot, 0)
|
|
138
|
-
XCTAssertNotNil(pixels.bytes)
|
|
139
|
-
// And it is a correct raster, not a degraded one.
|
|
140
|
-
assertPixel(pixels, 10, 10, .blue)
|
|
141
|
-
assertPixel(pixels, 190, 90, .green)
|
|
142
|
-
r.close()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/// A nil sink is the heap, which is what every pixel-reading caller uses.
|
|
146
|
-
func testANilSinkProducesAHeapRaster() throws {
|
|
147
|
-
let r = try rasterizer()
|
|
148
|
-
let pixels = try r.render(
|
|
149
|
-
request(width: 200, height: 100, scale: 1), cancellation: nil, sink: nil)
|
|
150
|
-
XCTAssertFalse(pixels.isInNativeSlot)
|
|
151
|
-
XCTAssertEqual(pixels.slot, 0)
|
|
152
|
-
XCTAssertNotNil(pixels.bytes)
|
|
153
|
-
r.close()
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
A raster ABANDONED after the draw releases its sink.
|
|
158
|
-
|
|
159
|
-
The one path the rasterizer owns: a cancellation observed after the pixels are
|
|
160
|
-
already finished. Without the abandon, a superseded epoch strands one whole
|
|
161
|
-
raster per tile in a process-wide map — megabytes per gesture, freed only by the
|
|
162
|
-
next teardown.
|
|
163
|
-
*/
|
|
164
|
-
func testACancellationAfterTheDrawAbandonsTheSink() throws {
|
|
165
|
-
let r = try rasterizer()
|
|
166
|
-
let sink = RecordingSink()
|
|
167
|
-
let signal = PdfCanvasCancellation()
|
|
168
|
-
// `PdfCanvasCancellation` is checked before start, after the pool wait, and
|
|
169
|
-
// after the draw. Cancelling up front would be caught by the first check and
|
|
170
|
-
// would never reach the sink, so this test needs the signal to fire only once
|
|
171
|
-
// the reservation has happened — which is what a subclass that cancels inside
|
|
172
|
-
// `reserveBytes` gives it.
|
|
173
|
-
final class CancelOnReserve: RecordingSink {
|
|
174
|
-
var signal: PdfCanvasCancellation?
|
|
175
|
-
override func pdfCanvasReserveBytes(_ length: UInt) -> UnsafeMutableRawPointer? {
|
|
176
|
-
let pointer = super.pdfCanvasReserveBytes(length)
|
|
177
|
-
signal?.cancel()
|
|
178
|
-
return pointer
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
let cancelling = CancelOnReserve()
|
|
182
|
-
cancelling.signal = signal
|
|
183
|
-
|
|
184
|
-
XCTAssertThrowsError(
|
|
185
|
-
try r.render(
|
|
186
|
-
request(width: 200, height: 100, scale: 1), cancellation: signal,
|
|
187
|
-
sink: cancelling)
|
|
188
|
-
) { error in
|
|
189
|
-
XCTAssertEqual(PdfCanvasErrorCodeOf(error as NSError), .cancelled)
|
|
190
|
-
XCTAssertTrue(
|
|
191
|
-
(error as NSError).localizedDescription.contains("after render"),
|
|
192
|
-
"the stage should say the draw had already finished: "
|
|
193
|
-
+ (error as NSError).localizedDescription)
|
|
194
|
-
}
|
|
195
|
-
XCTAssertEqual(cancelling.reserveCount, 1)
|
|
196
|
-
XCTAssertEqual(cancelling.publishCount, 0, "never published")
|
|
197
|
-
XCTAssertEqual(cancelling.abandonCount, 1, "and released")
|
|
198
|
-
_ = sink
|
|
199
|
-
r.close()
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
A refusal is NOT an error, which is the half of the fallback worth stating.
|
|
204
|
-
|
|
205
|
-
`out-of-memory` is reserved for a request neither destination can serve, and
|
|
206
|
-
that case is not reachable deterministically from here: the heap path allocates
|
|
207
|
-
through `NSMutableData dataWithLength:`, and the only size that reliably fails is
|
|
208
|
-
one the 64 Mpx backstop rejects first — which is a different error, covered by
|
|
209
|
-
`testAnAbsurdRasterIsRefused`. So what this pins is the boundary: a refusing sink
|
|
210
|
-
produces a raster, not a failure.
|
|
211
|
-
*/
|
|
212
|
-
func testARefusalIsNotAnError() throws {
|
|
213
|
-
let r = try rasterizer()
|
|
214
|
-
let sink = RecordingSink()
|
|
215
|
-
sink.refuse = true
|
|
216
|
-
let pixels = try r.render(
|
|
217
|
-
request(width: 10, height: 10, scale: 1), cancellation: nil, sink: sink)
|
|
218
|
-
XCTAssertFalse(pixels.isInNativeSlot)
|
|
219
|
-
XCTAssertEqual(pixels.width, 10)
|
|
220
|
-
r.close()
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
`releaseSlot` is idempotent and a no-op for a heap raster.
|
|
225
|
-
|
|
226
|
-
Called from every abandon path in the binding above it, some of which overlap —
|
|
227
|
-
a `@finally` that releases unless the slot was handed to the promise can
|
|
228
|
-
legitimately run after a take.
|
|
229
|
-
*/
|
|
230
|
-
func testReleaseSlotIsIdempotent() throws {
|
|
231
|
-
let r = try rasterizer()
|
|
232
|
-
let sink = RecordingSink()
|
|
233
|
-
let pixels = try r.render(
|
|
234
|
-
request(width: 20, height: 20, scale: 1), cancellation: nil, sink: sink)
|
|
235
|
-
XCTAssertEqual(sink.abandonCount, 0)
|
|
236
|
-
pixels.releaseSlot()
|
|
237
|
-
XCTAssertEqual(sink.abandonCount, 1)
|
|
238
|
-
pixels.releaseSlot()
|
|
239
|
-
pixels.releaseSlot()
|
|
240
|
-
XCTAssertEqual(sink.abandonCount, 1, "idempotent")
|
|
241
|
-
|
|
242
|
-
let heap = try r.render(
|
|
243
|
-
request(width: 20, height: 20, scale: 1), cancellation: nil, sink: nil)
|
|
244
|
-
heap.releaseSlot() // must not crash and must do nothing
|
|
245
|
-
XCTAssertNotNil(heap.bytes)
|
|
246
|
-
r.close()
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/// Reading pixels off a slot-backed raster fails by name rather than by NPE.
|
|
250
|
-
func testReadingASlotBackedRasterRaises() throws {
|
|
251
|
-
let r = try rasterizer()
|
|
252
|
-
let sink = RecordingSink()
|
|
253
|
-
let pixels = try r.render(
|
|
254
|
-
request(width: 20, height: 20, scale: 1), cancellation: nil, sink: sink)
|
|
255
|
-
// `NSException` from Objective-C is not a Swift error, so this is checked
|
|
256
|
-
// through the property that is safe to read instead. The message is what a
|
|
257
|
-
// developer sees, and it is asserted in the ObjC unit of the binding suite.
|
|
258
|
-
XCTAssertTrue(pixels.isInNativeSlot)
|
|
259
|
-
XCTAssertNil(pixels.bytes)
|
|
260
|
-
r.close()
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
The heap sink zeroes its buffer, which is the contract `transparent` relies on.
|
|
265
|
-
|
|
266
|
-
Checked directly rather than through a render, because a render with a white
|
|
267
|
-
background would overwrite the evidence.
|
|
268
|
-
*/
|
|
269
|
-
func testTheHeapSinkReservesZeroedMemory() {
|
|
270
|
-
let sink = PdfCanvasHeapSink()
|
|
271
|
-
let pointer = sink.pdfCanvasReserveBytes(1024)
|
|
272
|
-
XCTAssertNotNil(pointer)
|
|
273
|
-
let bytes = Array(UnsafeRawBufferPointer(start: pointer!, count: 1024))
|
|
274
|
-
XCTAssertTrue(bytes.allSatisfy { $0 == 0 }, "reserveBytes must zero")
|
|
275
|
-
XCTAssertEqual(sink.pdfCanvasPublish(), 0, "the heap sink has no identifier")
|
|
276
|
-
XCTAssertNotNil(sink.data)
|
|
277
|
-
sink.pdfCanvasAbandon()
|
|
278
|
-
XCTAssertNil(sink.data)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/// A second reservation on one sink is refused, so a double draw cannot happen quietly.
|
|
282
|
-
func testTheHeapSinkRefusesASecondReservation() {
|
|
283
|
-
let sink = PdfCanvasHeapSink()
|
|
284
|
-
XCTAssertNotNil(sink.pdfCanvasReserveBytes(64))
|
|
285
|
-
XCTAssertNil(sink.pdfCanvasReserveBytes(64))
|
|
286
|
-
}
|
|
287
|
-
}
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import XCTest
|
|
3
|
-
|
|
4
|
-
@testable import PdfCanvasCore
|
|
5
|
-
@testable import PdfCanvasSlots
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
The REAL slot store — the same storage the device runs, not a double.
|
|
9
|
-
|
|
10
|
-
`SinkTests.swift` covers the sink CONTRACT with a recording double, which is the right
|
|
11
|
-
tool for "does the rasterizer reserve once and abandon on the cancel path". This file
|
|
12
|
-
covers the storage itself, and the reason it can is an `#if __has_include(<jsi/jsi.h>)`
|
|
13
|
-
in `PdfCanvasSlots.mm`: with the header absent, `PdfCanvasSlotsInstall` reports NO and
|
|
14
|
-
everything else — allocate, fill, publish, take, release — compiles and runs on a
|
|
15
|
-
laptop. Android draws the identical split for the identical reason.
|
|
16
|
-
|
|
17
|
-
WHY IT IS WORTH THE IFDEF. Every failure mode here is a leak or a use-after-free rather
|
|
18
|
-
than a wrong pixel, so a device session does not show any of them: 48 tiles rendered
|
|
19
|
-
correctly on an iPhone with the store leaking every superseded raster would look exactly
|
|
20
|
-
like 48 tiles rendered correctly. The one thing the ifdef does not cover is the JSI call
|
|
21
|
-
itself, and that is proven on device by the negotiation probe.
|
|
22
|
-
*/
|
|
23
|
-
final class SlotsTests: XCTestCase {
|
|
24
|
-
|
|
25
|
-
override func setUp() {
|
|
26
|
-
super.setUp()
|
|
27
|
-
// The store is process-wide, and these tests assert on its size. Draining first
|
|
28
|
-
// makes each one independent of whatever the last left behind — including a
|
|
29
|
-
// deliberate leak a test was checking for.
|
|
30
|
-
PdfCanvasSlotsReleaseAll()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
override func tearDown() {
|
|
34
|
-
PdfCanvasSlotsReleaseAll()
|
|
35
|
-
super.tearDown()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------- //
|
|
39
|
-
// The probe
|
|
40
|
-
// ---------------------------------------------------------------- //
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
The probe bytes are the ones the TypeScript side and Android both expect.
|
|
44
|
-
|
|
45
|
-
Three copies of this list exist — here, `TRANSPORT_PROBE_BYTES` in
|
|
46
|
-
`native-bridge.ts`, and `PdfCanvasPixelBridge.PROBE_BYTES` in Java — and they are
|
|
47
|
-
restated rather than derived on purpose: they are the only place the transport's byte
|
|
48
|
-
fidelity is asserted, so a mismatch has to make the probe FAIL and the platform fall
|
|
49
|
-
back to base64, not silently pass with different bytes.
|
|
50
|
-
*/
|
|
51
|
-
func testProbeBytesMatchTheSharedList() {
|
|
52
|
-
let expected: [UInt8] = [0x00, 0x01, 0x02, 0xFD, 0xFE, 0xFF, 0x2A, 0x07]
|
|
53
|
-
XCTAssertEqual(Array(PdfCanvasSlotsProbeBytes()), expected)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/// The probe round-trips through the real store, which is what `negotiateTransport` does.
|
|
57
|
-
func testTheProbeRoundTrips() {
|
|
58
|
-
let slot = PdfCanvasSlotsStoreProbe()
|
|
59
|
-
XCTAssertGreaterThan(slot, 0, "a published slot id is never 0 — that is the heap sentinel")
|
|
60
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 1)
|
|
61
|
-
let taken = PdfCanvasSlotsTakeForTest(slot)
|
|
62
|
-
XCTAssertEqual(taken.map { Array($0) }, Array(PdfCanvasSlotsProbeBytes()))
|
|
63
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0, "a take removes the slot")
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------- //
|
|
67
|
-
// Taken exactly once
|
|
68
|
-
// ---------------------------------------------------------------- //
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
A slot is taken EXACTLY once, and a second take finds nothing.
|
|
72
|
-
|
|
73
|
-
The host function reports "nothing" by throwing, and that is deliberate: an empty
|
|
74
|
-
`ArrayBuffer` would reach Skia and draw a black tile. So the storage has to actually
|
|
75
|
-
erase, not merely hand out a copy.
|
|
76
|
-
*/
|
|
77
|
-
func testASlotIsTakenExactlyOnce() {
|
|
78
|
-
let slot = PdfCanvasSlotsStoreProbe()
|
|
79
|
-
XCTAssertNotNil(PdfCanvasSlotsTakeForTest(slot))
|
|
80
|
-
XCTAssertNil(PdfCanvasSlotsTakeForTest(slot), "the second take must find nothing")
|
|
81
|
-
XCTAssertNil(PdfCanvasSlotsTakeForTest(slot))
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/// Slot ids are monotonic per process, which is what makes a collision unrepresentable.
|
|
85
|
-
func testSlotIdsAreMonotonic() {
|
|
86
|
-
var ids: [Int64] = []
|
|
87
|
-
for _ in 0..<8 { ids.append(PdfCanvasSlotsStoreProbe()) }
|
|
88
|
-
XCTAssertEqual(ids, ids.sorted(), "ids must increase")
|
|
89
|
-
XCTAssertEqual(Set(ids).count, ids.count, "and never repeat")
|
|
90
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 8)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
Ids are NOT reused after a take.
|
|
95
|
-
|
|
96
|
-
Android's C++ has to REFUSE a colliding id, because its ids are handed in from Java
|
|
97
|
-
and React Native builds a fresh module on every reload while the old one may still
|
|
98
|
-
have rasters parked — an overwrite there would hand one module another module's
|
|
99
|
-
raster, which is a wrong image rather than an error. Allocating the id inside the
|
|
100
|
-
store, next to the map, removes that whole failure class; this pins it.
|
|
101
|
-
*/
|
|
102
|
-
func testIdsAreNotReusedAfterATake() {
|
|
103
|
-
let first = PdfCanvasSlotsStoreProbe()
|
|
104
|
-
_ = PdfCanvasSlotsTakeForTest(first)
|
|
105
|
-
let second = PdfCanvasSlotsStoreProbe()
|
|
106
|
-
XCTAssertGreaterThan(second, first, "a freed id must not come back")
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ---------------------------------------------------------------- //
|
|
110
|
-
// Release
|
|
111
|
-
// ---------------------------------------------------------------- //
|
|
112
|
-
|
|
113
|
-
func testReleaseDropsOneSlotAndIsIdempotent() {
|
|
114
|
-
let a = PdfCanvasSlotsStoreProbe()
|
|
115
|
-
let b = PdfCanvasSlotsStoreProbe()
|
|
116
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 2)
|
|
117
|
-
PdfCanvasSlotsRelease(a)
|
|
118
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 1)
|
|
119
|
-
PdfCanvasSlotsRelease(a)
|
|
120
|
-
PdfCanvasSlotsRelease(a)
|
|
121
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 1, "releasing twice is a no-op")
|
|
122
|
-
XCTAssertNil(PdfCanvasSlotsTakeForTest(a))
|
|
123
|
-
XCTAssertNotNil(PdfCanvasSlotsTakeForTest(b), "the other slot is untouched")
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
`releaseAll` drains everything, which is what a Fast Refresh depends on.
|
|
128
|
-
|
|
129
|
-
A reload drops the JS side without calling `close`, so every slot whose promise
|
|
130
|
-
resolved but whose continuation never ran is a whole raster — megabytes — held in C++
|
|
131
|
-
with nothing left that could take it. `PdfCanvasModule.invalidate` calls this.
|
|
132
|
-
*/
|
|
133
|
-
func testReleaseAllDrainsTheStore() {
|
|
134
|
-
for _ in 0..<16 { _ = PdfCanvasSlotsStoreProbe() }
|
|
135
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 16)
|
|
136
|
-
PdfCanvasSlotsReleaseAll()
|
|
137
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0)
|
|
138
|
-
PdfCanvasSlotsReleaseAll()
|
|
139
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0, "draining an empty store is a no-op")
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/// Releasing a slot id that never existed does nothing, rather than corrupting the map.
|
|
143
|
-
func testReleasingAnUnknownSlotIsHarmless() {
|
|
144
|
-
let real = PdfCanvasSlotsStoreProbe()
|
|
145
|
-
PdfCanvasSlotsRelease(0)
|
|
146
|
-
PdfCanvasSlotsRelease(-1)
|
|
147
|
-
PdfCanvasSlotsRelease(real + 100_000)
|
|
148
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 1)
|
|
149
|
-
XCTAssertNotNil(PdfCanvasSlotsTakeForTest(real))
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------------- //
|
|
153
|
-
// The real sink, through the real rasterizer
|
|
154
|
-
// ---------------------------------------------------------------- //
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
THE HEADLINE: the bytes taken out of a slot are byte-identical to the same raster
|
|
158
|
-
rendered to the heap.
|
|
159
|
-
|
|
160
|
-
This is the transport's whole promise, and it is the one thing that cannot be checked
|
|
161
|
-
by looking at the code: CoreGraphics is handed a pointer into a `SlotBuffer` and
|
|
162
|
-
writes the tile in place, so if the buffer, the stride or the publish were wrong the
|
|
163
|
-
result would be a plausible-looking wrong image rather than an error. Its Android
|
|
164
|
-
counterpart is `directFillBytesEqualTheHeapPathForARealRaster`, which has to run on a
|
|
165
|
-
device; this one runs on a laptop.
|
|
166
|
-
*/
|
|
167
|
-
func testSlotBytesAreIdenticalToTheHeapPathForARealRaster() throws {
|
|
168
|
-
let rasterizer = try openFixture(
|
|
169
|
-
TestPdfs.grid(width: 400, height: 300), name: "slots-fidelity.pdf")
|
|
170
|
-
|
|
171
|
-
let req = request(x: 37, y: 23, width: 64, height: 41, scale: 4)
|
|
172
|
-
let heap = try rasterizer.render(req, cancellation: nil, sink: nil)
|
|
173
|
-
XCTAssertNotNil(heap.bytes)
|
|
174
|
-
|
|
175
|
-
let sink = PdfCanvasSlotSink()
|
|
176
|
-
let slotted = try rasterizer.render(req, cancellation: nil, sink: sink)
|
|
177
|
-
XCTAssertTrue(slotted.isInNativeSlot, "the real sink must produce a slot")
|
|
178
|
-
XCTAssertNil(slotted.bytes, "and no heap bytes")
|
|
179
|
-
XCTAssertEqual(slotted.width, heap.width)
|
|
180
|
-
XCTAssertEqual(slotted.height, heap.height)
|
|
181
|
-
XCTAssertEqual(slotted.rowBytes, heap.rowBytes)
|
|
182
|
-
|
|
183
|
-
let taken = PdfCanvasSlotsTakeForTest(slotted.slot)
|
|
184
|
-
XCTAssertNotNil(taken, "the slot must still be there before anyone takes it")
|
|
185
|
-
XCTAssertEqual(
|
|
186
|
-
Array(taken!), Array(heap.bytes!),
|
|
187
|
-
"the slot path and the heap path must produce identical bytes")
|
|
188
|
-
XCTAssertEqual(taken!.count, heap.rowBytes * heap.height)
|
|
189
|
-
rasterizer.close()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/// And on a transparent background, where the untouched bytes are the reserved zeroes.
|
|
193
|
-
func testSlotBytesMatchTheHeapPathOnATransparentBackground() throws {
|
|
194
|
-
let rasterizer = try openFixture(
|
|
195
|
-
TestPdfs.page(mediaBox: (0, 0, 100, 100), content: "1 0 0 rg 0 0 40 40 re f\n"),
|
|
196
|
-
name: "slots-transparent.pdf")
|
|
197
|
-
let req = request(width: 100, height: 100, scale: 2, background: .transparent)
|
|
198
|
-
let heap = try rasterizer.render(req, cancellation: nil, sink: nil)
|
|
199
|
-
let sink = PdfCanvasSlotSink()
|
|
200
|
-
let slotted = try rasterizer.render(req, cancellation: nil, sink: sink)
|
|
201
|
-
let taken = PdfCanvasSlotsTakeForTest(slotted.slot)
|
|
202
|
-
XCTAssertEqual(Array(taken!), Array(heap.bytes!))
|
|
203
|
-
// And the reserved memory really was zeroed — otherwise the untouched area would
|
|
204
|
-
// carry whatever the allocator had, which is the "tile of somebody else's heap"
|
|
205
|
-
// failure the value-initialised buffer exists to prevent.
|
|
206
|
-
let bytes = Array(taken!)
|
|
207
|
-
let cornerOffset = 10 * heap.rowBytes + 190 * 4
|
|
208
|
-
XCTAssertEqual(
|
|
209
|
-
Array(bytes[cornerOffset..<(cornerOffset + 4)]), [0, 0, 0, 0],
|
|
210
|
-
"unpainted area on a transparent background must be exactly zero")
|
|
211
|
-
rasterizer.close()
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
A raster the rasterizer ABANDONS leaves nothing in the store.
|
|
216
|
-
|
|
217
|
-
The path is a cancellation observed after the draw finished. Without the abandon a
|
|
218
|
-
superseded epoch strands one whole raster per tile — megabytes per gesture, freed only
|
|
219
|
-
by the next reload — and nothing about the rendered output would look wrong.
|
|
220
|
-
*/
|
|
221
|
-
func testAnAbandonedRasterLeavesNoSlotBehind() throws {
|
|
222
|
-
let rasterizer = try openFixture(
|
|
223
|
-
TestPdfs.grid(width: 200, height: 200), name: "slots-abandon.pdf")
|
|
224
|
-
let signal = PdfCanvasCancellation()
|
|
225
|
-
|
|
226
|
-
// A sink that cancels once the buffer is reserved, so the cancel lands after the
|
|
227
|
-
// draw rather than before it.
|
|
228
|
-
final class CancelOnReserve: PdfCanvasSlotSink {
|
|
229
|
-
var signal: PdfCanvasCancellation?
|
|
230
|
-
override func pdfCanvasReserveBytes(_ length: UInt) -> UnsafeMutableRawPointer? {
|
|
231
|
-
let pointer = super.pdfCanvasReserveBytes(length)
|
|
232
|
-
signal?.cancel()
|
|
233
|
-
return pointer
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
let sink = CancelOnReserve()
|
|
237
|
-
sink.signal = signal
|
|
238
|
-
|
|
239
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0)
|
|
240
|
-
XCTAssertThrowsError(
|
|
241
|
-
try rasterizer.render(
|
|
242
|
-
request(width: 200, height: 200, scale: 2), cancellation: signal, sink: sink)
|
|
243
|
-
) { error in
|
|
244
|
-
XCTAssertEqual(PdfCanvasErrorCodeOf(error as NSError), .cancelled)
|
|
245
|
-
}
|
|
246
|
-
XCTAssertEqual(
|
|
247
|
-
PdfCanvasSlotsCount(), 0,
|
|
248
|
-
"the abandoned raster must not be parked in the store")
|
|
249
|
-
rasterizer.close()
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/// `releaseSlot` on a published raster takes it back out of the store.
|
|
253
|
-
func testReleaseSlotOnAPublishedRasterDrainsIt() throws {
|
|
254
|
-
let rasterizer = try openFixture(
|
|
255
|
-
TestPdfs.grid(width: 100, height: 100), name: "slots-release.pdf")
|
|
256
|
-
let pixels = try rasterizer.render(
|
|
257
|
-
request(width: 100, height: 100, scale: 1), cancellation: nil,
|
|
258
|
-
sink: PdfCanvasSlotSink())
|
|
259
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 1)
|
|
260
|
-
pixels.releaseSlot()
|
|
261
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0)
|
|
262
|
-
pixels.releaseSlot()
|
|
263
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0, "idempotent")
|
|
264
|
-
XCTAssertNil(PdfCanvasSlotsTakeForTest(pixels.slot))
|
|
265
|
-
rasterizer.close()
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/// A `releaseSlot` AFTER a successful take is harmless — the normal ordering in the binding.
|
|
269
|
-
func testReleaseSlotAfterATakeIsHarmless() throws {
|
|
270
|
-
let rasterizer = try openFixture(
|
|
271
|
-
TestPdfs.grid(width: 100, height: 100), name: "slots-late-release.pdf")
|
|
272
|
-
let pixels = try rasterizer.render(
|
|
273
|
-
request(width: 100, height: 100, scale: 1), cancellation: nil,
|
|
274
|
-
sink: PdfCanvasSlotSink())
|
|
275
|
-
XCTAssertNotNil(PdfCanvasSlotsTakeForTest(pixels.slot))
|
|
276
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0)
|
|
277
|
-
pixels.releaseSlot() // must not crash, must not free anything else
|
|
278
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0)
|
|
279
|
-
rasterizer.close()
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
A sink is single-use: a second reservation is refused.
|
|
284
|
-
|
|
285
|
-
Which is what makes "reserve once, before the draw" enforceable rather than merely
|
|
286
|
-
documented — a rasterizer that drew twice into one sink would otherwise silently
|
|
287
|
-
publish the second draw.
|
|
288
|
-
*/
|
|
289
|
-
func testASinkRefusesASecondReservation() {
|
|
290
|
-
let sink = PdfCanvasSlotSink()
|
|
291
|
-
XCTAssertNotNil(sink.pdfCanvasReserveBytes(1024))
|
|
292
|
-
XCTAssertNil(sink.pdfCanvasReserveBytes(1024))
|
|
293
|
-
XCTAssertNil(sink.pdfCanvasReserveBytes(8))
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/// A zero-length reservation is refused rather than publishing an empty buffer.
|
|
297
|
-
func testAZeroLengthReservationIsRefused() {
|
|
298
|
-
let sink = PdfCanvasSlotSink()
|
|
299
|
-
XCTAssertNil(sink.pdfCanvasReserveBytes(0))
|
|
300
|
-
XCTAssertEqual(sink.pdfCanvasPublish(), 0, "nothing to publish")
|
|
301
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
RESERVE DOES NOT PUBLISH. The buffer is unreachable from the store until `publish`.
|
|
306
|
-
|
|
307
|
-
THE SAFETY PROPERTY OF THE WHOLE FILE. A buffer reachable from the process-wide map
|
|
308
|
-
before it is filled can be freed by `PdfCanvasSlotsReleaseAll` — which runs on
|
|
309
|
-
another thread, on every Fast Refresh — while CoreGraphics is still writing into it.
|
|
310
|
-
The Android JSI file calls that outcome "silent heap corruption, weeks from the change
|
|
311
|
-
that caused it".
|
|
312
|
-
*/
|
|
313
|
-
func testAReservedBufferIsNotInTheStoreUntilPublished() {
|
|
314
|
-
let sink = PdfCanvasSlotSink()
|
|
315
|
-
let pointer = sink.pdfCanvasReserveBytes(4096)
|
|
316
|
-
XCTAssertNotNil(pointer)
|
|
317
|
-
XCTAssertEqual(
|
|
318
|
-
PdfCanvasSlotsCount(), 0,
|
|
319
|
-
"a reserved-but-unpublished buffer must be unreachable from the store")
|
|
320
|
-
// Draining now must not touch it — this is the reload-lands-mid-draw case.
|
|
321
|
-
PdfCanvasSlotsReleaseAll()
|
|
322
|
-
// Still writable. If releaseAll had freed it, this is where ASan would fire.
|
|
323
|
-
memset(pointer!, 0xAB, 4096)
|
|
324
|
-
let slot = sink.pdfCanvasPublish()
|
|
325
|
-
XCTAssertGreaterThan(slot, 0)
|
|
326
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 1)
|
|
327
|
-
let taken = PdfCanvasSlotsTakeForTest(slot)
|
|
328
|
-
XCTAssertEqual(taken?.count, 4096)
|
|
329
|
-
XCTAssertEqual(Array(taken!).first, 0xAB, "the bytes written after the drain survived")
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/// Reserved memory is zeroed, which is the contract `background: transparent` rests on.
|
|
333
|
-
func testReservedMemoryIsZeroed() {
|
|
334
|
-
let sink = PdfCanvasSlotSink()
|
|
335
|
-
let pointer = sink.pdfCanvasReserveBytes(8192)
|
|
336
|
-
XCTAssertNotNil(pointer)
|
|
337
|
-
let bytes = Array(UnsafeRawBufferPointer(start: pointer!, count: 8192))
|
|
338
|
-
XCTAssertTrue(bytes.allSatisfy { $0 == 0 })
|
|
339
|
-
sink.pdfCanvasAbandon()
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
The store is safe to publish into and take from concurrently.
|
|
344
|
-
|
|
345
|
-
The real schedule: several render lanes publishing while the JS thread takes. A map
|
|
346
|
-
without a lock would corrupt here rather than merely race.
|
|
347
|
-
*/
|
|
348
|
-
func testConcurrentPublishAndTake() {
|
|
349
|
-
let lock = NSLock()
|
|
350
|
-
var slots: [Int64] = []
|
|
351
|
-
DispatchQueue.concurrentPerform(iterations: 64) { index in
|
|
352
|
-
let sink = PdfCanvasSlotSink()
|
|
353
|
-
guard let pointer = sink.pdfCanvasReserveBytes(1024) else {
|
|
354
|
-
XCTFail("reservation \(index) failed")
|
|
355
|
-
return
|
|
356
|
-
}
|
|
357
|
-
memset(pointer, Int32(index & 0xFF), 1024)
|
|
358
|
-
let slot = sink.pdfCanvasPublish()
|
|
359
|
-
lock.lock()
|
|
360
|
-
slots.append(slot)
|
|
361
|
-
lock.unlock()
|
|
362
|
-
}
|
|
363
|
-
XCTAssertEqual(slots.count, 64)
|
|
364
|
-
XCTAssertEqual(Set(slots).count, 64, "every publish got its own id")
|
|
365
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 64)
|
|
366
|
-
|
|
367
|
-
let taken = NSLock()
|
|
368
|
-
var lengths: [Int] = []
|
|
369
|
-
DispatchQueue.concurrentPerform(iterations: slots.count) { index in
|
|
370
|
-
if let data = PdfCanvasSlotsTakeForTest(slots[index]) {
|
|
371
|
-
taken.lock()
|
|
372
|
-
lengths.append(data.count)
|
|
373
|
-
taken.unlock()
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
XCTAssertEqual(lengths.count, 64, "every slot was taken exactly once")
|
|
377
|
-
XCTAssertTrue(lengths.allSatisfy { $0 == 1024 })
|
|
378
|
-
XCTAssertEqual(PdfCanvasSlotsCount(), 0)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
`PdfCanvasSlotsInstall` reports NO in this build, honestly.
|
|
383
|
-
|
|
384
|
-
Not a stub for its own sake: this is the configuration that lets every test above
|
|
385
|
-
run, and the value it returns is what a build with no JSI genuinely has to say. A
|
|
386
|
-
`YES` here would be a lie that made the JS side arm a transport it cannot redeem.
|
|
387
|
-
*/
|
|
388
|
-
func testInstallDeclinesWithoutAJsiRuntime() {
|
|
389
|
-
XCTAssertFalse(PdfCanvasSlotsInstall(nil))
|
|
390
|
-
var dummy = 0
|
|
391
|
-
XCTAssertFalse(
|
|
392
|
-
PdfCanvasSlotsInstall(&dummy),
|
|
393
|
-
"the SwiftPM build has no jsi.h, so install must decline rather than "
|
|
394
|
-
+ "dereference whatever it was handed")
|
|
395
|
-
}
|
|
396
|
-
}
|