diff --git a/src/redux/actions/taskflows.js b/src/redux/actions/taskflows.js index a3353bad..8ee91c15 100644 --- a/src/redux/actions/taskflows.js +++ b/src/redux/actions/taskflows.js @@ -63,10 +63,10 @@ export function updateTaskflowJobLog(taskflowId, jobId) { // ---------------------------------------------------------------------------- // STATUSES export function updateTaskflowJobStatus(taskflowId, jobId, status) { + if (status) { + return { type: UPDATE_TASKFLOW_JOB_STATUS, taskflowId, jobId, status }; + } return dispatch => { - if (status) { - return { type: UPDATE_TASKFLOW_JOB_STATUS, taskflowId, jobId, status }; - } const action = netActions.addNetworkCall(`taskflow_job_status_${jobId}`, 'Check job status'); client.getJobStatus(jobId) @@ -117,13 +117,9 @@ export function startTaskflow(id, payload, simulationStep, location) { dispatch(netActions.successNetworkCall(action.id, resp)); if (simulationStep) { - const data = Object.assign( - {}, - simulationStep.data, - { metadata: Object.assign( - {}, - simulationStep.data.metadata, - { taskflowId: id }) }); + const data = Object.assign({}, simulationStep.data, + { metadata: Object.assign({}, simulationStep.data.metadata, { taskflowId: id }), + }); dispatch(projActions.updateSimulationStep(simulationStep.id, simulationStep.step, data, location)); } }, @@ -195,7 +191,7 @@ export function fetchTaskflow(id) { }); } if (taskflow.meta.cluster) { - clusterActions.fetchClusters(); + dispatch(clusterActions.fetchClusters()); } } }, diff --git a/src/redux/reducers/taskflows.js b/src/redux/reducers/taskflows.js index e3c253d8..5e31e28b 100644 --- a/src/redux/reducers/taskflows.js +++ b/src/redux/reducers/taskflows.js @@ -1,13 +1,13 @@ import * as Actions from '../actions/taskflows'; -const initialState = { +export const initialState = { mapById: {}, taskflowMapByTaskId: {}, taskflowMapByJobId: {}, updateLogs: [], }; -const initialTaskflow = { +export const taskflowTemplate = { taskMapById: {}, log: [], actions: [], @@ -28,14 +28,14 @@ export default function taskflowsReducer(state = initialState, action) { } const taskflow = Object.assign( {}, - initialTaskflow, + taskflowTemplate, state.mapById[action.taskflow._id], { flow: action.taskflow, jobMapById, primaryJob: action.primaryJob, - - }); + } + ); const mapById = Object.assign( {}, state.mapById, diff --git a/test/karma.redux.js b/test/karma.redux.js index 4bd56eda..d7d28060 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -14,9 +14,9 @@ module.exports = function(config) { reporters: ['spec'], plugins: [ 'karma-jasmine', + 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-webpack', - 'karma-phantomjs-launcher' ], preprocessors: { 'tests.webpack.js': ['webpack'], diff --git a/test/redux/taskflows.js b/test/redux/taskflows.js new file mode 100644 index 00000000..ce102f96 --- /dev/null +++ b/test/redux/taskflows.js @@ -0,0 +1,244 @@ +import * as Actions from '../../src/redux/actions/taskflows'; +import taskflowsReducer, { initialState } from '../../src/redux/reducers/taskflows'; +import client from '../../src/network'; + +import taskflowState from '../sampleData/basicTaskflowState'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +import deepClone from 'mout/src/lang/deepClone'; +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +Object.freeze(initialState); + +describe('taskflow actions', () => { + const taskflowId = '574c9d900640fd6e133b4b57'; + const taskflow = deepClone(taskflowState.mapById[taskflowId]); + const task = Object.assign({}, taskflow.taskMapById['574c9f350640fd6e13b11e39']); + describe('simple actions', () => { + it('should clear update logs', (done) => { + const expectedAction = { type: Actions.CLEAR_UPDATE_LOG }; + expect(Actions.clearUpdateLog()) + .toDispatchActions(expectedAction, complete(done)); + + const givenState = deepClone(initialState); + givenState.updateLogs = ['a1', 'b2']; + expect(taskflowsReducer(givenState, expectedAction)) + .toEqual(initialState); + }); + + it('should update a task\'s status for in a taskflow', (done) => { + // console.log(task, taskflow.taskMapById['574c9f350640fd6e13b11e39']); + const newStatus = 'running'; + const expectedAction = { + type: Actions.UPDATE_TASKFLOW_TASK_STATUS, + taskflowId, + status: newStatus, + taskId: task._id, + }; + expect(Actions.updateTaskflowTaskStatus(taskflowId, task._id, newStatus)) + .toDispatchActions(expectedAction, complete(done)); + + const expectedTask = Object.assign({}, task); + expectedTask.status = newStatus; + expect(taskflowsReducer(taskflowState, expectedAction).mapById[taskflowId].taskMapById[task._id]) + .toEqual(expectedTask); + }); + + it('should add a taskflow', (done) => { + const expectedAction = { type: Actions.ADD_TASKFLOW, taskflow: taskflow.flow, primaryJob: 'pyfr_run' }; + expect(Actions.addTaskflow(taskflow.flow, 'pyfr_run')) + .toDispatchActions(expectedAction, complete(done)); + + const newState = taskflowsReducer(deepClone(initialState), expectedAction); + const expectedTaskflow = deepClone(taskflowState); + // we don't have these properties from a taskflow that's just been added + expectedTaskflow.mapById[taskflowId].taskMapById = {}; + expectedTaskflow.mapById[taskflowId].log = []; + expectedTaskflow.taskflowMapByTaskId = {}; + expectedTaskflow.taskflowMapByJobId = {}; + delete expectedTaskflow.mapById[taskflowId].allComplete; + delete expectedTaskflow.mapById[taskflowId].stepName; + delete expectedTaskflow.mapById[taskflowId].simulation; + expect(newState) + .toEqual(expectedTaskflow); + }); + + it('should attach a simulation to a taskflow', (done) => { + const expectedAction = { type: Actions.BIND_SIMULATION_TO_TASKFLOW, + taskflowId, simulationId: 'a1', stepName: 'visuzlization' }; + expect(Actions.attachSimulationToTaskflow('a1', taskflowId, 'visuzlization')) + .toDispatchActions(expectedAction, complete(done)); + + const newState = deepClone(taskflowState); + newState.mapById[taskflowId].simulation = expectedAction.simulationId; + newState.mapById[taskflowId].stepName = expectedAction.stepName; + expect(taskflowsReducer(taskflowState, expectedAction)) + .toEqual(newState); + }); + + it('should update a taskflow\'s status', (done) => { + const newStatus = 'running'; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_STATUS, id: taskflowId, status: newStatus }; + expect(Actions.updateTaskflowStatus(taskflowId, newStatus)) + .toDispatchActions(expectedAction, complete(done)); + + expect(taskflowsReducer(taskflowState, expectedAction).mapById[taskflowId].flow.status) + .toEqual(newStatus); + }); + + it('should update taskflow properties', (done) => { + const newMeta = { + actions: ['stop', 'terminate'], + allComplete: false, + outputDirectory: '/my/dir/wow', + primaryJob: 'some_new_primary_job', + }; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_METADATA, id: taskflowId, metadata: newMeta }; + expect(Actions.updateTaskflowMetadata(taskflowId, newMeta)) + .toDispatchActions(expectedAction, complete(done)); + + const newState = deepClone(taskflowState); + newState.mapById[taskflowId].allComplete = newMeta.allComplete; + newState.mapById[taskflowId].actions = newMeta.actions; + newState.mapById[taskflowId].outputDirectory = newMeta.outputDirectory; + newState.mapById[taskflowId].primaryJob = newMeta.primaryJob; + + expect(taskflowsReducer(taskflowState, expectedAction)) + .toEqual(newState); + }); + }); + +// ---------------------------------------------------------------------------- +// AYSYNCHRONUS ACTIONS +// ---------------------------------------------------------------------------- + + describe('async actions', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should update taskflow log', (done) => { + const log = [{ entry: 'created...' }, { entry: 'running...' }]; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_LOG, taskflowId, log }; + setSpy(client, 'getTaskflowLog', { log }); + expect(Actions.updateTaskflowLog(taskflowId)) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should update taskflow job log', (done) => { + const log = [{ entry: 'created...' }, { entry: 'running...' }]; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_JOB_LOG, taskflowId, + jobId: 'a1', log }; + setSpy(client, 'getJobLog', { log }); + expect(Actions.updateTaskflowJobLog(taskflowId, 'a1')) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should update taskflow job status', (done) => { + const newStatus = 'running'; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_JOB_STATUS, taskflowId, + jobId: 'a1', status: newStatus }; + expect(Actions.updateTaskflowJobStatus(taskflowId, 'a1', newStatus)) + .toDispatchActions(expectedAction, complete(done)); + + // can be called without status parameter, calls async if it is + setSpy(client, 'getJobStatus', { status: newStatus }); + expect(Actions.updateTaskflowJobStatus(taskflowId, 'a1')) + .toDispatchActions(expectedAction, complete(done)); + + expect(client.getJobStatus) + .toHaveBeenCalled(); + }); + + it('should start taskflow', (done) => { + setSpy(client, 'startTaskflow', ''); + // calls projects @updateSimulationStep which we're testing elsewhere + // let's just make sure it's calling startTaskflow + expect(Actions.startTaskflow(taskflowId, {}, 'Visuzlization')) + .toDispatchActions([], complete(done)); + + expect(client.startTaskflow) + .toHaveBeenCalled(); + }); + + it('shuold create a taskflow', (done) => { + const expectedActions = [ + { type: Actions.ADD_TASKFLOW, primaryJob: 'pyfr' }, + { type: Actions.BIND_SIMULATION_TO_TASKFLOW, taskflowId, simulationId: 'mySimStep', stepName: 'Visuzlization' }, + // also starts taskflow + ]; + setSpy(client, 'createTaskflow', taskflow.flow); + expect(Actions.createTaskflow('myFlow', 'pyfr', {}, { id: 'mySimStep', step: 'Visuzlization' })) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should fetch taskflow tasks', (done) => { + const tasks = [{ name: 'task1' }, { name: 'task2' }]; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_TASKS, taskflowId, tasks }; + setSpy(client, 'getTaskflowTasks', tasks); + expect(Actions.fetchTaskflowTasks(taskflowId)) + .toDispatchActions(expectedAction, complete(done)); + }); + + // big test, this dispatches a lot of actions + it('should fetch a taskflow', (done) => { + const clusters = [{ _id: 'a1' }, { _id: 'b2' }]; + const log = [{ entry: 'created...' }, { entry: 'running...' }]; + const flow = deepClone(taskflow.flow); + flow.meta.jobs = [{ _id: 'job1', status: 'running' }]; + const expectedActions = [ + { type: Actions.ADD_TASKFLOW, taskflow: flow }, + { type: Actions.UPDATE_TASKFLOW_JOB_STATUS, taskflowId, jobId: 'job1', status: 'running' }, + { type: Actions.UPDATE_TASKFLOW_JOB_LOG, taskflowId, jobId: 'job1', log }, + { type: 'UPDATE_CLUSTERS', clusters }, + ]; + setSpy(client, 'getTaskflow', flow); + setSpy(client, 'getJobLog', { log }); + setSpy(client, 'getJobStatus', { status: 'running' }); + setSpy(client, 'listClusters', clusters); + expect(Actions.fetchTaskflow(taskflowId)) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should update a taskflow from a simulation', (done) => { + const simulation = { + _id: 'mySimulationId', + steps: { + Visuzlization: { + metadata: { taskflowId }, + }, + }, + }; + const flow = deepClone(taskflow.flow); + const expectedActions = [ + { type: Actions.ADD_TASKFLOW, taskflow: flow }, + { type: Actions.BIND_SIMULATION_TO_TASKFLOW, taskflowId, simulationId: 'mySimulationId', stepName: 'Visuzlization' }, + ]; + setSpy(client, 'getTaskflow', flow); + expect(Actions.updateTaskflowFromSimulation(simulation)) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should delete a taskflow', (done) => { + const expectedAction = { type: Actions.DELETE_TASKFLOW, id: taskflowId }; + setSpy(client, 'deleteTaskflow', null); + expect(Actions.deleteTaskflow(taskflowId)) + .toDispatchActions(expectedAction, complete(done)); + + expect(client.deleteTaskflow) + .toHaveBeenCalled(); + }); + }); +}); diff --git a/test/sampleData/basicTaskflowState.js b/test/sampleData/basicTaskflowState.js new file mode 100644 index 00000000..6f2e1c31 --- /dev/null +++ b/test/sampleData/basicTaskflowState.js @@ -0,0 +1,299 @@ +/* eslint-disable */ +export default { + "mapById": { + "574c9d900640fd6e133b4b57": { + "taskMapById": { + "574c9f350640fd6e13b11e39": { + "_accessLevel": 2, + "_id": "574c9f350640fd6e13b11e39", + "_modelType": "tasks", + "created": "2016-05-30T20:14:45.187000+00:00", + "log": [], + "name": "hpccloud.taskflow.pyfr.setup_input", + "status": "complete", + "taskFlowId": "574c9d900640fd6e133b4b57" + }, + "574c9e460640fd6e13b11e32": { + "_accessLevel": 2, + "_id": "574c9e460640fd6e13b11e32", + "_modelType": "tasks", + "created": "2016-05-30T20:10:46.658000+00:00", + "log": [], + "name": "hpccloud.taskflow.pyfr.pyfr_terminate", + "status": "complete", + "taskFlowId": "574c9d900640fd6e133b4b57" + }, + "574c9d910640fd6e133b4b59": { + "_accessLevel": 2, + "_id": "574c9d910640fd6e133b4b59", + "_modelType": "tasks", + "created": "2016-05-30T20:07:45.398000+00:00", + "log": [ + { + "args": [], + "created": 1464638865.5593, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 143, + "module": "__init__", + "msecs": 559.30709838867, + "msg": "Cluster name my new node", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7804605.1511765, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.0911, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 65, + "module": "__init__", + "msecs": 91.130971908569, + "msg": "Using source ip: 123.201.24.01", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805136.97505, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.2976, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 107, + "module": "__init__", + "msecs": 297.59693145752, + "msg": "Created cluster: 574c9d920640fd6e133b4b60", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805343.4410095, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.3557, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 112, + "module": "__init__", + "msecs": 355.6981086731, + "msg": "Starting cluster.", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805401.5421867, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464639285.1466, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 148, + "module": "__init__", + "msecs": 146.5539932251, + "msg": "Cluster started.", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 8224192.3980713, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + } + ], + "name": "hpccloud.taskflow.utility.setup_cluster", + "status": "complete", + "taskFlowId": "574c9d900640fd6e133b4b57" + } + }, + "log": [ + { + "args": [], + "created": 1464638865.5391, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 142, + "module": "__init__", + "msecs": 539.0989780426, + "msg": "We are creating an EC2 cluster.", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7804584.9430561, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.2742, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 106, + "module": "__init__", + "msecs": 274.18994903564, + "msg": "Created cluster: 574c9d920640fd6e133b4b60", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805320.0340271, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + } + ], + "actions": [], + "simulation": "574c8aa00640fd3f1a3b379f", + "stepName": "Simulation", + "flow": { + "_accessLevel": 2, + "_id": "574c9d900640fd6e133b4b57", + "_modelType": "taskflows", + "activeTaskCount": 0, + "log": [ + { + "args": [], + "created": 1464638865.5391, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 142, + "module": "__init__", + "msecs": 539.0989780426, + "msg": "We are creating an EC2 cluster.", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7804584.9430561, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.2742, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 106, + "module": "__init__", + "msecs": 274.18994903564, + "msg": "Created cluster: 574c9d920640fd6e133b4b60", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805320.0340271, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + } + ], + "meta": { + "cluster": { + "_id": "574c9d920640fd6e133b4b60", + "config": { + "launch": { + "params": { + "extra_rules": [ + { + "cidr_ip": "123.201.24.01", + "from_port": 9000, + "proto": "tcp", + "to_port": 9000 + } + ], + "gpu": 0, + "master_instance_ami": "some-ami", + "master_instance_type": "t2.nano", + "node_instance_ami": "another-ami", + "node_instance_count": 1, + "node_instance_type": "t2.nano", + "source_cidr_ip": "123.201.24.01" + }, + "spec": "ec2" + }, + "scheduler": { + "type": "sge" + }, + "ssh": { + "key": "mysuperstrongsshkeyw0w", + "user": "someuser" + } + }, + "name": "my new node", + "profileId": "574c8a770640fd3f1a3b377a", + "status": "created", + "type": "ec2", + "userId": "574c841b0640fd3f1a3b3741" + } + }, + "status": "terminated", + "taskFlowClass": "hpccloud.taskflow.pyfr.PyFrTaskFlow" + }, + "jobMapById": { + + }, + "primaryJob": "pyfr_run", + "allComplete": true + } + }, + "taskflowMapByTaskId": { + "574c9f350640fd6e13b11e39": "574c9d900640fd6e133b4b57", + "574c9e460640fd6e13b11e32": "574c9d900640fd6e133b4b57", + "574c9d910640fd6e133b4b59": "574c9d900640fd6e133b4b57" + }, + "taskflowMapByJobId": { + + }, + "updateLogs": [ + + ] +} diff --git a/test/webpack.redux.js b/test/webpack.redux.js index b6bea835..3a344a78 100644 --- a/test/webpack.redux.js +++ b/test/webpack.redux.js @@ -15,10 +15,10 @@ var definePlugin = new webpack.DefinePlugin({ module.exports = { entry: { - 'tests.webpack.js': './test/tests.webpack.js' + 'tests.webpack.js': './test/tests.webpack.js', }, plugins: [ - definePlugin + definePlugin, ], module: { loaders: [