workflow job elapsed timer

* Now with tests for elapsed timer feature.
This commit is contained in:
Chris Meyers
2017-02-01 15:45:28 -05:00
parent 6bc5fb5439
commit f62b6ca014
9 changed files with 508 additions and 8 deletions

View File

@@ -0,0 +1,63 @@
{
"id": 109,
"type": "workflow_job",
"url": "/api/v1/workflow_jobs/109/",
"related": {
"created_by": "/api/v1/users/1/",
"unified_job_template": "/api/v1/workflow_job_templates/27/",
"workflow_job_template": "/api/v1/workflow_job_templates/27/",
"notifications": "/api/v1/workflow_jobs/109/notifications/",
"workflow_nodes": "/api/v1/workflow_jobs/109/workflow_nodes/",
"labels": "/api/v1/workflow_jobs/109/labels/",
"activity_stream": "/api/v1/workflow_jobs/109/activity_stream/",
"relaunch": "/api/v1/workflow_jobs/109/relaunch/",
"cancel": "/api/v1/workflow_jobs/109/cancel/"
},
"summary_fields": {
"workflow_job_template": {
"id": 27,
"name": "workflow timer",
"description": ""
},
"unified_job_template": {
"id": 27,
"name": "workflow timer",
"description": "",
"unified_job_type": "workflow_job"
},
"created_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"user_capabilities": {
"start": true,
"delete": true
},
"labels": {
"count": 0,
"results": []
}
},
"created": "2017-02-01T14:56:47.416Z",
"modified": "2017-02-01T14:57:14.189Z",
"name": "workflow timer",
"description": "",
"unified_job_template": 27,
"launch_type": "manual",
"status": "successful",
"failed": false,
"started": "2017-02-01T14:56:47.754897Z",
"finished": "2017-02-01T14:57:14.182780Z",
"elapsed": 26.428,
"job_args": "",
"job_cwd": "",
"job_env": {},
"job_explanation": "",
"result_stdout": "stdout capture is missing",
"execution_node": "",
"result_traceback": "",
"workflow_job_template": 27,
"extra_vars": "{}"
}

View File

