Skip to content

Commit a87c1c9

Browse files
committed
fixup! feat: streaming debug logfile
1 parent e8a8f84 commit a87c1c9

File tree

2 files changed

+141
-96
lines changed

2 files changed

+141
-96
lines changed

test/fixtures/mock-globals.js

+111-96
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
// An initial implementation for a feature that will hopefully exist in tap
22
// https://github.com/tapjs/node-tap/issues/789
3-
// This file is only used in tests but it is still tested itself. There's
4-
// a lot going on in this file, but hopefully it can be removed from a
5-
// feature in tap in the future
3+
// This file is only used in tests but it is still tested itself.
4+
// Hopefully it can be removed for a feature in tap in the future
65

6+
// Path can be different cases across platform so get the original case
7+
// of the path before anything is changed
78
const originalPathKey = process.env.PATH ? 'PATH' : process.env.Path ? 'Path' : 'path'
9+
810
const last = (arr) => arr[arr.length - 1]
911
const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
10-
11-
// Get lineage of all object references for a path on `global`.
12-
// So `process.env.NODE_ENV` would return
13-
// [global, global.process, global.process.env, 'production']
14-
const getGlobalAncestors = (keys) =>
15-
keys.split('.').reduce((acc, k) => {
16-
const value = last(acc)[k]
17-
acc.push(value)
18-
return acc
19-
}, [global])
12+
const splitOnLast = (str) => {
13+
const index = str.lastIndexOf('.')
14+
return index > -1 && [str.slice(0, index), str.slice(index + 1)]
15+
}
2016

2117
// A weird getter that can look up keys on nested objects but also
2218
// match keys with dots in their names, eg { 'process.env': { TERM: 'a' } }
@@ -25,16 +21,24 @@ const get = (obj, fullKey, childKey) => {
2521
if (has(obj, fullKey)) {
2622
return childKey ? get(obj[fullKey], childKey) : obj[fullKey]
2723
} else {
28-
const lastDot = fullKey.lastIndexOf('.')
29-
return lastDot === -1 ? undefined : get(
24+
const split = splitOnLast(fullKey)
25+
return split ? get(
3026
obj,
31-
fullKey.slice(0, lastDot),
32-
fullKey.slice(lastDot + 1) + (childKey ? `.${childKey}` : '')
33-
)
27+
split[0],
28+
split[1] + (childKey ? `.${childKey}` : '')
29+
) : undefined
3430
}
3531
}
3632

