import { strict as assert } from "assert" ; import { ISequencedDocumentMessage , ISummaryTree } from "@fluidframework/protocol-definitions" ; import { IFluidDataStoreRuntime } from "@fluidframework/datastore-definitions" ; import { MockStorage } from "@fluidframework/test-runtime-utils" ; import { IMergeTreeOp } from "../ops" ; import { SnapshotV1 } from "../snapshotV1" ; import { TestSerializer } from "./testSerializer" ; import { TestClient } from "." ; async function loadSnapshot ( summary ) { const services = MockStorage . createFromSummary ( summary ) ; const client2 = new TestClient ( undefined ) ; const runtime = { logger : client2 . logger , clientId : "1" } ; const { catchupOpsP } = await client2 . load ( runtime as IFluidDataStoreRuntime , services , new TestSerializer ( ) ) ; await catchupOpsP ; return client2 ; } class TestString { private client = new TestClient ( ) ; private readonly pending : ISequencedDocumentMessage [ ] = [ ] ; private seq = 0 ; private minSeq = 0 ; constructor ( id ) { this . client . startOrUpdateCollaboration ( id ) ; } public insert ( pos , text , increaseMsn ) { this . queue ( this . client . insertTextLocal ( pos , text , { segment : this . pending . length } ) , increaseMsn ) ; } public append ( text , increaseMsn ) { this . insert ( this . client . getLength ( ) , text , increaseMsn ) ; } public removeRange ( start , end , increaseMsn ) { this . queue ( this . client . removeRangeLocal ( start , end ) , increaseMsn ) ; } public async expect ( expected ) { assert . equal ( this . client . getText ( ) , expected , "MergeTree must contain the expected text prior to applying ops." ) ; await this . checkSnapshot ( ) ; } public async checkSnapshot ( ) { this . applyPending ( ) ; const tree = this . getSummary ( ) ; const client2 = await loadSnapshot ( tree ) ; assert . equal ( this . client . getText ( ) , client2 . getText ( ) , "Snapshot must produce a MergeTree with the same text as the original" ) ; assert . equal ( this . client . getLength ( ) , client2 . getLength ( ) , "Snapshot must produce a MergeTree with the same length as the original" ) ; this . client = client2 ; } public getSummary ( ) { const snapshot = new SnapshotV1 ( this . client . mergeTree , this . client . logger ) ; snapshot . extractSync ( ) ; return snapshot . emit ( TestClient . serializer , undefined ! ) . summary ; } public getText ( ) { return this . client . getText ( ) ; } private applyPending ( ) { for ( const msg of this . pending ) { this . client . applyMsg ( msg ) ; } this . pending . splice ( 0 , this . pending . length ) ; } private queue ( op , increaseMsn ) { const refSeq = this . seq ; const seq = ++ this . seq ; this . pending . push ( this . client . makeOpMessage ( op , seq , refSeq , this . client . longClientId , this . minSeq = increaseMsn ? seq : this . minSeq ) ) ; } } describe ( "snapshot" , ( ) => { let str ; beforeEach ( ( ) => { str = new TestString ( "fakeId" ) ; } ) ; afterEach ( async ( ) => { await str . checkSnapshot ( ) ; } ) ; it ( "excludes un-acked segments" , async ( ) => { str . append ( "0" , false ) ; const client2 = await loadSnapshot ( str . getSummary ( ) ) ; assert . equal ( str . getText ( ) , "0" ) ; assert . equal ( client2 . getText ( ) , "" ) ; } ) ; it ( "includes segments below MSN" , async ( ) => { str . append ( "0" , true ) ; await str . expect ( "0" ) ; } ) ; it ( "includes ACKed segments above the MSN" , async ( ) => { str . append ( "0" , false ) ; await str . expect ( "0" ) ; } ) ; it ( "includes removals of segments above the MSN" , async ( ) => { str . append ( "0x" , false ) ; str . removeRange ( 1 , 2 , false ) ; await str . expect ( "0" ) ; } ) ; it ( "includes removals above the MSN of segments below the MSN" , async ( ) => { str . append ( "0x" , true ) ; str . removeRange ( 1 , 2 , false ) ; await str . expect ( "0" ) ; } ) ; it ( "can insert segments after loading removed segment" , async ( ) => { str . append ( "0x" , true ) ; str . removeRange ( 1 , 2 , false ) ; await str . expect ( "0" ) ; str . append ( "1" , false ) ; await str . expect ( "01" ) ; } ) ; it ( "can insert segments relative to removed segment" , async ( ) => { str . append ( "0x" , false ) ; str . append ( "2" , false ) ; str . removeRange ( 1 , 2 , false ) ; str . insert ( 1 , "1" , false ) ; str . append ( "3" , false ) ; await str . expect ( "0123" ) ; } ) ; it ( "can insert segments relative to removed segment loaded from snapshot" , async ( ) => { str . append ( "0x" , false ) ; str . append ( "2" , false ) ; str . removeRange ( 1 , 2 , false ) ; await str . expect ( "02" ) ; str . insert ( 1 , "1" , false ) ; str . append ( "3" , false ) ; await str . expect ( "0123" ) ; } ) ; it ( "includes ACKed segments below MSN in body" , async ( ) => { for ( let i = 0 ; i < SnapshotV1 . chunkSize + 10 ; i ++ ) { str . append ( `${ i % 10 }` , true ) ; } await str . checkSnapshot ( ) ; } ) ; it ( "includes ACKed segments above MSN in body" , async ( ) => { for ( let i = 0 ; i < SnapshotV1 . chunkSize + 10 ; i ++ ) { str . append ( `${ i % 10 }` , false ) ; } await str . checkSnapshot ( ) ; } ) ; } ) ;