UNPKG

@itwin/core-backend

Version:
246 lines • 12.4 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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