37-
// { a: 1, b: { c: 2 } } => ['a', 'b.c']
33+
// Get object reference for the parent of a full key path on `global`
34+
// So `process.env.NODE_ENV` would return a reference to global.process.env
35+
const getGlobalParent = (fullKey) => {
36+
const split = splitOnLast(fullKey)
37+
return split ? get(global, split[0]) : global
38+
}
39+
40+
// Map an object to an array of nested keys separated by dots
41+
// { a: 1, b: { c: 2, d: [1] } } => ['a', 'b.c', 'b.d']
3842
const getKeys = (values, p = '', acc = []) =>
3943
Object.entries(values).reduce((memo, [k, value]) => {
4044
const key = p ? `${p}.${k}` : k
@@ -45,18 +49,11 @@ const getKeys = (values, p = '', acc = []) =>
4549

4650
// Walk prototype chain to get first available descriptor. This is necessary
4751
// to get the current property descriptor for things like `process.on`.
48-
// `Object.getOwnPropertyDescriptor(process, 'on') === undefined` but if you
52+
// Since `getOPD(process, 'on') === undefined` but if you
4953
// walk up the prototype chain you get the original descriptor
50-
// `Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(process)), 'on')`
51-
// {
52-
// value: [Function: addListener],
53-
// writable: true,
54-
// enumerable: true,
55-
// configurable: true
56-
// }
54+
// `getOPD(getPO(getPO(process)), 'on') === { value: [Function], ... }`
5755
const getPropertyDescriptor = (obj, key, fullKey) => {
5856
if (fullKey.toUpperCase() === 'PROCESS.ENV.PATH') {
59-
// if getting original env.path value, use cross platform compatible key
6057
key = originalPathKey
6158
}
6259
let d = Object.getOwnPropertyDescriptor(obj, key)
@@ -70,99 +67,114 @@ const getPropertyDescriptor = (obj, key, fullKey) => {
7067
return d
7168
}
7269

70+
const createDescriptor = (currentDescriptor = {
71+
configurable: true,
72+
writable: true,
73+
enumerable: true,
74+
}, value) => {
75+
if (value === undefined) {
76+
// Mocking a global to undefined is the same
77+
// as deleting it so return early since no
78+
// descriptor will be created
79+
return value
80+
}
81+
// Either set the descriptor value or getter depending
82+
// on what the current descriptor has
83+
return {
84+
...currentDescriptor,
85+
...(currentDescriptor.get ? { get: () => value } : { value }),
86+
}
87+
}
88+
89+
// Define a descriptor or delete a key on an object
90+
const defineProperty = (obj, key, descriptor) => {
91+
if (descriptor === undefined) {
92+
delete obj[key]
93+
} else {
94+
Object.defineProperty(obj, key, descriptor)
95+
}
96+
}
97+
7398
const _pushDescriptor = Symbol('pushDescriptor')
7499
const _popDescriptor = Symbol('popDescriptor')
75-
const _createReset = Symbol('createReset')
76100
const _set = Symbol('set')
77101

78102
class MockGlobals {
79-
#cache = new Map()
80-
#resets = []
81-
#defaultDescriptor = {
82-
configurable: true,
83-
writable: true,
84-
enumerable: true,
103+
#skipDescriptor = Symbol('skipDescriptor')
104+
#descriptors = {
105+
// [fullKey]: [descriptor, descriptor, ...]
85106
}
86107

87108
teardown () {
88-
this.#resets.forEach(r => r.reset(true))
109+
Object.entries(this.#descriptors)
110+
.forEach(([fullKey, descriptors]) => {
111+
defineProperty(
112+
getGlobalParent(fullKey),
113+
last(fullKey.split('.')),
114+
// On teardown reset to the initial descriptor
115+
descriptors[0]
116+
)
117+
})
89118
}
90119

91120
registerGlobals (globals, { replace = false } = {}) {
121+
// Replace means dont merge in object values but replace them instead
92122
const keys = replace ? Object.keys(globals) : getKeys(globals)
93-
const resets = keys.map(k => this[_set](k, globals))
94-
this.#resets.push(...resets)
95-
return resets.reduce((acc, r) => {
96-
acc[r.fullKey] = r.reset
97-
return acc
98-
}, {})
123+
return keys
124+
// Set each property passed in and return fns to reset them
125+
.map(k => this[_set](k, globals))
126+
// Return an object with each path as a key for manually
127+
// resetting in each test
128+
.reduce((acc, r) => {
129+
acc[r.fullKey] = r.reset
130+
return acc
131+
}, {})
99132
}
100133

101-
[_pushDescriptor] (key, value) {
102-
const cache = this.#cache.get(key)
103-
if (cache) {
104-
this.#cache.get(key).push(value)
105-
} else {
106-
this.#cache.set(key, [value])
134+
[_pushDescriptor] (fullKey, value) {
135+
if (!this.#descriptors[fullKey]) {
136+
this.#descriptors[fullKey] = []
107137
}
108-
return value
138+
this.#descriptors[fullKey].push(value)
109139
}
110140

111-
[_popDescriptor] (key) {
112-
const cache = this.#cache.get(key)
113-
if (!cache) {
114-
return null
115-
}
116-
const value = cache.pop()
117-
if (!cache.length) {
118-
this.#cache.delete(key)
141+
[_popDescriptor] (fullKey) {
142+
const descriptors = this.#descriptors[fullKey]
143+
if (!descriptors) {
144+
return this.#skipDescriptor
119145
}
120-
return value
121-
}
122-
123-
[_createReset] (parent, key, fullKey) {
124-
return {
125-
fullKey,
126-
key,
127-
reset: () => {
128-
const popped = this[_popDescriptor](fullKey)
129-
// undefined means delete the property so only skip
130-
// if it is explicitly null
131-
if (popped === null) {
132-
return
133-
}
134-
return popped
135-
? Object.defineProperty(parent, key, popped)
136-
: (delete parent[key])
137-
},
146+
const descriptor = descriptors.pop()
147+
if (!descriptors.length) {
148+
delete this.#descriptors[fullKey]
138149
}
150+
return descriptor
139151
}
140152

141153
[_set] (fullKey, globals) {
142-
const values = getGlobalAncestors(fullKey)
143-
const parentValue = values[values.length - 2]
144-
154+
const obj = getGlobalParent(fullKey)
145155
const key = last(fullKey.split('.'))
146-
const newValue = get(globals, fullKey)
147156

148-
const currentDescriptor = getPropertyDescriptor(parentValue, key, fullKey)
157+
const currentDescriptor = getPropertyDescriptor(obj, key, fullKey)
149158
this[_pushDescriptor](fullKey, currentDescriptor)
150159

151-
const reset = this[_createReset](parentValue, key, fullKey)
152-
153-
if (newValue === undefined) {
154-
delete parentValue[key]
155-
} else {
156-
const newDescriptor = { ...(currentDescriptor || this.#defaultDescriptor) }
157-
if (newDescriptor.get) {
158-
newDescriptor.get = () => newValue
159-
} else {
160-
newDescriptor.value = newValue
161-
}
162-
Object.defineProperty(parentValue, key, newDescriptor)
163-
}
160+
defineProperty(
161+
obj,
162+
key,
163+
createDescriptor(
164+
currentDescriptor,
165+
get(globals, fullKey)
166+
)
167+
)
164168

165-
return reset
169+
return {
170+
fullKey,
171+
reset: () => {
172+
const lastDescriptor = this[_popDescriptor](fullKey)
173+
if (lastDescriptor !== this.#skipDescriptor) {
174+
defineProperty(obj, key, lastDescriptor)
175+
}
176+
},
177+
}
166178
}
167179
}
168180

@@ -173,15 +185,18 @@ const cache = new Map()
173185
const mockGlobals = (t, globals, options) => {
174186
const hasInstance = cache.has(t)
175187
const instance = hasInstance ? cache.get(t) : new MockGlobals()
176-
const reset = instance.registerGlobals(globals, options)
188+
177189
if (!hasInstance) {
178190
cache.set(t, instance)
179191
t.teardown(() => {
180192
instance.teardown()
181193
cache.delete(t)
182194
})
183195
}
184-
return { reset }
196+
197+
return {
198+
reset: instance.registerGlobals(globals, options),
199+
}
185200
}
186201

187202
module.exports = mockGlobals

test/lib/fixtures/mock-globals.js

+30
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const originals = {
99
shell: process.env.SHELL,
1010
home: process.env.HOME,
1111
argv: process.argv,
12+
env: process.env,
13+
setInterval,
1214
}
1315

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

176+
t.test('deletes prop', async t => {
177+
await t.test('mocks', async t => {
178+
mockGlobals(t, { 'process.platform': undefined })
179+
t.equal(process.platform, undefined)
180+
})
181+
182+
t.equal(process.platform, originals.platform)
183+
})
184+
174185
t.test('date', async t => {
175186
await t.test('mocks', async t => {
176187
mockGlobals(t, {
@@ -196,6 +207,24 @@ t.test('argv', async t => {
196207
t.strictSame(process.argv, originals.argv)
197208
})
198209

210+
t.test('replace', async (t) => {
211+
await t.test('env', async t => {
212+
mockGlobals(t, { 'process.env': { HOME: '1' } }, { replace: true })
213+
t.strictSame(process.env, { HOME: '1' })
214+
})
215+
216+
t.strictSame(process.env, originals.env)
217+
})
218+
219+
t.test('top level global', async (t) => {
220+
await t.test('setInterval', async t => {
221+
mockGlobals(t, { setInterval: 0 }, { replace: true })
222+
t.strictSame(setInterval, 0)
223+
})
224+
225+
t.strictSame(setInterval, originals.setInterval)
226+
})
227+
199228
// XXX: This behavior should be tested if the role of mockGlobals
200229
// is expanded to its own thing, so keep these tests around but
201230
// currently it is undecided what should actually happen in these
@@ -237,6 +266,7 @@ t.skip('multiple mocks and resets', async (t) => {
237266
t.equal(process.platform, 'b')
238267

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

241271
resetB['process.platform']()
242272
resetB['process.platform']()

0 commit comments

Comments
 (0)