mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-03 21:40:28 -05:00
e4442ab7ac
* refactor: remove global.root & use in requires * fix types
376 lines
10 KiB
JavaScript
376 lines
10 KiB
JavaScript
require('../../spec_helper')
|
|
|
|
const os = require('os')
|
|
const path = require('path')
|
|
const Promise = require('bluebird')
|
|
const lockFile = Promise.promisifyAll(require('lockfile'))
|
|
const { fs } = require(`../../../lib/util/fs`)
|
|
const env = require(`../../../lib/util/env`)
|
|
const exit = require(`../../../lib/util/exit`)
|
|
const FileUtil = require(`../../../lib/util/file`)
|
|
|
|
describe('lib/util/file', () => {
|
|
beforeEach(function () {
|
|
this.dir = path.join(os.tmpdir(), 'cypress', 'file_spec')
|
|
this.path = path.join(this.dir, 'file.json')
|
|
|
|
return fs.removeAsync(this.dir).catch(() => {})
|
|
})
|
|
|
|
// ignore error if directory didn't exist in the first place
|
|
it('throws if path is not specified', () => {
|
|
expect(() => {
|
|
return new FileUtil()
|
|
}).to.throw('Must specify path to file when creating new FileUtil()')
|
|
})
|
|
|
|
it('unlocks file on exit', function () {
|
|
sinon.spy(lockFile, 'unlockSync')
|
|
sinon.stub(exit, 'ensure')
|
|
new FileUtil({ path: this.path })
|
|
exit.ensure.yield()
|
|
|
|
expect(lockFile.unlockSync).to.be.called
|
|
})
|
|
|
|
context('#transaction', () => {
|
|
beforeEach(function () {
|
|
this.fileUtil = new FileUtil({ path: this.path })
|
|
})
|
|
|
|
it('ensures returned promise completely resolves before moving on with queue', function () {
|
|
return Promise.all([
|
|
this.fileUtil.transaction((tx) => {
|
|
return tx.get('items', []).then((items) => {
|
|
return tx.set('items', items.concat('foo'))
|
|
})
|
|
}),
|
|
|
|
this.fileUtil.transaction((tx) => {
|
|
return tx.get('items', []).then((items) => {
|
|
return tx.set('items', items.concat('bar'))
|
|
})
|
|
}),
|
|
|
|
this.fileUtil.transaction((tx) => {
|
|
return tx.get('items', []).then((items) => {
|
|
return tx.set('items', items.concat('baz'))
|
|
})
|
|
}),
|
|
])
|
|
.then(() => {
|
|
return this.fileUtil.transaction((tx) => {
|
|
return tx.get('items').then((items) => {
|
|
expect(items).to.eql(['foo', 'bar', 'baz'])
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
context('#get', () => {
|
|
beforeEach(function () {
|
|
this.fileUtil = new FileUtil({ path: this.path })
|
|
})
|
|
|
|
it('resolves entire object if given no key', function () {
|
|
return this.fileUtil.get().then((contents) => {
|
|
expect(contents).to.eql({})
|
|
})
|
|
})
|
|
|
|
it('resolves value for key when one is set', function () {
|
|
return this.fileUtil.set('foo', 'bar')
|
|
.then(() => {
|
|
return this.fileUtil.get('foo')
|
|
}).then((value) => {
|
|
expect(value).to.equal('bar')
|
|
})
|
|
})
|
|
|
|
it('resolves value for path when one is set', function () {
|
|
return this.fileUtil.set('foo.baz', 'bar')
|
|
.then(() => {
|
|
return this.fileUtil.get('foo.baz')
|
|
}).then((value) => {
|
|
expect(value).to.equal('bar')
|
|
})
|
|
})
|
|
|
|
it('resolves default value if given key is undefined', function () {
|
|
return this.fileUtil.get('foo', 'default').then((value) => {
|
|
expect(value).to.equal('default')
|
|
})
|
|
})
|
|
|
|
it('resolves undefined if value is undefined', function () {
|
|
return this.fileUtil.get('foo').then((value) => {
|
|
expect(value).to.be.undefined
|
|
})
|
|
})
|
|
|
|
it('resolves null if value is null', function () {
|
|
return this.fileUtil.set('foo', null)
|
|
.then(() => {
|
|
return this.fileUtil.get('foo')
|
|
}).then((value) => {
|
|
expect(value).to.be.null
|
|
})
|
|
})
|
|
|
|
it('resolves empty object when contents file does not exist', function () {
|
|
return fs.removeAsync(this.dir)
|
|
.then(() => {
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({})
|
|
})
|
|
})
|
|
|
|
it('resolves empty object when contents file is empty', function () {
|
|
return fs.ensureDirAsync(this.dir)
|
|
.then(() => {
|
|
return fs.writeFileAsync(path.join(this.dir, 'file.json'), '')
|
|
}).then(() => {
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({})
|
|
})
|
|
})
|
|
|
|
it('resolves empty object when it can\'t get lock on file on initial read', function () {
|
|
return fs.ensureDirAsync(this.dir)
|
|
.then(() => {
|
|
return fs.writeJsonAsync(this.path, { foo: 'bar' })
|
|
}).then(() => {
|
|
sinon.stub(lockFile, 'lockAsync').rejects({ name: '', message: '', code: 'EEXIST' })
|
|
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({})
|
|
})
|
|
})
|
|
|
|
it('resolves cached contents when it can\'t get lock on file after an initial read', function () {
|
|
return this.fileUtil.set('foo', 'bar')
|
|
.then(() => {
|
|
sinon.stub(lockFile, 'lockAsync').rejects({ name: '', message: '', code: 'EEXIST' })
|
|
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({ foo: 'bar' })
|
|
})
|
|
})
|
|
|
|
it('resolves empty object when contents file has invalid json', function () {
|
|
return fs.ensureDirAsync(this.dir)
|
|
.then(() => {
|
|
return fs.writeFileAsync(path.join(this.dir, 'file.json'), '{')
|
|
}).then(() => {
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({})
|
|
})
|
|
})
|
|
|
|
it('debounces reading from disk', function () {
|
|
sinon.stub(fs, 'readJsonAsync').resolves({})
|
|
|
|
return Promise.all([
|
|
this.fileUtil.get(),
|
|
this.fileUtil.get(),
|
|
this.fileUtil.get(),
|
|
])
|
|
.then(() => {
|
|
expect(fs.readJsonAsync).to.be.calledOnce
|
|
})
|
|
})
|
|
|
|
it('locks file while reading', function () {
|
|
sinon.spy(lockFile, 'lockAsync')
|
|
|
|
return this.fileUtil.get().then(() => {
|
|
expect(lockFile.lockAsync).to.be.called
|
|
})
|
|
})
|
|
|
|
it('unlocks file when finished reading', function () {
|
|
sinon.spy(lockFile, 'unlockAsync')
|
|
|
|
return this.fileUtil.get().then(() => {
|
|
expect(lockFile.unlockAsync).to.be.called
|
|
})
|
|
})
|
|
|
|
it('unlocks file even if reading fails', function () {
|
|
sinon.spy(lockFile, 'unlockAsync')
|
|
sinon.stub(fs, 'readJsonAsync').rejects(new Error('fail!'))
|
|
|
|
return this.fileUtil.get().catch(() => {
|
|
expect(lockFile.unlockAsync).to.be.called
|
|
})
|
|
})
|
|
|
|
it('times out and carries on if unlocking times out', function () {
|
|
sinon.stub(lockFile, 'lockAsync').resolves()
|
|
sinon.stub(lockFile, 'unlockAsync').callsFake(() => {
|
|
return Promise.delay(1e9)
|
|
})
|
|
|
|
sinon.stub(fs, 'readJsonAsync').resolves({})
|
|
sinon.stub(env, 'get').withArgs('FILE_UNLOCK_TIMEOUT').returns(100)
|
|
|
|
return this.fileUtil.get()
|
|
})
|
|
})
|
|
|
|
context('#set', () => {
|
|
beforeEach(function () {
|
|
this.fileUtil = new FileUtil({ path: this.path })
|
|
})
|
|
|
|
it('throws if 1st argument is not a string or plain object', function () {
|
|
expect(() => {
|
|
return this.fileUtil.set(1)
|
|
}).to.throw('Expected `key` to be of type `string` or `object`, got `number`')
|
|
|
|
expect(() => {
|
|
return this.fileUtil.set([])
|
|
}).to.throw('Expected `key` to be of type `string` or `object`, got `array`')
|
|
})
|
|
|
|
it('sets value for given key', function () {
|
|
return this.fileUtil.set('foo', 'bar')
|
|
.then(() => {
|
|
return this.fileUtil.get('foo')
|
|
}).then((value) => {
|
|
expect(value).to.equal('bar')
|
|
})
|
|
})
|
|
|
|
it('sets value for given path', function () {
|
|
return this.fileUtil.set('foo.baz', 'bar')
|
|
.then(() => {
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({
|
|
foo: {
|
|
baz: 'bar',
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
it('sets values for object', function () {
|
|
return this.fileUtil.set({
|
|
foo: 'bar',
|
|
baz: {
|
|
qux: 'lolz',
|
|
},
|
|
})
|
|
.then(() => {
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({
|
|
foo: 'bar',
|
|
baz: {
|
|
qux: 'lolz',
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
it('leaves existing values alone', function () {
|
|
return this.fileUtil.set('foo', 'bar')
|
|
.then(() => {
|
|
return this.fileUtil.set('baz', 'qux')
|
|
}).then(() => {
|
|
return this.fileUtil.get()
|
|
}).then((contents) => {
|
|
expect(contents).to.eql({
|
|
foo: 'bar',
|
|
baz: 'qux',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('updates file on disk', function () {
|
|
return this.fileUtil.set('foo', 'bar')
|
|
.then(() => {
|
|
return fs.readFileAsync(path.join(this.dir, 'file.json'), 'utf8')
|
|
}).then((contents) => {
|
|
expect(JSON.parse(contents)).to.eql({ foo: 'bar' })
|
|
})
|
|
})
|
|
|
|
it('locks file while writing', function () {
|
|
sinon.spy(lockFile, 'lockAsync')
|
|
|
|
return this.fileUtil.set('foo', 'bar').then(() => {
|
|
expect(lockFile.lockAsync).to.be.called
|
|
})
|
|
})
|
|
|
|
it('unlocks file when finished writing', function () {
|
|
sinon.spy(lockFile, 'unlockAsync')
|
|
|
|
return this.fileUtil.set('foo', 'bar').then(() => {
|
|
expect(lockFile.unlockAsync).to.be.called
|
|
})
|
|
})
|
|
|
|
it('unlocks file even if writing fails', function () {
|
|
sinon.spy(lockFile, 'unlockAsync')
|
|
sinon.stub(fs, 'outputJsonAsync').rejects(new Error('fail!'))
|
|
|
|
return this.fileUtil.set('foo', 'bar').catch(() => {
|
|
expect(lockFile.unlockAsync).to.be.called
|
|
})
|
|
})
|
|
})
|
|
|
|
context('#remove', () => {
|
|
beforeEach(function () {
|
|
this.fileUtil = new FileUtil({ path: this.path })
|
|
})
|
|
|
|
it('removes the file', function () {
|
|
return this.fileUtil.remove()
|
|
.then(() => {
|
|
return fs.statAsync(this.path)
|
|
}).catch(() => {})
|
|
})
|
|
|
|
it('locks file while removing', function () {
|
|
sinon.spy(lockFile, 'lockAsync')
|
|
|
|
return this.fileUtil.remove().then(() => {
|
|
expect(lockFile.lockAsync).to.be.called
|
|
})
|
|
})
|
|
|
|
it('unlocks file when finished removing', function () {
|
|
sinon.spy(lockFile, 'unlockAsync')
|
|
|
|
return this.fileUtil.remove()
|
|
.then(() => {
|
|
expect(lockFile.unlockAsync).to.be.called
|
|
})
|
|
})
|
|
|
|
it('unlocks file even if removing fails', function () {
|
|
sinon.spy(lockFile, 'unlockAsync')
|
|
sinon.stub(fs, 'removeAsync').rejects(new Error('fail!'))
|
|
|
|
return this.fileUtil.remove()
|
|
.then(() => {
|
|
throw new Error('should have caught!')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq('fail!')
|
|
|
|
expect(lockFile.unlockAsync).to.be.called
|
|
})
|
|
})
|
|
})
|
|
})
|