Skip to content

Commit

Permalink
fix(core): correct caching of touched declarations #4344
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Nov 26, 2022
1 parent 4263a24 commit 233f014
Show file tree
Hide file tree
Showing 17 changed files with 331 additions and 47 deletions.
19 changes: 19 additions & 0 deletions libs/ng-mocks/src/lib/common/core.def-stack.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import CoreDefStack from './core.def-stack';

describe('CoreDefStack', () => {
it('returns empty map on empty pop', () => {
const stack = new CoreDefStack();

const result1 = stack.pop();
const result2 = stack.pop();

expect(result1).toBeDefined();
expect(result2).toBeDefined();
expect(result1).not.toBe(result2);
});

it('returns undefined on empty get', () => {
const stack = new CoreDefStack();
expect(stack.get(CoreDefStack)).toBeUndefined();
});
});
53 changes: 53 additions & 0 deletions libs/ng-mocks/src/lib/common/core.def-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { mapEntries } from './core.helpers';

export default class<K, V> {
protected stack: Array<Map<K, V>> = [];

public constructor() {
this.push();
}

public push() {
this.stack.push(new Map());
}

public pop(): Map<V, V> {
return this.stack.pop() ?? new Map();
}

public has(key: K): ReturnType<Map<K, V>['has']> {
for (let i = this.stack.length - 1; i >= 0; i -= 1) {
if (this.stack[i].has(key)) {
return true;
}
}

return false;
}

public get(key: K): ReturnType<Map<K, V>['get']> {
for (let i = this.stack.length - 1; i >= 0; i -= 1) {
if (this.stack[i].has(key)) {
return this.stack[i].get(key);
}
}

return undefined;
}

public set(key: K, value: V): this {
for (let i = this.stack.length - 1; i >= 0; i -= 1) {
this.stack[i].set(key, value);
}

return this;
}

public merge(resolutions: Map<K, V>): this {
for (const [key, value] of mapEntries(resolutions)) {
this.set(key, value);
}

return this;
}
}
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/common/func.is-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ export default <T>(
__template?: TemplateRef<any>;
__vcr?: ViewContainerRef;
} => {
return value && typeof value === 'object' && !!(value as any).__ngMocksConfig;
return value && typeof value === 'object' && !!(value as any).__ngMocks;
};
5 changes: 3 additions & 2 deletions libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NgModule, Provider } from '@angular/core';
import { TestBed, TestBedStatic, TestModuleMetadata } from '@angular/core/testing';

