Added test cases for flipper import and export

Summary: Adds tests for flipper import and export. Also fixed few edge cases which was discovered through tests. The edge case was that it didn't export device plugin states.

Reviewed By: danielbuechele

Differential Revision: D13828357

fbshipit-source-id: ddc4352b729ca7f05c5875f2f3fbdd2c7f4e2186
This commit is contained in:
Pritesh Nandgaonkar
2019-01-29 08:49:41 -08:00
committed by Facebook Github Bot
parent 5ef970e5b5
commit cbd70f573c
4 changed files with 475 additions and 46 deletions

View File

@@ -27,6 +27,11 @@ export type ClientQuery = {|
device_id: string, device_id: string,
|}; |};
export type ClientExport = {|
id: string,
query: ClientQuery,
|};
type ErrorType = {message: string, stacktrace: string, name: string}; type ErrorType = {message: string, stacktrace: string, name: string};
type RequestMetadata = {method: string, id: number, params: ?Object}; type RequestMetadata = {method: string, id: number, params: ?Object};
@@ -271,7 +276,7 @@ export default class Client extends EventEmitter {
} }
} }
toJSON() { toJSON(): ClientExport {
return {id: this.id, query: this.query}; return {id: this.id, query: this.query};
} }

View File

@@ -36,6 +36,13 @@ export type DeviceLogListener = (entry: DeviceLogEntry) => void;
export type DeviceType = 'emulator' | 'physical'; export type DeviceType = 'emulator' | 'physical';
export type DeviceExport = {|
os: string,
title: string,
deviceType: DeviceType,
serial: string,
|};
export type OS = 'iOS' | 'Android' | 'Windows'; export type OS = 'iOS' | 'Android' | 'Windows';
export default class BaseDevice { export default class BaseDevice {
@@ -67,7 +74,7 @@ export default class BaseDevice {
return os.toLowerCase() === this.os.toLowerCase(); return os.toLowerCase() === this.os.toLowerCase();
} }
toJSON() { toJSON(): DeviceExport {
return { return {
os: this.os, os: this.os,
title: this.title, title: this.title,

View File

@@ -0,0 +1,345 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import {default as BaseDevice} from '../../devices/BaseDevice';
import {processStore} from '../exportData';
import {IOSDevice} from '../../..';
import {FlipperDevicePlugin} from '../../plugin.js';
import type {Notification} from '../../plugin.js';
import type {ClientExport} from '../../Client.js';
class TestDevicePlugin extends FlipperDevicePlugin {
static id = 'TestDevicePlugin';
}
function generateNotifications(
id: string,
title: string,
message: string,
severity: 'warning' | 'error',
): Notification {
return {id, title, message, severity};
}
function generateClientIdentifier(device: BaseDevice, app: string): string {
const {os, deviceType, serial} = device;
const identifier = `${app}#${os}#${deviceType}#${serial}`;
return identifier;
}
function generateClientFromDevice(
device: BaseDevice,
app: string,
): ClientExport {
const {os, deviceType, serial} = device;
const identifier = generateClientIdentifier(device, app);
return {
id: identifier,
query: {app, os, device: deviceType, device_id: serial},
};
}
test('test generateClientFromDevice helper function', () => {
const device = new IOSDevice('serial', 'emulator', 'TestiPhone');
const client = generateClientFromDevice(device, 'app');
expect(client).toEqual({
id: 'app#iOS#emulator#serial',
query: {app: 'app', os: 'iOS', device: 'emulator', device_id: 'serial'},
});
});
test('test generateClientIdentifier helper function', () => {
const device = new IOSDevice('serial', 'emulator', 'TestiPhone');
const identifier = generateClientIdentifier(device, 'app');
expect(identifier).toEqual('app#iOS#emulator#serial');
});
test('test generateNotifications helper function', () => {
const notification = generateNotifications('id', 'title', 'msg', 'error');
expect(notification).toEqual({
id: 'id',
title: 'title',
message: 'msg',
severity: 'error',
});
});
test('test processStore function for empty state', () => {
const json = processStore([], null, {}, [], new Map());
expect(json).toBeNull();
});
test('test processStore function for an iOS device connected', () => {
const json = processStore(
[],
new IOSDevice('serial', 'emulator', 'TestiPhone'),
{},
[],
new Map(),
);
expect(json).toBeDefined();
// $FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {device, clients} = json;
expect(device).toBeDefined();
expect(clients).toEqual([]);
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {serial, deviceType, title, os} = device;
expect(serial).toEqual('serial');
expect(deviceType).toEqual('emulator');
expect(title).toEqual('TestiPhone');
expect(os).toEqual('iOS');
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {pluginStates, activeNotifications} = json.store;
expect(pluginStates).toEqual({});
expect(activeNotifications).toEqual([]);
});
test('test processStore function for an iOS device connected with client plugin data', () => {
const device = new IOSDevice('serial', 'emulator', 'TestiPhone');
const clientIdentifier = generateClientIdentifier(device, 'testapp');
const json = processStore(
[],
device,
{[clientIdentifier]: {msg: 'Test plugin'}},
[generateClientFromDevice(device, 'testapp')],
new Map(),
);
expect(json).toBeDefined();
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {pluginStates} = json.store;
let expectedPluginState = {
[clientIdentifier]: {msg: 'Test plugin'},
};
expect(pluginStates).toEqual(expectedPluginState);
});
test('test processStore function to have only the client for the selected device', () => {
const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone');
const unselectedDevice = new IOSDevice(
'identifier',
'emulator',
'TestiPhone',
);
const unselectedDeviceClientIdentifier = generateClientIdentifier(
unselectedDevice,
'testapp',
);
const selectedDeviceClientIdentifier = generateClientIdentifier(
selectedDevice,
'testapp',
);
const selectedDeviceClient = generateClientFromDevice(
selectedDevice,
'testapp',
);
const json = processStore(
[],
selectedDevice,
{
[unselectedDeviceClientIdentifier + '#testapp']: {
msg: 'Test plugin unselected device',
},
[selectedDeviceClientIdentifier + '#testapp']: {
msg: 'Test plugin selected device',
},
},
[
selectedDeviceClient,
generateClientFromDevice(unselectedDevice, 'testapp'),
],
new Map(),
);
expect(json).toBeDefined();
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already added
const {clients} = json;
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already added
const {pluginStates} = json.store;
let expectedPluginState = {
[selectedDeviceClientIdentifier + '#testapp']: {
msg: 'Test plugin selected device',
},
};
expect(clients).toEqual([selectedDeviceClient]);
expect(pluginStates).toEqual(expectedPluginState);
});
test('test processStore function to have multiple clients for the selected device', () => {
const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone');
const clientIdentifierApp1 = generateClientIdentifier(
selectedDevice,
'testapp1',
);
const clientIdentifierApp2 = generateClientIdentifier(
selectedDevice,
'testapp2',
);
const client1 = generateClientFromDevice(selectedDevice, 'testapp1');
const client2 = generateClientFromDevice(selectedDevice, 'testapp2');
const json = processStore(
[],
selectedDevice,
{
[clientIdentifierApp1 + '#testapp1']: {
msg: 'Test plugin App1',
},
[clientIdentifierApp2 + '#testapp2']: {
msg: 'Test plugin App2',
},
},
[
generateClientFromDevice(selectedDevice, 'testapp1'),
generateClientFromDevice(selectedDevice, 'testapp2'),
],
new Map(),
);
expect(json).toBeDefined();
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already added
const {clients} = json;
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already added
const {pluginStates} = json.store;
let expectedPluginState = {
[clientIdentifierApp1 + '#testapp1']: {
msg: 'Test plugin App1',
},
[clientIdentifierApp2 + '#testapp2']: {
msg: 'Test plugin App2',
},
};
expect(clients).toEqual([client1, client2]);
expect(pluginStates).toEqual(expectedPluginState);
});
test('test processStore function for device plugin state and no clients', () => {
// Test case to verify that device plugin data is exported even if there are no clients
const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone');
const json = processStore(
[],
selectedDevice,
{
'serial#TestDevicePlugin': {
msg: 'Test Device plugin',
},
},
[],
new Map([['TestDevicePlugin', TestDevicePlugin]]),
);
expect(json).toBeDefined();
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {pluginStates} = json.store;
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {clients} = json;
let expectedPluginState = {
'serial#TestDevicePlugin': {msg: 'Test Device plugin'},
};
expect(pluginStates).toEqual(expectedPluginState);
expect(clients).toEqual([]);
});
test('test processStore function for unselected device plugin state and no clients', () => {
// Test case to verify that device plugin data is exported even if there are no clients
const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone');
const json = processStore(
[],
selectedDevice,
{
'unselectedDeviceIdentifier#TestDevicePlugin': {
msg: 'Test Device plugin',
},
},
[],
new Map([['TestDevicePlugin', TestDevicePlugin]]),
);
expect(json).toBeDefined();
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {pluginStates} = json.store;
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {clients} = json;
expect(pluginStates).toEqual({});
expect(clients).toEqual([]);
});
test('test processStore function for notifications for selected device', () => {
// Test case to verify that device plugin data is exported even if there are no clients
const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone');
const client = generateClientFromDevice(selectedDevice, 'testapp1');
const notification = generateNotifications(
'notificationID',
'title',
'Notification Message',
'warning',
);
const activeNotification = {
pluginId: 'TestNotification',
notification,
client: client.id,
};
const json = processStore(
[activeNotification],
selectedDevice,
{},
[client],
new Map([['TestDevicePlugin', TestDevicePlugin]]),
);
expect(json).toBeDefined();
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {pluginStates} = json.store;
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {clients} = json;
expect(pluginStates).toEqual({});
expect(clients).toEqual([client]);
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {activeNotifications} = json.store;
expect(activeNotifications).toEqual([activeNotification]);
});
test('test processStore function for notifications for unselected device', () => {
// Test case to verify that device plugin data is exported even if there are no clients
const selectedDevice = new IOSDevice('serial', 'emulator', 'TestiPhone');
const unselectedDevice = new IOSDevice(
'identifier',
'emulator',
'TestiPhone',
);
const client = generateClientFromDevice(selectedDevice, 'testapp1');
const unselectedclient = generateClientFromDevice(
unselectedDevice,
'testapp1',
);
const notification = generateNotifications(
'notificationID',
'title',
'Notification Message',
'warning',
);
const activeNotification = {
pluginId: 'TestNotification',
notification,
client: unselectedclient.id,
};
const json = processStore(
[activeNotification],
selectedDevice,
{},
[client, unselectedclient],
new Map(),
);
expect(json).toBeDefined();
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {pluginStates} = json.store;
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {clients} = json;
expect(pluginStates).toEqual({});
expect(clients).toEqual([client]);
//$FlowFixMe Flow doesn't that its a test and the assertion for null is already done
const {activeNotifications} = json.store;
expect(activeNotifications).toEqual([]);
});

View File

@@ -4,6 +4,15 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
* @format * @format
*/ */
import type {Store} from '../reducers';
import type {DeviceExport} from '../devices/BaseDevice';
import type {State as PluginStates} from '../reducers/pluginStates';
import type {PluginNotification} from '../reducers/notifications.js';
import type {ClientExport} from '../Client.js';
import type {State as PluginStatesState} from '../reducers/pluginStates';
import {FlipperDevicePlugin} from '../plugin.js';
import {default as BaseDevice} from '../devices/BaseDevice';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
@@ -14,54 +23,117 @@ const exportFilePath = path.join(
'FlipperExport.json', 'FlipperExport.json',
); );
export const exportStoreToFile = (data: Store): Promise<void> => { export type ExportType = {|
const state = data.getState(); fileVersion: '1.0.0',
const json = { clients: Array<ClientExport>,
fileVersion: '1.0.0', device: ?DeviceExport,
device: {}, store: {
clients: [], pluginStates: PluginStates,
store: { activeNotifications: Array<PluginNotification>,
pluginStates: {}, },
activeNotifications: [], |};
},
};
const device = state.connections.selectedDevice; export function processClients(
clients: Array<ClientExport>,
serial: string,
): Array<ClientExport> {
return clients.filter(client => client.query.device_id === serial);
}
export function processPluginStates(
clients: Array<ClientExport>,
serial: string,
allPluginStates: PluginStatesState,
devicePlugins: Map<string, Class<FlipperDevicePlugin<>>>,
): PluginStatesState {
let pluginStates = {};
for (let key in allPluginStates) {
let keyArray = key.split('#');
const pluginName = keyArray.pop();
const filteredClients = clients.filter(client => {
// Remove the last entry related to plugin
return client.id.includes(keyArray.join('#'));
});
if (
filteredClients.length > 0 ||
(devicePlugins.has(pluginName) && serial === keyArray[0])
) {
// There need not be any client for device Plugins
pluginStates = {...pluginStates, [key]: allPluginStates[key]};
}
}
return pluginStates;
}
export function processNotificationStates(
clients: Array<ClientExport>,
serial: string,
allActiveNotifications: Array<PluginNotification>,
devicePlugins: Map<string, Class<FlipperDevicePlugin<>>>,
): Array<PluginNotification> {
let activeNotifications = allActiveNotifications.filter(notif => {
const filteredClients = clients.filter(
client => (notif.client ? client.id.includes(notif.client) : false),
);
return (
filteredClients.length > 0 ||
(devicePlugins.has(notif.pluginId) && serial === notif.client)
); // There need not be any client for device Plugins
});
return activeNotifications;
}
export const processStore = (
activeNotifications: Array<PluginNotification>,
device: ?BaseDevice,
pluginStates: PluginStatesState,
clients: Array<ClientExport>,
devicePlugins: Map<string, Class<FlipperDevicePlugin<>>>,
): ?ExportType => {
if (device) { if (device) {
const {serial} = device; const {serial} = device;
json.device = device.toJSON(); const processedClients = processClients(clients, serial);
const clients = state.connections.clients let processedPluginStates = processPluginStates(
.filter(client => { processedClients,
return client.query.device_id === serial; serial,
}) pluginStates,
.map(client => { devicePlugins,
return client.toJSON(); );
}); const processedActiveNotifications = processNotificationStates(
processedClients,
serial,
activeNotifications,
devicePlugins,
);
return {
fileVersion: '1.0.0',
clients: processedClients,
device: device.toJSON(),
store: {
pluginStates: processedPluginStates,
activeNotifications: processedActiveNotifications,
},
};
}
return null;
};
json.clients = clients; export const exportStoreToFile = (data: Store): Promise<void> => {
const state = data.getState();
const allPluginStates = state.pluginStates; const {activeNotifications} = state.notifications;
let pluginStates = {}; const {selectedDevice, clients} = state.connections;
for (let key in allPluginStates) { const {pluginStates} = state;
const filteredClients = clients.filter(client => { const {devicePlugins} = state.plugins;
let keyArray = key.split('#'); // TODO: T39612653 Make Client mockable. Currently rsocket logic is tightly coupled.
keyArray.pop(); // Remove the last entry related to plugin // Not passing the entire state as currently Client is not mockable.
return client.id.includes(keyArray.join('#')); const json = processStore(
}); activeNotifications,
if (filteredClients.length > 0) { selectedDevice,
pluginStates = {...pluginStates, [key]: allPluginStates[key]}; pluginStates,
json.store.pluginStates = pluginStates; clients.map(client => client.toJSON()),
} devicePlugins,
} );
if (json) {
const allActiveNotifications = state.notifications.activeNotifications;
let activeNotifications = allActiveNotifications.filter(notif => {
const filteredClients = clients.filter(client =>
client.id.includes(notif.client),
);
return filteredClients.length > 0;
});
json.store.activeNotifications = activeNotifications;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(exportFilePath, JSON.stringify(json), err => { fs.writeFile(exportFilePath, JSON.stringify(json), err => {
if (err) { if (err) {