import { strict as assert } from "assert" ; import { ContainerRuntimeFactoryWithDefaultDataStore , DataObjectFactory } from "@fluidframework/aqueduct" ; import { TelemetryNullLogger } from "@fluidframework/common-utils" ; import { IContainer , IRuntimeFactory } from "@fluidframework/container-definitions" ; import { ContainerRuntime , IAckedSummary , IContainerRuntimeOptions , SummaryCollection , } from "@fluidframework/container-runtime" ; import { ISummaryContext } from "@fluidframework/driver-definitions" ; import { ISummaryTree , SummaryType } from "@fluidframework/protocol-definitions" ; import { channelsTreeName , IContainerRuntimeBase , IGarbageCollectionDetailsBase } from "@fluidframework/runtime-definitions" ; import { IRequest } from "@fluidframework/core-interfaces" ; import { requestFluidObject } from "@fluidframework/runtime-utils" ; import { ITestObjectProvider } from "@fluidframework/test-utils" ; import { describeFullCompat , getContainerRuntimeApi } from "@fluidframework/test-version-utils" ; import { pkgVersion } from "../../packageVersion" ; import { wrapDocumentServiceFactory } from "./gcDriverWrappers" ; import { loadSummarizer , TestDataObject , submitAndAckSummary } from "./mockSummarizerClient" ; /**
 * These tests validate the compatibility of the GC data in the summary tree across the past 2 container runtime
 * versions. A version of container runtime generates the summary and then we validate that another version can
 * read and process it successfully.
 */ describeFullCompat ( "GC summary compatibility tests" , ( getTestObjectProvider ) => { const currentVersionNumber = 0 ; const oldVersionNumbers = [ - 1 , - 2 ] ; let provider ; const dataObjectFactory = new DataObjectFactory ( "TestDataObject" , TestDataObject , [ ] , [ ] ) ; const runtimeOptions = { summaryOptions : { disableSummaries : true } , gcOptions : { gcAllowed : true } } ; const innerRequestHandler = async ( request , runtime ) => runtime . IFluidHandleContext . resolveHandle ( request ) ; const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore ( dataObjectFactory , [ [ dataObjectFactory . type , Promise . resolve ( dataObjectFactory ) ] , ] , undefined , [ innerRequestHandler ] , runtimeOptions ) ; const logger = new TelemetryNullLogger ( ) ; let latestUploadedSummary ; let latestSummaryContext ; let latestAckedSummary ; let mainContainer ; let dataStoreA ; const createContainer = async ( factory ) : Promise < IContainer > => { return provider . createContainer ( factory ) ; } ; const getNewSummarizer = async ( factory , summaryVersion ? ) => { return loadSummarizer ( provider , factory , mainContainer . deltaManager . lastSequenceNumber , summaryVersion ) ; } ; /**
     * Callback that will be called by the document storage service whenever a summary is uploaded by the client.
     * Update the summary context to include the summary proposal and ack handle as per the latest ack for the
     * document.
     */ function uploadSummaryCb ( summaryTree , context ) { latestUploadedSummary = summaryTree ; latestSummaryContext = context ; const newSummaryContext = { ... context } ; if ( latestAckedSummary !== undefined ) { newSummaryContext . ackHandle = latestAckedSummary . summaryAck . contents . handle ; newSummaryContext . proposalHandle = latestAckedSummary . summaryOp . contents . handle ; } return newSummaryContext ; } param version function getContainerRuntimeFactory ( version ) { const containerRuntimeApi = getContainerRuntimeApi ( pkgVersion , version ) ; return new containerRuntimeApi . ContainerRuntimeFactoryWithDefaultDataStore ( dataObjectFactory , [ [ dataObjectFactory . type , Promise . resolve ( dataObjectFactory ) ] , ] , undefined , [ innerRequestHandler ] , runtimeOptions ) ; } beforeEach ( async ( ) => { provider = getTestObjectProvider ( ) ; ( provider as any ) . _documentServiceFactory = wrapDocumentServiceFactory ( provider . documentServiceFactory , uploadSummaryCb ) ; mainContainer = await createContainer ( runtimeFactory ) ; dataStoreA = await requestFluidObject < TestDataObject > ( mainContainer , "default" ) ; await provider . ensureSynchronized ( ) ; } ) ; afterEach ( ( ) => { latestAckedSummary = undefined ; latestSummaryContext = undefined ; latestUploadedSummary = undefined ; provider . reset ( ) ; } ) ; const tests = ( version1 , version2 ) => { const v1Str = version1 === 0 ? `N` : `N${ version1 }` ; const v2Str = version2 === 0 ? `N` : `N${ version2 }` ; returns async function getUnreferencedTimestamps ( summarizerClient ) { const summary = await submitAndAckSummary ( provider , summarizerClient , logger , true ) ; latestAckedSummary = summary . ackedSummary ; assert ( latestSummaryContext && latestSummaryContext . referenceSequenceNumber >= summary . summarySequenceNumber , `Did not get expected summary. Expected: ${ summary . summarySequenceNumber }. ` + `Actual: ${ latestSummaryContext ?. referenceSequenceNumber }.` ) ; assert ( latestUploadedSummary !== undefined , "Did not get a summary" ) ; const channelsTree = ( latestUploadedSummary . tree [ channelsTreeName ] as ISummaryTree ) ?. tree ; const nodeTimestamps = new Map ( ) ; for ( const [ id , summaryObject ] of Object . entries ( channelsTree ) ) { assert ( summaryObject . type === SummaryType . Tree , `Channel summary ${ id } is not a tree` ) ; const gcBlob = summaryObject . tree . gc ; assert ( gcBlob ?. type === SummaryType . Blob , `Data store ${ id } does not have GC blob` ) ; const gcSummaryDetails = JSON . parse ( gcBlob . content as string ) as IGarbageCollectionDetailsBase ; nodeTimestamps . set ( id , gcSummaryDetails . unrefTimestamp ) ; } return nodeTimestamps ; } /**
         * This test validates that the unreferenced timestamp in the summary generated by a container runtime can
         * be read by older / newer versions of the container runtime.
         */ it ( `runtime version ${ v2Str } validates unreferenced timestamp from summary by version ${ v1Str }` , async ( ) => { const summarizerClient1 = await getNewSummarizer ( getContainerRuntimeFactory ( version1 ) ) ; const dataStoreB = await dataObjectFactory . createInstance ( dataStoreA . containerRuntime ) ; dataStoreA . _root . set ( "dataStoreB" , dataStoreB . handle ) ; const timestamps1 = await getUnreferencedTimestamps ( summarizerClient1 ) ; const dsBTimestamp1 = timestamps1 . get ( dataStoreB . id ) ; assert ( dsBTimestamp1 === undefined , `new data store should not have unreferenced timestamp` ) ; dataStoreA . _root . delete ( "dataStoreB" ) ; const timestamps2 = await getUnreferencedTimestamps ( summarizerClient1 ) ; const dsBTimestamp2 = timestamps2 . get ( dataStoreB . id ) ; assert ( dsBTimestamp2 !== undefined , `new data store should have unreferenced timestamp` ) ; assert ( latestAckedSummary !== undefined , "Summary ack isn't available as expected" ) ; const summarizerClient2 = await getNewSummarizer ( getContainerRuntimeFactory ( version2 ) , latestAckedSummary . summaryAck . contents . handle ) ; const timestamps3 = await getUnreferencedTimestamps ( summarizerClient2 ) ; const dsBTimestamp3 = timestamps3 . get ( dataStoreB . id ) ; assert ( dsBTimestamp3 !== undefined , `new data store should still have unreferenced timestamp` ) ; assert . strictEqual ( dsBTimestamp3 , dsBTimestamp2 , "The unreferenced timestamp should not have changed" ) ; } ) . timeout ( 20000 ) ; } ; for ( const oldVersion of oldVersionNumbers ) { tests ( currentVersionNumber , oldVersion ) ; tests ( oldVersion , currentVersionNumber ) ; } } ) ;