Add unit tests for PluginManager

Reviewed By: mweststrate

Differential Revision: D34307076

fbshipit-source-id: 7cb6c808bea10d15627958e8fabc02b4563183d4
This commit is contained in:
Andrey Goncharov
2022-02-28 03:50:34 -08:00
committed by Facebook GitHub Bot
parent d96cf8127f
commit 9e4ac0aaa9
4 changed files with 258 additions and 27 deletions

View File

@@ -50,7 +50,7 @@ const isExecuteMessage = (message: object): message is ExecuteMessage =>
(message as ExecuteMessage).method === 'execute';
export class PluginManager {
private readonly serverAddOns = new Map<string, ServerAddOnManager>();
public readonly serverAddOns = new Map<string, ServerAddOnManager>();
constructor(private readonly flipperServer: FlipperServerForServerAddOn) {}

View File

@@ -0,0 +1,214 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {ServerAddOnStartDetails} from 'flipper-common';
import {loadServerAddOn} from '../loadServerAddOn';
import {PluginManager} from '../PluginManager';
import {ServerAddOnManager} from '../ServerAddManager';
import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection';
import {
createControlledPromise,
detailsBundled,
detailsInstalled,
initialOwner,
pluginName,
} from './utils';
jest.mock('../loadServerAddOn');
const loadServerAddOnMock = loadServerAddOn as jest.Mock;
describe('PluginManager', () => {
describe('server add-ons', () => {
const startServerAddOn = async (details: ServerAddOnStartDetails) => {
const addOnCleanupMock = jest.fn();
const addOnMock = jest.fn().mockImplementation(() => addOnCleanupMock);
loadServerAddOnMock.mockImplementation(() => ({
default: addOnMock,
}));
const flipperServer = {
connect: jest.fn(),
on: jest.fn(),
off: jest.fn(),
exec: jest.fn(),
close: jest.fn(),
emit: jest.fn(),
};
expect(loadServerAddOnMock).toBeCalledTimes(0);
expect(addOnMock).toBeCalledTimes(0);
expect(addOnCleanupMock).toBeCalledTimes(0);
const pluginManager = new PluginManager(flipperServer);
await pluginManager.startServerAddOn(pluginName, details, initialOwner);
expect(loadServerAddOnMock).toBeCalledTimes(1);
expect(loadServerAddOnMock).toBeCalledWith(pluginName, details);
expect(addOnMock).toBeCalledTimes(1);
expect(addOnMock).toBeCalledWith(
expect.any(ServerAddOnModuleToDesktopConnection),
{
flipperServer,
},
);
expect(pluginManager.serverAddOns.size).toBe(1);
const serverAddOn = pluginManager.serverAddOns.get(pluginName);
expect(serverAddOn).toBeInstanceOf(ServerAddOnManager);
expect(serverAddOn!.state.is('active')).toBeTruthy();
return {
addOnCleanupMock,
addOnMock,
flipperServer,
pluginManager,
};
};
describe.each([
['bundled', detailsBundled],
['installed', detailsInstalled],
])('%s', (_name, details) => {
test('stops the add-on when the initial owner is removed', async () => {
const {pluginManager, addOnCleanupMock} = await startServerAddOn(
details,
);
const controlledP = createControlledPromise<void>();
addOnCleanupMock.mockImplementation(() => controlledP.promise);
const stopPromise = pluginManager.stopServerAddOn(
pluginName,
initialOwner,
);
expect(pluginManager.serverAddOns.size).toBe(1);
controlledP.resolve();
await stopPromise;
expect(addOnCleanupMock).toBeCalledTimes(1);
expect(pluginManager.serverAddOns.size).toBe(0);
});
test('adds a new owner, stops the add-on when all owners are removed', async () => {
const {pluginManager, addOnCleanupMock} = await startServerAddOn(
details,
);
const newOwner = 'luke';
await pluginManager.startServerAddOn(pluginName, details, newOwner);
expect(pluginManager.serverAddOns.size).toBe(1);
await pluginManager.stopServerAddOn(pluginName, initialOwner);
expect(addOnCleanupMock).toBeCalledTimes(0);
expect(pluginManager.serverAddOns.size).toBe(1);
const serverAddOn = pluginManager.serverAddOns.get(pluginName)!;
expect(serverAddOn.state.is('active')).toBeTruthy();
await pluginManager.stopServerAddOn(pluginName, newOwner);
expect(addOnCleanupMock).toBeCalledTimes(1);
expect(pluginManager.serverAddOns.size).toBe(0);
await serverAddOn.state.wait('inactive');
});
test('concurrent calls to startServerAddOn start a single add-on', async () => {
const addOnCleanupMock = jest.fn();
const controlledP = createControlledPromise<() => void>();
const addOnMock = jest
.fn()
.mockImplementation(() => controlledP.promise);
loadServerAddOnMock.mockImplementation(() => ({
default: addOnMock,
}));
const flipperServer = {
connect: jest.fn(),
on: jest.fn(),
off: jest.fn(),
exec: jest.fn(),
close: jest.fn(),
emit: jest.fn(),
};
expect(loadServerAddOnMock).toBeCalledTimes(0);
expect(addOnMock).toBeCalledTimes(0);
expect(addOnCleanupMock).toBeCalledTimes(0);
const pluginManager = new PluginManager(flipperServer);
const startP1 = pluginManager.startServerAddOn(
pluginName,
details,
initialOwner,
);
expect(loadServerAddOnMock).toBeCalledTimes(1);
expect(loadServerAddOnMock).toBeCalledWith(pluginName, details);
expect(addOnMock).toBeCalledTimes(1);
expect(addOnMock).toBeCalledWith(
expect.any(ServerAddOnModuleToDesktopConnection),
{
flipperServer,
},
);
expect(pluginManager.serverAddOns.size).toBe(1);
const newOwner = 'luke';
const startP2 = pluginManager.startServerAddOn(
pluginName,
details,
newOwner,
);
expect(loadServerAddOnMock).toBeCalledTimes(1);
expect(addOnMock).toBeCalledTimes(1);
expect(pluginManager.serverAddOns.size).toBe(1);
controlledP.resolve(addOnCleanupMock);
await startP1;
await startP2;
expect(loadServerAddOnMock).toBeCalledTimes(1);
expect(addOnMock).toBeCalledTimes(1);
expect(pluginManager.serverAddOns.size).toBe(1);
const serverAddOn = pluginManager.serverAddOns.get(pluginName);
expect(serverAddOn).toBeInstanceOf(ServerAddOnManager);
expect(serverAddOn!.state.is('active')).toBeTruthy();
});
test('concurrent calls to stopServerAddOn stop add-on only once', async () => {
const {pluginManager, addOnCleanupMock} = await startServerAddOn(
details,
);
const controlledP = createControlledPromise<void>();
addOnCleanupMock.mockImplementation(() => controlledP.promise);
expect(addOnCleanupMock).toBeCalledTimes(0);
const stopP1 = pluginManager.stopServerAddOn(pluginName, initialOwner);
expect(addOnCleanupMock).toBeCalledTimes(1);
const stopP2 = pluginManager.stopServerAddOn(pluginName, initialOwner);
expect(addOnCleanupMock).toBeCalledTimes(1);
controlledP.resolve();
await stopP1;
await stopP2;
expect(addOnCleanupMock).toBeCalledTimes(1);
});
});
});
});

View File

@@ -11,34 +11,18 @@ import {ServerAddOnStartDetails} from 'flipper-common';
import {loadServerAddOn} from '../loadServerAddOn';
import {ServerAddOn} from '../ServerAddOn';
import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection';
import {
createControlledPromise,
detailsBundled,
detailsInstalled,
initialOwner,
pluginName,
} from './utils';
jest.mock('../loadServerAddOn');
const loadServerAddOnMock = loadServerAddOn as jest.Mock;
const createControlledPromise = () => {
let resolve!: () => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<void>((resolveP, rejectP) => {
resolve = resolveP;
reject = rejectP;
});
return {
promise,
resolve,
reject,
};
};
describe('ServerAddOn', () => {
const pluginName = 'lightSaber';
const initialOwner = 'yoda';
const detailsBundled: ServerAddOnStartDetails = {
isBundled: true,
};
const detailsInstalled: ServerAddOnStartDetails = {
path: '/dagobar/',
};
const startServerAddOn = async (details: ServerAddOnStartDetails) => {
const addOnCleanupMock = jest.fn();
const addOnMock = jest.fn().mockImplementation(() => addOnCleanupMock);
@@ -91,7 +75,7 @@ describe('ServerAddOn', () => {
test('stops the add-on when the initial owner is removed', async () => {
const {serverAddOn, addOnCleanupMock} = await startServerAddOn(details);
const controlledP = createControlledPromise();
const controlledP = createControlledPromise<void>();
addOnCleanupMock.mockImplementation(() => controlledP.promise);
const removeOwnerRes = serverAddOn.removeOwner(initialOwner);
@@ -109,7 +93,7 @@ describe('ServerAddOn', () => {
const newOwner = 'luke';
serverAddOn.addOwner(newOwner);
const controlledP = createControlledPromise();
const controlledP = createControlledPromise<void>();
addOnCleanupMock.mockImplementation(() => controlledP.promise);
const removeOwnerRes1 = serverAddOn.removeOwner(initialOwner);
@@ -138,7 +122,7 @@ describe('ServerAddOn', () => {
test('calls stop only once when removeOwner is called twice with the same owner', async () => {
const {serverAddOn, addOnCleanupMock} = await startServerAddOn(details);
const controlledP = createControlledPromise();
const controlledP = createControlledPromise<void>();
addOnCleanupMock.mockImplementation(() => controlledP.promise);
const removeOwnerRes1 = serverAddOn.removeOwner(initialOwner);

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {ServerAddOnStartDetails} from 'flipper-common';
export const pluginName = 'lightSaber';
export const initialOwner = 'yoda';
export const detailsBundled: ServerAddOnStartDetails = {
isBundled: true,
};
export const detailsInstalled: ServerAddOnStartDetails = {
path: '/dagobar/',
};
export const createControlledPromise = <T,>() => {
let resolve!: (...res: T extends void ? [] : [T]) => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((resolveP, rejectP) => {
resolve = resolveP as typeof resolve;
reject = rejectP;
});
return {
promise,
resolve,
reject,
};
};