@itwin/core-backend
Version:
iTwin.js backend components
246 lines • 12.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.HubMock = void 0;
const path_1 = require("path");
const core_bentley_1 = require("@itwin/core-bentley");
const CheckpointManager_1 = require("../CheckpointManager");
const IModelHost_1 = require("../IModelHost");
const IModelJsFs_1 = require("../IModelJsFs");
const LocalHub_1 = require("../LocalHub");
const Symbols_1 = require("./Symbols");
const BriefcaseManager_1 = require("../BriefcaseManager");
const path = require("path");
function wasStarted(val) {
if (undefined === val)
throw new Error("Call HubMock.startup first");
}
function doDownload(args) {
HubMock.findLocalHub(args.iModelId).downloadCheckpoint(args);
}
const mockCheckpoint = {
mockAttach: (checkpoint) => {
const targetFile = path.join(BriefcaseManager_1.BriefcaseManager.getBriefcaseBasePath(checkpoint.iModelId), `${checkpoint.changeset.index}.bim`);
doDownload({ ...checkpoint, targetFile });
return targetFile;
},
mockDownload: (request) => {
doDownload({ ...request.checkpoint, targetFile: request.localFile });
}
};
/**
* Mocks iModelHub for testing creating Briefcases, downloading checkpoints, and simulating multiple users pushing and pulling changesets, etc.
*
* Generally, tests for apis that *create or modify* iModels can and should be mocked. Otherwise they:
* - create tremendous load on the test servers when they run on programmer's desktops and in CI jobs
* - waste network and data center resources (i.e. $$$s),
* - interfere with other tests running on the same or other systems, and
* - (far worse) are the source of test flakiness outside of the api being tested.
*
* This class can be used to create tests that do not require authentication, are synchronous,
* are guaranteed to be self-contained (i.e. do not interfere with other tests running at the same time or later), and do not fail for reasons outside
* of the control of the test itself. As a bonus, in addition to making tests more reliable, mocking IModelHub generally makes tests run *much* faster.
*
* On the other hand, tests that expect to find an existing iModels, checkpoints, changesets, etc. in IModelHub cannot be mocked. In that case, those tests
* should be careful to NOT modify the data, since doing so causes interference with other tests running simultaneously. These tests should be limited to
* low level testing of the core apis only.
*
* To initialize HubMock, call [[startup]] at the beginning of your test, usually in `describe.before`. Thereafter, all access to iModelHub for an iModel will be
* directed to a [[LocalHub]] - your test code does not change. After the test(s) complete, call [[shutdown]] (usually in `describe.after`) to stop mocking IModelHub and clean
* up any resources used by the test(s). If you want to mock a single test, call [[startup]] as the first line and [[shutdown]] as the last. If you wish to run the
* test against a "real" IModelHub, you can simply comment off the call [[startup]], though in that case you should make sure the name of your
* iModel is unique so your test won't collide with other tests (iModel name uniqueness is not necessary for mocked tests.)
*
* Mocked tests must always start by creating a new iModel via [[IModelHost[_hubAccess].createNewIModel]] with a `version0` iModel.
* They use mock (aka "bogus") credentials for `AccessTokens`, which is fine since [[HubMock]] never accesses resources outside the current
* computer.
*
* @note Only one HubMock at a time, *running in a single process*, may be active. The comments above about multiple simultaneous tests refer to tests
* running on different computers, or on a single computer in multiple processes. All of those scenarios are problematic without mocking.
*
* @internal
*/
class HubMock {
static mockRoot;
static hubs = new Map();
static _saveHubAccess;
static _iTwinId;
/** Determine whether a test us currently being run under HubMock */
static get isValid() { return undefined !== this.mockRoot; }
static get iTwinId() {
wasStarted(this._iTwinId);
return this._iTwinId;
}
/**
* Begin mocking IModelHub access. After this call, all access to IModelHub will be directed to a [[LocalHub]].
* @param mockName a unique name (e.g. "MyTest") for this HubMock to disambiguate tests when more than one is simultaneously active.
* It is used to create a private directory used by the HubMock for a test. That directory is removed when [[shutdown]] is called.
*/
static startup(mockName, outputDir) {
if (this.isValid)
throw new Error("Either a previous test did not call HubMock.shutdown() properly, or more than one test is simultaneously attempting to use HubMock, which is not allowed");
this.hubs.clear();
this.mockRoot = (0, path_1.join)(outputDir, "HubMock", mockName);
IModelJsFs_1.IModelJsFs.recursiveMkDirSync(this.mockRoot);
IModelJsFs_1.IModelJsFs.purgeDirSync(this.mockRoot);
this._saveHubAccess = IModelHost_1.IModelHost[Symbols_1._getHubAccess]();
IModelHost_1.IModelHost[Symbols_1._setHubAccess](this);
HubMock._iTwinId = core_bentley_1.Guid.createValue(); // all iModels for this test get the same "iTwinId"
CheckpointManager_1.V2CheckpointManager[Symbols_1._mockCheckpoint] = mockCheckpoint;
}
/** Stop a HubMock that was previously started with [[startup]]
* @note this function throws an exception if any of the iModels used during the tests are left open.
*/
static shutdown() {
if (this.mockRoot === undefined)
return;
CheckpointManager_1.V2CheckpointManager[Symbols_1._mockCheckpoint] = undefined;
HubMock._iTwinId = undefined;
for (const hub of this.hubs)
hub[1].cleanup();
this.hubs.clear();
IModelJsFs_1.IModelJsFs.purgeDirSync(this.mockRoot);
IModelJsFs_1.IModelJsFs.removeSync(this.mockRoot);
IModelHost_1.IModelHost[Symbols_1._setHubAccess](this._saveHubAccess);
this.mockRoot = undefined;
}
static findLocalHub(iModelId) {
const hub = this.hubs.get(iModelId);
if (!hub)
throw new Error(`local hub for iModel ${iModelId} not created`);
return hub;
}
/** create a [[LocalHub]] for an iModel. */
static async createNewIModel(arg) {
wasStarted(this.mockRoot);
const props = { ...arg, iModelId: core_bentley_1.Guid.createValue() };
const mock = new LocalHub_1.LocalHub((0, path_1.join)(this.mockRoot, props.iModelId), props);
this.hubs.set(props.iModelId, mock);
return props.iModelId;
}
/** remove the [[LocalHub]] for an iModel */
static destroy(iModelId) {
this.findLocalHub(iModelId).cleanup();
this.hubs.delete(iModelId);
}
/** All methods below are mocks of the [[BackendHubAccess]] interface */
static async getChangesetFromNamedVersion(arg) {
return this.findLocalHub(arg.iModelId).findNamedVersion(arg.versionName);
}
static changesetIndexFromArg(arg) {
return (undefined !== arg.changeset.index) ? arg.changeset.index : this.findLocalHub(arg.iModelId).getChangesetIndex(arg.changeset.id);
}
static async getChangesetFromVersion(arg) {
const hub = this.findLocalHub(arg.iModelId);
const version = arg.version;
if (version.isFirst)
return hub.getChangesetByIndex(0);
const asOf = version.getAsOfChangeSet();
if (asOf)
return hub.getChangesetById(asOf);
const versionName = version.getName();
if (versionName)
return hub.findNamedVersion(versionName);
return hub.getLatestChangeset();
}
static async getLatestChangeset(arg) {
return this.findLocalHub(arg.iModelId).getLatestChangeset();
}
static async getAccessToken(arg) {
return arg.accessToken ?? await IModelHost_1.IModelHost.getAccessToken();
}
static async getMyBriefcaseIds(arg) {
const accessToken = await this.getAccessToken(arg);
return this.findLocalHub(arg.iModelId).getBriefcaseIds(accessToken);
}
static async acquireNewBriefcaseId(arg) {
const accessToken = await this.getAccessToken(arg);
return this.findLocalHub(arg.iModelId).acquireNewBriefcaseId(accessToken, arg.briefcaseAlias);
}
/** Release a briefcaseId. After this call it is illegal to generate changesets for the released briefcaseId. */
static async releaseBriefcase(arg) {
return this.findLocalHub(arg.iModelId).releaseBriefcaseId(arg.briefcaseId);
}
static async downloadChangeset(arg) {
const changesetProps = this.findLocalHub(arg.iModelId).downloadChangeset({ index: this.changesetIndexFromArg(arg), targetDir: arg.targetDir });
if (arg.progressCallback) {
const totalSize = IModelJsFs_1.IModelJsFs.lstatSync(changesetProps.pathname)?.size;
if (totalSize)
await HubMock.mockProgressReporting(arg.progressCallback, totalSize);
}
return changesetProps;
}
static async downloadChangesets(arg) {
const changesetProps = this.findLocalHub(arg.iModelId).downloadChangesets({ range: arg.range, targetDir: arg.targetDir });
if (arg.progressCallback) {
const totalSize = changesetProps.reduce((sum, props) => sum + (IModelJsFs_1.IModelJsFs.lstatSync(props.pathname)?.size ?? 0), 0);
await HubMock.mockProgressReporting(arg.progressCallback, totalSize);
}
return changesetProps;
}
static async queryChangeset(arg) {
return this.findLocalHub(arg.iModelId).getChangesetByIndex(this.changesetIndexFromArg(arg));
}
static async queryChangesets(arg) {
return this.findLocalHub(arg.iModelId).queryChangesets(arg.range);
}
static async pushChangeset(arg) {
return this.findLocalHub(arg.iModelId).addChangeset(arg.changesetProps);
}
static async queryV2Checkpoint(arg) {
return {
accountName: "none",
sasToken: "none",
containerId: core_bentley_1.Guid.createValue(),
dbName: `${arg.changeset.index ?? 0}.bim`,
storageType: "mock",
isMock: true,
checkpoint: arg,
};
}
static async releaseAllLocks(arg) {
const hub = this.findLocalHub(arg.iModelId);
hub.releaseAllLocks({ briefcaseId: arg.briefcaseId, changesetIndex: hub.getIndexFromChangeset(arg.changeset) });
}
static async queryAllLocks(_arg) {
return [];
}
static async acquireLocks(arg, locks) {
this.findLocalHub(arg.iModelId).acquireLocks(locks, arg);
}
static async queryIModelByName(arg) {
for (const hub of this.hubs) {
const localHub = hub[1];
if (localHub.iTwinId === arg.iTwinId && localHub.iModelName === arg.iModelName)
return localHub.iModelId;
}
return undefined;
}
static async deleteIModel(arg) {
return this.destroy(arg.iModelId);
}
static async mockProgressReporting(progressCallback, totalSize) {
await new Promise((resolve, reject) => {
let rejected = false;
const mockProgress = (index) => {
const bytesDownloaded = Math.floor(totalSize * (index / 4));
if (!rejected && progressCallback(bytesDownloaded, totalSize) === CheckpointManager_1.ProgressStatus.Abort) {
rejected = true;
reject(new Error("AbortError"));
}
};
mockProgress(1);
setTimeout(() => mockProgress(2), 50);
setTimeout(() => mockProgress(3), 100);
setTimeout(() => {
mockProgress(4);
resolve(undefined);
}, 150);
});
}
}
exports.HubMock = HubMock;
//# sourceMappingURL=HubMock.js.map
;