@@ -0,0 +1,203 @@
{
"name": "Workflow Job Detail",
"description": "# Retrieve Workflow Job:\n\nMake GET request to this resource to retrieve a single workflow job\nrecord containing the following fields:\n\n* `id`: Database ID for this workflow job. (integer)\n* `type`: Data type for this workflow job. (choice)\n* `url`: URL for this workflow job. (string)\n* `related`: Data structure with URLs of related resources. (object)\n* `summary_fields`: Data structure with name/description for related resources. (object)\n* `created`: Timestamp when this workflow job was created. (datetime)\n* `modified`: Timestamp when this workflow job was last modified. (datetime)\n* `name`: Name of this workflow job. (string)\n* `description`: Optional description of this workflow job. (string)\n* `unified_job_template`: (field)\n* `launch_type`: (choice)\n - `manual`: Manual\n - `relaunch`: Relaunch\n - `callback`: Callback\n - `scheduled`: Scheduled\n - `dependency`: Dependency\n - `workflow`: Workflow\n - `sync`: Sync\n* `status`: (choice)\n - `new`: New\n - `pending`: Pending\n - `waiting`: Waiting\n - `running`: Running\n - `successful`: Successful\n - `failed`: Failed\n - `error`: Error\n - `canceled`: Canceled\n* `failed`: (boolean)\n* `started`: The date and time the job was queued for starting. (datetime)\n* `finished`: The date and time the job finished execution. (datetime)\n* `elapsed`: Elapsed time in seconds that the job ran. (decimal)\n* `job_args`: (string)\n* `job_cwd`: (string)\n* `job_env`: (field)\n* `job_explanation`: A status field to indicate the state of the job if it wasn't able to run and capture stdout (string)\n* `result_stdout`: (field)\n* `execution_node`: The Tower node the job executed on. (string)\n* `result_traceback`: (string)\n* `workflow_job_template`: (field)\n* `extra_vars`: (string)\n\n\n\n# Delete Workflow Job:\n\nMake a DELETE request to this resource to delete this workflow job.\n\n\n\n\n\n\n\n\n\n\n\n> _New in Ansible Tower 3.1.0_",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json"
],
"actions": {
"GET": {
"id": {
"type": "integer",
"label": "ID",
"help_text": "Database ID for this workflow job."
},
"type": {
"type": "choice",
"label": "Type",
"help_text": "Data type for this workflow job.",
"choices": [
[
"workflow_job",
"Workflow Job"
]
]
},
"url": {
"type": "string",
"label": "Url",
"help_text": "URL for this workflow job."
},
"related": {
"type": "object",
"label": "Related",
"help_text": "Data structure with URLs of related resources."
},
"summary_fields": {
"type": "object",
"label": "Summary fields",
"help_text": "Data structure with name/description for related resources."
},
"created": {
"type": "datetime",
"label": "Created",
"help_text": "Timestamp when this workflow job was created."
},
"modified": {
"type": "datetime",
"label": "Modified",
"help_text": "Timestamp when this workflow job was last modified."
},
"name": {
"type": "string",
"label": "Name",
"help_text": "Name of this workflow job."
},
"description": {
"type": "string",
"label": "Description",
"help_text": "Optional description of this workflow job."
},
"unified_job_template": {
"type": "field",
"label": "unified job template"
},
"launch_type": {
"type": "choice",
"label": "Launch type",
"choices": [
[
"manual",
"Manual"
],
[
"relaunch",
"Relaunch"
],
[
"callback",
"Callback"
],
[
"scheduled",
"Scheduled"
],
[
"dependency",
"Dependency"
],
[
"workflow",
"Workflow"
],
[
"sync",
"Sync"
]
]
},
"status": {
"type": "choice",
"label": "Status",
"choices": [
[
"new",
"New"
],
[
"pending",
"Pending"
],
[
"waiting",
"Waiting"
],
[
"running",
"Running"
],
[
"successful",
"Successful"
],
[
"failed",
"Failed"
],
[
"error",
"Error"
],
[
"canceled",
"Canceled"
]
]
},
"failed": {
"type": "boolean",
"label": "Failed"
},
"started": {
"type": "datetime",
"label": "Started",
"help_text": "The date and time the job was queued for starting."
},
"finished": {
"type": "datetime",
"label": "Finished",
"help_text": "The date and time the job finished execution."
},
"elapsed": {
"type": "decimal",
"label": "Elapsed",
"help_text": "Elapsed time in seconds that the job ran."
},
"job_args": {
"type": "string",
"label": "Job args"
},
"job_cwd": {
"type": "string",
"label": "Job cwd"
},
"job_env": {
"type": "field",
"label": "job_env"
},
"job_explanation": {
"type": "string",
"label": "Job explanation",
"help_text": "A status field to indicate the state of the job if it wasn't able to run and capture stdout"
},
"result_stdout": {
"type": "field",
"label": "Result stdout"
},
"execution_node": {
"type": "string",
"label": "Execution node",
"help_text": "The Tower node the job executed on."
},
"result_traceback": {
"type": "string",
"label": "Result traceback"
},
"workflow_job_template": {
"type": "field",
"label": "Workflow job template"
},
"extra_vars": {
"type": "string",
"label": "Extra vars"
}
}
},
"added_in_version": "3.1.0",
"types": [
"workflow_job"
]
}

View File

