Skip to content

Commit

Permalink
fixup! feat: streaming debug logfile
Browse files Browse the repository at this point in the history
  • Loading branch information
lukekarrys committed Dec 1, 2021
1 parent e8a8f84 commit a87c1c9
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 96 deletions.
207 changes: 111 additions & 96 deletions test/fixtures/mock-globals.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
// An initial implementation for a feature that will hopefully exist in tap
// https://github.com/tapjs/node-tap/issues/789
// This file is only used in tests but it is still tested itself. There's
// a lot going on in this file, but hopefully it can be removed from a
// feature in tap in the future
// This file is only used in tests but it is still tested itself.
// Hopefully it can be removed for a feature in tap in the future

// Path can be different cases across platform so get the original case
// of the path before anything is changed
const originalPathKey = process.env.PATH ? 'PATH' : process.env.Path ? 'Path' : 'path'

const last = (arr) => arr[arr.length - 1]
const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)

// Get lineage of all object references for a path on `global`.
// So `process.env.NODE_ENV` would return
// [global, global.process, global.process.env, 'production']
const getGlobalAncestors = (keys) =>
keys.split('.').reduce((acc, k) => {
const value = last(acc)[k]
acc.push(value)
return acc
}, [global])
const splitOnLast = (str) => {
const index = str.lastIndexOf('.')
return index > -1 && [str.slice(0, index), str.slice(index + 1)]
}

// A weird getter that can look up keys on nested objects but also
// match keys with dots in their names, eg { 'process.env': { TERM: 'a' } }
Expand All @@ -25,16 +21,24 @@ const get = (obj, fullKey, childKey) => {
if (has(obj, fullKey)) {
return childKey ? get(obj[fullKey], childKey) : obj[fullKey]
} else {
const lastDot = fullKey.lastIndexOf('.')
return lastDot === -1 ? undefined : get(
const split = splitOnLast(fullKey)
return split ? get(
obj,
fullKey.slice(0, lastDot),
fullKey.slice(lastDot + 1) + (childKey ? `.${childKey}` : '')
)
split[0],
split[1] + (childKey ? `.${childKey}` : '')
) : undefined
}
}

// { a: 1, b: { c: 2 } } => ['a', 'b.c']
// Get object reference for the parent of a full key path on `global`
// So `process.env.NODE_ENV` would return a reference to global.process.env
const getGlobalParent = (fullKey) => {
const split = splitOnLast(fullKey)
return split ? get(global, split[0]) : global
}

