@@ -17,28 +17,40 @@ import { CheckpointProps, DownloadRequest, MockCheckpoint, ProgressFunction, Pro
1717import { IModelHost } from "../IModelHost" ;
1818import { IModelJsFs } from "../IModelJsFs" ;
1919import { LocalHub } from "../LocalHub" ;
20- import { TokenArg } from "../IModelDb" ;
21- import { _getHubAccess , _mockCheckpoint , _setHubAccess } from "./Symbols" ;
20+ import { SnapshotDb , TokenArg } from "../IModelDb" ;
21+ import { _getHubAccess , _mockCheckpoint , _nativeDb , _setHubAccess } from "./Symbols" ;
2222import { BriefcaseManager } from "../BriefcaseManager" ;
23- import * as path from "path" ;
2423
2524function wasStarted ( val : string | undefined ) : asserts val is string {
2625 if ( undefined === val )
2726 throw new Error ( "Call HubMock.startup first" ) ;
2827}
2928
30- function doDownload ( args : { iModelId : string , changeset : ChangesetIndexOrId , targetFile : string } ) {
29+ // Used by mockAttach: copies the nearest *prior* checkpoint (never newer) so the
30+ // returned file is already at or before the requested version.
31+ function doDownloadPrior ( args : { iModelId : string , changeset : ChangesetIndexOrId , targetFile : string } ) {
3132 HubMock . findLocalHub ( args . iModelId ) . downloadCheckpoint ( args ) ;
3233}
34+
35+ // Used by mockDownload: copies the *nearest* checkpoint (forward or backward).
36+ // When the nearest is newer than the requested version, CheckpointManager.updateToRequestedVersion
37+ // will reverse it to the requested version via BriefcaseManager.pullAndApplyChangesets.
38+ function doDownloadNearest ( args : { iModelId : string , changeset : ChangesetIndexOrId , targetFile : string } ) {
39+ const hub = HubMock . findLocalHub ( args . iModelId ) ;
40+ const requestedIndex = hub . getIndexFromChangeset ( args . changeset ) ;
41+ const nearest = hub . queryNearestCheckpoint ( requestedIndex ) ;
42+ IModelJsFs . copySync ( join ( hub . checkpointDir , hub . checkpointNameFromIndex ( nearest ) ) , args . targetFile ) ;
43+ }
44+
3345const mockCheckpoint : MockCheckpoint = {
3446 mockAttach : ( checkpoint : CheckpointProps ) => {
35- const targetFile = path . join ( BriefcaseManager . getBriefcaseBasePath ( checkpoint . iModelId ) , `${ checkpoint . changeset . index } .bim` ) ;
36- doDownload ( { ...checkpoint , targetFile } )
47+ const targetFile = join ( BriefcaseManager . getBriefcaseBasePath ( checkpoint . iModelId ) , `${ checkpoint . changeset . index } .bim` ) ;
48+ doDownloadPrior ( { ...checkpoint , targetFile } ) ;
3749 return targetFile ;
3850 } ,
3951
4052 mockDownload : ( request : DownloadRequest ) => {
41- doDownload ( { ...request . checkpoint , targetFile : request . localFile } ) ;
53+ doDownloadNearest ( { ...request . checkpoint , targetFile : request . localFile } ) ;
4254 }
4355} ;
4456
@@ -74,11 +86,22 @@ const mockCheckpoint: MockCheckpoint = {
7486 *
7587 * @internal
7688 */
89+ /** Options for [[HubMock.startup]]. @internal */
90+ export interface HubMockStartupOptions {
91+ /**
92+ * When `true`, every successful [[HubMock.pushChangeset]] call automatically uploads a V1 checkpoint
93+ * (the current briefcase `.bim` file) for the tip changeset index into [[LocalHub]].
94+ * This lets tests download a tip checkpoint and reverse changesets from it.
95+ */
96+ createTipCheckpointOnPush ?: boolean ;
97+ }
98+
7799export class HubMock {
78100 private static mockRoot : LocalDirName | undefined ;
79101 private static hubs = new Map < string , LocalHub > ( ) ;
80102 private static _saveHubAccess : BackendHubAccess | undefined ;
81103 private static _iTwinId : GuidString | undefined ;
104+ private static _createTipCheckpointOnPush = false ;
82105
83106 /** Determine whether a test us currently being run under HubMock */
84107 public static get isValid ( ) { return undefined !== this . mockRoot ; }
@@ -92,7 +115,7 @@ export class HubMock {
92115 * @param mockName a unique name (e.g. "MyTest") for this HubMock to disambiguate tests when more than one is simultaneously active.
93116 * It is used to create a private directory used by the HubMock for a test. That directory is removed when [[shutdown]] is called.
94117 */
95- public static startup ( mockName : LocalDirName , outputDir : string ) {
118+ public static startup ( mockName : LocalDirName , outputDir : string , options ?: HubMockStartupOptions ) {
96119 if ( this . isValid )
97120 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" ) ;
98121
@@ -104,6 +127,7 @@ export class HubMock {
104127
105128 IModelHost [ _setHubAccess ] ( this ) ;
106129 HubMock . _iTwinId = Guid . createValue ( ) ; // all iModels for this test get the same "iTwinId"
130+ this . _createTipCheckpointOnPush = options ?. createTipCheckpointOnPush ?? false ;
107131
108132 V2CheckpointManager [ _mockCheckpoint ] = mockCheckpoint ;
109133 }
@@ -116,6 +140,7 @@ export class HubMock {
116140 return ;
117141
118142 V2CheckpointManager [ _mockCheckpoint ] = undefined ;
143+ this . _createTipCheckpointOnPush = false ;
119144
120145 HubMock . _iTwinId = undefined ;
121146 for ( const hub of this . hubs )
@@ -232,7 +257,46 @@ export class HubMock {
232257 }
233258
234259 public static async pushChangeset ( arg : IModelIdArg & { changesetProps : ChangesetFileProps } ) : Promise < ChangesetIndex > {
235- return this . findLocalHub ( arg . iModelId ) . addChangeset ( arg . changesetProps ) ;
260+ const csIndex = this . findLocalHub ( arg . iModelId ) . addChangeset ( arg . changesetProps ) ;
261+ if ( this . _createTipCheckpointOnPush )
262+ await this . createTipCheckpoint ( arg . iModelId ) ;
263+ return csIndex ;
264+ }
265+
266+ /**
267+ * Build and upload a V1 checkpoint at the current tip changeset of the given iModel.
268+ *
269+ * The checkpoint is constructed by copying the nearest prior checkpoint from [[LocalHub]] as a
270+ * starting base, then applying all subsequent changesets forward to the latest index via
271+ * [[BriefcaseManager.pullAndApplyChangesets]]. The result is registered in [[LocalHub]] so that
272+ * [[V2CheckpointManager]] (mock path) can serve it to consumers.
273+ *
274+ * When [[HubMockStartupOptions.createTipCheckpointOnPush]] is `true` this is called automatically
275+ * after every successful [[pushChangeset]]. Tests can also call it explicitly to create a single
276+ * checkpoint at the tip after all changesets have been pushed.
277+ */
278+ public static async createTipCheckpoint ( iModelId : GuidString ) : Promise < void > {
279+ const hub = this . findLocalHub ( iModelId ) ;
280+ const csIndex = hub . latestChangesetIndex ;
281+ // Find the nearest checkpoint that precedes the new tip and use it as the base.
282+ const prevIndex = hub . queryPreviousCheckpoint ( csIndex ) ;
283+ const prevCheckpointFile = join ( hub . checkpointDir , hub . checkpointNameFromIndex ( prevIndex ) ) ;
284+ const tempFile = join ( hub . rootDir , `checkpoint-building-${ csIndex } .bim` ) ;
285+ IModelJsFs . copySync ( prevCheckpointFile , tempFile ) ;
286+ try {
287+ const db = SnapshotDb . openForApplyChangesets ( tempFile ) ;
288+ try {
289+ await BriefcaseManager . pullAndApplyChangesets ( db , { accessToken : "" , toIndex : csIndex } ) ;
290+ db [ _nativeDb ] . saveChanges ( ) ;
291+ } finally {
292+ db . close ( ) ;
293+ }
294+
295+ hub . uploadCheckpoint ( { changesetIndex : csIndex , localFile : tempFile } ) ;
296+ } finally {
297+ if ( IModelJsFs . existsSync ( tempFile ) )
298+ IModelJsFs . removeSync ( tempFile ) ;
299+ }
236300 }
237301
238302 public static async queryV2Checkpoint ( arg : CheckpointProps ) : Promise < V2CheckpointAccessProps | undefined > {
0 commit comments