@@ -0,0 +1,138 @@
'use strict';
import moment from 'moment';
import workflow_job_options_json from 'json!./data/workflow_job_options.json';
import workflow_job_json from 'json!./data/workflow_job.json';
describe('Controller: workflowResults', () => {
let $controller;
let workflowResults;
let $rootScope;
let ParseVariableString;
let workflowResultsService;
let $interval;
let treeData = {
data: {
children: []
}
};
beforeEach(angular.mock.module('VariablesHelper'));
beforeEach(angular.mock.module('workflowResults', ($provide) => {
['PromptDialog', 'Prompt', 'Wait', 'Rest', '$state', 'ProcessErrors',
'InitiatePlaybookRun', 'jobLabels', 'workflowNodes', 'count',
].forEach((item) => {
$provide.value(item, {});
});
$provide.value('$stateExtender', { addState: jasmine.createSpy('addState'), });
$provide.value('moment', moment);
$provide.value('workflowData', workflow_job_json);
$provide.value('workflowDataOptions', workflow_job_options_json);
$provide.value('ParseTypeChange', function() {});
$provide.value('i18n', { '_': (a) => { return a; } });
$provide.provider('$stateProvider', { '$get': function() { return function() {} } });
$provide.service('WorkflowService', function($q) {
return {
buildTree: function() {
var deferred = $q.defer();
deferred.resolve(treeData);
return deferred.promise;
}
}
});
}));
beforeEach(angular.mock.inject(function(_$controller_, _$rootScope_, _ParseVariableString_, _workflowResultsService_, _$interval_){
$controller = _$controller_;
$rootScope = _$rootScope_;
ParseVariableString = _ParseVariableString_;
workflowResultsService = _workflowResultsService_;
$interval = _$interval_;
}));
describe('elapsed timer', () => {
let scope;
beforeEach(() => {
scope = $rootScope.$new();
spyOn(workflowResultsService, 'createOneSecondTimer').and.callThrough();
spyOn(workflowResultsService, 'destroyTimer').and.callThrough();
});
function jobWaitingWorkflowResultsControllerFixture(started, status) {
workflow_job_json.started = started;
workflow_job_json.status = status;
workflowResults = $controller('workflowResultsController', {
$scope: scope,
$rootScope: $rootScope,
});
}
describe('init()', () => {
describe('job running', () => {
beforeEach(() => {
jobWaitingWorkflowResultsControllerFixture(moment(), 'running');
});
// Note: Ensuring the outside service method is called to create a timer may
// be overkill. Especially since we validate the side effect in the next test.
it('should call to start timer on load when job is already running', () => {
expect(workflowResultsService.createOneSecondTimer).toHaveBeenCalled();
expect(workflowResultsService.createOneSecondTimer.calls.argsFor(0)[0]).toBe(workflow_job_json.started);
});
it('should set update scope var with elapsed time', () => {
$interval.flush(10 * 1000);
// TODO: mock moment() so when we fast-forward time with $interval
// the system clocks seems to fast forward too.
//expect(scope.workflow.elapsed).toBe(10);
});
it('should call to destroy timer on destroy', () => {
scope.$destroy();
expect(workflowResultsService.destroyTimer).toHaveBeenCalled();
expect(workflowResultsService.destroyTimer.calls.argsFor(0)[0]).not.toBe(null);
});
});
describe('job is not running', () => {
beforeEach(() => {
jobWaitingWorkflowResultsControllerFixture(null, 'waiting');
});
it('should not start elapsed timer', () => {
expect(workflowResultsService.createOneSecondTimer).not.toHaveBeenCalled();
});
});
});
describe('job transitions to running', () => {
beforeEach(() => {
jobWaitingWorkflowResultsControllerFixture(null, 'waiting');
$rootScope.$broadcast('ws-jobs', { unified_job_id: workflow_job_json.id, status: "running" });
});
it('should start elapsed timer', () => {
expect(scope.workflow.status).toBe("running");
expect(workflowResultsService.createOneSecondTimer).toHaveBeenCalled();
});
});
describe('job finished', () => {
beforeEach(() => {
jobWaitingWorkflowResultsControllerFixture(null, 'waiting');
$rootScope.$broadcast('ws-jobs', { unified_job_id: workflow_job_json.id, status: "running" });
});
it('should start elapsed timer', () => {
expect(scope.workflow.status).toBe("running");
expect(workflowResultsService.createOneSecondTimer).toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,59 @@
'use strict';
import moment from 'moment';
describe('workflowResultsService', () => {
let workflowResultsService;
let $interval;
beforeEach(angular.mock.module('workflowResults', ($provide) => {
['PromptDialog', 'Prompt', 'Wait', 'Rest', 'ProcessErrors', 'InitiatePlaybookRun', '$state'].forEach(function(item) {
$provide.value(item, {});
});
$provide.value('$stateExtender', { addState: jasmine.createSpy('addState'), });
$provide.value('moment', moment);
}));
beforeEach(angular.mock.inject((_workflowResultsService_, _$interval_) => {
workflowResultsService = _workflowResultsService_;
$interval = _$interval_;
}));
describe('createOneSecondTimer()', () => {
it('should create a timer that runs every second, incremented by a second', (done) => {
let ticks = 0;
let ticks_expected = 10;
workflowResultsService.createOneSecondTimer(moment(), function(time) {
ticks += 1;
if (ticks >= ticks_expected) {
expect(ticks).toBe(ticks_expected);
// TODO: should verify time is 10 but this requires mocking moment()
// because we "artificially" accelerate time.
done();
}
});
$interval.flush(ticks_expected * 1000);
});
});
describe('destroyTimer()', () => {
beforeEach(() => {
$interval.cancel = jasmine.createSpy('cancel');
});
it('should not destroy null timer', () => {
workflowResultsService.destroyTimer(null);
expect($interval.cancel).not.toHaveBeenCalled();
});
it('should destroy passed in timer', () => {
let timer = jasmine.createSpy('timer');
workflowResultsService.destroyTimer(timer);
expect($interval.cancel).toHaveBeenCalledWith(timer);
});
});
});