// Map an object to an array of nested keys separated by dots
// { a: 1, b: { c: 2, d: [1] } } => ['a', 'b.c', 'b.d']
const getKeys = (values, p = '', acc = []) =>
Object.entries(values).reduce((memo, [k, value]) => {
const key = p ? `${p}.${k}` : k
Expand All @@ -45,18 +49,11 @@ const getKeys = (values, p = '', acc = []) =>

// Walk prototype chain to get first available descriptor. This is necessary
// to get the current property descriptor for things like `process.on`.
// `Object.getOwnPropertyDescriptor(process, 'on') === undefined` but if you
// Since `getOPD(process, 'on') === undefined` but if you
// walk up the prototype chain you get the original descriptor
// `Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(process)), 'on')`
// {
// value: [Function: addListener],
// writable: true,
// enumerable: true,
// configurable: true
// }
// `getOPD(getPO(getPO(process)), 'on') === { value: [Function], ... }`
const getPropertyDescriptor = (obj, key, fullKey) => {
if (fullKey.toUpperCase() === 'PROCESS.ENV.PATH') {
// if getting original env.path value, use cross platform compatible key
key = originalPathKey
}
let d = Object.getOwnPropertyDescriptor(obj, key)
Expand All @@ -70,99 +67,114 @@ const getPropertyDescriptor = (obj, key, fullKey) => {
return d
}

const createDescriptor = (currentDescriptor = {
configurable: true,
writable: true,
enumerable: true,
}, value) => {
if (value === undefined) {
// Mocking a global to undefined is the same
// as deleting it so return early since no
// descriptor will be created
return value
}
// Either set the descriptor value or getter depending
// on what the current descriptor has
return {
...currentDescriptor,
...(currentDescriptor.get ? { get: () => value } : { value }),
}
}

// Define a descriptor or delete a key on an object
const defineProperty = (obj, key, descriptor) => {
if (descriptor === undefined) {
delete obj[key]
} else {
Object.defineProperty(obj, key, descriptor)
}
}

const _pushDescriptor = Symbol('pushDescriptor')
const _popDescriptor = Symbol('popDescriptor')
const _createReset = Symbol('createReset')
const _set = Symbol('set')

class MockGlobals {
#cache = new Map()
#resets = []
#defaultDescriptor = {
configurable: true,
writable: true,
enumerable: true,
#skipDescriptor = Symbol('skipDescriptor')
#descriptors = {
// [fullKey]: [descriptor, descriptor, ...]
}

teardown () {
this.#resets.forEach(r => r.reset(true))
Object.entries(this.#descriptors)
.forEach(([fullKey, descriptors]) => {
defineProperty(
getGlobalParent(fullKey),
last(fullKey.split('.')),
// On teardown reset to the initial descriptor
descriptors[0]
)
})
}

registerGlobals (globals, { replace = false } = {}) {
// Replace means dont merge in object values but replace them instead
const keys = replace ? Object.keys(globals) : getKeys(globals)
const resets = keys.map(k => this[_set](k, globals))
this.#resets.push(...resets)
return resets.reduce((acc, r) => {
acc[r.fullKey] = r.reset
return acc
}, {})
return keys
// Set each property passed in and return fns to reset them
.map(k => this[_set](k, globals))
// Return an object with each path as a key for manually
// resetting in each test
.reduce((acc, r) => {
acc[r.fullKey] = r.reset
return acc
}, {})
}

[_pushDescriptor] (key, value) {
const cache = this.#cache.get(key)
if (cache) {
this.#cache.get(key).push(value)
} else {
this.#cache.set(key, [value])
[_pushDescriptor] (fullKey, value) {
if (!this.#descriptors[fullKey]) {
this.#descriptors[fullKey] = []
}
return value
this.#descriptors[fullKey].push(value)
}

[_popDescriptor] (key) {
const cache = this.#cache.get(key)
if (!cache) {
return null
}
const value = cache.pop()
if (!cache.length) {
this.#cache.delete(key)
[_popDescriptor] (fullKey) {
const descriptors = this.#descriptors[fullKey]
if (!descriptors) {
return this.#skipDescriptor
}
return value
}

[_createReset] (parent, key, fullKey) {
return {
fullKey,
key,
reset: () => {
const popped = this[_popDescriptor](fullKey)
// undefined means delete the property so only skip
// if it is explicitly null
if (popped === null) {
return
}
return popped
? Object.defineProperty(parent, key, popped)
: (delete parent[key])
},
const descriptor = descriptors.pop()
if (!descriptors.length) {
delete this.#descriptors[fullKey]
}
return descriptor
}

[_set] (fullKey, globals) {
const values = getGlobalAncestors(fullKey)
const parentValue = values[values.length - 2]

const obj = getGlobalParent(fullKey)
const key = last(fullKey.split('.'))
const newValue = get(globals, fullKey)

const currentDescriptor = getPropertyDescriptor(parentValue, key, fullKey)
const currentDescriptor = getPropertyDescriptor(obj, key, fullKey)
this[_pushDescriptor](fullKey, currentDescriptor)

const reset = this[_createReset](parentValue, key, fullKey)

if (newValue === undefined) {
delete parentValue[key]
} else {
const newDescriptor = { ...(currentDescriptor || this.#defaultDescriptor) }
if (newDescriptor.get) {
newDescriptor.get = () => newValue
} else {
newDescriptor.value = newValue
}
Object.defineProperty(parentValue, key, newDescriptor)
}
defineProperty(
obj,
key,
createDescriptor(
currentDescriptor,
get(globals, fullKey)
)
)

return reset
return {
fullKey,
reset: () => {
const lastDescriptor = this[_popDescriptor](fullKey)
if (lastDescriptor !== this.#skipDescriptor) {
defineProperty(obj, key, lastDescriptor)
}
},
}
}
}

Expand All @@ -173,15 +185,18 @@ const cache = new Map()
const mockGlobals = (t, globals, options) => {
const hasInstance = cache.has(t)
const instance = hasInstance ? cache.get(t) : new MockGlobals()
const reset = instance.registerGlobals(globals, options)

if (!hasInstance) {
cache.set(t, instance)
t.teardown(() => {
instance.teardown()
cache.delete(t)
})
}
return { reset }

return {
reset: instance.registerGlobals(globals, options),
}
}

module.exports = mockGlobals
30 changes: 30 additions & 0 deletions test/lib/fixtures/mock-globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const originals = {
shell: process.env.SHELL,
home: process.env.HOME,
argv: process.argv,
env: process.env,
setInterval,
}

t.test('console', async t => {
Expand Down Expand Up @@ -171,6 +173,15 @@ t.test('mixed object/string mode', async t => {
t.equal(process.env.TEST, undefined)
})

t.test('deletes prop', async t => {
await t.test('mocks', async t => {
mockGlobals(t, { 'process.platform': undefined })
t.equal(process.platform, undefined)
})

t.equal(process.platform, originals.platform)
})

t.test('date', async t => {
await t.test('mocks', async t => {
mockGlobals(t, {
Expand All @@ -196,6 +207,24 @@ t.test('argv', async t => {
t.strictSame(process.argv, originals.argv)
})

t.test('replace', async (t) => {
await t.test('env', async t => {
mockGlobals(t, { 'process.env': { HOME: '1' } }, { replace: true })
t.strictSame(process.env, { HOME: '1' })
})

t.strictSame(process.env, originals.env)
})

t.test('top level global', async (t) => {
await t.test('setInterval', async t => {
mockGlobals(t, { setInterval: 0 }, { replace: true })
t.strictSame(setInterval, 0)
})

t.strictSame(setInterval, originals.setInterval)
})

// XXX: This behavior should be tested if the role of mockGlobals
// is expanded to its own thing, so keep these tests around but
// currently it is undecided what should actually happen in these
Expand Down Expand Up @@ -237,6 +266,7 @@ t.skip('multiple mocks and resets', async (t) => {
t.equal(process.platform, 'b')

const { reset: resetC } = mockGlobals(t, { 'process.platform': 'c' })
t.equal(process.platform, 'c')

resetB['process.platform']()
resetB['process.platform']()
Expand Down

0 comments on commit a87c1c9

Please sign in to comment.