Skip to content

Commit 636f3c5

Browse files
Improve perfomance: implement RecordsSet (#232)
* improve proxy creation function * use node contexts to simplify node logic * implement custom records set RecordsSet implements the higher order filter and map methods which get applied when retrieving the iterator. This increases performance as it doesn't create arrays on each .map or .filter -> the iterator yields only the end values with one iteration
1 parent 77a2032 commit 636f3c5

File tree

7 files changed

+175
-139
lines changed

7 files changed

+175
-139
lines changed

src/Recorder.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,44 @@
11
import { inspect, InspectOptions } from 'util'
22
import { SubstituteNodeBase } from './SubstituteNodeBase'
3+
import { RecordsSet } from './RecordsSet'
34

45
export class Recorder {
5-
private _records: SubstituteNodeBase[]
6-
private _indexedRecords: Map<PropertyKey, SubstituteNodeBase[]>
6+
private _records: RecordsSet<SubstituteNodeBase>
7+
private _indexedRecords: Map<PropertyKey, RecordsSet<SubstituteNodeBase>>
78

89
constructor() {
9-
this._records = []
10+
this._records = new RecordsSet()
1011
this._indexedRecords = new Map()
1112
}
1213

13-
public get records(): SubstituteNodeBase[] {
14+
public get records(): RecordsSet<SubstituteNodeBase> {
1415
return this._records
1516
}
1617

17-
public get indexedRecords(): Map<PropertyKey, SubstituteNodeBase[]> {
18+
public get indexedRecords(): Map<PropertyKey, RecordsSet<SubstituteNodeBase>> {
1819
return this._indexedRecords
1920
}
2021

2122
public addIndexedRecord(node: SubstituteNodeBase): void {
23+
this.addRecord(node)
2224
const existingNodes = this.indexedRecords.get(node.key)
23-
if (typeof existingNodes === 'undefined') this._indexedRecords.set(node.key, [node])
24-
else existingNodes.push(node)
25+
if (typeof existingNodes === 'undefined') this._indexedRecords.set(node.key, new RecordsSet([node]))
26+
else existingNodes.add(node)
2527
}
2628

2729
public addRecord(node: SubstituteNodeBase): void {
28-
this._records.push(node)
30+
this._records.add(node)
2931
}
3032

31-
public getSiblingsOf(node: SubstituteNodeBase): SubstituteNodeBase[] {
32-
const siblingNodes = this.indexedRecords.get(node.key) ?? []
33+
public getSiblingsOf(node: SubstituteNodeBase): RecordsSet<SubstituteNodeBase> {
34+
const siblingNodes = this.indexedRecords.get(node.key) ?? new RecordsSet()
3335
return siblingNodes.filter(siblingNode => siblingNode !== node)
3436
}
3537

3638
public [inspect.custom](_: number, options: InspectOptions): string {
3739
const entries = [...this.indexedRecords.entries()]
38-
return entries.map(([key, value]) => `\n ${key.toString()}: {\n${value.map(v => ` ${inspect(v, options)}`).join(',\n')}\n }`).join()
40+
return entries.map(
41+
([key, value]) => `\n ${key.toString()}: {\n${[...value.map(v => ` ${inspect(v, options)}`)].join(',\n')}\n }`
42+
).join()
3943
}
4044
}

src/RecordsSet.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
type FilterFunction<T> = (item: T) => boolean
2+
type MapperFunction<T, R> = (item: T) => R
3+
type Transformer<T, R> = { type: 'filter', fnc: FilterFunction<T> } | { type: 'mapper', fnc: MapperFunction<T, R> }
4+
5+
export class RecordsSet<T> extends Set<T> {
6+
private _transformer: Transformer<any, any>
7+
private _prevIter: RecordsSet<T>
8+
9+
constructor(value?: Iterable<T> | readonly T[]) {
10+
super(value instanceof RecordsSet ? undefined : value)
11+
if (value instanceof RecordsSet) this._prevIter = value
12+
}
13+
14+
filter(predicate: (item: T) => boolean): RecordsSet<T> {
15+
const newInstance = new RecordsSet<T>(this)
16+
newInstance._transformer = { type: 'filter', fnc: predicate }
17+
return newInstance
18+
}
19+
20+
map<R>(predicate: (item: T) => R): RecordsSet<R> {
21+
const newInstance = new RecordsSet<R | T>(this)
22+
newInstance._transformer = { type: 'mapper', fnc: predicate }
23+
return newInstance as RecordsSet<R>
24+
}
25+
26+
*[Symbol.iterator](): IterableIterator<T> {
27+
yield* this.values()
28+
}
29+
30+
private *applyTransform(itarable: Iterable<T>): IterableIterator<T> {
31+
const transform = this._transformer
32+
if (typeof transform === 'undefined') return yield* itarable
33+
for (const value of itarable) {
34+
if (transform.type === 'mapper') yield transform.fnc(value)
35+
if (transform.type === 'filter' && transform.fnc(value)) yield value
36+
}
37+
}
38+
39+
*values(): IterableIterator<T> {
40+
if (this._prevIter instanceof RecordsSet) yield* this.applyTransform(this._prevIter)
41+
yield* this.applyTransform(super.values())
42+
}
43+
}

src/Substitute.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@ export class Substitute extends SubstituteBase {
1515

1616
constructor() {
1717
super()
18-
this._proxy = new Proxy(
18+
this._proxy = createSubstituteProxy(
1919
this,
20-
createSubstituteProxy<Substitute>({
20+
{
2121
get: (target, _property, _, node) => {
2222
if (target.context.disableAssertions) node.disableAssertions()
23-
},
24-
set: () => { }
23+
}
2524
// apply: (target, _, args, __, proxy) => {
2625
// const rootProperty = proxy.get(target, '()', proxy) TODO: Implement to support callable interfaces
2726
// return Reflect.apply(rootProperty, rootProperty, args)
2827
// }
29-
})
28+
}
3029
)
3130
}
3231

src/SubstituteNode.ts

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,60 @@
11
import { inspect, InspectOptions } from 'util'
22

3-
import { PropertyType, isSubstitutionMethod, isAssertionMethod, SubstitutionMethod, textModifier } from './Utilities'
3+
import { PropertyType, isSubstitutionMethod, isAssertionMethod, AssertionMethod, SubstitutionMethod, textModifier, isSubstituteMethod } from './Utilities'
44
import { SubstituteException } from './SubstituteException'
55
import { RecordedArguments } from './RecordedArguments'
66
import { SubstituteNodeBase } from './SubstituteNodeBase'
77
import { SubstituteBase } from './SubstituteBase'
88
import { createSubstituteProxy } from './SubstituteProxy'
99

10+
type SubstituteContext = SubstitutionMethod | AssertionMethod | 'none'
11+
1012
export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
1113
private _proxy: SubstituteNode
1214
private _propertyType: PropertyType = PropertyType.property
1315
private _accessorType: 'get' | 'set' = 'get'
1416
private _recordedArguments: RecordedArguments = RecordedArguments.none()
1517

16-
private _hasSubstitution: boolean = false
17-
private _isSubstitution: boolean = false
18-
18+
private _context: SubstituteContext = 'none'
1919
private _disabledAssertions: boolean = false
20-
private _isAssertion: boolean = false
2120

2221
constructor(property: PropertyKey, parent: SubstituteNode | SubstituteBase) {
2322
super(property, parent)
24-
this._proxy = new Proxy(
23+
this._proxy = createSubstituteProxy(
2524
this,
26-
createSubstituteProxy<SubstituteNode>({
27-
get: (target, _, __, node) => {
28-
if (target.isAssertion) node.executeAssertion()
25+
{
26+
get: (node, _, __, nextNode) => {
27+
if (node.isAssertion) nextNode.executeAssertion()
2928
},
30-
set: (target, _, __, ___, node) => {
31-
if (target.isAssertion) node.executeAssertion()
29+
set: (node, _, __, ___, nextNode) => {
30+
if (node.isAssertion) nextNode.executeAssertion()
3231
},
33-
apply: (target, _, rawArguments) => {
34-
target.handleMethod(rawArguments)
35-
return target.isIntermediateNode() && target.parent.isAssertion
36-
? target.executeAssertion()
37-
: target.read()
32+
apply: (node, _, rawArguments) => {
33+
node.handleMethod(rawArguments)
34+
return node.parent?.isAssertion ?? false ? node.executeAssertion() : node.read()
3835
}
39-
})
36+
}
4037
)
4138
}
4239

4340
public get proxy() {
4441
return this._proxy
4542
}
4643

47-
get isSubstitution(): boolean {
48-
return this._isSubstitution
44+
get context(): SubstituteContext {
45+
return this._context
4946
}
5047

51-
get hasSubstitution(): boolean {
52-
return this._hasSubstitution
48+
get hasContext(): boolean {
49+
return this.context !== 'none'
50+
}
51+
52+
get isSubstitution(): boolean {
53+
return isSubstitutionMethod(this.context)
5354
}
5455

5556
get isAssertion(): boolean {
56-
return this._isAssertion
57+
return isAssertionMethod(this.context)
5758
}
5859

5960
get property() {
@@ -76,24 +77,16 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
7677
return this._disabledAssertions
7778
}
7879

79-
public labelAsSubstitution(): void {
80-
this._isSubstitution = true
81-
}
82-
83-
public enableSubstitution(): void {
84-
this._hasSubstitution = true
85-
}
86-
87-
public labelAsAssertion(): void {
88-
this._isAssertion = true
80+
public assignContext(context: SubstituteContext): void {
81+
this._context = context
8982
}
9083

9184
public disableAssertions() {
9285
this._disabledAssertions = true
9386
}
9487

9588
public read(): SubstituteNode | void | never {
96-
if (this.isSubstitution) return
89+
if (this.parent?.isSubstitution ?? false) return
9790
if (this.isAssertion) return this.proxy
9891

9992
const mostSuitableSubstitution = this.getMostSuitableSubstitution()
@@ -108,7 +101,7 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
108101
}
109102

110103
public executeSubstitution(contextArguments: RecordedArguments) {
111-
const substitutionMethod = this.child.property as SubstitutionMethod
104+
const substitutionMethod = this.context as SubstitutionMethod
112105
const substitutionValue = this.child.recordedArguments.value.length > 1
113106
? this.child.recordedArguments.value.shift()
114107
: this.child.recordedArguments.value[0]
@@ -130,8 +123,7 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
130123
}
131124

132125
public executeAssertion(): void | never {
133-
const siblings = this.getAllSiblings().filter(n => !n.isAssertion && !n.hasSubstitution && n.accessorType === this.accessorType) // isSubstitution should return this.parent.hasSubstitution
134-
126+
const siblings = [...this.getAllSiblings().filter(n => !n.hasContext && n.accessorType === this.accessorType)]
135127
if (!this.isIntermediateNode()) throw new Error('Not possible')
136128

137129
const expectedCount = this.parent.recordedArguments.value[0] ?? undefined
@@ -165,40 +157,37 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
165157
public handleMethod(rawArguments: any[]): void {
166158
this._propertyType = PropertyType.method
167159
this._recordedArguments = RecordedArguments.from(rawArguments)
160+
if (!isSubstituteMethod(this.property)) return
168161

169-
if (!this.disabledAssertions && isAssertionMethod(this.property)) {
170-
if (this.property === 'didNotReceive') this._recordedArguments = RecordedArguments.from([0])
171-
this.labelAsAssertion()
172-
}
162+
if (this.isIntermediateNode() && isSubstitutionMethod(this.property)) return this.parent.assignContext(this.property)
163+
if (this.disabledAssertions || !this.isHead()) return
173164

174-
if (isSubstitutionMethod(this.property)) {
175-
this.labelAsSubstitution()
176-
if (this.isIntermediateNode()) this.parent.enableSubstitution()
177-
}
165+
this.assignContext(this.property)
166+
if (this.context === 'didNotReceive') this._recordedArguments = RecordedArguments.from([0])
178167
}
179168

180169
private getMostSuitableSubstitution(): SubstituteNode {
181-
const nodes = this.getAllSiblings().filter(node => node.hasSubstitution &&
170+
const nodes = this.getAllSiblings().filter(node => node.isSubstitution &&
182171
node.propertyType === this.propertyType &&
183172
node.recordedArguments.match(this.recordedArguments)
184173
)
185-
const sortedNodes = RecordedArguments.sort(nodes)
174+
const sortedNodes = RecordedArguments.sort([...nodes])
186175
return sortedNodes[0]
187176
}
188177

189178
protected printableForm(_: number, options: InspectOptions): string {
190-
const isMockNode = this.hasSubstitution || this.isAssertion
179+
const hasContext = this.hasContext
191180
const args = inspect(this.recordedArguments, options)
192-
const label = this.hasSubstitution
181+
const label = this.isSubstitution
193182
? '=> '
194183
: this.isAssertion
195184
? `${this.child.property.toString()}`
196185
: ''
197-
const s = isMockNode
198-
? ` ${label}${inspect(this.child.recordedArguments, options)}`
186+
const s = hasContext
187+
? ` ${label}${inspect(this.child?.recordedArguments, options)}`
199188
: ''
200189

201190
const printableNode = `${this.propertyType}<${this.property.toString()}>: ${args}${s}`
202-
return isMockNode ? textModifier.italic(printableNode) : printableNode
191+
return hasContext ? textModifier.italic(printableNode) : printableNode
203192
}
204193
}

src/SubstituteNodeBase.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
import { SubstituteBase } from './SubstituteBase'
22
import { Substitute } from './Substitute'
3+
import { RecordsSet } from './RecordsSet'
34

45
export abstract class SubstituteNodeBase<T extends SubstituteNodeBase = SubstituteNodeBase<any>> extends SubstituteBase {
5-
private _parent?: T = undefined
6+
private _parent?: T
67
private _child?: T
78
private _head: T & { parent: undefined }
89
private _root: Substitute
910

10-
constructor(private _key: PropertyKey, caller: T | SubstituteBase) {
11+
constructor(private _key: PropertyKey, caller: SubstituteBase) {
1112
super()
1213

1314
if (caller instanceof Substitute) {
1415
caller.recorder.addIndexedRecord(this)
1516
this._root = caller
1617
}
18+
if (!(caller instanceof SubstituteNodeBase)) return
1719

18-
if (caller instanceof SubstituteNodeBase) {
19-
this._parent = caller
20-
this._head = caller.head as T & { parent: undefined }
21-
caller.child = this
22-
}
23-
24-
this.root.recorder.addRecord(this)
20+
this._parent = caller as T
21+
this._head = caller.head as T & { parent: undefined }
22+
caller.child = this
2523
}
2624

2725
get key(): PropertyKey {
@@ -60,12 +58,8 @@ export abstract class SubstituteNodeBase<T extends SubstituteNodeBase = Substitu
6058
return !this.isHead()
6159
}
6260

63-
protected getAllSiblings(): T[] {
64-
return this.root.recorder.getSiblingsOf(this) as T[]
65-
}
66-
67-
protected getAllSiblingsOfHead(): T[] {
68-
return this.root.recorder.getSiblingsOf(this.head) as T[]
61+
protected getAllSiblings(): RecordsSet<T> {
62+
return this.root.recorder.getSiblingsOf(this) as RecordsSet<T>
6963
}
7064

7165
public abstract read(): void

0 commit comments

Comments
 (0)