@sasjs/cli 6.1.0 → 6.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/build/commands/build/internal/mockSite/assets/persist.js +13 -6
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +636 -22
- package/build/commands/build/internal/mockSite/assets/sw.js +100 -11
- package/build/commands/build/internal/mockSite/mockSite.d.ts +11 -0
- package/build/commands/build/internal/mockSite/mockSite.js +87 -20
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +103 -11
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +61 -3
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +506 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +356 -1
- package/package.json +1 -1
|
@@ -216,6 +216,487 @@ describe('browser runtime', function () {
|
|
|
216
216
|
});
|
|
217
217
|
expect(result.headers).toBe('content-type: application/json');
|
|
218
218
|
});
|
|
219
|
+
it('hands a byte body back to the host as base64', function () {
|
|
220
|
+
var runtime = loadRuntime();
|
|
221
|
+
// the shape a service that streams a file uses on the server: write the
|
|
222
|
+
// body to the fileref, and say in the headers what it is
|
|
223
|
+
var result = runtime.run({
|
|
224
|
+
code: [
|
|
225
|
+
"fs.writeFileSync(_SASJS_WEBOUT_HEADERS, 'Content-type: application/zip')",
|
|
226
|
+
'fs.writeFileSync(weboutPath, Uint8Array.from([80, 75, 3, 4]))'
|
|
227
|
+
].join('\n')
|
|
228
|
+
});
|
|
229
|
+
expect(result.error).toBeNull();
|
|
230
|
+
expect(result.binary).toBe(true);
|
|
231
|
+
expect(result.webout).toBe('UEsDBA==');
|
|
232
|
+
expect(result.headers).toContain('application/zip');
|
|
233
|
+
});
|
|
234
|
+
it('accepts a Buffer, as a program written for the server uses', function () {
|
|
235
|
+
var runtime = loadRuntime();
|
|
236
|
+
expect(runtime.run({
|
|
237
|
+
code: 'fs.writeFileSync(weboutPath, Buffer.from([80, 75, 3, 4]))'
|
|
238
|
+
}).webout).toBe('UEsDBA==');
|
|
239
|
+
expect(runtime.run({
|
|
240
|
+
code: "fs.writeFileSync(weboutPath, Buffer.from('UEsDBA==', 'base64'))"
|
|
241
|
+
}).webout).toBe('UEsDBA==');
|
|
242
|
+
});
|
|
243
|
+
it('decodes a Buffer from a binary string, from hex and from text', function () {
|
|
244
|
+
var runtime = loadRuntime();
|
|
245
|
+
expect(runtime.run({
|
|
246
|
+
code: "fs.writeFileSync(weboutPath, Buffer.from('PK\\u0003\\u0004', 'binary'))"
|
|
247
|
+
}).webout).toBe('UEsDBA==');
|
|
248
|
+
expect(runtime.run({
|
|
249
|
+
code: "fs.writeFileSync(weboutPath, Buffer.from('504b0304', 'hex'))"
|
|
250
|
+
}).webout).toBe('UEsDBA==');
|
|
251
|
+
// no encoding given means utf8, as in node
|
|
252
|
+
expect(runtime.run({ code: "fs.writeFileSync(weboutPath, Buffer.from('ab'))" })
|
|
253
|
+
.webout).toBe('YWI=');
|
|
254
|
+
});
|
|
255
|
+
it('decodes a chunk of a Buffer, as a service streaming a file does', function () {
|
|
256
|
+
var runtime = loadRuntime();
|
|
257
|
+
var result = runtime.run({
|
|
258
|
+
code: [
|
|
259
|
+
"const file = '/sasjs/drive/files/pack.bin'",
|
|
260
|
+
'fs.writeFileSync(file, Uint8Array.from([80, 75, 3, 4]))',
|
|
261
|
+
'const read = fs.readFileSync(file)',
|
|
262
|
+
'const chunk = read.subarray(0, 2)',
|
|
263
|
+
"_webout = [chunk.toString('base64'), read.slice(0, 2).toString('base64'), read.subarray(2).toString('hex'), Buffer.from([1, 2, 3]).subarray(1).toString('hex'), Buffer.isBuffer(chunk)].join('|')"
|
|
264
|
+
].join('\n')
|
|
265
|
+
});
|
|
266
|
+
expect(result.error).toBeNull();
|
|
267
|
+
expect(result.webout).toBe('UEs=|UEs=|0304|0203|true');
|
|
268
|
+
});
|
|
269
|
+
it('decodes hex and base64 the way node does, including what it rejects', function () {
|
|
270
|
+
var runtime = loadRuntime();
|
|
271
|
+
var result = runtime.run({
|
|
272
|
+
code: [
|
|
273
|
+
// node stops at the first pair that is not hex, and ignores a trailing
|
|
274
|
+
// nibble
|
|
275
|
+
"const hexCases = ['zz', 'abc', '0g', '504b0304'].map((text) => Buffer.from(text, 'hex').toString('hex'))",
|
|
276
|
+
// node ignores junk characters, drops a lone trailing character, and
|
|
277
|
+
// decodes unpadded input
|
|
278
|
+
"const b64Cases = ['zz', 'A', '!!!', 'UEsDBA'].map((text) => Buffer.from(text, 'base64').length)",
|
|
279
|
+
"_webout = [hexCases.join(','), b64Cases.join(',')].join('|')"
|
|
280
|
+
].join('\n')
|
|
281
|
+
});
|
|
282
|
+
expect(result.error).toBeNull();
|
|
283
|
+
expect(result.webout).toBe(',ab,,504b0304|1,0,0,4');
|
|
284
|
+
});
|
|
285
|
+
it('clamps a toString range the way node does', function () {
|
|
286
|
+
var runtime = loadRuntime();
|
|
287
|
+
var result = runtime.run({
|
|
288
|
+
code: [
|
|
289
|
+
'const buf = Buffer.from([1, 2, 3, 4])',
|
|
290
|
+
"_webout = [buf.toString('hex', -1), buf.toString('hex', 0, -1), buf.toString('hex', 1, 3), buf.toString('hex', 3, 1)].map((value) => JSON.stringify(value)).join('|')"
|
|
291
|
+
].join('\n')
|
|
292
|
+
});
|
|
293
|
+
expect(result.webout).toBe('"01020304"|""|"0203"|""');
|
|
294
|
+
});
|
|
295
|
+
it('serialises a Buffer the way node does, with no marker in the JSON', function () {
|
|
296
|
+
var runtime = loadRuntime();
|
|
297
|
+
var result = runtime.run({
|
|
298
|
+
code: "_webout = [JSON.stringify(Buffer.from([80, 75])), Object.keys(Buffer.from([1])).join()].join('|')"
|
|
299
|
+
});
|
|
300
|
+
expect(result.webout).toBe('{"type":"Buffer","data":[80,75]}|0');
|
|
301
|
+
});
|
|
302
|
+
it('does not call an empty byte write a byte body', function () {
|
|
303
|
+
var runtime = loadRuntime();
|
|
304
|
+
// a service that writes no bytes and returns a string can only be answered
|
|
305
|
+
// with that string, so the reply is text rather than base64-decoded text
|
|
306
|
+
var result = runtime.run({
|
|
307
|
+
code: [
|
|
308
|
+
'fs.writeFileSync(weboutPath, new Uint8Array(0))',
|
|
309
|
+
"return 'from the return'"
|
|
310
|
+
].join('\n')
|
|
311
|
+
});
|
|
312
|
+
expect(result.binary).toBe(false);
|
|
313
|
+
expect(result.webout).toBe('from the return');
|
|
314
|
+
});
|
|
315
|
+
it('handles the encodings node handles, and refuses the rest', function () {
|
|
316
|
+
var runtime = loadRuntime();
|
|
317
|
+
var result = runtime.run({
|
|
318
|
+
code: [
|
|
319
|
+
"const codes = ['utf16le', 'ucs2', 'ascii'].map((encoding) => Buffer.from('PK', encoding).toString('hex'))",
|
|
320
|
+
// node encodes 'ascii' as latin1 and keeps the high bits
|
|
321
|
+
"const latin = Buffer.from('\\u00e9\\u00ff', 'ascii').toString('hex')",
|
|
322
|
+
"const url = Buffer.from([251, 255]).toString('base64url')",
|
|
323
|
+
"const back = Buffer.from('-_8', 'base64url').toString('hex')",
|
|
324
|
+
"const high = Buffer.from([0xc8]).toString('ascii').charCodeAt(0)",
|
|
325
|
+
'let refused = "none"',
|
|
326
|
+
"try { Buffer.from('x', 'not-an-encoding') } catch (e) { refused = e.message }",
|
|
327
|
+
"_webout = [codes.join(','), latin, url, back, high, refused].join('|')"
|
|
328
|
+
].join('\n')
|
|
329
|
+
});
|
|
330
|
+
expect(result.error).toBeNull();
|
|
331
|
+
// 'P' is 0x50 and 'K' is 0x4b, written little-endian for the utf16 pair;
|
|
332
|
+
// ascii encodes as latin1 (e9ff) and drops the high bit only when decoding
|
|
333
|
+
// 0xc8, leaving 0x48
|
|
334
|
+
expect(result.webout).toBe('50004b00,50004b00,504b|e9ff|-_8|fbff|72|Unknown encoding: not-an-encoding');
|
|
335
|
+
});
|
|
336
|
+
it('decodes the url-safe alphabet and keeps a utf16 byte order mark', function () {
|
|
337
|
+
var runtime = loadRuntime();
|
|
338
|
+
// node's base64 decoder accepts the url-safe characters, and its utf16le
|
|
339
|
+
// decoder keeps a leading BOM as a character
|
|
340
|
+
var result = runtime.run({
|
|
341
|
+
code: [
|
|
342
|
+
"const bytes = Buffer.from('-_8', 'base64')",
|
|
343
|
+
'const bom = Buffer.from([0xff, 0xfe, 0x41, 0x00])',
|
|
344
|
+
"_webout = [bytes.toString('hex'), JSON.stringify(bom.toString('utf16le'))].join('|')"
|
|
345
|
+
].join('\n')
|
|
346
|
+
});
|
|
347
|
+
expect(result.error).toBeNull();
|
|
348
|
+
expect(result.webout).toBe('fbff|"\ufeffA"');
|
|
349
|
+
});
|
|
350
|
+
it('refuses what node refuses, with the same complaint', function () {
|
|
351
|
+
var runtime = loadRuntime();
|
|
352
|
+
var result = runtime.run({
|
|
353
|
+
code: [
|
|
354
|
+
'const messages = []',
|
|
355
|
+
"try { Buffer.concat(['ab', Buffer.from([1])]) } catch (e) { messages.push(e instanceof TypeError, e.message) }",
|
|
356
|
+
'try { Buffer.byteLength({}) } catch (e) { messages.push(e instanceof TypeError, e.message) }',
|
|
357
|
+
'_webout = messages.join("|")'
|
|
358
|
+
].join('\n')
|
|
359
|
+
});
|
|
360
|
+
expect(result.webout).toContain('true|The "list[0]" argument must be an instance of Buffer or Uint8Array. Received type string (\'ab\')');
|
|
361
|
+
expect(result.webout).toContain('true|The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received an instance of Object');
|
|
362
|
+
});
|
|
363
|
+
it('refuses a write node would refuse, rather than stringifying it', function () {
|
|
364
|
+
var runtime = loadRuntime();
|
|
365
|
+
var result = runtime.run({
|
|
366
|
+
code: [
|
|
367
|
+
'const outcomes = []',
|
|
368
|
+
"try { fs.writeFileSync('/sasjs/drive/files/list.bin', [1, 2, 3]) } catch (e) { outcomes.push(e instanceof TypeError, e.message) }",
|
|
369
|
+
"try { fs.writeFileSync('/sasjs/drive/files/count.txt', 7) } catch (e) { outcomes.push(e instanceof TypeError, e.message) }",
|
|
370
|
+
'_webout = outcomes.join("|")'
|
|
371
|
+
].join('\n')
|
|
372
|
+
});
|
|
373
|
+
expect(result.webout).toContain('true|The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Array');
|
|
374
|
+
expect(result.webout).toContain('Received type number (7)');
|
|
375
|
+
});
|
|
376
|
+
it('reads text with an encoding, and never mistakes a marker-like value for bytes', function () {
|
|
377
|
+
var runtime = loadRuntime();
|
|
378
|
+
var result = runtime.run({
|
|
379
|
+
code: [
|
|
380
|
+
"const file = '/sasjs/drive/files/notes.txt'",
|
|
381
|
+
// a value that begins with the drive's own binary marker
|
|
382
|
+
"fs.writeFileSync(file, '\\u0000base64:AAAA')",
|
|
383
|
+
'const raw = fs.readFileSync(file)',
|
|
384
|
+
"const b64 = fs.readFileSync(file, 'base64')",
|
|
385
|
+
'const size = fs.statSync(file).size',
|
|
386
|
+
"fs.copyFileSync(file, '/sasjs/drive/files/copy.txt')",
|
|
387
|
+
"const copied = fs.readFileSync('/sasjs/drive/files/copy.txt')",
|
|
388
|
+
'_webout = [raw === "\\u0000base64:AAAA", b64, size, copied === raw].join("|")'
|
|
389
|
+
].join('\n')
|
|
390
|
+
});
|
|
391
|
+
expect(result.error).toBeNull();
|
|
392
|
+
// base64 of "\u0000base64:AAAA" - the text, not a decode of it
|
|
393
|
+
expect(result.webout).toBe('true|AGJhc2U2NDpBQUFB|12|true');
|
|
394
|
+
});
|
|
395
|
+
it('takes an encoding on an options object, and hands an escaped body back unchanged', function () {
|
|
396
|
+
var runtime = loadRuntime();
|
|
397
|
+
var result = runtime.run({
|
|
398
|
+
code: [
|
|
399
|
+
"const file = '/sasjs/drive/files/notes.txt'",
|
|
400
|
+
"fs.writeFileSync(file, 'plain notes')",
|
|
401
|
+
"const options = fs.readFileSync(file, { encoding: 'utf8' })",
|
|
402
|
+
// a body that looks like the drive's own marker
|
|
403
|
+
"_webout = '\\u0000text:not really escaped'"
|
|
404
|
+
].join('\n')
|
|
405
|
+
});
|
|
406
|
+
expect(result.error).toBeNull();
|
|
407
|
+
// the webout comes back exactly as written, escape and all
|
|
408
|
+
expect(result.webout).toBe('\u0000text:not really escaped');
|
|
409
|
+
var read = runtime.run({
|
|
410
|
+
code: [
|
|
411
|
+
"const file = '/sasjs/drive/files/notes.txt'",
|
|
412
|
+
"fs.writeFileSync(file, 'plain notes')",
|
|
413
|
+
"_webout = fs.readFileSync(file, { encoding: 'utf8' })"
|
|
414
|
+
].join('\n')
|
|
415
|
+
});
|
|
416
|
+
expect(read.webout).toBe('plain notes');
|
|
417
|
+
});
|
|
418
|
+
it('hands bytes back for an explicit null encoding, as node does', function () {
|
|
419
|
+
var runtime = loadRuntime();
|
|
420
|
+
var result = runtime.run({
|
|
421
|
+
code: [
|
|
422
|
+
"const file = '/sasjs/drive/files/notes.txt'",
|
|
423
|
+
"fs.writeFileSync(file, 'h\\u00e9llo')",
|
|
424
|
+
'const nulled = fs.readFileSync(file, { encoding: null })',
|
|
425
|
+
'const undef = fs.readFileSync(file, { encoding: undefined })',
|
|
426
|
+
'const plain = fs.readFileSync(file)',
|
|
427
|
+
"_webout = [Buffer.isBuffer(nulled), nulled.toString('base64'), nulled.length, Buffer.isBuffer(undef), undef.length, typeof plain].join('|')"
|
|
428
|
+
].join('\n')
|
|
429
|
+
});
|
|
430
|
+
expect(result.error).toBeNull();
|
|
431
|
+
// node: fs.readFileSync(f, { encoding: null }) is a Buffer of the utf8 bytes
|
|
432
|
+
expect(result.webout).toBe('true|aMOpbGxv|6|true|6|string');
|
|
433
|
+
});
|
|
434
|
+
it('refuses a corrupt byte entry wherever it is read, and measures what it hands back', function () {
|
|
435
|
+
var runtime = loadRuntime();
|
|
436
|
+
runtime.load({
|
|
437
|
+
'/sasjs/drive/files/bad.bin': '\u0000base64:!!!not base64!!!',
|
|
438
|
+
// a snapshot carrying a line break inside the base64 still decodes
|
|
439
|
+
'/sasjs/drive/files/spaced.bin': '\u0000base64:AQID\n'
|
|
440
|
+
});
|
|
441
|
+
var result = runtime.run({
|
|
442
|
+
code: [
|
|
443
|
+
"const bad = '/sasjs/drive/files/bad.bin'",
|
|
444
|
+
'let readError = "none"',
|
|
445
|
+
'try { fs.readFileSync(bad) } catch (e) { readError = [e.status, e.message].join("|") }',
|
|
446
|
+
'let statError = "none"',
|
|
447
|
+
'try { fs.statSync(bad) } catch (e) { statError = e.status }',
|
|
448
|
+
"const spaced = '/sasjs/drive/files/spaced.bin'",
|
|
449
|
+
'_webout = [readError, statError, fs.readFileSync(spaced).length, fs.statSync(spaced).size].join(";")'
|
|
450
|
+
].join('\n')
|
|
451
|
+
});
|
|
452
|
+
expect(result.error).toBeNull();
|
|
453
|
+
// the same complaint the worker gives when it serves the file, and a size
|
|
454
|
+
// that is the bytes read hands back rather than the stored string
|
|
455
|
+
expect(result.webout).toBe('500|Corrupt binary entry on the drive: not valid base64;500;3;3');
|
|
456
|
+
// the drive total is a read of every entry, so it refuses one too
|
|
457
|
+
expect(function () { return runtime.stats(); }).toThrow('Corrupt binary entry');
|
|
458
|
+
});
|
|
459
|
+
it('measures a byte entry from its stored length, whatever the padding', function () {
|
|
460
|
+
var runtime = loadRuntime();
|
|
461
|
+
var entry = function (bytes) {
|
|
462
|
+
return '\u0000base64:' + Buffer.from(bytes).toString('base64');
|
|
463
|
+
};
|
|
464
|
+
runtime.load({
|
|
465
|
+
'/sasjs/drive/files/b4.bin': entry([1, 2, 3, 4]),
|
|
466
|
+
'/sasjs/drive/files/b5.bin': entry([1, 2, 3, 4, 5]),
|
|
467
|
+
'/sasjs/drive/files/b6.bin': entry([1, 2, 3, 4, 5, 6])
|
|
468
|
+
});
|
|
469
|
+
var result = runtime.run({
|
|
470
|
+
code: [
|
|
471
|
+
"const paths = ['b4.bin', 'b5.bin', 'b6.bin']",
|
|
472
|
+
"_webout = paths.map((name) => { const file = '/sasjs/drive/files/' + name; return [fs.readFileSync(file).length, fs.statSync(file).size].join('/') }).join('|')"
|
|
473
|
+
].join('\n')
|
|
474
|
+
});
|
|
475
|
+
expect(result.error).toBeNull();
|
|
476
|
+
// the size is the bytes the entry decodes to, padding included
|
|
477
|
+
expect(result.webout).toBe('4/4|5/5|6/6');
|
|
478
|
+
});
|
|
479
|
+
it('fails a run whose byte body will not decode, rather than serving it', function () {
|
|
480
|
+
var runtime = loadRuntime();
|
|
481
|
+
runtime.load({
|
|
482
|
+
'/sasjs/drive/files/broken.bin': '\u0000base64:!!!not base64!!!'
|
|
483
|
+
});
|
|
484
|
+
var result = runtime.run({
|
|
485
|
+
code: "fs.copyFileSync('/sasjs/drive/files/broken.bin', weboutPath)"
|
|
486
|
+
});
|
|
487
|
+
// the body is validated where it is read, so a corrupt entry never reaches
|
|
488
|
+
// the worker's decoder as raw base64
|
|
489
|
+
expect(result.binary).toBeFalsy();
|
|
490
|
+
expect(result.error).toContain('Corrupt binary entry');
|
|
491
|
+
});
|
|
492
|
+
it('reports a service that redeclares a prelude name as a program error', function () {
|
|
493
|
+
var runtime = loadRuntime();
|
|
494
|
+
var result;
|
|
495
|
+
expect(function () {
|
|
496
|
+
result = runtime.run({ code: "const Buffer = 1\n_webout = 'x'" });
|
|
497
|
+
}).not.toThrow();
|
|
498
|
+
expect(result.error).toContain('Buffer');
|
|
499
|
+
});
|
|
500
|
+
it('measures a text file in bytes, as node does', function () {
|
|
501
|
+
var runtime = loadRuntime();
|
|
502
|
+
// a two-byte character: node's statSync().size is the utf8 length
|
|
503
|
+
var result = runtime.run({
|
|
504
|
+
code: [
|
|
505
|
+
"fs.writeFileSync('/sasjs/drive/files/accent.txt', '\\u00e9')",
|
|
506
|
+
"_webout = String(fs.statSync('/sasjs/drive/files/accent.txt').size)"
|
|
507
|
+
].join('\n')
|
|
508
|
+
});
|
|
509
|
+
expect(result.error).toBeNull();
|
|
510
|
+
expect(result.webout).toBe('2');
|
|
511
|
+
});
|
|
512
|
+
it('measures a Buffer or a string, and refuses what it cannot encode', function () {
|
|
513
|
+
var runtime = loadRuntime();
|
|
514
|
+
expect(runtime.run({
|
|
515
|
+
code: "_webout = [Buffer.isBuffer('no'), Buffer.byteLength('ab'), Buffer.byteLength(Buffer.from([1, 2])), Buffer.allocUnsafe(3).length, Buffer.from([65]).toString()].join('|')"
|
|
516
|
+
}).webout).toBe('false|2|2|3|A');
|
|
517
|
+
var refused = runtime.run({ code: 'Buffer.from({})' });
|
|
518
|
+
expect(refused.error).toContain('unsupported input');
|
|
519
|
+
});
|
|
520
|
+
it('offers the Buffer helpers a service uses to build a body', function () {
|
|
521
|
+
var runtime = loadRuntime();
|
|
522
|
+
var result = runtime.run({
|
|
523
|
+
code: [
|
|
524
|
+
"const joined = Buffer.concat([Buffer.from([80, 75]), Buffer.from('0304', 'hex')])",
|
|
525
|
+
"_webout = [Buffer.isBuffer(joined), Buffer.byteLength(joined), joined.toString('base64'), Buffer.alloc(2).length, joined.toString('latin1').charCodeAt(2)].join('|')"
|
|
526
|
+
].join('\n')
|
|
527
|
+
});
|
|
528
|
+
expect(result.error).toBeNull();
|
|
529
|
+
expect(result.webout).toBe('true|4|UEsDBA==|2|3');
|
|
530
|
+
});
|
|
531
|
+
it('fills a Buffer.alloc with the value node repeats', function () {
|
|
532
|
+
var runtime = loadRuntime();
|
|
533
|
+
var result = runtime.run({
|
|
534
|
+
code: [
|
|
535
|
+
"const bytes = Buffer.alloc(4, 0xff).toString('hex')",
|
|
536
|
+
"const pair = Buffer.alloc(3, 'ab').toString()",
|
|
537
|
+
"const latin = Buffer.alloc(3, '\\u00e9', 'latin1').toString('hex')",
|
|
538
|
+
"const zeros = Buffer.alloc(2).toString('hex')",
|
|
539
|
+
'const truncated = Buffer.alloc(1.5).length',
|
|
540
|
+
'let notNumber = "none"',
|
|
541
|
+
"try { Buffer.alloc('x') } catch (e) { notNumber = e.constructor.name }",
|
|
542
|
+
'let range = "none"',
|
|
543
|
+
'try { Buffer.alloc(-1) } catch (e) { range = e.constructor.name }',
|
|
544
|
+
'_webout = [bytes, pair, latin, zeros, truncated, notNumber, range].join("|")'
|
|
545
|
+
].join('\n')
|
|
546
|
+
});
|
|
547
|
+
expect(result.error).toBeNull();
|
|
548
|
+
// node truncates a fractional size and refuses a non-number or a negative
|
|
549
|
+
expect(result.webout).toBe('ffffffff|aba|e9e9e9|0000|1|TypeError|RangeError');
|
|
550
|
+
});
|
|
551
|
+
it('slices as a view, and names a Buffer method the shim does not carry', function () {
|
|
552
|
+
var runtime = loadRuntime();
|
|
553
|
+
var result = runtime.run({
|
|
554
|
+
code: [
|
|
555
|
+
'const whole = Buffer.from([1, 2, 3])',
|
|
556
|
+
'const part = whole.slice(0, 2)',
|
|
557
|
+
'part[0] = 9',
|
|
558
|
+
'let named = "none"',
|
|
559
|
+
"try { whole.write('x') } catch (e) { named = e.message }",
|
|
560
|
+
'_webout = [whole.toString("hex"), part.length, named].join("|")'
|
|
561
|
+
].join('\n')
|
|
562
|
+
});
|
|
563
|
+
expect(result.error).toBeNull();
|
|
564
|
+
// node's Buffer#slice is a view, so the edit reaches the original
|
|
565
|
+
expect(result.webout).toBe('090203|2|Buffer.write is not implemented in the browser host');
|
|
566
|
+
});
|
|
567
|
+
it('honours the encoding and the append flag node takes on a write', function () {
|
|
568
|
+
var runtime = loadRuntime();
|
|
569
|
+
var result = runtime.run({
|
|
570
|
+
code: [
|
|
571
|
+
"const file = '/sasjs/drive/files/notes.md'",
|
|
572
|
+
"fs.writeFileSync(file, 'one')",
|
|
573
|
+
"fs.writeFileSync(file, '-two', { flag: 'a' })",
|
|
574
|
+
'const appended = fs.readFileSync(file)',
|
|
575
|
+
"const latin = '/sasjs/drive/files/latin.txt'",
|
|
576
|
+
"fs.writeFileSync(latin, '\\u00e9', 'latin1')",
|
|
577
|
+
"const latinHex = fs.readFileSync(latin).toString('hex')",
|
|
578
|
+
"const latinText = fs.readFileSync(latin, 'latin1')",
|
|
579
|
+
"const chunks = '/sasjs/drive/files/chunks.bin'",
|
|
580
|
+
'fs.writeFileSync(chunks, Buffer.from([1, 2]))',
|
|
581
|
+
"fs.writeFileSync(chunks, Buffer.from([3]), { flag: 'a' })",
|
|
582
|
+
"const chunked = fs.readFileSync(chunks).toString('hex')",
|
|
583
|
+
'let refused = "none"',
|
|
584
|
+
"try { fs.writeFileSync(file, 'x', { flag: 'wx' }) } catch (e) { refused = e.message }",
|
|
585
|
+
"_webout = [appended, latinHex, latinText, chunked, refused].join('|')"
|
|
586
|
+
].join('\n')
|
|
587
|
+
});
|
|
588
|
+
expect(result.error).toBeNull();
|
|
589
|
+
expect(result.webout).toBe("one-two|e9|é|010203|Unsupported flag 'wx' in the browser host - 'w' and 'a' only");
|
|
590
|
+
});
|
|
591
|
+
it('lets an assigned webout replace a byte body, as the generated program does', function () {
|
|
592
|
+
var runtime = loadRuntime();
|
|
593
|
+
var result = runtime.run({
|
|
594
|
+
code: [
|
|
595
|
+
'fs.writeFileSync(weboutPath, Uint8Array.from([80, 75, 3, 4]))',
|
|
596
|
+
"_webout = 'text wins'"
|
|
597
|
+
].join('\n')
|
|
598
|
+
});
|
|
599
|
+
expect(result.binary).toBe(false);
|
|
600
|
+
expect(result.webout).toBe('text wins');
|
|
601
|
+
});
|
|
602
|
+
it('reads a file written as bytes back as bytes', function () {
|
|
603
|
+
var runtime = loadRuntime();
|
|
604
|
+
var result = runtime.run({
|
|
605
|
+
code: [
|
|
606
|
+
"const file = '/sasjs/drive/files/data.bin'",
|
|
607
|
+
'fs.writeFileSync(file, Uint8Array.from([1, 2, 3]))',
|
|
608
|
+
'const read = fs.readFileSync(file)',
|
|
609
|
+
"_webout = [read.length, read.toString('base64'), fs.statSync(file).size].join('|')"
|
|
610
|
+
].join('\n')
|
|
611
|
+
});
|
|
612
|
+
expect(result.error).toBeNull();
|
|
613
|
+
expect(result.webout).toBe('3|AQID|3');
|
|
614
|
+
});
|
|
615
|
+
it('takes an ArrayBuffer or a view over one as bytes', function () {
|
|
616
|
+
var runtime = loadRuntime();
|
|
617
|
+
expect(runtime.run({
|
|
618
|
+
code: 'fs.writeFileSync(weboutPath, new Uint8Array([80, 75, 3, 4]).buffer)'
|
|
619
|
+
}).webout).toBe('UEsDBA==');
|
|
620
|
+
expect(runtime.run({
|
|
621
|
+
code: 'fs.writeFileSync(weboutPath, new DataView(new Uint8Array([80, 75, 3, 4]).buffer))'
|
|
622
|
+
}).webout).toBe('UEsDBA==');
|
|
623
|
+
// and a service can read one back as hex
|
|
624
|
+
expect(runtime.run({
|
|
625
|
+
code: 'fs.writeFileSync(weboutPath, Uint8Array.from([80, 75, 3, 4]))'
|
|
626
|
+
}).binary).toBe(true);
|
|
627
|
+
expect(runtime.run({
|
|
628
|
+
code: [
|
|
629
|
+
"const file = '/sasjs/drive/files/a.bin'",
|
|
630
|
+
'fs.writeFileSync(file, Uint8Array.from([80, 75, 3, 4]))',
|
|
631
|
+
"_webout = fs.readFileSync(file).toString('hex')"
|
|
632
|
+
].join('\n')
|
|
633
|
+
}).webout).toBe('504b0304');
|
|
634
|
+
});
|
|
635
|
+
it('takes the offset and length node takes for an ArrayBuffer, and a length for concat', function () {
|
|
636
|
+
var runtime = loadRuntime();
|
|
637
|
+
var result = runtime.run({
|
|
638
|
+
code: [
|
|
639
|
+
'const ab = new Uint8Array([1, 2, 3, 4, 5]).buffer',
|
|
640
|
+
"const chunk = Buffer.from(ab, 2, 2).toString('hex')",
|
|
641
|
+
"const rest = Buffer.from(ab, 2).toString('hex')",
|
|
642
|
+
"const whole = Buffer.from(ab, 'utf8').toString('hex')",
|
|
643
|
+
'let bounds = "none"',
|
|
644
|
+
'try { Buffer.from(ab, 2, 100) } catch (e) { bounds = e.constructor.name }',
|
|
645
|
+
'let offsetError = "none"',
|
|
646
|
+
'try { Buffer.from(ab, 9) } catch (e) { offsetError = e.constructor.name }',
|
|
647
|
+
"const padded = Buffer.concat([Buffer.from([1, 2])], 4).toString('hex')",
|
|
648
|
+
"const cut = Buffer.concat([Buffer.from([1, 2]), Buffer.from([3])], 2).toString('hex')",
|
|
649
|
+
'let badLength = "none"',
|
|
650
|
+
'try { Buffer.concat([Buffer.from([1])], -1) } catch (e) { badLength = e.constructor.name }',
|
|
651
|
+
'_webout = [chunk, rest, whole, bounds, offsetError, padded, cut, badLength].join("|")'
|
|
652
|
+
].join('\n')
|
|
653
|
+
});
|
|
654
|
+
expect(result.error).toBeNull();
|
|
655
|
+
expect(result.webout).toBe('0304|030405|0102030405|RangeError|RangeError|01020000|0102|RangeError');
|
|
656
|
+
});
|
|
657
|
+
it('reports a directory and a missing file the way node does', function () {
|
|
658
|
+
var runtime = loadRuntime();
|
|
659
|
+
var result = runtime.run({
|
|
660
|
+
code: [
|
|
661
|
+
"fs.mkdirSync('/sasjs/drive/files/folder')",
|
|
662
|
+
"const isDir = fs.statSync('/sasjs/drive/files/folder').isDirectory()",
|
|
663
|
+
'let copyError = "none"',
|
|
664
|
+
"try { fs.copyFileSync('/sasjs/drive/files/none.bin', '/sasjs/drive/files/there.bin') } catch (e) { copyError = e.message }",
|
|
665
|
+
'let statError = "none"',
|
|
666
|
+
"try { fs.statSync('/sasjs/drive/files/none.bin') } catch (e) { statError = e.code }",
|
|
667
|
+
"_webout = [isDir, copyError.includes('ENOENT'), statError].join('|')"
|
|
668
|
+
].join('\n')
|
|
669
|
+
});
|
|
670
|
+
expect(result.error).toBeNull();
|
|
671
|
+
expect(result.webout).toBe('true|true|ENOENT');
|
|
672
|
+
});
|
|
673
|
+
it('logs a value it cannot serialise rather than failing the run', function () {
|
|
674
|
+
var runtime = loadRuntime();
|
|
675
|
+
var result = runtime.run({
|
|
676
|
+
code: [
|
|
677
|
+
'const loop = {}',
|
|
678
|
+
'loop.self = loop',
|
|
679
|
+
'console.log(loop)',
|
|
680
|
+
"_webout = 'logged'"
|
|
681
|
+
].join('\n')
|
|
682
|
+
});
|
|
683
|
+
expect(result.error).toBeNull();
|
|
684
|
+
expect(result.webout).toBe('logged');
|
|
685
|
+
expect(result.log).toContain('[object Object]');
|
|
686
|
+
});
|
|
687
|
+
it('carries a byte file through a copy', function () {
|
|
688
|
+
var runtime = loadRuntime();
|
|
689
|
+
var result = runtime.run({
|
|
690
|
+
code: [
|
|
691
|
+
"const from = '/sasjs/drive/files/a.bin'",
|
|
692
|
+
"const to = '/sasjs/drive/files/b.bin'",
|
|
693
|
+
'fs.writeFileSync(from, Uint8Array.from([1, 2, 3]))',
|
|
694
|
+
'fs.copyFileSync(from, to)',
|
|
695
|
+
"_webout = fs.readFileSync(to).toString('base64')"
|
|
696
|
+
].join('\n')
|
|
697
|
+
});
|
|
698
|
+
expect(result.webout).toBe('AQID');
|
|
699
|
+
});
|
|
219
700
|
it('lists folders and builds the drive tree', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
220
701
|
var runtime, listing, tree;
|
|
221
702
|
var _a;
|
|
@@ -264,6 +745,31 @@ describe('browser runtime', function () {
|
|
|
264
745
|
}
|
|
265
746
|
});
|
|
266
747
|
}); });
|
|
748
|
+
it('reports the drive total in bytes, not in stored characters', function () {
|
|
749
|
+
var runtime = loadRuntime();
|
|
750
|
+
runtime.load({});
|
|
751
|
+
runtime.run({
|
|
752
|
+
code: [
|
|
753
|
+
"const file = '/sasjs/drive/files/data/notes.txt'",
|
|
754
|
+
"fs.writeFileSync('/sasjs/drive/files/data/pack.bin', Uint8Array.from([80, 75, 3, 4]))",
|
|
755
|
+
"fs.writeFileSync(file, 'h\\u00e9llo')"
|
|
756
|
+
].join('\n')
|
|
757
|
+
});
|
|
758
|
+
// the drive total is measured per entry: a byte entry counts its bytes, not
|
|
759
|
+
// the base64 and marker it is stored behind, and text counts utf8 bytes
|
|
760
|
+
var storedBytes = function (contents) {
|
|
761
|
+
return contents.startsWith('\u0000base64:')
|
|
762
|
+
? Buffer.from(contents.slice('\u0000base64:'.length), 'base64').length
|
|
763
|
+
: Buffer.byteLength(contents.replace(/^\u0000text:/, ''));
|
|
764
|
+
};
|
|
765
|
+
var drive = runtime.dumpAll();
|
|
766
|
+
var expected = Object.values(drive).reduce(function (total, contents) { return total + storedBytes(contents); }, 0);
|
|
767
|
+
var written = Object.keys(drive).filter(function (k) { return k.includes('/data/'); });
|
|
768
|
+
expect(written.length).toBe(2);
|
|
769
|
+
expect(runtime.stats().bytes).toBe(expected);
|
|
770
|
+
// the stored representation is longer than the bytes it holds
|
|
771
|
+
expect(Object.values(drive).reduce(function (total, contents) { return total + contents.length; }, 0)).toBeGreaterThan(expected);
|
|
772
|
+
});
|
|
267
773
|
it('clears the drive and re-seeds on reset', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
268
774
|
var runtime;
|
|
269
775
|
var _a;
|