Skip to content

Commit

Permalink
Add test for legacy chunking (#12999)
Browse files Browse the repository at this point in the history
Related to ADO:2465

There is a plan to refactor some of the op processing code inside the
container runtime, along with the upcoming plan to use chunking to
bypass the 1MB payload size limit for compressed batches. This implies
few changes in the way we process incoming ops at the runtime layer and
a resurrection of the code which produces chunked ops.

But before that happens, we need to ensure that the new work is not
breaking backwards compatibility wrt to processing legacy chunked ops,
as these might already be serialized in older document snapshots
(Excluding instances of really old clients collaborating on the same doc
with new clients). This test will make sure that we don't break the
basic scenario of processing chunked ops.

Also adding a bit of test infra to allow for installing specific older
versions of fluid.
  • Loading branch information
andre4i authored Nov 23, 2022
1 parent 7b3f64a commit 699e8dc
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 17 deletions.
107 changes: 107 additions & 0 deletions packages/test/test-end-to-end-tests/src/test/legacyChunking.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert } from "assert";
import { SharedMap } from "@fluidframework/map";
import { requestFluidObject } from "@fluidframework/runtime-utils";
import {
ITestFluidObject,
ChannelFactoryRegistry,
ITestObjectProvider,
ITestContainerConfig,
DataObjectFactoryType,
TestFluidObjectFactory,
} from "@fluidframework/test-utils";
import { describeInstallVersions, getContainerRuntimeApi } from "@fluidframework/test-version-utils";
import { IContainer } from "@fluidframework/container-definitions";
import { FlushMode, IContainerRuntimeBase } from "@fluidframework/runtime-definitions";
import { IRequest } from "@fluidframework/core-interfaces";

const versionWithChunking = "0.56.0";

describeInstallVersions(
{
requestAbsoluteVersions: [versionWithChunking],
}
)(
"Legacy chunking",
(getTestObjectProvider) => {
let provider: ITestObjectProvider;
let oldMap: SharedMap;
let newMap: SharedMap;
beforeEach(() => {
provider = getTestObjectProvider();
});
afterEach(async () => provider.reset());

const innerRequestHandler = async (request: IRequest, runtime: IContainerRuntimeBase) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
runtime.IFluidHandleContext.resolveHandle(request);
const mapId = "map";
const registry: ChannelFactoryRegistry = [
[mapId, SharedMap.getFactory()],
];
const factory: TestFluidObjectFactory = new TestFluidObjectFactory(
registry,
"default",
);
const testContainerConfig: ITestContainerConfig = {
fluidDataObjectType: DataObjectFactoryType.Test,
registry,
};

const createOldContainer = async (): Promise<IContainer> => {
const oldContainerRuntimeFactoryWithDefaultDataStore =
getContainerRuntimeApi(versionWithChunking).ContainerRuntimeFactoryWithDefaultDataStore;
const oldRuntimeFactory =
new oldContainerRuntimeFactoryWithDefaultDataStore(
factory,
[
[factory.type, Promise.resolve(factory)],
],
undefined,
[innerRequestHandler],
{
// Chunking did not work with FlushMode.TurnBased,
// as it was breaking batching semantics. So we need
// to force the container to flush the ops as soon as
// they are produced.
flushMode: FlushMode.Immediate,
gcOptions: {
gcAllowed: true,
},
},
);

return provider.createContainer(oldRuntimeFactory);
};

const setupContainers = async () => {
const oldContainer = await createOldContainer();
const oldDataObject = await requestFluidObject<ITestFluidObject>(oldContainer, "default");
oldMap = await oldDataObject.getSharedObject<SharedMap>(mapId);

const containerOnLatest = await provider.loadTestContainer(testContainerConfig);
const newDataObject = await requestFluidObject<ITestFluidObject>(containerOnLatest, "default");
newMap = await newDataObject.getSharedObject<SharedMap>(mapId);

await provider.ensureSynchronized();
};

const generateStringOfSize = (sizeInBytes: number): string => new Array(sizeInBytes + 1).join("0");

it("If an old container sends a large chunked op, a new container is able to process it successfully", async () => {
await setupContainers();
// Ops larger than 16k will end up chunked in older versions of fluid
const messageSizeInBytes = 300 * 1024;
const value = generateStringOfSize(messageSizeInBytes);
oldMap.set("key1", value);
oldMap.set("key2", value);

await provider.ensureSynchronized();
assert.strictEqual(newMap.get("key1"), value, "Wrong value found in the new map");
assert.strictEqual(newMap.get("key2"), value, "Wrong value found in the new map");
});
});
151 changes: 151 additions & 0 deletions packages/test/test-version-utils/src/describeWithVersions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { getUnexpectedLogErrorException, ITestObjectProvider, TestObjectProvider } from "@fluidframework/test-utils";
import { driver, r11sEndpointName, tenantIndex } from "./compatOptions";
import { getVersionedTestObjectProvider } from "./compatUtils";
import { ITestObjectProviderOptions } from "./describeCompat";
import { pkgVersion } from "./packageVersion";
import { ensurePackageInstalled, InstalledPackage } from "./testApi";

