Skip to content

Commit

Permalink
[Mockery replacement] - Replaced mockery with similar adapter (#968)
Browse files Browse the repository at this point in the history
* [Mockery replacement] - Replaced mockery with similar adapter

- Replaced Mockery test-lib since it has vulnerabilities and is no longer maintained
- Replaced with self-written adapter inspired by mockery for backward compatibility with tasks.
- All methods which are not used in the tasks are not implemented since they shouldn't be used separately from toolrunner.
- Added unit Tests
- Task-Lib version was bumped to 5.0.0-preview
- The lib was fully tested with all node-based tasks from azure-pipelines-task-lib repository

* [Mockery replacement] - Replaced mockery with similar adapter

- Replaced Mockery test-lib since it has vulnerabilities and is no longer maintained
- Replaced with self-written adapter inspired by mockery for backward compatibility with tasks.
- All methods which are not used in the tasks are not implemented since they shouldn't be used separately from toolrunner.
- Added unit Tests
- Task-Lib version was bumped to 5.0.0-preview
- The lib was fully tested with all node-based tasks from azure-pipelines-task-lib repository

* [Mockery replacement] - Replaced mockery with similar adapter

- Changed preview version
  • Loading branch information
DmitriiBobreshev committed Sep 12, 2023
1 parent 44e727d commit 997f9fe
Show file tree
Hide file tree
Showing 10 changed files with 598 additions and 24 deletions.
213 changes: 213 additions & 0 deletions node/lib-mocker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
const m = require('module');

export interface MockOptions {
useCleanCache?: boolean,
warnOnReplace?: boolean,
warnOnUnregistered?: boolean
};

let registeredMocks = {};
let registeredAllowables = new Set<string>();
let originalLoader: Function | null = null;
let originalCache: Record<string, any> = {};
let options: MockOptions = {};
let defaultOptions: MockOptions = {
useCleanCache: false,
warnOnReplace: true,
warnOnUnregistered: true
};

function _getEffectiveOptions(opts: MockOptions): MockOptions {
var options: MockOptions = {};

Object.keys(defaultOptions).forEach(function (key) {
if (opts && opts.hasOwnProperty(key)) {
options[key] = opts[key];
} else {
options[key] = defaultOptions[key];
}
});
return options;
}

/*
* Loader function that used when hooking is enabled.
* if the requested module is registered as a mock, return the mock.
* otherwise, invoke the original loader + put warning in the output.
*/
function _hookedLoader(request: string, parent, isMain: boolean) {
if (!originalLoader) {
throw new Error("Loader has not been hooked");
}

if (registeredMocks.hasOwnProperty(request)) {
return registeredMocks[request];
}

if (!registeredAllowables.has(request) && options.warnOnUnregistered) {
console.warn("WARNING: loading non-allowed module: " + request);
}

return originalLoader(request, parent, isMain);
}


/**
* Remove references to modules in the cache from
* their parents' children.
*/
function _removeParentReferences(): void {
Object.keys(m._cache).forEach(function (k) {
if (k.indexOf('\.node') === -1) {
// don't touch native modules, because they're special
const mod = m._cache[k];
const idx = mod?.parent?.children.indexOf(mod);
if (idx > -1) {
mod.parent.children.splice(idx, 1);
}
}
});
}

/*
* Starting in node 0.12 node won't reload native modules
* The reason is that native modules can register themselves to be loaded automatically
* This will re-populate the cache with the native modules that have not been mocked
*/
function _repopulateNative(): void {
Object.keys(originalCache).forEach(function (k) {
if (k.indexOf('\.node') > -1 && !m._cache[k]) {
m._cache[k] = originalCache[k];
}
});
}

/*
* Enable function, hooking the Node loader with options.
*/
export function enable(opts: MockOptions): void {
if (originalLoader) {
// Already hooked
return;
}

options = _getEffectiveOptions(opts);

if (options.useCleanCache) {
originalCache = m._cache;
m._cache = {};
_repopulateNative();
}

originalLoader = m._load;
m._load = _hookedLoader;
}

/*
* Disables mock loading, reverting to normal 'require' behaviour.
*/
export function disable(): void {
if (!originalLoader) return;

if (options.useCleanCache) {
Object.keys(m._cache).forEach(function (k) {
if (k.indexOf('\.node') > -1 && !originalCache[k]) {
originalCache[k] = m._cache[k];
}
});
_removeParentReferences();
m._cache = originalCache;
originalCache = {};
}

m._load = originalLoader;
originalLoader = null;
}

/*
* If the clean cache option is in effect, reset the module cache to an empty
* state. Calling this function when the clean cache option is not in effect
* will have no ill effects, but will do nothing.
*/
export function resetCache(): void {
if (options.useCleanCache && originalCache) {
_removeParentReferences();
m._cache = {};
_repopulateNative();
}
}

/*
* Enable or disable warnings to the console when previously registered mocks are replaced.
*/
export function warnOnReplace(enable: boolean): void {
options.warnOnReplace = enable;
}

/*
* Enable or disable warnings to the console when modules are loaded that have
* not been registered as a mock.
*/
export function warnOnUnregistered(enable: boolean): void {
options.warnOnUnregistered = enable;
}

/*
* Register a mock object for the specified module.
*/
export function registerMock(mod: string, mock): void {
if (options.warnOnReplace && registeredMocks.hasOwnProperty(mod)) {
console.warn("WARNING: Replacing existing mock for module: " + mod);
}
registeredMocks[mod] = mock;
}

/*
* Deregister a mock object for the specified module.
*/
export function deregisterMock(mod: string): void {
if (registeredMocks.hasOwnProperty(mod)) {
delete registeredMocks[mod];
}
}

/*
* Deregister all mocks.
*/
export function deregisterAll(): void {
registeredMocks = {};
registeredAllowables = new Set();
}

/*
Register a module as 'allowed'.
This will allow the module to be loaded without mock otherwise a warning would be thrown.
*/
export function registerAllowable(mod: string): void {
registeredAllowables.add(mod);
}

/*
* Register an array of 'allowed' modules.
*/
export function registerAllowables(mods: string[]): void {
mods.forEach((mod) => registerAllowable(mod));
}

/*
* Deregister a module as 'allowed'.
*/
export function deregisterAllowable(mod: string): void {
if (registeredAllowables.hasOwnProperty(mod)) {
registeredAllowables.delete(mod);
}
}

/*
* Deregister an array of modules as 'allowed'.
*/
export function deregisterAllowables(mods) {
mods.forEach(function (mod) {
deregisterAllowable(mod);
});
}
10 changes: 5 additions & 5 deletions node/mock-run.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ma = require('./mock-answer');
import mockery = require('mockery');
import im = require('./internal');
import mocker = require('./lib-mocker');

export class TaskMockRunner {
constructor(taskPath: string) {
Expand Down Expand Up @@ -46,7 +46,7 @@ export class TaskMockRunner {
*/
public registerMock(modName: string, mod: any): void {
this._moduleCount++;
mockery.registerMock(modName, mod);
mocker.registerMock(modName, mod);
}

/**
Expand All @@ -69,9 +69,9 @@ export class TaskMockRunner {
* @returns void
*/
public run(noMockTask?: boolean): void {
// determine whether to enable mockery
// determine whether to enable mocker
if (!noMockTask || this._moduleCount) {
mockery.enable({warnOnUnregistered: false});
mocker.enable({warnOnUnregistered: false});
}

// answers and exports not compatible with "noMockTask" mode
Expand All @@ -92,7 +92,7 @@ export class TaskMockRunner {
tlm[key] = this._exports[key];
});

mockery.registerMock('azure-pipelines-task-lib/task', tlm);
mocker.registerMock('azure-pipelines-task-lib/task', tlm);
}

// run it
Expand Down
13 changes: 1 addition & 12 deletions node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azure-pipelines-task-lib",
"version": "4.4.0",
"version": "5.0.0-preview.0",
"description": "Azure Pipelines Task SDK",
"main": "./task.js",
"typings": "./task.d.ts",
Expand Down Expand Up @@ -28,7 +28,6 @@
"homepage": "https://github.com/Microsoft/azure-pipelines-task-lib",
"dependencies": {
"minimatch": "3.0.5",
"mockery": "^2.1.0",
"q": "^1.5.1",
"semver": "^5.1.0",
"shelljs": "^0.8.5",
Expand All @@ -38,7 +37,6 @@
"devDependencies": {
"@types/minimatch": "3.0.3",
"@types/mocha": "^9.1.1",
"@types/mockery": "^1.4.29",
"@types/node": "^16.11.39",
"@types/q": "^1.5.4",
"@types/semver": "^7.3.4",
Expand Down
7 changes: 7 additions & 0 deletions node/test/fakeModules/fakemodule1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function testFuncLibrary() {
return 'testFuncLibrary';
}

export function otherFuncLibrary() {
return 'otherFuncLibrary';
}
7 changes: 7 additions & 0 deletions node/test/fakeModules/fakemodule2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function testFuncLibrary2() {
return 'testFuncLibrary2';
}

export function otherFuncLibrary2() {
return 'otherFuncLibrary2';
}
3 changes: 1 addition & 2 deletions node/test/internalhelpertests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as path from 'path';
import * as testutil from './testutil';
import * as tl from '../_build/task';
import * as im from '../_build/internal';
import * as mockery from 'mockery'
import * as mockery from '../_build/lib-mocker'

describe('Internal Path Helper Tests', function () {

Expand Down Expand Up @@ -404,7 +404,6 @@ describe('Internal Path Helper Tests', function () {
});

it('ReportMissingStrings', (done) => {

mockery.registerAllowable('../_build/internal')
const fsMock = {
statSync: function (path) { return null; }
Expand Down
Loading

0 comments on commit 997f9fe

Please sign in to comment.