From 2d0805c4530b9436d363bb04519f20d432fcd39e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 20 Aug 2018 15:04:33 -0700 Subject: [PATCH] Test coverage for scheduler dry-run addition to the plan page --- ui/app/models/job.js | 2 +- ui/app/templates/jobs/run.hbs | 6 +-- ui/mirage/config.js | 28 ++++++++++-- ui/mirage/factories/evaluation.js | 30 +++++++------ ui/tests/acceptance/job-run-test.js | 66 ++++++++++++++++++++++++++--- ui/tests/pages/jobs/run.js | 10 ++++- 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 6bb5959c0..fc9dfd555 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -210,7 +210,7 @@ export default Model.extend({ try { // If the definition is already JSON then it doesn't need to be parsed. const json = JSON.parse(definition); - this.set('_newDefinitionJSON', definition); + this.set('_newDefinitionJSON', json); this.setIDByPayload(json); promise = RSVP.resolve(definition); } catch (err) { diff --git a/ui/app/templates/jobs/run.hbs b/ui/app/templates/jobs/run.hbs index 2e3856030..9039cae7f 100644 --- a/ui/app/templates/jobs/run.hbs +++ b/ui/app/templates/jobs/run.hbs @@ -74,9 +74,9 @@ {{job-diff data-test-plan-output diff=planOutput.diff verbose=false}} -
-
Scheduler dry-run
-
+
+
Scheduler dry-run
+
{{#if planOutput.failedTGAllocs}} {{#each planOutput.failedTGAllocs as |placementFailure|}} {{placement-failure failedTGAlloc=placementFailure}} diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 21e4c07e6..d3b2a9778 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -3,6 +3,7 @@ import Response from 'ember-cli-mirage/response'; import { HOSTS } from './common'; import { logFrames, logEncode } from './data/logs'; import { generateDiff } from './factories/job-version'; +import { generateTaskGroupFailures } from './factories/evaluation'; const { copy } = Ember; @@ -56,7 +57,7 @@ export default function() { }) ); - this.post('/jobs', function({ jobs }, req) { + this.post('/jobs', function(schema, req) { const body = JSON.parse(req.requestBody); if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); @@ -64,7 +65,7 @@ export default function() { return okEmpty(); }); - this.post('/jobs/parse', function({ jobs }, req) { + this.post('/jobs/parse', function(schema, req) { const body = JSON.parse(req.requestBody); if (!body.JobHCL) @@ -84,13 +85,19 @@ export default function() { return new Response(200, {}, this.serialize(job)); }); - this.post('/job/:id/plan', function({ jobs }, req) { + this.post('/job/:id/plan', function(schema, req) { const body = JSON.parse(req.requestBody); if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload'); if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true'); - return new Response(200, {}, JSON.stringify({ Diff: generateDiff(req.params.id) })); + const FailedTGAllocs = body.Job.Unschedulable && generateFailedTGAllocs(body.Job); + + return new Response( + 200, + {}, + JSON.stringify({ FailedTGAllocs, Diff: generateDiff(req.params.id) }) + ); }); this.get( @@ -319,3 +326,16 @@ function filterKeys(object, ...keys) { function okEmpty() { return new Response(200, {}, '{}'); } + +function generateFailedTGAllocs(job, taskGroups) { + const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name'); + + let tgNames = ['tg-one', 'tg-two']; + if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec; + if (taskGroups && taskGroups.length) tgNames = taskGroups; + + return tgNames.reduce((hash, tgName) => { + hash[tgName] = generateTaskGroupFailures(); + return hash; + }, {}); +} diff --git a/ui/mirage/factories/evaluation.js b/ui/mirage/factories/evaluation.js index ebf01e217..0dbfa25dc 100644 --- a/ui/mirage/factories/evaluation.js +++ b/ui/mirage/factories/evaluation.js @@ -72,19 +72,7 @@ export default Factory.extend({ } const placementFailures = failedTaskGroupNames.reduce((hash, name) => { - hash[name] = { - CoalescedFailures: faker.random.number({ min: 1, max: 20 }), - NodesEvaluated: faker.random.number({ min: 1, max: 100 }), - NodesExhausted: faker.random.number({ min: 1, max: 100 }), - - NodesAvailable: Math.random() > 0.7 ? generateNodesAvailable() : null, - ClassFiltered: Math.random() > 0.7 ? generateClassFiltered() : null, - ConstraintFiltered: Math.random() > 0.7 ? generateConstraintFiltered() : null, - ClassExhausted: Math.random() > 0.7 ? generateClassExhausted() : null, - DimensionExhausted: Math.random() > 0.7 ? generateDimensionExhausted() : null, - QuotaExhausted: Math.random() > 0.7 ? generateQuotaExhausted() : null, - Scores: Math.random() > 0.7 ? generateScores() : null, - }; + hash[name] = generateTaskGroupFailures(); return hash; }, {}); @@ -111,3 +99,19 @@ function assignJob(evaluation, server) { job_id: job.id, }); } + +export function generateTaskGroupFailures() { + return { + CoalescedFailures: faker.random.number({ min: 1, max: 20 }), + NodesEvaluated: faker.random.number({ min: 1, max: 100 }), + NodesExhausted: faker.random.number({ min: 1, max: 100 }), + + NodesAvailable: Math.random() > 0.7 ? generateNodesAvailable() : null, + ClassFiltered: Math.random() > 0.7 ? generateClassFiltered() : null, + ConstraintFiltered: Math.random() > 0.7 ? generateConstraintFiltered() : null, + ClassExhausted: Math.random() > 0.7 ? generateClassExhausted() : null, + DimensionExhausted: Math.random() > 0.7 ? generateDimensionExhausted() : null, + QuotaExhausted: Math.random() > 0.7 ? generateQuotaExhausted() : null, + Scores: Math.random() > 0.7 ? generateScores() : null, + }; +} diff --git a/ui/tests/acceptance/job-run-test.js b/ui/tests/acceptance/job-run-test.js index e168f8ec1..5b56eafcb 100644 --- a/ui/tests/acceptance/job-run-test.js +++ b/ui/tests/acceptance/job-run-test.js @@ -5,6 +5,7 @@ import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; import JobRun from 'nomad-ui/tests/pages/jobs/run'; const newJobName = 'new-job'; +const newJobTaskGroupName = 'redis'; const jsonJob = overrides => { return JSON.stringify( @@ -15,15 +16,17 @@ const jsonJob = overrides => { Namespace: 'default', Datacenters: ['dc1'], Priority: 50, - TaskGroups: { - redis: { - Tasks: { - redis: { + TaskGroups: [ + { + Name: newJobTaskGroupName, + Tasks: [ + { + Name: 'redis', Driver: 'docker', }, - }, + ], }, - }, + ], }, overrides ), @@ -37,7 +40,7 @@ job "${newJobName}" { namespace = "default" datacenters = ["dc1"] - task "redis" { + task "${newJobTaskGroupName}" { driver = "docker" } } @@ -313,3 +316,52 @@ test('when submitting a job to a different namespace, the redirect to the job ov ); }); }); + +test('when the scheduler dry-run has warnings, the warnings are shown to the user', function(assert) { + // Unschedulable is a hint to Mirage to respond with warnings from the plan endpoint + const spec = jsonJob({ Unschedulable: true }); + + JobRun.visit(); + + andThen(() => { + JobRun.editor.fillIn(spec); + JobRun.plan(); + }); + + andThen(() => { + assert.ok( + JobRun.dryRunMessage.errored, + 'The scheduler dry-run message is in the warning state' + ); + assert.notOk( + JobRun.dryRunMessage.succeeded, + 'The success message is not shown in addition to the warning message' + ); + assert.ok( + JobRun.dryRunMessage.body.includes(newJobTaskGroupName), + 'The scheduler dry-run message includes the warning from send back by the API' + ); + }); +}); + +test('when the scheduler dry-run has no warnings, a success message is shown to the user', function(assert) { + const spec = hclJob(); + + JobRun.visit(); + + andThen(() => { + JobRun.editor.fillIn(spec); + JobRun.plan(); + }); + + andThen(() => { + assert.ok( + JobRun.dryRunMessage.succeeded, + 'The scheduler dry-run message is in the success state' + ); + assert.notOk( + JobRun.dryRunMessage.errored, + 'The warning message is not shown in addition to the success message' + ); + }); +}); diff --git a/ui/tests/pages/jobs/run.js b/ui/tests/pages/jobs/run.js index 9192b03c1..08c1bcb7c 100644 --- a/ui/tests/pages/jobs/run.js +++ b/ui/tests/pages/jobs/run.js @@ -1,4 +1,4 @@ -import { clickable, create, isPresent, text, visitable } from 'ember-cli-page-object'; +import { clickable, create, hasClass, isPresent, text, visitable } from 'ember-cli-page-object'; import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror'; import error from 'nomad-ui/tests/pages/components/error'; @@ -35,4 +35,12 @@ export default create({ contents: code('[data-test-editor]'), fillIn: codeFillable('[data-test-editor]'), }, + + dryRunMessage: { + scope: '[data-test-dry-run-message]', + title: text('[data-test-dry-run-title]'), + body: text('[data-test-dry-run-body]'), + errored: hasClass('is-warning'), + succeeded: hasClass('is-primary'), + }, });