/**
* Interface to hold the requested versions which should be installed
* prior to running the test suite. The properties are cumulative, as all
* versions deduced from all properties will be installed.
*/
export interface IRequestedFluidVersions {
/**
* Delta of versions to be installed with the current
* package version as the baseline.
*/
requestRelativeVersions?: number;
/**
* Array of specific versions to be installed
*/
requestAbsoluteVersions?: string[];
}

const installRequiredVersions = async (config: IRequestedFluidVersions) => {
const installPromises: Promise<InstalledPackage | undefined>[] = [];
if (config.requestAbsoluteVersions !== undefined) {
installPromises.push(
...config.requestAbsoluteVersions
.map(async (version) => ensurePackageInstalled(version, 0, /* force */ false)));
}

if (config.requestRelativeVersions !== undefined) {
installPromises.push(ensurePackageInstalled(pkgVersion, config.requestRelativeVersions, /* force */ false));
}

let hadErrors = false;
for (const promise of installPromises) {
try {
await promise;
} catch (e) {
console.error(e);
hadErrors = true;
}
}

if (hadErrors) {
throw new Error("Exceptions while installing package versions. Check STDERR");
}
};

const defaultTimeoutMs = 20000;
const defaultRequestedVersions: IRequestedFluidVersions = { requestRelativeVersions: -2 };

function createTestSuiteWithInstalledVersion(
tests: (this: Mocha.Suite, provider: () => ITestObjectProvider) => void,
requiredVersions: IRequestedFluidVersions = defaultRequestedVersions,
timeoutMs: number = defaultTimeoutMs,
) {
return function(this: Mocha.Suite) {
let defaultProvider: TestObjectProvider;
let resetAfterEach: boolean;
before(async function() {
this.timeout(Math.max(defaultTimeoutMs, timeoutMs));

await installRequiredVersions(requiredVersions);
defaultProvider = await getVersionedTestObjectProvider(
pkgVersion, // baseVersion
pkgVersion, // loaderVersion
{
type: driver,
version: pkgVersion,
config: {
r11s: { r11sEndpointName },
odsp: { tenantIndex },
},
}, // driverConfig
pkgVersion, // runtimeVersion
pkgVersion, // dataRuntimeVersion
);

Object.defineProperty(this, "__fluidTestProvider", { get: () => defaultProvider });
});

tests.bind(this)((options?: ITestObjectProviderOptions) => {
resetAfterEach = options?.resetAfterEach ?? true;
if (options?.syncSummarizer === true) {
defaultProvider.resetLoaderContainerTracker(true /* syncSummarizerClients */);
}

return defaultProvider;
});

afterEach(function(done: Mocha.Done) {
const logErrors = getUnexpectedLogErrorException(defaultProvider.logger);
// if the test failed for another reason
// then we don't need to check errors
// and fail the after each as well
if (this.currentTest?.state === "passed") {
done(logErrors);
} else {
done();
}

if (resetAfterEach) {
defaultProvider.reset();
}
});
};
}

type DescribeSuiteWithVersions =
(name: string,
tests: (
this: Mocha.Suite,
provider: (options?: ITestObjectProviderOptions) => ITestObjectProvider) => void
) => Mocha.Suite | void;

