516 lines
12 KiB
JavaScript
516 lines
12 KiB
JavaScript
'use strict'
|
||
|
||
const { test } = require('tap')
|
||
const fs = require('fs')
|
||
const proxyquire = require('proxyquire')
|
||
const SonicBoom = require('../')
|
||
const { file, runTests } = require('./helper')
|
||
|
||
runTests(buildTests)
|
||
|
||
function buildTests (test, sync) {
|
||
// Reset the umask for testing
|
||
process.umask(0o000)
|
||
|
||
test('write things to a file descriptor', (t) => {
|
||
t.plan(6)
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, sync })
|
||
|
||
stream.on('ready', () => {
|
||
t.pass('ready emitted')
|
||
})
|
||
|
||
t.ok(stream.write('hello world\n'))
|
||
t.ok(stream.write('something else\n'))
|
||
|
||
stream.end()
|
||
|
||
stream.on('finish', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\nsomething else\n')
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('write things in a streaming fashion', (t) => {
|
||
t.plan(8)
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, sync })
|
||
|
||
stream.once('drain', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\n')
|
||
t.ok(stream.write('something else\n'))
|
||
})
|
||
|
||
stream.once('drain', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\nsomething else\n')
|
||
stream.end()
|
||
})
|
||
})
|
||
})
|
||
|
||
t.ok(stream.write('hello world\n'))
|
||
|
||
stream.on('finish', () => {
|
||
t.pass('finish emitted')
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('can be piped into', (t) => {
|
||
t.plan(4)
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, sync })
|
||
const source = fs.createReadStream(__filename, { encoding: 'utf8' })
|
||
|
||
source.pipe(stream)
|
||
|
||
stream.on('finish', () => {
|
||
fs.readFile(__filename, 'utf8', (err, expected) => {
|
||
t.error(err)
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, expected)
|
||
})
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('write things to a file', (t) => {
|
||
t.plan(6)
|
||
|
||
const dest = file()
|
||
const stream = new SonicBoom({ dest, sync })
|
||
|
||
stream.on('ready', () => {
|
||
t.pass('ready emitted')
|
||
})
|
||
|
||
t.ok(stream.write('hello world\n'))
|
||
t.ok(stream.write('something else\n'))
|
||
|
||
stream.end()
|
||
|
||
stream.on('finish', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\nsomething else\n')
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('minLength', (t) => {
|
||
t.plan(8)
|
||
|
||
const dest = file()
|
||
const stream = new SonicBoom({ dest, minLength: 4096, sync })
|
||
|
||
stream.on('ready', () => {
|
||
t.pass('ready emitted')
|
||
})
|
||
|
||
t.ok(stream.write('hello world\n'))
|
||
t.ok(stream.write('something else\n'))
|
||
|
||
const fail = t.fail
|
||
stream.on('drain', fail)
|
||
|
||
// bad use of timer
|
||
// TODO refactor
|
||
setTimeout(function () {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, '')
|
||
|
||
stream.end()
|
||
|
||
stream.on('finish', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\nsomething else\n')
|
||
})
|
||
})
|
||
})
|
||
}, 100)
|
||
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('write later on recoverable error', (t) => {
|
||
t.plan(8)
|
||
|
||
const fakeFs = Object.create(fs)
|
||
const SonicBoom = proxyquire('../', {
|
||
fs: fakeFs
|
||
})
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 0, sync })
|
||
|
||
stream.on('ready', () => {
|
||
t.pass('ready emitted')
|
||
})
|
||
stream.on('error', () => {
|
||
t.pass('error emitted')
|
||
})
|
||
|
||
if (sync) {
|
||
fakeFs.writeSync = function (fd, buf, enc) {
|
||
t.pass('fake fs.writeSync called')
|
||
throw new Error('recoverable error')
|
||
}
|
||
} else {
|
||
fakeFs.write = function (fd, buf, ...args) {
|
||
t.pass('fake fs.write called')
|
||
setTimeout(() => args.pop()(new Error('recoverable error')), 0)
|
||
}
|
||
}
|
||
|
||
t.ok(stream.write('hello world\n'))
|
||
|
||
setTimeout(() => {
|
||
if (sync) {
|
||
fakeFs.writeSync = fs.writeSync
|
||
} else {
|
||
fakeFs.write = fs.write
|
||
}
|
||
|
||
t.ok(stream.write('something else\n'))
|
||
|
||
stream.end()
|
||
stream.on('finish', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\nsomething else\n')
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
}, 0)
|
||
})
|
||
|
||
test('emit write events', (t) => {
|
||
t.plan(7)
|
||
|
||
const dest = file()
|
||
const stream = new SonicBoom({ dest, sync })
|
||
|
||
stream.on('ready', () => {
|
||
t.pass('ready emitted')
|
||
})
|
||
|
||
let length = 0
|
||
stream.on('write', (bytes) => {
|
||
length += bytes
|
||
})
|
||
|
||
t.ok(stream.write('hello world\n'))
|
||
t.ok(stream.write('something else\n'))
|
||
|
||
stream.end()
|
||
|
||
stream.on('finish', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\nsomething else\n')
|
||
t.equal(length, 27)
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('write multi-byte characters string over than maxWrite', (t) => {
|
||
const fakeFs = Object.create(fs)
|
||
const MAX_WRITE = 65535
|
||
fakeFs.write = function (fd, buf, ...args) {
|
||
// only write byteLength === MAX_WRITE
|
||
const _buf = Buffer.from(buf).subarray(0, MAX_WRITE)
|
||
fs.writeSync(fd, _buf)
|
||
setImmediate(args[args.length - 1], null, MAX_WRITE)
|
||
fakeFs.write = function (fd, buf, ...args) {
|
||
fs.write(fd, buf, ...args)
|
||
}
|
||
}
|
||
const SonicBoom = proxyquire('../', {
|
||
fs: fakeFs
|
||
})
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 0, sync, maxWrite: MAX_WRITE })
|
||
let buf = Buffer.alloc(MAX_WRITE).fill('x')
|
||
buf = '🌲' + buf.toString()
|
||
stream.write(buf)
|
||
stream.end()
|
||
|
||
stream.on('finish', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, buf)
|
||
t.end()
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
stream.on('error', () => {
|
||
t.pass('error emitted')
|
||
})
|
||
})
|
||
|
||
test('partial writes must preserve split utf8 characters', (t) => {
|
||
t.plan(4)
|
||
|
||
const fakeFs = Object.create(fs)
|
||
const SonicBoom = proxyquire('../', {
|
||
fs: fakeFs
|
||
})
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 0, sync })
|
||
|
||
const input = 'hello🌍world'
|
||
let calls = 0
|
||
|
||
if (sync) {
|
||
fakeFs.writeSync = function (fd, buf, enc) {
|
||
calls++
|
||
if (calls === 1) {
|
||
const first = Buffer.from(buf).subarray(0, 7)
|
||
fs.writeSync(fd, first)
|
||
return 7
|
||
}
|
||
return fs.writeSync(fd, buf)
|
||
}
|
||
} else {
|
||
fakeFs.write = function (fd, buf, ...args) {
|
||
calls++
|
||
const cb = args[args.length - 1]
|
||
if (calls === 1) {
|
||
const first = Buffer.from(buf).subarray(0, 7)
|
||
fs.write(fd, first, (err, n) => cb(err, n))
|
||
return
|
||
}
|
||
fs.write(fd, buf, cb)
|
||
}
|
||
}
|
||
|
||
stream.write(input)
|
||
stream.end()
|
||
|
||
stream.on('close', () => {
|
||
const data = fs.readFileSync(dest, 'utf8')
|
||
t.equal(calls, 2)
|
||
t.equal(data, input)
|
||
t.equal(data.includes('<27>'), false)
|
||
t.equal(data.includes('🌍'), true)
|
||
})
|
||
})
|
||
}
|
||
|
||
test('write buffers that are not totally written', (t) => {
|
||
t.plan(9)
|
||
|
||
const fakeFs = Object.create(fs)
|
||
fakeFs.write = function (fd, buf, ...args) {
|
||
t.pass('fake fs.write called')
|
||
fakeFs.write = function (fd, buf, ...args) {
|
||
t.pass('calling real fs.write, ' + buf)
|
||
fs.write(fd, buf, ...args)
|
||
}
|
||
process.nextTick(args[args.length - 1], null, 0)
|
||
}
|
||
const SonicBoom = proxyquire('../', {
|
||
fs: fakeFs
|
||
})
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 0, sync: false })
|
||
|
||
stream.on('ready', () => {
|
||
t.pass('ready emitted')
|
||
})
|
||
|
||
t.ok(stream.write('hello world\n'))
|
||
t.ok(stream.write('something else\n'))
|
||
|
||
stream.end()
|
||
|
||
stream.on('finish', () => {
|
||
fs.readFile(dest, 'utf8', (err, data) => {
|
||
t.error(err)
|
||
t.equal(data, 'hello world\nsomething else\n')
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('write enormously large buffers async', (t) => {
|
||
t.plan(3)
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 0, sync: false })
|
||
|
||
const buf = Buffer.alloc(1024).fill('x').toString() // 1 MB
|
||
let length = 0
|
||
|
||
for (let i = 0; i < 1024 * 512; i++) {
|
||
length += buf.length
|
||
stream.write(buf)
|
||
}
|
||
|
||
stream.end()
|
||
|
||
stream.on('finish', () => {
|
||
fs.stat(dest, (err, stat) => {
|
||
t.error(err)
|
||
t.equal(stat.size, length)
|
||
})
|
||
})
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('make sure `maxWrite` is passed', (t) => {
|
||
t.plan(1)
|
||
const dest = file()
|
||
const stream = new SonicBoom({ dest, maxLength: 65536 })
|
||
t.equal(stream.maxLength, 65536)
|
||
})
|
||
|
||
test('write enormously large buffers async atomicly', (t) => {
|
||
const fakeFs = Object.create(fs)
|
||
const SonicBoom = proxyquire('../', {
|
||
fs: fakeFs
|
||
})
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 0, sync: false })
|
||
|
||
const buf = Buffer.alloc(1023).fill('x').toString()
|
||
|
||
fakeFs.write = function (fd, _buf, ...args) {
|
||
if (_buf.length % buf.length !== 0) {
|
||
t.fail('write called with wrong buffer size')
|
||
}
|
||
|
||
setImmediate(args[args.length - 1], null, _buf.length)
|
||
}
|
||
|
||
for (let i = 0; i < 1024 * 512; i++) {
|
||
stream.write(buf)
|
||
}
|
||
|
||
setImmediate(() => {
|
||
for (let i = 0; i < 1024 * 512; i++) {
|
||
stream.write(buf)
|
||
}
|
||
|
||
stream.end()
|
||
})
|
||
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
t.end()
|
||
})
|
||
})
|
||
|
||
test('write should not drop new data if buffer is not full', (t) => {
|
||
t.plan(2)
|
||
const fakeFs = Object.create(fs)
|
||
const SonicBoom = proxyquire('../', {
|
||
fs: fakeFs
|
||
})
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 101, maxLength: 102, sync: false })
|
||
|
||
const buf = Buffer.alloc(100).fill('x').toString()
|
||
|
||
fakeFs.write = function (fd, _buf, ...args) {
|
||
t.equal(_buf.length, buf.length + 2)
|
||
setImmediate(args[args.length - 1], null, _buf.length)
|
||
fakeFs.write = () => t.error('shouldnt call write again')
|
||
stream.end()
|
||
}
|
||
|
||
stream.on('drop', (data) => {
|
||
t.error('should not drop')
|
||
})
|
||
|
||
stream.write(buf)
|
||
stream.write('aa')
|
||
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|
||
|
||
test('write should drop new data if buffer is full', (t) => {
|
||
t.plan(3)
|
||
const fakeFs = Object.create(fs)
|
||
const SonicBoom = proxyquire('../', {
|
||
fs: fakeFs
|
||
})
|
||
|
||
const dest = file()
|
||
const fd = fs.openSync(dest, 'w')
|
||
const stream = new SonicBoom({ fd, minLength: 101, maxLength: 102, sync: false })
|
||
|
||
const buf = Buffer.alloc(100).fill('x').toString()
|
||
|
||
fakeFs.write = function (fd, _buf, ...args) {
|
||
t.equal(_buf.length, buf.length)
|
||
setImmediate(args[args.length - 1], null, _buf.length)
|
||
fakeFs.write = () => t.error('shouldnt call write more than once')
|
||
}
|
||
|
||
stream.on('drop', (data) => {
|
||
t.equal(data.length, 3)
|
||
stream.end()
|
||
})
|
||
|
||
stream.write(buf)
|
||
stream.write('aaa')
|
||
|
||
stream.on('close', () => {
|
||
t.pass('close emitted')
|
||
})
|
||
})
|