pulsar/spec/native-watcher-registry-spec.js
2017-08-03 08:51:44 -04:00

363 lines
10 KiB
JavaScript

/** @babel */
import {it, beforeEach} from './async-spec-helpers'
import path from 'path'
import {Emitter} from 'event-kit'
import {NativeWatcherRegistry} from '../src/native-watcher-registry'
function findRootDirectory () {
let current = process.cwd()
while (true) {
let next = path.resolve(current, '..')
if (next === current) {
return next
} else {
current = next
}
}
}
const ROOT = findRootDirectory()
function absolute (...parts) {
const candidate = path.join(...parts)
return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate)
}
function parts (fullPath) {
return fullPath.split(path.sep).filter(part => part.length > 0)
}
class MockWatcher {
constructor (normalizedPath) {
this.normalizedPath = normalizedPath
this.native = null
}
getNormalizedPathPromise () {
return Promise.resolve(this.normalizedPath)
}
attachToNative (native, nativePath) {
if (this.normalizedPath.startsWith(nativePath)) {
if (this.native) {
this.native.attached = this.native.attached.filter(each => each !== this)
}
this.native = native
this.native.attached.push(this)
}
}
}
class MockNative {
constructor (name) {
this.name = name
this.attached = []
this.disposed = false
this.stopped = false
this.emitter = new Emitter()
}
reattachTo (newNative, nativePath) {
for (const watcher of this.attached) {
watcher.attachToNative(newNative, nativePath)
}
}
onWillStop (callback) {
return this.emitter.on('will-stop', callback)
}
dispose () {
this.disposed = true
}
stop () {
this.stopped = true
this.emitter.emit('will-stop')
}
}
describe('NativeWatcherRegistry', function () {
let createNative, registry
beforeEach(function () {
registry = new NativeWatcherRegistry(normalizedPath => createNative(normalizedPath))
})
it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function () {
const watcher = new MockWatcher(absolute('some', 'path'))
const NATIVE = new MockNative('created')
createNative = () => NATIVE
await registry.attach(watcher)
expect(watcher.native).toBe(NATIVE)
})
it('reuses an existing NativeWatcher on the same directory', async function () {
const EXISTING = new MockNative('existing')
const existingPath = absolute('existing', 'path')
let firstTime = true
createNative = () => {
if (firstTime) {
firstTime = false
return EXISTING
}
return new MockNative('nope')
}
await registry.attach(new MockWatcher(existingPath))
const watcher = new MockWatcher(existingPath)
await registry.attach(watcher)
expect(watcher.native).toBe(EXISTING)
})
it('attaches to an existing NativeWatcher on a parent directory', async function () {
const EXISTING = new MockNative('existing')
const parentDir = absolute('existing', 'path')
const subDir = path.join(parentDir, 'sub', 'directory')
let firstTime = true
createNative = () => {
if (firstTime) {
firstTime = false
return EXISTING
}
return new MockNative('nope')
}
await registry.attach(new MockWatcher(parentDir))
const watcher = new MockWatcher(subDir)
await registry.attach(watcher)
expect(watcher.native).toBe(EXISTING)
})
it('adopts Watchers from NativeWatchers on child directories', async function () {
const parentDir = absolute('existing', 'path')
const childDir0 = path.join(parentDir, 'child', 'directory', 'zero')
const childDir1 = path.join(parentDir, 'child', 'directory', 'one')
const otherDir = absolute('another', 'path')
const CHILD0 = new MockNative('existing0')
const CHILD1 = new MockNative('existing1')
const OTHER = new MockNative('existing2')
const PARENT = new MockNative('parent')
createNative = dir => {
if (dir === childDir0) {
return CHILD0
} else if (dir === childDir1) {
return CHILD1
} else if (dir === otherDir) {
return OTHER
} else if (dir === parentDir) {
return PARENT
} else {
throw new Error(`Unexpected path: ${dir}`)
}
}
const watcher0 = new MockWatcher(childDir0)
await registry.attach(watcher0)
const watcher1 = new MockWatcher(childDir1)
await registry.attach(watcher1)
const watcher2 = new MockWatcher(otherDir)
await registry.attach(watcher2)
expect(watcher0.native).toBe(CHILD0)
expect(watcher1.native).toBe(CHILD1)
expect(watcher2.native).toBe(OTHER)
// Consolidate all three watchers beneath the same native watcher on the parent directory
const watcher = new MockWatcher(parentDir)
await registry.attach(watcher)
expect(watcher.native).toBe(PARENT)
expect(watcher0.native).toBe(PARENT)
expect(CHILD0.stopped).toBe(true)
expect(CHILD0.disposed).toBe(true)
expect(watcher1.native).toBe(PARENT)
expect(CHILD1.stopped).toBe(true)
expect(CHILD1.disposed).toBe(true)
expect(watcher2.native).toBe(OTHER)
expect(OTHER.stopped).toBe(false)
expect(OTHER.disposed).toBe(false)
})
describe('removing NativeWatchers', function () {
it('happens when they stop', async function () {
const STOPPED = new MockNative('stopped')
const RUNNING = new MockNative('running')
const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped')
const stoppedPathParts = stoppedPath.split(path.sep).filter(part => part.length > 0)
const runningPath = absolute('watcher', 'that', 'will', 'continue', 'to', 'exist')
const runningPathParts = runningPath.split(path.sep).filter(part => part.length > 0)
createNative = dir => {
if (dir === stoppedPath) {
return STOPPED
} else if (dir === runningPath) {
return RUNNING
} else {
throw new Error(`Unexpected path: ${dir}`)
}
}
const stoppedWatcher = new MockWatcher(stoppedPath)
await registry.attach(stoppedWatcher)
const runningWatcher = new MockWatcher(runningPath)
await registry.attach(runningWatcher)
STOPPED.stop()
const runningNode = registry.tree.root.lookup(runningPathParts).when({
parent: node => node,
missing: () => false,
children: () => false
})
expect(runningNode).toBeTruthy()
expect(runningNode.getNativeWatcher()).toBe(RUNNING)
const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({
parent: () => false,
missing: () => true,
children: () => false
})
expect(stoppedNode).toBe(true)
})
it('reassigns new child watchers when a parent watcher is stopped', async function () {
const CHILD0 = new MockNative('child0')
const CHILD1 = new MockNative('child1')
const PARENT = new MockNative('parent')
const parentDir = absolute('parent')
const childDir0 = path.join(parentDir, 'child0')
const childDir1 = path.join(parentDir, 'child1')
createNative = dir => {
if (dir === parentDir) {
return PARENT
} else if (dir === childDir0) {
return CHILD0
} else if (dir === childDir1) {
return CHILD1
} else {
throw new Error(`Unexpected directory ${dir}`)
}
}
const parentWatcher = new MockWatcher(parentDir)
const childWatcher0 = new MockWatcher(childDir0)
const childWatcher1 = new MockWatcher(childDir1)
await registry.attach(parentWatcher)
await Promise.all([
registry.attach(childWatcher0),
registry.attach(childWatcher1)
])
// All three watchers should share the parent watcher's native watcher.
expect(parentWatcher.native).toBe(PARENT)
expect(childWatcher0.native).toBe(PARENT)
expect(childWatcher1.native).toBe(PARENT)
// Stopping the parent should detach and recreate the child watchers.
PARENT.stop()
expect(childWatcher0.native).toBe(CHILD0)
expect(childWatcher1.native).toBe(CHILD1)
expect(registry.tree.root.lookup(parts(parentDir)).when({
parent: () => false,
missing: () => false,
children: () => true
})).toBe(true)
expect(registry.tree.root.lookup(parts(childDir0)).when({
parent: () => true,
missing: () => false,
children: () => false
})).toBe(true)
expect(registry.tree.root.lookup(parts(childDir1)).when({
parent: () => true,
missing: () => false,
children: () => false
})).toBe(true)
})
it('consolidates children when splitting a parent watcher', async function () {
const CHILD0 = new MockNative('child0')
const PARENT = new MockNative('parent')
const parentDir = absolute('parent')
const childDir0 = path.join(parentDir, 'child0')
const childDir1 = path.join(parentDir, 'child0', 'child1')
createNative = dir => {
if (dir === parentDir) {
return PARENT
} else if (dir === childDir0) {
return CHILD0
} else {
throw new Error(`Unexpected directory ${dir}`)
}
}
const parentWatcher = new MockWatcher(parentDir)
const childWatcher0 = new MockWatcher(childDir0)
const childWatcher1 = new MockWatcher(childDir1)
await registry.attach(parentWatcher)
await Promise.all([
registry.attach(childWatcher0),
registry.attach(childWatcher1)
])
// All three watchers should share the parent watcher's native watcher.
expect(parentWatcher.native).toBe(PARENT)
expect(childWatcher0.native).toBe(PARENT)
expect(childWatcher1.native).toBe(PARENT)
// Stopping the parent should detach and create the child watchers. Both child watchers should
// share the same native watcher.
PARENT.stop()
expect(childWatcher0.native).toBe(CHILD0)
expect(childWatcher1.native).toBe(CHILD0)
expect(registry.tree.root.lookup(parts(parentDir)).when({
parent: () => false,
missing: () => false,
children: () => true
})).toBe(true)
expect(registry.tree.root.lookup(parts(childDir0)).when({
parent: () => true,
missing: () => false,
children: () => false
})).toBe(true)
expect(registry.tree.root.lookup(parts(childDir1)).when({
parent: () => true,
missing: () => false,
children: () => false
})).toBe(true)
})
})
})