import CoreDefStack from '../common/core.def-stack';
import { flatten, mapValues } from '../common/core.helpers';
import { Type } from '../common/core.types';
import funcGetName from '../common/func.get-name';
Expand Down Expand Up @@ -69,15 +70,15 @@ export class MockBuilderPromise implements IMockBuilder {

public build(): TestModuleMetadata {
this.stash.backup();
ngMocksUniverse.config.set('mockNgDefResolver', new Map());
ngMocksUniverse.config.set('mockNgDefResolver', new CoreDefStack());
ngMocksUniverse.flags.add('hasRootModule');

try {
const params = this.combineParams();

const ngModule = initNgModules(params, initUniverse(params));
addRequestedProviders(ngModule, params);
handleRootProviders(ngModule, params);
handleRootProviders(ngModule, params, ngMocksUniverse.config.get('mockNgDefResolver'));
handleEntryComponents(ngModule);
applyPlatformModules();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreDefStack from '../../common/core.def-stack';
import { mapValues } from '../../common/core.helpers';
import { NG_MOCKS_ROOT_PROVIDERS } from '../../common/core.tokens';
import { isNgInjectionToken } from '../../common/func.is-ng-injection-token';
Expand All @@ -9,13 +10,12 @@ import getRootProviderParameters from './get-root-provider-parameters';
import { BuilderData, NgMeta } from './types';

// Mocking root providers.
export default (ngModule: NgMeta, { keepDef, mockDef }: BuilderData): void => {
export default (ngModule: NgMeta, { keepDef, mockDef }: BuilderData, resolutions: CoreDefStack<any, any>): void => {
// Adding missed providers.
const parameters = keepDef.has(NG_MOCKS_ROOT_PROVIDERS) ? new Set() : getRootProviderParameters(mockDef);
if (parameters.size > 0) {
const parametersMap = new Map();
for (const parameter of mapValues(parameters)) {
const mock = helperResolveProvider(parameter, parametersMap);
const mock = helperResolveProvider(parameter, resolutions);
if (mock) {
ngModule.providers.push(mock);
} else if (isNgInjectionToken(parameter)) {
Expand Down
3 changes: 2 additions & 1 deletion libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TestModuleMetadata } from '@angular/core/testing';

import CoreDefStack from '../common/core.def-stack';
import { flatten, mapKeys, mapValues } from '../common/core.helpers';
import coreReflectModuleResolve from '../common/core.reflect.module-resolve';
import funcGetProvider from '../common/func.get-provider';
Expand Down Expand Up @@ -226,7 +227,7 @@ export default (keep: any, mock: any = null, exclude: any = null): TestModuleMet
resolutions.set(mockDef, 'exclude');
}

ngMocksUniverse.config.set('mockNgDefResolver', new Map());
ngMocksUniverse.config.set('mockNgDefResolver', new CoreDefStack());
for (const def of mapValues(data.mock)) {
resolutions.set(def, 'mock');
if (data.optional.has(def)) {
Expand Down
19 changes: 14 additions & 5 deletions libs/ng-mocks/src/lib/mock-module/create-resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Provider } from '@angular/core';

import CoreDefStack from '../common/core.def-stack';
import { isNgDef } from '../common/func.is-ng-def';
import { isNgModuleDefWithProviders } from '../common/func.is-ng-module-def-with-providers';
import ngMocksUniverse from '../common/ng-mocks-universe';
Expand Down Expand Up @@ -35,14 +36,18 @@ const processDef = (def: any) => {

// resolveProvider is a special case because of the def structure.
const createResolveProvider =
(resolutions: Map<any, any>, change: () => void): ((def: Provider) => any) =>
(resolutions: CoreDefStack<any, any>, change: () => void): ((def: Provider) => any) =>
(def: Provider) =>
helperMockService.resolveProvider(def, resolutions, change);

const createResolveWithProviders = (def: any, mockDef: any): boolean =>
mockDef && mockDef.ngModule && isNgModuleDefWithProviders(def);

const createResolveExisting = (def: any, resolutions: Map<any, any>, change: (flag?: boolean) => void): any => {
const createResolveExisting = (
def: any,
resolutions: CoreDefStack<any, any>,
change: (flag?: boolean) => void,
): any => {
const mockDef = resolutions.get(def);
if (def !== mockDef) {
change();
Expand All @@ -51,14 +56,18 @@ const createResolveExisting = (def: any, resolutions: Map<any, any>, change: (fl
return mockDef;
};

const createResolveExcluded = (def: any, resolutions: Map<any, any>, change: (flag?: boolean) => void): void => {
const createResolveExcluded = (
def: any,
resolutions: CoreDefStack<any, any>,
change: (flag?: boolean) => void,
): void => {
resolutions.set(def, undefined);

change();
};

const createResolve =
(resolutions: Map<any, any>, change: (flag?: boolean) => void): ((def: any) => any) =>
(resolutions: CoreDefStack<any, any>, change: (flag?: boolean) => void): ((def: any) => any) =>
(def: any) => {
if (resolutions.has(def)) {
return createResolveExisting(def, resolutions, change);
Expand All @@ -85,7 +94,7 @@ const createResolve =

export default (
change: () => void,
resolutions: Map<any, any>,
resolutions: CoreDefStack<any, any>,
): {
resolve: (def: any) => any;
resolveProvider: (def: Provider) => any;
Expand Down
27 changes: 11 additions & 16 deletions libs/ng-mocks/src/lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';

import coreConfig from '../common/core.config';
import coreDefineProperty from '../common/core.define-property';
import { extendClass } from '../common/core.helpers';
import coreReflectModuleResolve from '../common/core.reflect.module-resolve';
import { AnyType, Type } from '../common/core.types';
Expand All @@ -12,6 +13,7 @@ import { isNgDef } from '../common/func.is-ng-def';
import { isNgModuleDefWithProviders, NgModuleWithProviders } from '../common/func.is-ng-module-def-with-providers';
import { Mock } from '../common/mock';
import ngMocksUniverse from '../common/ng-mocks-universe';
import returnCachedMock from '../mock/return-cached-mock';

import mockNgDef from './mock-ng-def';

Expand Down Expand Up @@ -104,7 +106,7 @@ const getExistingMockModule = (ngModule: Type<any>, isRootModule: boolean): Type
// Every module should be replaced with its mock copy only once to avoid errors like:
// Failed: Type ...Component is part of the declarations of 2 modules: ...Module and ...Module...
if (ngMocksUniverse.flags.has('cacheModule') && ngMocksUniverse.cacheDeclarations.has(ngModule)) {
return ngMocksUniverse.cacheDeclarations.get(ngModule);
return returnCachedMock(ngModule);
}

// Now we check if we need to keep the original module or to replace it with some other.
Expand All @@ -122,27 +124,20 @@ const getExistingMockModule = (ngModule: Type<any>, isRootModule: boolean): Type
return undefined;
};

const getMockModuleDef = (ngModule: Type<any>, mockModule?: Type<any>): NgModule | undefined => {
if (!mockModule) {
const meta = coreReflectModuleResolve(ngModule);
const [changed, ngModuleDef] = mockNgDef(meta, ngModule);
if (changed) {
return ngModuleDef;
}
}

return undefined;
};

const detectMockModule = (ngModule: Type<any>, mockModule?: Type<any>): Type<any> => {
const mockModuleDef = getMockModuleDef(ngModule, mockModule);
const [changed, ngModuleDef, resolutions] = mockModule
? [false]
: mockNgDef(coreReflectModuleResolve(ngModule), ngModule);
if (resolutions) {
coreDefineProperty(ngModule, '__ngMocksResolutions', resolutions);
}

if (mockModuleDef) {
if (changed) {
const parent = ngMocksUniverse.flags.has('skipMock') ? ngModule : Mock;
const mock = extendClass(parent);

// the last thing is to apply decorators.
NgModule(mockModuleDef)(mock);
NgModule(ngModuleDef)(mock);
decorateMock(mock, ngModule);

return mock;
Expand Down
12 changes: 8 additions & 4 deletions libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, Directive, NgModule, Pipe, Provider } from '@angular/core';

import CoreDefStack from '../common/core.def-stack';
import { flatten } from '../common/core.helpers';
import { dependencyKeys, Type } from '../common/core.types';
import { isNgModuleDefWithProviders } from '../common/func.is-ng-module-def-with-providers';
Expand All @@ -18,11 +19,11 @@ const configureProcessMetaKeys = (
resolveProvider: (def: Provider) => any,
): Array<[dependencyKeys, (def: any) => any]> => [
['declarations', resolve],
['imports', resolve],
['entryComponents', resolve],
['bootstrap', resolve],
['providers', resolveProvider],
['viewProviders', resolveProvider],
['imports', resolve],
['exports', resolve],
['schemas', v => v],
];
Expand Down Expand Up @@ -117,11 +118,13 @@ export default (
skipMarkProviders?: boolean;
},
ngModule?: Type<any>,
): [boolean, NgModule] => {
): [boolean, NgModule, Map<any, any>] => {
const hasResolver = ngMocksUniverse.config.has('mockNgDefResolver');
if (!hasResolver) {
ngMocksUniverse.config.set('mockNgDefResolver', new Map());
ngMocksUniverse.config.set('mockNgDefResolver', new CoreDefStack());
}
ngMocksUniverse.config.get('mockNgDefResolver').push();

let changed = !ngMocksUniverse.flags.has('skipMock');
const change = (flag = true) => {
changed = changed || flag;
Expand All @@ -130,9 +133,10 @@ export default (
const mockModuleDef = processMeta(ngModuleDef, resolve, resolveProvider);
addExports(resolve, change, ngModuleDef, mockModuleDef, ngModule);

const resolutions = ngMocksUniverse.config.get('mockNgDefResolver').pop();
if (!hasResolver) {
ngMocksUniverse.config.delete('mockNgDefResolver');
}

return [changed, mockModuleDef];
return [changed, mockModuleDef, resolutions];
};
3 changes: 2 additions & 1 deletion libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isMockNgDef } from '../common/func.is-mock-ng-def';
import { Mock } from '../common/mock';
import ngMocksUniverse from '../common/ng-mocks-universe';
import helperMockService from '../mock-service/helper.mock-service';
import returnCachedMock from '../mock/return-cached-mock';

import { MockedPipe } from './types';

Expand Down Expand Up @@ -79,7 +80,7 @@ export function MockPipe<TPipe extends PipeTransform>(

// istanbul ignore next
if (ngMocksUniverse.flags.has('cachePipe') && ngMocksUniverse.cacheDeclarations.has(pipe)) {
return ngMocksUniverse.cacheDeclarations.get(pipe);
return returnCachedMock(pipe);
}

const mock = getMockClass(pipe, transform);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreDefStack from '../common/core.def-stack';
import { extractDependency } from '../common/core.helpers';
import { NG_MOCKS_INTERCEPTORS } from '../common/core.tokens';
import funcExtractForwardRef from '../common/func.extract-forward-ref';
Expand Down Expand Up @@ -179,7 +180,7 @@ const isPreconfiguredUseExisting = (provider: any, provide: any): boolean => {
};

// tries to resolve a provider based on current universe state.
export default (provider: any, resolutions: Map<any, any>, changed?: () => void) => {
export default (provider: any, resolutions: CoreDefStack<any, any>, changed?: () => void) => {
const { provide, multi, change } = parseProvider(provider, changed);
if (isPreconfiguredDependency(provider, provide)) {
return change();
Expand Down
7 changes: 4 additions & 3 deletions libs/ng-mocks/src/lib/mock/clone-providers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Provider } from '@angular/core';

import CoreDefStack from '../common/core.def-stack';
import coreForm from '../common/core.form';
import { flatten } from '../common/core.helpers';
import { AnyType } from '../common/core.types';
Expand Down Expand Up @@ -49,7 +50,7 @@ const processProvider = (
sourceType: AnyType<any>,
mockType: AnyType<any>,
provider: any,
resolutions: Map<any, any>,
resolutions: CoreDefStack<any, any>,
): any => {
const token = processTokens(mockType, provider);
if (token) {
Expand All @@ -67,14 +68,14 @@ const processProvider = (
export default (
sourceType: AnyType<any>,
mockType: AnyType<any>,
providers?: any[],
providers: any[],
resolutions: CoreDefStack<any, any>,
): {
providers: Provider[];
setControlValueAccessor?: boolean;
} => {
const result: Provider[] = [];
let setControlValueAccessor: boolean | undefined;
const resolutions = new Map();

for (const provider of flatten(providers || /* istanbul ignore next */ [])) {
const provide = funcGetProvider(provider);
Expand Down
Loading

0 comments on commit 233f014

Please sign in to comment.