UNPKG

@itwin/core-backend

Version:
270 lines • 12.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { join } from "path"; import { Guid } from "@itwin/core-bentley"; import { ProgressStatus, V2CheckpointManager } from "../CheckpointManager"; import { IModelHost } from "../IModelHost"; import { IModelJsFs } from "../IModelJsFs"; import { LocalHub } from "../LocalHub"; import { SnapshotDb } from "../IModelDb"; import { _getHubAccess, _mockCheckpoint, _nativeDb, _setHubAccess } from "./Symbols"; import { BriefcaseManager } from "../BriefcaseManager"; function wasStarted(val) { if (undefined === val) throw new Error("Call HubMock.startup first"); } // Used by mockAttach: copies the nearest *prior* checkpoint (never newer) so the // returned file is already at or before the requested version. function doDownloadPrior(args) { HubMock.findLocalHub(args.iModelId).downloadCheckpoint(args); } // Used by mockDownload: copies the *nearest* checkpoint (forward or backward). // When the nearest is newer than the requested version, CheckpointManager.updateToRequestedVersion // will reverse it to the requested version via BriefcaseManager.pullAndApplyChangesets. function doDownloadNearest(args) { const hub = HubMock.findLocalHub(args.iModelId); const requestedIndex = hub.getIndexFromChangeset(args.changeset); const nearest = hub.queryNearestCheckpoint(requestedIndex); IModelJsFs.copySync(join(hub.checkpointDir, hub.checkpointNameFromIndex(nearest)), args.targetFile); } const mockCheckpoint = { mockAttach: (checkpoint) => { const targetFile = join(BriefcaseManager.getBriefcaseBasePath(checkpoint.iModelId), `${checkpoint.changeset.index}.bim`); doDownloadPrior({ ...checkpoint, targetFile }); return targetFile; }, mockDownload: (request) => { doDownloadNearest({ ...request.checkpoint, targetFile: request.localFile }); } }; export class HubMock { static mockRoot; static hubs = new Map(); static _saveHubAccess; static _iTwinId; static _createTipCheckpointOnPush = false; /** 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, options) { 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 = join(outputDir, "HubMock", mockName); IModelJsFs.recursiveMkDirSync(this.mockRoot); IModelJsFs.purgeDirSync(this.mockRoot); this._saveHubAccess = IModelHost[_getHubAccess](); IModelHost[_setHubAccess](this); HubMock._iTwinId = Guid.createValue(); // all iModels for this test get the same "iTwinId" this._createTipCheckpointOnPush = options?.createTipCheckpointOnPush ?? false; V2CheckpointManager[_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; V2CheckpointManager[_mockCheckpoint] = undefined; this._createTipCheckpointOnPush = false; HubMock._iTwinId = undefined; for (const hub of this.hubs) hub[1].cleanup(); this.hubs.clear(); IModelJsFs.purgeDirSync(this.mockRoot); IModelJsFs.removeSync(this.mockRoot); IModelHost[_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: Guid.createValue() }; const mock = new LocalHub(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.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.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.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) { const csIndex = this.findLocalHub(arg.iModelId).addChangeset(arg.changesetProps); if (this._createTipCheckpointOnPush) await this.createTipCheckpoint(arg.iModelId); return csIndex; } /** * Build and upload a V1 checkpoint at the current tip changeset of the given iModel. * * The checkpoint is constructed by copying the nearest prior checkpoint from [[LocalHub]] as a * starting base, then applying all subsequent changesets forward to the latest index via * [[BriefcaseManager.pullAndApplyChangesets]]. The result is registered in [[LocalHub]] so that * [[V2CheckpointManager]] (mock path) can serve it to consumers. * * When [[HubMockStartupOptions.createTipCheckpointOnPush]] is `true` this is called automatically * after every successful [[pushChangeset]]. Tests can also call it explicitly to create a single * checkpoint at the tip after all changesets have been pushed. */ static async createTipCheckpoint(iModelId) { const hub = this.findLocalHub(iModelId); const csIndex = hub.latestChangesetIndex; // Find the nearest checkpoint that precedes the new tip and use it as the base. const prevIndex = hub.queryPreviousCheckpoint(csIndex); const prevCheckpointFile = join(hub.checkpointDir, hub.checkpointNameFromIndex(prevIndex)); const tempFile = join(hub.rootDir, `checkpoint-building-${csIndex}.bim`); IModelJsFs.copySync(prevCheckpointFile, tempFile); try { const db = SnapshotDb.openForApplyChangesets(tempFile); try { await BriefcaseManager.pullAndApplyChangesets(db, { accessToken: "", toIndex: csIndex }); db[_nativeDb].saveChanges(); } finally { db.close(); } hub.uploadCheckpoint({ changesetIndex: csIndex, localFile: tempFile }); } finally { if (IModelJsFs.existsSync(tempFile)) IModelJsFs.removeSync(tempFile); } } static async queryV2Checkpoint(arg) { return { accountName: "none", sasToken: "none", containerId: 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 abandonAllLocks(arg) { const hub = this.findLocalHub(arg.iModelId); hub.abandonAllLocks(arg); } static async queryAllLocks(_arg) { return []; } static async acquireLocks(arg, locks) { this.findLocalHub(arg.iModelId).acquireLocks(locks, arg); } static async abandonLocks(arg, locks) { this.findLocalHub(arg.iModelId).abandonLocks(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) === ProgressStatus.Abort) { rejected = true; reject(new Error("AbortError")); } }; mockProgress(1); setTimeout(() => mockProgress(2), 50); setTimeout(() => mockProgress(3), 100); setTimeout(() => { mockProgress(4); resolve(undefined); }, 150); }); } } //# sourceMappingURL=HubMock.js.map