Add unit tests for PluginManager
Reviewed By: mweststrate Differential Revision: D34307076 fbshipit-source-id: 7cb6c808bea10d15627958e8fabc02b4563183d4
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d96cf8127f
commit
9e4ac0aaa9
@@ -50,7 +50,7 @@ const isExecuteMessage = (message: object): message is ExecuteMessage =>
|
|||||||
(message as ExecuteMessage).method === 'execute';
|
(message as ExecuteMessage).method === 'execute';
|
||||||
|
|
||||||
export class PluginManager {
|
export class PluginManager {
|
||||||
private readonly serverAddOns = new Map<string, ServerAddOnManager>();
|
public readonly serverAddOns = new Map<string, ServerAddOnManager>();
|
||||||
|
|
||||||
constructor(private readonly flipperServer: FlipperServerForServerAddOn) {}
|
constructor(private readonly flipperServer: FlipperServerForServerAddOn) {}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,34 +11,18 @@ import {ServerAddOnStartDetails} from 'flipper-common';
|
|||||||
import {loadServerAddOn} from '../loadServerAddOn';
|
import {loadServerAddOn} from '../loadServerAddOn';
|
||||||
import {ServerAddOn} from '../ServerAddOn';
|
import {ServerAddOn} from '../ServerAddOn';
|
||||||
import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection';
|
import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection';
|
||||||
|
import {
|
||||||
|
createControlledPromise,
|
||||||
|
detailsBundled,
|
||||||
|
detailsInstalled,
|
||||||
|
initialOwner,
|
||||||
|
pluginName,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
jest.mock('../loadServerAddOn');
|
jest.mock('../loadServerAddOn');
|
||||||
const loadServerAddOnMock = loadServerAddOn as jest.Mock;
|
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', () => {
|
describe('ServerAddOn', () => {
|
||||||
const pluginName = 'lightSaber';
|
|
||||||
const initialOwner = 'yoda';
|
|
||||||
const detailsBundled: ServerAddOnStartDetails = {
|
|
||||||
isBundled: true,
|
|
||||||
};
|
|
||||||
const detailsInstalled: ServerAddOnStartDetails = {
|
|
||||||
path: '/dagobar/',
|
|
||||||
};
|
|
||||||
|
|
||||||
const startServerAddOn = async (details: ServerAddOnStartDetails) => {
|
const startServerAddOn = async (details: ServerAddOnStartDetails) => {
|
||||||
const addOnCleanupMock = jest.fn();
|
const addOnCleanupMock = jest.fn();
|
||||||
const addOnMock = jest.fn().mockImplementation(() => addOnCleanupMock);
|
const addOnMock = jest.fn().mockImplementation(() => addOnCleanupMock);
|
||||||
@@ -91,7 +75,7 @@ describe('ServerAddOn', () => {
|
|||||||
test('stops the add-on when the initial owner is removed', async () => {
|
test('stops the add-on when the initial owner is removed', async () => {
|
||||||
const {serverAddOn, addOnCleanupMock} = await startServerAddOn(details);
|
const {serverAddOn, addOnCleanupMock} = await startServerAddOn(details);
|
||||||
|
|
||||||
const controlledP = createControlledPromise();
|
const controlledP = createControlledPromise<void>();
|
||||||
addOnCleanupMock.mockImplementation(() => controlledP.promise);
|
addOnCleanupMock.mockImplementation(() => controlledP.promise);
|
||||||
|
|
||||||
const removeOwnerRes = serverAddOn.removeOwner(initialOwner);
|
const removeOwnerRes = serverAddOn.removeOwner(initialOwner);
|
||||||
@@ -109,7 +93,7 @@ describe('ServerAddOn', () => {
|
|||||||
const newOwner = 'luke';
|
const newOwner = 'luke';
|
||||||
serverAddOn.addOwner(newOwner);
|
serverAddOn.addOwner(newOwner);
|
||||||
|
|
||||||
const controlledP = createControlledPromise();
|
const controlledP = createControlledPromise<void>();
|
||||||
addOnCleanupMock.mockImplementation(() => controlledP.promise);
|
addOnCleanupMock.mockImplementation(() => controlledP.promise);
|
||||||
|
|
||||||
const removeOwnerRes1 = serverAddOn.removeOwner(initialOwner);
|
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 () => {
|
test('calls stop only once when removeOwner is called twice with the same owner', async () => {
|
||||||
const {serverAddOn, addOnCleanupMock} = await startServerAddOn(details);
|
const {serverAddOn, addOnCleanupMock} = await startServerAddOn(details);
|
||||||
|
|
||||||
const controlledP = createControlledPromise();
|
const controlledP = createControlledPromise<void>();
|
||||||
addOnCleanupMock.mockImplementation(() => controlledP.promise);
|
addOnCleanupMock.mockImplementation(() => controlledP.promise);
|
||||||
|
|
||||||
const removeOwnerRes1 = serverAddOn.removeOwner(initialOwner);
|
const removeOwnerRes1 = serverAddOn.removeOwner(initialOwner);
|
||||||
|
|||||||
33
desktop/flipper-server-core/src/plugins/__tests__/utils.tsx
Normal file
33
desktop/flipper-server-core/src/plugins/__tests__/utils.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user