@g9tm/lynxcontrols-native 0.0.0-stage → 1.4.2
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/README.md +85 -2
- package/android/build.gradle.kts +45 -0
- package/android/consumer-rules.pro +3 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/g9lynx/g9native/G9Bridge.kt +67 -0
- package/android/src/main/java/com/g9lynx/g9native/G9HostModule.kt +172 -0
- package/android/src/main/java/com/g9lynx/g9native/G9SqliteModule.kt +49 -0
- package/android/src/main/java/com/g9lynx/g9native/generated/G9HostModuleSpec.java +48 -0
- package/android/src/main/java/com/g9lynx/g9native/generated/G9SqliteModuleSpec.java +41 -0
- package/android/src/main/java/com/g9lynx/g9native/host/core/G9PreferencesCore.kt +100 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/androidx/AndroidxSqlEngine.kt +88 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqlEngine.kt +61 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteCore.kt +260 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteService.kt +131 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteTransport.kt +125 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteValues.kt +71 -0
- package/android/src/main/java/com/g9lynx/g9native/video/G9VideoUI.kt +220 -0
- package/android/src/main/java/com/g9lynx/g9native/video/core/G9VideoCore.kt +67 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/driver.d.ts +20 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +159 -0
- package/dist/driver.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/media.d.ts +60 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.jsx +119 -0
- package/dist/media.jsx.map +1 -0
- package/dist/protocol.d.ts +115 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +1 -0
- package/dist/services.d.ts +36 -0
- package/dist/services.d.ts.map +1 -0
- package/dist/services.js +111 -0
- package/dist/services.js.map +1 -0
- package/dist/transport.d.ts +10 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +77 -0
- package/dist/transport.js.map +1 -0
- package/generated/G9HostModule.ts +71 -0
- package/generated/G9SqliteModule.ts +68 -0
- package/ios/Tests/G9NativeTestSuite.h +25 -0
- package/ios/Tests/G9NativeTestSuite.m +560 -0
- package/ios/Tests/G9NativeTests.m +52 -0
- package/ios/g9lynx-native.podspec +29 -0
- package/ios/src/G9HostModule.h +14 -0
- package/ios/src/G9HostModule.m +121 -0
- package/ios/src/G9SqliteModule.h +14 -0
- package/ios/src/G9SqliteModule.m +167 -0
- package/ios/src/G9VideoUI.h +21 -0
- package/ios/src/G9VideoUI.m +250 -0
- package/ios/src/core/G9HostPaths.h +20 -0
- package/ios/src/core/G9HostPaths.m +23 -0
- package/ios/src/core/G9PreferencesCore.h +43 -0
- package/ios/src/core/G9PreferencesCore.m +114 -0
- package/ios/src/core/G9SqliteCore.h +62 -0
- package/ios/src/core/G9SqliteCore.m +497 -0
- package/ios/src/core/G9SqliteTransport.h +16 -0
- package/ios/src/core/G9SqliteTransport.m +85 -0
- package/ios/src/core/G9VideoCore.h +26 -0
- package/ios/src/core/G9VideoCore.m +37 -0
- package/ios/src/generated/G9HostModuleSpec.h +23 -0
- package/ios/src/generated/G9HostModuleSpec.m +2 -0
- package/ios/src/generated/G9SqliteModuleSpec.h +20 -0
- package/ios/src/generated/G9SqliteModuleSpec.m +2 -0
- package/lynx.lib.json +12 -0
- package/package.json +49 -4
- package/types/platform-native-module.d.ts +46 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
#import "G9NativeTestSuite.h"
|
|
2
|
+
|
|
3
|
+
#include <math.h>
|
|
4
|
+
#include <unistd.h>
|
|
5
|
+
|
|
6
|
+
#import "../src/core/G9HostPaths.h"
|
|
7
|
+
#import "../src/core/G9PreferencesCore.h"
|
|
8
|
+
#import "../src/core/G9VideoCore.h"
|
|
9
|
+
#import "../src/core/G9SqliteCore.h"
|
|
10
|
+
#import "../src/core/G9SqliteTransport.h"
|
|
11
|
+
|
|
12
|
+
#pragma mark - Values
|
|
13
|
+
|
|
14
|
+
static NSData *G9Hex(NSString *hex) {
|
|
15
|
+
NSMutableData *data = [NSMutableData dataWithCapacity:[hex length] / 2];
|
|
16
|
+
for (NSUInteger i = 0; i + 1 < [hex length]; i += 2) {
|
|
17
|
+
unsigned int byte = 0;
|
|
18
|
+
[[NSScanner scannerWithString:[hex substringWithRange:NSMakeRange(i, 2)]] scanHexInt:&byte];
|
|
19
|
+
uint8_t b = (uint8_t)byte;
|
|
20
|
+
[data appendBytes:&b length:1];
|
|
21
|
+
}
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static NSString *G9ToHex(NSData *data) {
|
|
26
|
+
NSMutableString *out = [NSMutableString stringWithCapacity:[data length] * 2];
|
|
27
|
+
const uint8_t *bytes = [data bytes];
|
|
28
|
+
for (NSUInteger i = 0; i < [data length]; i++) [out appendFormat:@"%02x", bytes[i]];
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A cases.json value → what the JS adapter sends (numbers as doubles, BLOBs as NSData). */
|
|
33
|
+
static id G9ToBridge(id value) {
|
|
34
|
+
if (value == nil || value == (id)[NSNull null]) return [NSNull null];
|
|
35
|
+
if ([value isKindOfClass:[NSString class]]) return value;
|
|
36
|
+
if ([value isKindOfClass:[NSNumber class]]) return [NSNumber numberWithDouble:[value doubleValue]];
|
|
37
|
+
NSDictionary *d = value;
|
|
38
|
+
if ([d objectForKey:@"int64"] != nil) return [NSDictionary dictionaryWithObject:[d objectForKey:@"int64"] forKey:@"int64"];
|
|
39
|
+
if ([d objectForKey:@"blob"] != nil) return G9Hex([d objectForKey:@"blob"]);
|
|
40
|
+
if ([d objectForKey:@"blobRepeat"] != nil) {
|
|
41
|
+
NSDictionary *r = [d objectForKey:@"blobRepeat"];
|
|
42
|
+
NSMutableData *data = [NSMutableData dataWithLength:[[r objectForKey:@"count"] unsignedIntegerValue]];
|
|
43
|
+
memset([data mutableBytes], [[r objectForKey:@"byte"] intValue], [data length]);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
if ([d objectForKey:@"real"] != nil) {
|
|
47
|
+
return [NSNumber numberWithDouble:[[d objectForKey:@"real"] isEqual:@"Infinity"] ? INFINITY : -INFINITY];
|
|
48
|
+
}
|
|
49
|
+
return [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:@"undefined"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `Object.is` between a decoded cell and an expected contract value. */
|
|
53
|
+
static BOOL G9Same(id actual, id expected) {
|
|
54
|
+
id e = G9ToBridge(expected);
|
|
55
|
+
if (e == (id)[NSNull null]) return actual == (id)[NSNull null];
|
|
56
|
+
if ([e isKindOfClass:[NSData class]]) return [actual isKindOfClass:[NSData class]] && [actual isEqualToData:e];
|
|
57
|
+
if ([e isKindOfClass:[NSDictionary class]]) {
|
|
58
|
+
return [actual isKindOfClass:[NSDictionary class]] && [[actual objectForKey:@"int64"] isEqual:[e objectForKey:@"int64"]];
|
|
59
|
+
}
|
|
60
|
+
if ([e isKindOfClass:[NSNumber class]]) {
|
|
61
|
+
if (![actual isKindOfClass:[NSNumber class]]) return NO;
|
|
62
|
+
double a = [actual doubleValue], b = [e doubleValue];
|
|
63
|
+
return a == b && signbit(a) == signbit(b);
|
|
64
|
+
}
|
|
65
|
+
return [actual isEqual:e];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static NSString *G9Describe(id value) {
|
|
69
|
+
if ([value isKindOfClass:[NSData class]]) {
|
|
70
|
+
return [value length] > 32 ? [NSString stringWithFormat:@"blob(%lu)", (unsigned long)[value length]]
|
|
71
|
+
: [NSString stringWithFormat:@"blob(%@)", G9ToHex(value)];
|
|
72
|
+
}
|
|
73
|
+
return [value description];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#pragma mark - Decoder (twin of packages/native/src/transport.ts)
|
|
77
|
+
|
|
78
|
+
static NSArray *G9Decode(NSDictionary *value) {
|
|
79
|
+
NSMutableArray *out = [NSMutableArray array];
|
|
80
|
+
BOOL rows = [[value objectForKey:@"transport"] isEqual:@"rows"];
|
|
81
|
+
for (NSDictionary *result in [value objectForKey:@"results"]) {
|
|
82
|
+
if (rows) {
|
|
83
|
+
[out addObject:result];
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
NSArray *columns = [result objectForKey:@"columns"];
|
|
87
|
+
NSInteger rowCount = [[result objectForKey:@"rowCount"] integerValue];
|
|
88
|
+
const uint8_t *types = [[result objectForKey:@"types"] bytes];
|
|
89
|
+
const uint8_t *numbers = [[result objectForKey:@"numbers"] bytes];
|
|
90
|
+
NSData *blobs = [result objectForKey:@"blobs"];
|
|
91
|
+
NSArray *strings = [result objectForKey:@"strings"];
|
|
92
|
+
NSUInteger cell = 0, number = 0, string = 0, blob = 0;
|
|
93
|
+
NSMutableArray *decoded = [NSMutableArray array];
|
|
94
|
+
for (NSInteger r = 0; r < rowCount; r++) {
|
|
95
|
+
NSMutableArray *row = [NSMutableArray array];
|
|
96
|
+
for (NSUInteger c = 0; c < [columns count]; c++) {
|
|
97
|
+
uint8_t tag = types[cell++];
|
|
98
|
+
if (tag == 1 || tag == 4) {
|
|
99
|
+
double d;
|
|
100
|
+
memcpy(&d, numbers + number * 8, 8);
|
|
101
|
+
number++;
|
|
102
|
+
if (tag == 1) {
|
|
103
|
+
[row addObject:[NSNumber numberWithDouble:d]];
|
|
104
|
+
} else {
|
|
105
|
+
[row addObject:[blobs subdataWithRange:NSMakeRange(blob, (NSUInteger)d)]];
|
|
106
|
+
blob += (NSUInteger)d;
|
|
107
|
+
}
|
|
108
|
+
} else if (tag == 2) {
|
|
109
|
+
[row addObject:[NSDictionary dictionaryWithObject:[strings objectAtIndex:string++] forKey:@"int64"]];
|
|
110
|
+
} else if (tag == 3) {
|
|
111
|
+
[row addObject:[strings objectAtIndex:string++]];
|
|
112
|
+
} else {
|
|
113
|
+
[row addObject:[NSNull null]];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
[decoded addObject:row];
|
|
117
|
+
}
|
|
118
|
+
[out addObject:[NSDictionary dictionaryWithObjectsAndKeys:columns, @"columns", decoded, @"rows",
|
|
119
|
+
[result objectForKey:@"changes"], @"changes",
|
|
120
|
+
[result objectForKey:@"lastInsertRowId"], @"lastInsertRowId", nil]];
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#pragma mark - Contract runner (step for step as run-contract.ts / ContractRunner.kt)
|
|
126
|
+
|
|
127
|
+
@interface G9ContractRun : NSObject {
|
|
128
|
+
@public
|
|
129
|
+
G9SqliteCore *core;
|
|
130
|
+
NSString *transport;
|
|
131
|
+
NSString *dir;
|
|
132
|
+
}
|
|
133
|
+
@end
|
|
134
|
+
|
|
135
|
+
@implementation G9ContractRun
|
|
136
|
+
|
|
137
|
+
- (NSArray *)pragmas:(NSDictionary *)options {
|
|
138
|
+
NSMutableArray *out = [NSMutableArray array];
|
|
139
|
+
NSDictionary *p = options;
|
|
140
|
+
if (p == nil) {
|
|
141
|
+
p = [NSDictionary dictionaryWithObjectsAndKeys:@"wal", @"journalMode", [NSNumber numberWithInt:0], @"busyTimeoutMs", nil];
|
|
142
|
+
}
|
|
143
|
+
if ([p objectForKey:@"journalMode"] != nil) {
|
|
144
|
+
[out addObject:[NSString stringWithFormat:@"PRAGMA journal_mode=%@", [[p objectForKey:@"journalMode"] uppercaseString]]];
|
|
145
|
+
}
|
|
146
|
+
[out addObject:@"PRAGMA synchronous=FULL"];
|
|
147
|
+
if ([p objectForKey:@"busyTimeoutMs"] != nil) {
|
|
148
|
+
[out addObject:[NSString stringWithFormat:@"PRAGMA busy_timeout=%d", [[p objectForKey:@"busyTimeoutMs"] intValue]]];
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
- (NSString *)path:(NSString *)caseId kind:(NSString *)kind {
|
|
154
|
+
if ([kind isEqual:@"missing"]) return [dir stringByAppendingPathComponent:[caseId stringByAppendingString:@"-missing.db"]];
|
|
155
|
+
if ([kind isEqual:@"nested"]) return [[[dir stringByAppendingPathComponent:caseId] stringByAppendingPathComponent:@"nested/deeper"] stringByAppendingPathComponent:@"db.sqlite"];
|
|
156
|
+
return [dir stringByAppendingPathComponent:[caseId stringByAppendingString:@".db"]];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
- (NSString *)check:(NSDictionary *)result expected:(NSDictionary *)expected index:(NSUInteger)k {
|
|
160
|
+
if ([expected objectForKey:@"changes"] != nil && [[result objectForKey:@"changes"] doubleValue] != [[expected objectForKey:@"changes"] doubleValue]) {
|
|
161
|
+
return [NSString stringWithFormat:@"statement %lu: changes %@, expected %@", (unsigned long)k, [result objectForKey:@"changes"], [expected objectForKey:@"changes"]];
|
|
162
|
+
}
|
|
163
|
+
if ([expected objectForKey:@"lastInsertRowId"] != nil && !G9Same([result objectForKey:@"lastInsertRowId"], [expected objectForKey:@"lastInsertRowId"])) {
|
|
164
|
+
return [NSString stringWithFormat:@"statement %lu: lastInsertRowId %@", (unsigned long)k, G9Describe([result objectForKey:@"lastInsertRowId"])];
|
|
165
|
+
}
|
|
166
|
+
if ([expected objectForKey:@"columns"] != nil && ![[result objectForKey:@"columns"] isEqual:[expected objectForKey:@"columns"]]) {
|
|
167
|
+
return [NSString stringWithFormat:@"statement %lu: columns %@", (unsigned long)k, [result objectForKey:@"columns"]];
|
|
168
|
+
}
|
|
169
|
+
NSArray *want = [expected objectForKey:@"rows"];
|
|
170
|
+
if (want != nil) {
|
|
171
|
+
NSArray *got = [result objectForKey:@"rows"];
|
|
172
|
+
if ([got count] != [want count]) return [NSString stringWithFormat:@"statement %lu: %lu rows, expected %lu", (unsigned long)k, (unsigned long)[got count], (unsigned long)[want count]];
|
|
173
|
+
for (NSUInteger r = 0; r < [want count]; r++) {
|
|
174
|
+
NSArray *w = [want objectAtIndex:r], *g = [got objectAtIndex:r];
|
|
175
|
+
if ([g count] != [w count]) return [NSString stringWithFormat:@"statement %lu row %lu: %lu values", (unsigned long)k, (unsigned long)r, (unsigned long)[g count]];
|
|
176
|
+
for (NSUInteger c = 0; c < [w count]; c++) {
|
|
177
|
+
if (!G9Same([g objectAtIndex:c], [w objectAtIndex:c])) {
|
|
178
|
+
return [NSString stringWithFormat:@"statement %lu row %lu col %lu: %@", (unsigned long)k, (unsigned long)r, (unsigned long)c, G9Describe([g objectAtIndex:c])];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return nil;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** nil on success, otherwise the failure. */
|
|
187
|
+
- (NSString *)runCase:(NSDictionary *)testCase {
|
|
188
|
+
NSString *caseId = [testCase objectForKey:@"id"];
|
|
189
|
+
NSMutableDictionary *sessions = [NSMutableDictionary dictionary];
|
|
190
|
+
NSMutableDictionary *txs = [NSMutableDictionary dictionary];
|
|
191
|
+
NSString *main = [self path:caseId kind:@"main"];
|
|
192
|
+
NSString *failure = nil;
|
|
193
|
+
NSDictionary *error = nil;
|
|
194
|
+
NSString *a = [core open:[NSDictionary dictionaryWithObjectsAndKeys:main, @"path", [NSNumber numberWithBool:YES], @"create",
|
|
195
|
+
[self pragmas:nil], @"pragmas", nil]
|
|
196
|
+
error:&error];
|
|
197
|
+
if (a == nil) return [NSString stringWithFormat:@"%@ setup: %@", caseId, error];
|
|
198
|
+
[sessions setObject:a forKey:@"a"];
|
|
199
|
+
NSArray *steps = [testCase objectForKey:@"steps"];
|
|
200
|
+
for (NSUInteger i = 0; i < [steps count] && failure == nil; i++) {
|
|
201
|
+
NSDictionary *step = [steps objectAtIndex:i];
|
|
202
|
+
NSDictionary *options = [step objectForKey:@"options"];
|
|
203
|
+
NSString *sessionName = [step objectForKey:@"session"] != nil ? [step objectForKey:@"session"] : @"a";
|
|
204
|
+
NSArray *outcome = nil;
|
|
205
|
+
error = nil;
|
|
206
|
+
BOOL ok = YES;
|
|
207
|
+
if ([step objectForKey:@"run"] != nil) {
|
|
208
|
+
NSString *token = [sessions objectForKey:sessionName] != nil ? [sessions objectForKey:sessionName] : @"closed";
|
|
209
|
+
NSMutableArray *statements = [NSMutableArray array];
|
|
210
|
+
for (NSDictionary *s in [step objectForKey:@"run"]) {
|
|
211
|
+
NSMutableArray *params = [NSMutableArray array];
|
|
212
|
+
for (id p in [s objectForKey:@"params"]) [params addObject:G9ToBridge(p)];
|
|
213
|
+
[statements addObject:[NSDictionary dictionaryWithObjectsAndKeys:[s objectForKey:@"sql"], @"sql", params, @"params", nil]];
|
|
214
|
+
}
|
|
215
|
+
NSMutableDictionary *run = [NSMutableDictionary dictionaryWithObject:transport forKey:@"transport"];
|
|
216
|
+
if ([step objectForKey:@"tx"] != nil) {
|
|
217
|
+
id tx = [txs objectForKey:[step objectForKey:@"tx"]];
|
|
218
|
+
[run setObject:tx != nil ? tx : [@"finished-" stringByAppendingString:[step objectForKey:@"tx"]] forKey:@"tx"];
|
|
219
|
+
}
|
|
220
|
+
if ([[options objectForKey:@"atomic"] boolValue]) [run setObject:[NSNumber numberWithBool:YES] forKey:@"atomic"];
|
|
221
|
+
if ([options objectForKey:@"maxRows"] != nil) [run setObject:[options objectForKey:@"maxRows"] forKey:@"maxRows"];
|
|
222
|
+
if ([[options objectForKey:@"abortedSignal"] boolValue]) {
|
|
223
|
+
// The JS adapter rejects an aborted batch before it reaches the module.
|
|
224
|
+
error = G9SqliteErrorMake(@"cancelled", @"Cancelled before the batch started.");
|
|
225
|
+
ok = NO;
|
|
226
|
+
} else {
|
|
227
|
+
NSArray *results = [core run:token statements:statements options:run error:&error];
|
|
228
|
+
ok = results != nil;
|
|
229
|
+
if (ok) outcome = G9Decode([G9SqliteTransport encode:results transport:transport]);
|
|
230
|
+
}
|
|
231
|
+
} else if ([step objectForKey:@"open"] != nil) {
|
|
232
|
+
NSString *target = [[step objectForKey:@"missingPath"] boolValue] ? [self path:caseId kind:@"missing"]
|
|
233
|
+
: [[step objectForKey:@"nestedPath"] boolValue] ? [self path:caseId kind:@"nested"] : main;
|
|
234
|
+
NSMutableDictionary *open = [NSMutableDictionary dictionaryWithObject:target forKey:@"path"];
|
|
235
|
+
BOOL create = [options objectForKey:@"create"] != nil ? [[options objectForKey:@"create"] boolValue]
|
|
236
|
+
: ![[step objectForKey:@"missingPath"] boolValue];
|
|
237
|
+
[open setObject:[NSNumber numberWithBool:create] forKey:@"create"];
|
|
238
|
+
if ([[options objectForKey:@"readOnly"] boolValue]) [open setObject:[NSNumber numberWithBool:YES] forKey:@"readOnly"];
|
|
239
|
+
NSDictionary *pragmas = [options objectForKey:@"pragmas"];
|
|
240
|
+
[open setObject:[self pragmas:pragmas != nil ? pragmas : [NSDictionary dictionary]] forKey:@"pragmas"];
|
|
241
|
+
NSString *token = [core open:open error:&error];
|
|
242
|
+
ok = token != nil;
|
|
243
|
+
if (ok) [sessions setObject:token forKey:[step objectForKey:@"open"]];
|
|
244
|
+
} else if ([step objectForKey:@"begin"] != nil) {
|
|
245
|
+
NSString *token = [sessions objectForKey:sessionName] != nil ? [sessions objectForKey:sessionName] : @"closed";
|
|
246
|
+
NSString *tx = [core begin:token mode:[step objectForKey:@"mode"] != nil ? [step objectForKey:@"mode"] : @"deferred" error:&error];
|
|
247
|
+
ok = tx != nil;
|
|
248
|
+
if (ok) [txs setObject:tx forKey:[step objectForKey:@"begin"]];
|
|
249
|
+
} else if ([step objectForKey:@"commit"] != nil) {
|
|
250
|
+
NSString *tx = [txs objectForKey:[step objectForKey:@"commit"]];
|
|
251
|
+
ok = tx != nil && [core commit:tx error:&error];
|
|
252
|
+
if (tx == nil) error = G9SqliteErrorMake(@"misuse", @"unknown tx");
|
|
253
|
+
if (ok) [txs removeObjectForKey:[step objectForKey:@"commit"]];
|
|
254
|
+
} else if ([step objectForKey:@"rollback"] != nil) {
|
|
255
|
+
NSString *tx = [txs objectForKey:[step objectForKey:@"rollback"]];
|
|
256
|
+
ok = tx != nil && [core rollback:tx error:&error];
|
|
257
|
+
if (tx == nil) error = G9SqliteErrorMake(@"misuse", @"unknown tx");
|
|
258
|
+
if (ok) [txs removeObjectForKey:[step objectForKey:@"rollback"]];
|
|
259
|
+
} else if ([step objectForKey:@"close"] != nil) {
|
|
260
|
+
NSString *token = [sessions objectForKey:[step objectForKey:@"close"]];
|
|
261
|
+
[sessions removeObjectForKey:[step objectForKey:@"close"]];
|
|
262
|
+
ok = [core close:token != nil ? token : @"closed" error:&error];
|
|
263
|
+
}
|
|
264
|
+
NSString *expectError = [step objectForKey:@"expectError"];
|
|
265
|
+
if (!ok) {
|
|
266
|
+
if (expectError == nil) {
|
|
267
|
+
failure = [NSString stringWithFormat:@"%@ step %lu: unexpected error %@", caseId, (unsigned long)i, error];
|
|
268
|
+
} else if (![[error objectForKey:@"code"] isEqual:expectError]) {
|
|
269
|
+
failure = [NSString stringWithFormat:@"%@ step %lu: error code %@, expected %@", caseId, (unsigned long)i, [error objectForKey:@"code"], expectError];
|
|
270
|
+
} else if ([step objectForKey:@"expectCompletedStatements"] != nil &&
|
|
271
|
+
![[error objectForKey:@"completedStatements"] isEqual:[step objectForKey:@"expectCompletedStatements"]]) {
|
|
272
|
+
failure = [NSString stringWithFormat:@"%@ step %lu: completedStatements %@", caseId, (unsigned long)i, [error objectForKey:@"completedStatements"]];
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (expectError != nil) {
|
|
277
|
+
failure = [NSString stringWithFormat:@"%@ step %lu: expected error %@, got success", caseId, (unsigned long)i, expectError];
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
NSArray *expect = [step objectForKey:@"expect"];
|
|
281
|
+
if (expect != nil && outcome != nil) {
|
|
282
|
+
if ([outcome count] != [expect count]) {
|
|
283
|
+
failure = [NSString stringWithFormat:@"%@ step %lu: %lu results", caseId, (unsigned long)i, (unsigned long)[outcome count]];
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
for (NSUInteger k = 0; k < [expect count] && failure == nil; k++) {
|
|
287
|
+
NSString *problem = [self check:[outcome objectAtIndex:k] expected:[expect objectAtIndex:k] index:k];
|
|
288
|
+
if (problem != nil) failure = [NSString stringWithFormat:@"%@ step %lu: %@", caseId, (unsigned long)i, problem];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (NSString *tx in [txs allValues]) [core rollback:tx error:NULL];
|
|
293
|
+
for (NSString *token in [sessions allValues]) [core close:token error:NULL];
|
|
294
|
+
return failure;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@end
|
|
298
|
+
|
|
299
|
+
#pragma mark - Interrupt helper
|
|
300
|
+
|
|
301
|
+
@interface G9InterruptProbe : NSObject {
|
|
302
|
+
@public
|
|
303
|
+
G9SqliteCore *core;
|
|
304
|
+
NSString *session;
|
|
305
|
+
NSDictionary *error;
|
|
306
|
+
volatile BOOL done;
|
|
307
|
+
}
|
|
308
|
+
- (void)runLongQuery:(id)unused;
|
|
309
|
+
@end
|
|
310
|
+
|
|
311
|
+
@implementation G9InterruptProbe
|
|
312
|
+
- (void)runLongQuery:(id)unused {
|
|
313
|
+
@autoreleasepool {
|
|
314
|
+
NSDictionary *e = nil;
|
|
315
|
+
NSString *sql = @"WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c LIMIT 2000000000) SELECT count(*) FROM c";
|
|
316
|
+
NSArray *results = [core run:session
|
|
317
|
+
statements:[NSArray arrayWithObject:[NSDictionary dictionaryWithObjectsAndKeys:sql, @"sql", [NSArray array], @"params", nil]]
|
|
318
|
+
options:[NSDictionary dictionary]
|
|
319
|
+
error:&e];
|
|
320
|
+
error = results == nil ? [e copy] : nil;
|
|
321
|
+
}
|
|
322
|
+
done = YES;
|
|
323
|
+
}
|
|
324
|
+
@end
|
|
325
|
+
|
|
326
|
+
#pragma mark - Suite
|
|
327
|
+
|
|
328
|
+
@implementation G9NativeTestSuite
|
|
329
|
+
|
|
330
|
+
+ (NSArray *)contractWithRepo:(NSString *)repo transport:(NSString *)transport passed:(NSInteger *)passed {
|
|
331
|
+
NSData *json = [NSData dataWithContentsOfFile:[repo stringByAppendingPathComponent:@"packages/sqlite/contract/cases.json"]];
|
|
332
|
+
NSDictionary *cases = json != nil ? [NSJSONSerialization JSONObjectWithData:json options:0 error:NULL] : nil;
|
|
333
|
+
if (cases == nil) return [NSArray arrayWithObject:@"cases.json not found or not JSON"];
|
|
334
|
+
G9ContractRun *run = [[G9ContractRun alloc] init];
|
|
335
|
+
run->core = [[G9SqliteCore alloc] init];
|
|
336
|
+
run->transport = transport;
|
|
337
|
+
run->dir = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"g9-ios-contract-%@-%d", transport, (int)getpid()]];
|
|
338
|
+
[[NSFileManager defaultManager] removeItemAtPath:run->dir error:NULL];
|
|
339
|
+
[[NSFileManager defaultManager] createDirectoryAtPath:run->dir withIntermediateDirectories:YES attributes:nil error:NULL];
|
|
340
|
+
NSMutableArray *failures = [NSMutableArray array];
|
|
341
|
+
NSArray *list = [cases objectForKey:@"cases"];
|
|
342
|
+
for (NSDictionary *testCase in list) {
|
|
343
|
+
NSString *failure = [run runCase:testCase];
|
|
344
|
+
if (failure != nil) [failures addObject:failure];
|
|
345
|
+
}
|
|
346
|
+
if (passed != NULL) *passed = (NSInteger)[list count] - (NSInteger)[failures count];
|
|
347
|
+
if ([list count] < 27) [failures addObject:[NSString stringWithFormat:@"only %lu cases", (unsigned long)[list count]]];
|
|
348
|
+
[[NSFileManager defaultManager] removeItemAtPath:run->dir error:NULL];
|
|
349
|
+
return failures;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Golden JSON → bridge values: `{ $bytes }` → NSData, `{ $number }` → NSNumber. */
|
|
353
|
+
static id G9Revive(id value) {
|
|
354
|
+
if ([value isKindOfClass:[NSArray class]]) {
|
|
355
|
+
NSMutableArray *out = [NSMutableArray array];
|
|
356
|
+
for (id v in value) [out addObject:G9Revive(v)];
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
if ([value isKindOfClass:[NSDictionary class]]) {
|
|
360
|
+
if ([value objectForKey:@"$bytes"] != nil) return G9Hex([value objectForKey:@"$bytes"]);
|
|
361
|
+
NSString *number = [value objectForKey:@"$number"];
|
|
362
|
+
if (number != nil) {
|
|
363
|
+
// NSString parses neither "Infinity" nor "-Infinity" (Kotlin's Double.toString spelling).
|
|
364
|
+
if ([number isEqualToString:@"Infinity"]) return [NSNumber numberWithDouble:INFINITY];
|
|
365
|
+
if ([number isEqualToString:@"-Infinity"]) return [NSNumber numberWithDouble:-INFINITY];
|
|
366
|
+
return [NSNumber numberWithDouble:[number doubleValue]];
|
|
367
|
+
}
|
|
368
|
+
NSMutableDictionary *out = [NSMutableDictionary dictionary];
|
|
369
|
+
for (NSString *k in value) [out setObject:G9Revive([value objectForKey:k]) forKey:k];
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Deep equality where numbers compare as doubles (incl. the sign of zero) and NSData by bytes. */
|
|
376
|
+
static BOOL G9DeepEqual(id a, id b) {
|
|
377
|
+
if ([a isKindOfClass:[NSNumber class]] && [b isKindOfClass:[NSNumber class]]) {
|
|
378
|
+
double x = [a doubleValue], y = [b doubleValue];
|
|
379
|
+
return x == y && signbit(x) == signbit(y);
|
|
380
|
+
}
|
|
381
|
+
if ([a isKindOfClass:[NSArray class]] && [b isKindOfClass:[NSArray class]]) {
|
|
382
|
+
if ([a count] != [b count]) return NO;
|
|
383
|
+
for (NSUInteger i = 0; i < [a count]; i++) if (!G9DeepEqual([a objectAtIndex:i], [b objectAtIndex:i])) return NO;
|
|
384
|
+
return YES;
|
|
385
|
+
}
|
|
386
|
+
if ([a isKindOfClass:[NSDictionary class]] && [b isKindOfClass:[NSDictionary class]]) {
|
|
387
|
+
if ([a count] != [b count]) return NO;
|
|
388
|
+
for (NSString *k in a) if (!G9DeepEqual([a objectForKey:k], [b objectForKey:k])) return NO;
|
|
389
|
+
return YES;
|
|
390
|
+
}
|
|
391
|
+
return [a isEqual:b];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Rebuilds core results from the golden's `expected` (rows form): int64 tags and integral numbers become integers. */
|
|
395
|
+
static NSArray *G9CoreResultsFromExpected(NSArray *expected) {
|
|
396
|
+
NSMutableArray *out = [NSMutableArray array];
|
|
397
|
+
for (NSDictionary *r in expected) {
|
|
398
|
+
NSMutableArray *rows = [NSMutableArray array];
|
|
399
|
+
for (NSArray *row in [r objectForKey:@"rows"]) {
|
|
400
|
+
NSMutableArray *cells = [NSMutableArray array];
|
|
401
|
+
for (id cell in row) {
|
|
402
|
+
if ([cell isKindOfClass:[NSDictionary class]]) {
|
|
403
|
+
[cells addObject:[NSNumber numberWithLongLong:[[cell objectForKey:@"int64"] longLongValue]]];
|
|
404
|
+
} else {
|
|
405
|
+
[cells addObject:cell];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
[rows addObject:cells];
|
|
409
|
+
}
|
|
410
|
+
id last = [r objectForKey:@"lastInsertRowId"];
|
|
411
|
+
long long lastId = [last isKindOfClass:[NSDictionary class]] ? [[last objectForKey:@"int64"] longLongValue] : [last longLongValue];
|
|
412
|
+
[out addObject:[NSDictionary dictionaryWithObjectsAndKeys:[r objectForKey:@"columns"], @"columns", rows, @"rows",
|
|
413
|
+
[NSNumber numberWithLongLong:[[r objectForKey:@"changes"] longLongValue]], @"changes",
|
|
414
|
+
[NSNumber numberWithLongLong:lastId], @"lastInsertRowId", nil]];
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
+ (NSArray *)goldenWithRepo:(NSString *)repo {
|
|
420
|
+
NSData *json = [NSData dataWithContentsOfFile:[repo stringByAppendingPathComponent:@"packages/native/contract/transport-golden.json"]];
|
|
421
|
+
NSDictionary *golden = json != nil ? G9Revive([NSJSONSerialization JSONObjectWithData:json options:0 error:NULL]) : nil;
|
|
422
|
+
if (golden == nil) return [NSArray arrayWithObject:@"transport-golden.json not found"];
|
|
423
|
+
NSMutableArray *failures = [NSMutableArray array];
|
|
424
|
+
NSArray *results = G9CoreResultsFromExpected([golden objectForKey:@"expected"]);
|
|
425
|
+
for (NSString *transport in [NSArray arrayWithObjects:@"columnar", @"rows", nil]) {
|
|
426
|
+
NSDictionary *mine = [G9SqliteTransport encode:results transport:transport];
|
|
427
|
+
if (!G9DeepEqual(mine, [golden objectForKey:transport])) {
|
|
428
|
+
[failures addObject:[NSString stringWithFormat:@"%@ frame differs from the golden:\n%@", transport, mine]];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return failures;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
+ (NSArray *)interrupt {
|
|
435
|
+
NSMutableArray *failures = [NSMutableArray array];
|
|
436
|
+
G9InterruptProbe *probe = [[G9InterruptProbe alloc] init];
|
|
437
|
+
probe->core = [[G9SqliteCore alloc] init];
|
|
438
|
+
probe->session = [probe->core open:[NSDictionary dictionaryWithObject:@":memory:" forKey:@"path"] error:NULL];
|
|
439
|
+
[NSThread detachNewThreadSelector:@selector(runLongQuery:) toTarget:probe withObject:nil];
|
|
440
|
+
usleep(200000);
|
|
441
|
+
[probe->core interrupt:probe->session];
|
|
442
|
+
for (int i = 0; i < 200 && !probe->done; i++) usleep(50000);
|
|
443
|
+
if (!probe->done) [failures addObject:@"the interrupted statement did not stop within 10 s"];
|
|
444
|
+
else if (![[probe->error objectForKey:@"code"] isEqual:@"interrupted"]) {
|
|
445
|
+
[failures addObject:[NSString stringWithFormat:@"interrupt gave %@", probe->error]];
|
|
446
|
+
}
|
|
447
|
+
// The session stays usable after an interrupt.
|
|
448
|
+
NSArray *after = [probe->core run:probe->session
|
|
449
|
+
statements:[NSArray arrayWithObject:[NSDictionary dictionaryWithObjectsAndKeys:@"SELECT 1", @"sql", nil]]
|
|
450
|
+
options:[NSDictionary dictionary]
|
|
451
|
+
error:NULL];
|
|
452
|
+
if ([after count] != 1) [failures addObject:@"the session failed after an interrupt"];
|
|
453
|
+
[probe->core interrupt:@"unknown"];
|
|
454
|
+
[probe->core closeAll];
|
|
455
|
+
return failures;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
+ (NSArray *)coreEdges {
|
|
459
|
+
NSMutableArray *f = [NSMutableArray array];
|
|
460
|
+
G9SqliteCore *core = [[G9SqliteCore alloc] init];
|
|
461
|
+
NSDictionary *e = nil;
|
|
462
|
+
NSDictionary *caps = [core capabilitiesWithError:&e];
|
|
463
|
+
if (![[caps objectForKey:@"interrupt"] boolValue] || ![[caps objectForKey:@"returning"] boolValue]) [f addObject:@"capabilities"];
|
|
464
|
+
if ([core open:[NSDictionary dictionary] error:&e] != nil || ![[e objectForKey:@"code"] isEqual:@"misuse"]) [f addObject:@"open without path"];
|
|
465
|
+
NSString *s = [core open:[NSDictionary dictionaryWithObject:@"" forKey:@"path"] error:NULL];
|
|
466
|
+
NSDictionary *bad = [NSDictionary dictionaryWithObjectsAndKeys:@"SELECT ?", @"sql", [NSArray arrayWithObject:[NSNumber numberWithDouble:NAN]], @"params", nil];
|
|
467
|
+
e = nil;
|
|
468
|
+
if ([core run:s statements:[NSArray arrayWithObject:bad] options:[NSDictionary dictionary] error:&e] != nil ||
|
|
469
|
+
![[e objectForKey:@"code"] isEqual:@"misuse"] || ![[e objectForKey:@"sql"] isEqual:@"SELECT ?"]) {
|
|
470
|
+
[f addObject:[NSString stringWithFormat:@"NaN: %@", e]];
|
|
471
|
+
}
|
|
472
|
+
NSDictionary *badInt = [NSDictionary dictionaryWithObjectsAndKeys:@"SELECT ?", @"sql",
|
|
473
|
+
[NSArray arrayWithObject:[NSDictionary dictionaryWithObject:@"12x" forKey:@"int64"]], @"params", nil];
|
|
474
|
+
if ([core run:s statements:[NSArray arrayWithObject:badInt] options:[NSDictionary dictionary] error:&e] != nil) [f addObject:@"bad int64"];
|
|
475
|
+
NSDictionary *other = [NSDictionary dictionaryWithObjectsAndKeys:@"SELECT ?", @"sql", [NSArray arrayWithObject:[NSArray array]], @"params", nil];
|
|
476
|
+
if ([core run:s statements:[NSArray arrayWithObject:other] options:[NSDictionary dictionary] error:&e] != nil) [f addObject:@"array param"];
|
|
477
|
+
if ([core run:s statements:[NSArray arrayWithObject:@"SELECT 1"] options:[NSDictionary dictionary] error:&e] != nil) [f addObject:@"statement shape"];
|
|
478
|
+
// Statement cache eviction (64) keeps working.
|
|
479
|
+
for (int i = 0; i < 70; i++) {
|
|
480
|
+
NSString *sql = [NSString stringWithFormat:@"SELECT %d", i];
|
|
481
|
+
NSArray *r = [core run:s statements:[NSArray arrayWithObject:[NSDictionary dictionaryWithObject:sql forKey:@"sql"]] options:[NSDictionary dictionary] error:NULL];
|
|
482
|
+
if ([[[[[r objectAtIndex:0] objectForKey:@"rows"] objectAtIndex:0] objectAtIndex:0] intValue] != i) [f addObject:sql];
|
|
483
|
+
}
|
|
484
|
+
NSString *tx = [core begin:s mode:@"immediate" error:NULL];
|
|
485
|
+
if (![[core sessionOfTx:tx] isEqual:s]) [f addObject:@"sessionOfTx"];
|
|
486
|
+
if ([core begin:s mode:@"deferred" error:&e] != nil) [f addObject:@"nested begin"];
|
|
487
|
+
if ([core sessionOfTx:(id)[NSNull null]] != nil) [f addObject:@"sessionOfTx type"];
|
|
488
|
+
[core close:s error:NULL];
|
|
489
|
+
if ([core commit:tx error:&e] || ![[e objectForKey:@"code"] isEqual:@"misuse"]) [f addObject:@"commit after close"];
|
|
490
|
+
if ([core close:(id)[NSNull null] error:&e]) [f addObject:@"close type"];
|
|
491
|
+
if (![G9SqliteErrorCodeFor(2067) isEqual:@"constraint"] || ![G9SqliteErrorCodeFor(26) isEqual:@"corrupt"] ||
|
|
492
|
+
![G9SqliteErrorCodeFor(13) isEqual:@"full"] || ![G9SqliteErrorCodeFor(21) isEqual:@"misuse"] ||
|
|
493
|
+
![G9SqliteErrorCodeFor(262) isEqual:@"locked"]) {
|
|
494
|
+
[f addObject:@"error codes"];
|
|
495
|
+
}
|
|
496
|
+
return f;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
+ (NSArray *)preferences {
|
|
500
|
+
NSMutableArray *f = [NSMutableArray array];
|
|
501
|
+
G9PreferencesCore *prefs = [[G9PreferencesCore alloc] initWithBackend:[[G9MemoryPreferenceBackend alloc] init]];
|
|
502
|
+
NSDictionary *e = nil;
|
|
503
|
+
[prefs set:@"g9.a" kind:@"int" value:@"-2147483648" error:NULL];
|
|
504
|
+
[prefs set:@"g9.b" kind:@"string" value:@"سلام" error:NULL];
|
|
505
|
+
[prefs set:@"other" kind:@"string" value:@"x" error:NULL];
|
|
506
|
+
if (![[[prefs get:@"g9.a"] objectForKey:@"type"] isEqual:@"int"] || [[[prefs get:@"g9.a"] objectForKey:@"value"] doubleValue] != -2147483648.0) [f addObject:@"int"];
|
|
507
|
+
if (![[[prefs get:@"g9.b"] objectForKey:@"value"] isEqual:@"سلام"]) [f addObject:@"string"];
|
|
508
|
+
if ([prefs get:@"missing"] != (id)[NSNull null]) [f addObject:@"missing"];
|
|
509
|
+
if ([[prefs entries:@"g9."] count] != 2) [f addObject:@"entries"];
|
|
510
|
+
if ([prefs set:@"k" kind:@"int" value:@"2147483648" error:&e] || ![[e objectForKey:@"code"] isEqual:@"preference-value"]) [f addObject:@"int range"];
|
|
511
|
+
if ([prefs set:@"" kind:@"string" value:@"v" error:&e] || ![[e objectForKey:@"code"] isEqual:@"preference-key"]) [f addObject:@"empty key"];
|
|
512
|
+
if ([prefs set:@"k" kind:@"float" value:@"1" error:&e]) [f addObject:@"kind"];
|
|
513
|
+
[prefs set:@"g9.b" kind:@"string" value:nil error:NULL];
|
|
514
|
+
[prefs set:@"g9.a" kind:@"remove" value:nil error:NULL];
|
|
515
|
+
if ([[prefs entries:@"g9."] count] != 0) [f addObject:@"remove"];
|
|
516
|
+
return f;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
+ (NSArray *)hostPaths {
|
|
520
|
+
NSMutableArray *f = [NSMutableArray array];
|
|
521
|
+
NSFileManager *files = [NSFileManager defaultManager];
|
|
522
|
+
NSString *dir = [G9HostPaths appDataDirectory];
|
|
523
|
+
BOOL isDir = NO;
|
|
524
|
+
if (![dir isAbsolutePath] || ![[dir lastPathComponent] isEqual:@"Library"]) [f addObject:@"app data directory path"];
|
|
525
|
+
if (![files fileExistsAtPath:dir isDirectory:&isDir] || !isDir) [f addObject:@"app data directory exists"];
|
|
526
|
+
NSString *root = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
|
|
527
|
+
[files createDirectoryAtPath:[root stringByAppendingPathComponent:@"Onboarding"] withIntermediateDirectories:YES attributes:nil error:NULL];
|
|
528
|
+
[@"x" writeToFile:[root stringByAppendingPathComponent:@"Onboarding/2video.mp4"] atomically:YES encoding:NSUTF8StringEncoding error:NULL];
|
|
529
|
+
NSString *uri = [G9HostPaths assetURIForPath:@"/Onboarding/2video.mp4" resourceRoot:root];
|
|
530
|
+
if (![uri hasPrefix:@"file://"] || ![uri hasSuffix:@"/Onboarding/2video.mp4"]) [f addObject:[NSString stringWithFormat:@"asset uri %@", uri]];
|
|
531
|
+
if ([G9HostPaths assetURIForPath:@"Onboarding/missing.mp4" resourceRoot:root] != nil) [f addObject:@"missing asset"];
|
|
532
|
+
if ([G9HostPaths assetURIForPath:@"Onboarding" resourceRoot:root] != nil) [f addObject:@"directory as asset"];
|
|
533
|
+
if ([G9HostPaths assetURIForPath:@"../etc/passwd" resourceRoot:root] != nil) [f addObject:@"escaping path"];
|
|
534
|
+
if ([G9HostPaths assetURIForPath:@"/" resourceRoot:root] != nil) [f addObject:@"empty path"];
|
|
535
|
+
[files removeItemAtPath:root error:NULL];
|
|
536
|
+
return f;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
+ (NSArray *)videoCore {
|
|
540
|
+
NSMutableArray *f = [NSMutableArray array];
|
|
541
|
+
G9VideoLoopTracker *tracker = [[G9VideoLoopTracker alloc] init];
|
|
542
|
+
[tracker setNearEndSeconds:0.5];
|
|
543
|
+
if ([tracker progressPositionMs:1000 durationMs:10000] != nil) [f addObject:@"early"];
|
|
544
|
+
NSNumber *left = [tracker progressPositionMs:9600 durationMs:10000];
|
|
545
|
+
if (left == nil || fabs([left doubleValue] - 0.4) > 1e-9) [f addObject:@"near end"];
|
|
546
|
+
if ([tracker progressPositionMs:9800 durationMs:10000] != nil) [f addObject:@"once per loop"];
|
|
547
|
+
[tracker looped];
|
|
548
|
+
if ([tracker progressPositionMs:9900 durationMs:10000] == nil) [f addObject:@"re-armed after loop"];
|
|
549
|
+
[tracker looped];
|
|
550
|
+
if ([tracker progressPositionMs:9900 durationMs:0] != nil) [f addObject:@"unknown duration"];
|
|
551
|
+
[tracker setNearEndSeconds:NAN];
|
|
552
|
+
if ([tracker nearEndSeconds] != 0 || [tracker progressPositionMs:9900 durationMs:10000] != nil) [f addObject:@"off"];
|
|
553
|
+
if ([[G9VideoMath startPositionMsForSeconds:2.5 durationMs:10000] doubleValue] != 2500) [f addObject:@"start"];
|
|
554
|
+
if ([G9VideoMath startPositionMsForSeconds:10 durationMs:10000] != nil) [f addObject:@"start past end"];
|
|
555
|
+
if ([G9VideoMath startPositionMsForSeconds:0 durationMs:10000] != nil) [f addObject:@"no start"];
|
|
556
|
+
if ([[G9VideoMath startPositionMsForSeconds:3 durationMs:-1] doubleValue] != 3000) [f addObject:@"start, unknown duration"];
|
|
557
|
+
return f;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
@end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// XCTest target (simulator, macOS runner): the portable suite in G9NativeTestSuite.m. Paths come from this file's
|
|
2
|
+
// location in the repository (the simulator can read the host file system).
|
|
3
|
+
#import <XCTest/XCTest.h>
|
|
4
|
+
#import "G9NativeTestSuite.h"
|
|
5
|
+
|
|
6
|
+
@interface G9NativeTests : XCTestCase
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
@implementation G9NativeTests
|
|
10
|
+
|
|
11
|
+
- (NSString *)repo {
|
|
12
|
+
// packages/native/ios/Tests/G9NativeTests.m → repository root.
|
|
13
|
+
NSString *file = [NSString stringWithUTF8String:__FILE__];
|
|
14
|
+
for (int i = 0; i < 5; i++) file = [file stringByDeletingLastPathComponent];
|
|
15
|
+
return file;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
- (void)testContractColumnar {
|
|
19
|
+
NSInteger passed = 0;
|
|
20
|
+
XCTAssertEqualObjects([G9NativeTestSuite contractWithRepo:self.repo transport:@"columnar" passed:&passed], @[]);
|
|
21
|
+
XCTAssertGreaterThanOrEqual(passed, 27);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
- (void)testContractRows {
|
|
25
|
+
XCTAssertEqualObjects([G9NativeTestSuite contractWithRepo:self.repo transport:@"rows" passed:NULL], @[]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (void)testTransportGolden {
|
|
29
|
+
XCTAssertEqualObjects([G9NativeTestSuite goldenWithRepo:self.repo], @[]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (void)testInterrupt {
|
|
33
|
+
XCTAssertEqualObjects([G9NativeTestSuite interrupt], @[]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (void)testCoreEdges {
|
|
37
|
+
XCTAssertEqualObjects([G9NativeTestSuite coreEdges], @[]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
- (void)testPreferences {
|
|
41
|
+
XCTAssertEqualObjects([G9NativeTestSuite preferences], @[]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (void)testHostPaths {
|
|
45
|
+
XCTAssertEqualObjects([G9NativeTestSuite hostPaths], @[]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (void)testVideoCore {
|
|
49
|
+
XCTAssertEqualObjects([G9NativeTestSuite videoCore], @[]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @g9tm/lynxcontrols-native for iOS (plan/specs/06 §5, specs/08 §3). Linked into a host by Lynx Autolink (CocoaPods) from
|
|
2
|
+
# node_modules. Lynx 4.0.3 is the latest stable release on CocoaPods trunk (4.1.x is nightly-only, 2026-10-03).
|
|
3
|
+
# The pod name `g9lynx-native` is an internal identifier that did not follow the npm rename (D12); the version is the
|
|
4
|
+
# package version, which always equals the pinned G9MAUIControls version (npm run check:versions).
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'g9lynx-native'
|
|
7
|
+
s.version = '1.4.2'
|
|
8
|
+
s.summary = 'G9 SQLite and host native modules and the <g9-video> element for Lynx (sqlite3, NSUserDefaults, UIKit, AVFoundation).'
|
|
9
|
+
s.homepage = 'https://github.com/imankari/g9lynxcontrols'
|
|
10
|
+
s.license = { :type => 'MIT' }
|
|
11
|
+
s.author = 'G9LynxControls'
|
|
12
|
+
s.platform = :ios, '15.0'
|
|
13
|
+
s.source = { :path => '.' }
|
|
14
|
+
s.source_files = 'src/**/*.{h,m}'
|
|
15
|
+
# Keep src/'s folders in the public headers: G9HostModule.h imports "generated/G9HostModuleSpec.h", which a
|
|
16
|
+
# flattened Pods/Headers/Public cannot resolve from the host app (found building apps/ios-host, 2026-10-06).
|
|
17
|
+
s.header_mappings_dir = 'src'
|
|
18
|
+
s.libraries = 'sqlite3'
|
|
19
|
+
s.frameworks = 'UIKit', 'AVFoundation'
|
|
20
|
+
s.requires_arc = true
|
|
21
|
+
s.dependency 'Lynx', '4.0.3'
|
|
22
|
+
|
|
23
|
+
# XCTest on a simulator: the shared contract (packages/sqlite/contract/cases.json), the Kotlin-written transport
|
|
24
|
+
# golden, interrupt, core edges and preferences (Tests/G9NativeTestSuite.m, also run on Linux by test:ios-core).
|
|
25
|
+
s.test_spec 'Tests' do |t|
|
|
26
|
+
t.source_files = 'Tests/G9NativeTestSuite.{h,m}', 'Tests/G9NativeTests.m'
|
|
27
|
+
t.frameworks = 'XCTest'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <Lynx/LynxModule.h>
|
|
3
|
+
#import "generated/G9HostModuleSpec.h"
|
|
4
|
+
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
|
|
7
|
+
// Lynx 4.0.3 (the iOS pods) has no @LynxNativeModuleRegister: hosts register the module on their LynxConfig
|
|
8
|
+
// (`[config registerModule:G9HostModule.class]`, apps/ios-host/Sources/GalleryViewController.m).
|
|
9
|
+
|
|
10
|
+
/** `NativeModules.G9HostModule` on iOS (plan/specs/06 §7): NSUserDefaults preferences, keyboard dismiss, haptics, insets. */
|
|
11
|
+
@interface G9HostModule : NSObject <G9HostModuleSpec>
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
NS_ASSUME_NONNULL_END
|