type DescribeWithVersions = DescribeSuiteWithVersions & Record<"skip" | "only", DescribeSuiteWithVersions>;

/**
* Creates a test suite which will priorly install a set of requested Fluid versions for the tests to use.
* The packages are installed before any test code runs, so it is guaranteed that the package is present
* when the test code is invoked, including the top level scope inside the `describeInstallVersions` block.
*
* If package installation fails for any of the requested versions, the test suite will not be created and
* the test run will fail.
*
* @param requestedVersions - See {@link IRequestedFluidVersions}.
* If unspecified, the test will install the last 2 versions.
* @param timeoutMs - the timeout for the tests in milliseconds, as package installation is time consuming.
* If unspecified, the timeout is 20000 ms.
* @returns A mocha test suite
*/
export function describeInstallVersions(
requestedVersions?: IRequestedFluidVersions,
timeoutMs?: number,
): DescribeWithVersions {
const d: DescribeWithVersions =
(name, tests) => describe(name, createTestSuiteWithInstalledVersion(tests, requestedVersions, timeoutMs));
d.skip =
(name, tests) => describe.skip(name, createTestSuiteWithInstalledVersion(tests, requestedVersions, timeoutMs));
d.only =
(name, tests) => describe.only(name, createTestSuiteWithInstalledVersion(tests, requestedVersions, timeoutMs));
return d;
}
31 changes: 16 additions & 15 deletions packages/test/test-version-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@
*/
export { mochaGlobalSetup } from "./compatConfig";
export {
getDataStoreFactory,
getVersionedTestObjectProvider,
ITestDataObject,
TestDataObjectType,
getDataStoreFactory,
getVersionedTestObjectProvider,
ITestDataObject,
TestDataObjectType,
} from "./compatUtils";
export { describeInstallVersions } from "./describeWithVersions";
export {
DescribeCompat,
DescribeCompatSuite,
describeFullCompat,
describeLoaderCompat,
describeNoCompat,
ITestObjectProviderOptions,
DescribeCompat,
DescribeCompatSuite,
describeFullCompat,
describeLoaderCompat,
describeNoCompat,
ITestObjectProviderOptions,
} from "./describeCompat";
export { ExpectedEvents, ExpectsTest, itExpects } from "./itExpects";
export {
ensurePackageInstalled,
getContainerRuntimeApi,
getDataRuntimeApi,
getDriverApi,
getLoaderApi,
ensurePackageInstalled,
getContainerRuntimeApi,
getDataRuntimeApi,
getDriverApi,
getLoaderApi,
} from "./testApi";
7 changes: 6 additions & 1 deletion packages/test/test-version-utils/src/testApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,13 @@ const packageList = [
"@fluidframework/routerlicious-driver",
];

export interface InstalledPackage {
version: string;
modulePath: string;
}

export const ensurePackageInstalled =
async (baseVersion: string, version: number | string, force: boolean) =>
async (baseVersion: string, version: number | string, force: boolean): Promise<InstalledPackage | undefined> =>
ensureInstalled(getRequestedRange(baseVersion, version), packageList, force);

// Current versions of the APIs
Expand Down
7 changes: 6 additions & 1 deletion packages/test/test-version-utils/src/versionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { existsSync, mkdirSync, rmdirSync, readdirSync, readFileSync, writeFileS
import { lock } from "proper-lockfile";
import * as semver from "semver";
import { pkgVersion } from "./packageVersion";
import { InstalledPackage } from "./testApi";

// Assuming this file is in dist\test, so go to ..\node_modules\.legacy as the install location
const baseModulePath = path.join(__dirname, "..", "node_modules", ".legacy");
Expand Down Expand Up @@ -149,7 +150,11 @@ async function ensureModulePath(version: string, modulePath: string) {
}
}

export async function ensureInstalled(requested: string, packageList: string[], force: boolean) {
export async function ensureInstalled(
requested: string,
packageList: string[],
force: boolean,
): Promise<InstalledPackage | undefined> {
if (requested === pkgVersion) { return; }
const version = resolveVersion(requested, false);
const modulePath = getModulePath(version);
Expand Down

0 comments on commit 699e8dc

Please sign in to comment.