diff --git a/.github/workflows/test-ui.yml b/.github/workflows/test-ui.yml index f72ca7269..ee33d00fc 100644 --- a/.github/workflows/test-ui.yml +++ b/.github/workflows/test-ui.yml @@ -40,6 +40,7 @@ jobs: run: working-directory: ui strategy: + fail-fast: false matrix: partition: [1, 2, 3, 4] split: [4] @@ -61,17 +62,28 @@ jobs: secrets: |- kv/data/teams/nomad/ui PERCY_TOKEN ; - name: ember exam + id: ember_exam env: PERCY_TOKEN: ${{ env.PERCY_TOKEN || secrets.PERCY_TOKEN }} PERCY_PARALLEL_NONCE: ${{ needs.pre-test.outputs.nonce }} run: | yarn exam:parallel --split=${{ matrix.split }} --partition=${{ matrix.partition }} --json-report=test-results/test-results.json continue-on-error: true - - name: Express timeout failure - if: ${{ failure() }} - run: exit 1 + - name: Express failure + if: steps.ember_exam.outcome == 'failure' + run: | + echo "Tests failed in ember-exam for partition ${{ matrix.partition }}" + echo "Failed tests:" + node -e " + const results = JSON.parse(require('fs').readFileSync('test-results/test-results.json')); + results.tests.filter(t => !t.passed).forEach(test => { + console.error('\n❌ ' + test.name); + if (test.error) console.error(test.error); + }); + " + exit 1 - name: Upload partition test results - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: test-results-${{ matrix.partition }} diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index ccaa605e4..f0cfe859a 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -175,6 +175,9 @@ export default Factory.extend({ // When true, the job will have no versions or deployments (and in turn no latest deployment) noDeployments: false, + // When true, the job will have a previous stable version. Useful for testing "start job" loop. + withPreviousStableVersion: false, + // When true, an evaluation with a high modify index and placement failures is created failedPlacements: false, @@ -317,8 +320,20 @@ export default Factory.extend({ version: index, noActiveDeployment: job.noActiveDeployment, activeDeployment: job.activeDeployment, + stable: true, }); }); + + if (job.withPreviousStableVersion) { + server.create('job-version', { + job, + namespace: job.namespace, + version: 1, + noActiveDeployment: job.noActiveDeployment, + activeDeployment: job.activeDeployment, + stable: true, + }); + } } if (job.activeDeployment) { diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index a525b8fc3..1896c743c 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -27,6 +27,7 @@ moduleForJob('Acceptance | job detail (batch)', 'allocations', () => allocStatusDistribution: { running: 1, }, + withPreviousStableVersion: true, }) ); @@ -39,6 +40,7 @@ moduleForJob('Acceptance | job detail (system)', 'allocations', () => allocStatusDistribution: { running: 1, }, + withPreviousStableVersion: true, }) ); @@ -52,6 +54,7 @@ moduleForJob('Acceptance | job detail (sysbatch)', 'allocations', () => running: 1, failed: 1, }, + withPreviousStableVersion: true, }) ); @@ -65,6 +68,7 @@ moduleForJobWithClientStatus( type: 'sysbatch', createAllocations: false, noActiveDeployment: true, + withPreviousStableVersion: true, }); } ); @@ -80,6 +84,7 @@ moduleForJobWithClientStatus( namespaceId: namespace.name, createAllocations: false, noActiveDeployment: true, + withPreviousStableVersion: true, }); } ); @@ -95,6 +100,7 @@ moduleForJobWithClientStatus( namespaceId: namespace.name, createAllocations: false, noActiveDeployment: true, + withPreviousStableVersion: true, }); } ); @@ -109,6 +115,7 @@ moduleForJob('Acceptance | job detail (sysbatch child)', 'allocations', () => { running: 1, }, noActiveDeployment: true, + withPreviousStableVersion: true, }); return server.db.jobs.where({ parentId: parent.id })[0]; }); @@ -212,6 +219,7 @@ moduleForJob( server.create('job', 'parameterized', { shallow: true, noActiveDeployment: true, + withPreviousStableVersion: true, }), { 'the default sort is submitTime descending': async (job, assert) => { @@ -292,7 +300,15 @@ moduleForJob( moduleForJob( 'Acceptance | job detail (service)', 'allocations', - () => server.create('job', { type: 'service', noActiveDeployment: true }), + () => + server.create('job', { + type: 'service', + noActiveDeployment: true, + withPreviousStableVersion: true, + allocStatusDistribution: { + running: 1, + }, + }), { 'the subnav links to deployment': async (job, assert) => { await JobDetail.tabFor('deployments').visit(); diff --git a/ui/tests/acceptance/regions-test.js b/ui/tests/acceptance/regions-test.js index a17529fd3..60131ca69 100644 --- a/ui/tests/acceptance/regions-test.js +++ b/ui/tests/acceptance/regions-test.js @@ -5,7 +5,7 @@ /* eslint-disable qunit/require-expect */ /* eslint-disable qunit/no-conditional-assertions */ -import { currentURL } from '@ember/test-helpers'; +import { currentURL, settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { selectChoose } from 'ember-power-select/test-support'; @@ -80,9 +80,11 @@ module('Acceptance | regions (only one)', function (hooks) { await JobsList.jobs.objectAt(0).clickRow(); await Layout.gutter.visitClients(); await Layout.gutter.visitServers(); - server.pretender.handledRequests.forEach((req) => { - assert.notOk(req.url.includes('region='), req.url); - }); + server.pretender.handledRequests + .filter((req) => !req.url.includes('/v1/status/leader')) + .forEach((req) => { + assert.notOk(req.url.includes('region='), req.url); + }); }); }); @@ -114,7 +116,10 @@ module('Acceptance | regions (many)', function (hooks) { }); test('when on the default region, pages do not include the region query param', async function (assert) { + let managementToken = server.create('token'); + window.localStorage.nomadTokenSecret = managementToken.secretId; await JobsList.visit(); + await settled(); assert.equal(currentURL(), '/jobs', 'No region query param'); assert.equal( @@ -143,11 +148,13 @@ module('Acceptance | regions (many)', function (hooks) { }); test('switching regions to the default region, unsets the region query param', async function (assert) { + let managementToken = server.create('token'); + window.localStorage.nomadTokenSecret = managementToken.secretId; const startingRegion = server.db.regions[1].id; const defaultRegion = server.db.regions[0].id; await JobsList.visit({ region: startingRegion }); - + await settled(); await selectChoose('[data-test-region-switcher-parent]', defaultRegion); assert.notOk( @@ -197,7 +204,8 @@ module('Acceptance | regions (many)', function (hooks) { const appRequests = server.pretender.handledRequests.filter( (req) => !req.responseURL.includes('/v1/regions') && - !req.responseURL.includes('/v1/operator/license') + !req.responseURL.includes('/v1/operator/license') && + !req.responseURL.includes('/v1/status/leader') ); assert.notOk( diff --git a/ui/tests/acceptance/server-detail-test.js b/ui/tests/acceptance/server-detail-test.js index d5420b1a4..7fe311fa0 100644 --- a/ui/tests/acceptance/server-detail-test.js +++ b/ui/tests/acceptance/server-detail-test.js @@ -20,6 +20,7 @@ module('Acceptance | server detail', function (hooks) { hooks.beforeEach(async function () { server.createList('agent', 3); + server.create('region', { id: 'global' }); agent = server.db.agents[0]; await ServerDetail.visit({ name: agent.name }); }); diff --git a/ui/tests/acceptance/task-logs-test.js b/ui/tests/acceptance/task-logs-test.js index 83e0738d3..00b907e33 100644 --- a/ui/tests/acceptance/task-logs-test.js +++ b/ui/tests/acceptance/task-logs-test.js @@ -62,10 +62,7 @@ module('Acceptance | task logs', function (hooks) { test('the stdout log immediately starts streaming', async function (assert) { await TaskLogs.visit({ id: allocation.id, name: task.name }); - const node = server.db.nodes.find(allocation.nodeId); - const logUrlRegex = new RegExp( - `${node.httpAddr}/v1/client/fs/logs/${allocation.id}` - ); + const logUrlRegex = new RegExp(`/v1/client/fs/logs/${allocation.id}`); assert.ok( server.pretender.handledRequests.filter((req) => logUrlRegex.test(req.url) diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js index 5a73348a6..b02cf8523 100644 --- a/ui/tests/helpers/module-for-job.js +++ b/ui/tests/helpers/module-for-job.js @@ -92,7 +92,11 @@ export default function moduleForJob( test('the title buttons are dependent on job status', async function (assert) { if (job.status === 'dead') { - assert.ok(JobDetail.start.isPresent); + if (job.stopped) { + assert.ok(JobDetail.start.isPresent); + } else { + assert.ok(JobDetail.revert.isPresent); + } assert.ok(JobDetail.purge.isPresent); assert.notOk(JobDetail.stop.isPresent); assert.notOk(JobDetail.execButton.isPresent); diff --git a/ui/tests/integration/components/job-page/periodic-test.js b/ui/tests/integration/components/job-page/periodic-test.js index b6d665a25..f2e9b5cad 100644 --- a/ui/tests/integration/components/job-page/periodic-test.js +++ b/ui/tests/integration/components/job-page/periodic-test.js @@ -202,6 +202,8 @@ module('Integration | Component | job-page/periodic', function (hooks) { childrenCount: 0, createAllocations: false, status: 'dead', + withPreviousStableVersion: true, + stopped: true, }); await this.store.findAll('job'); @@ -223,6 +225,8 @@ module('Integration | Component | job-page/periodic', function (hooks) { childrenCount: 0, createAllocations: false, status: 'dead', + withPreviousStableVersion: true, + stopped: true, }); await this.store.findAll('job'); @@ -243,6 +247,8 @@ module('Integration | Component | job-page/periodic', function (hooks) { childrenCount: 0, createAllocations: false, status: 'dead', + withPreviousStableVersion: true, + stopped: true, }); await this.store.findAll('job'); diff --git a/ui/tests/integration/components/job-page/service-test.js b/ui/tests/integration/components/job-page/service-test.js index 90156fcab..994ec4bc6 100644 --- a/ui/tests/integration/components/job-page/service-test.js +++ b/ui/tests/integration/components/job-page/service-test.js @@ -112,7 +112,11 @@ module('Integration | Component | job-page/service', function (hooks) { test('Starting a job sends a post request for the job using the current definition', async function (assert) { assert.expect(1); - const mirageJob = makeMirageJob(this.server, { status: 'dead' }); + const mirageJob = makeMirageJob(this.server, { + status: 'dead', + withPreviousStableVersion: true, + stopped: true, + }); await this.store.findAll('job'); const job = this.store.peekAll('job').findBy('plainId', mirageJob.id); @@ -129,7 +133,11 @@ module('Integration | Component | job-page/service', function (hooks) { this.server.pretender.post('/v1/job/:id', () => [403, {}, '']); - const mirageJob = makeMirageJob(this.server, { status: 'dead' }); + const mirageJob = makeMirageJob(this.server, { + status: 'dead', + withPreviousStableVersion: true, + stopped: true, + }); await this.store.findAll('job'); const job = this.store.peekAll('job').findBy('plainId', mirageJob.id); @@ -144,7 +152,10 @@ module('Integration | Component | job-page/service', function (hooks) { test('Purging a job sends a purge request for the job', async function (assert) { assert.expect(1); - const mirageJob = makeMirageJob(this.server, { status: 'dead' }); + const mirageJob = makeMirageJob(this.server, { + status: 'dead', + withPreviousStableVersion: true, + }); await this.store.findAll('job'); const job = this.store.peekAll('job').findBy('plainId', mirageJob.id); diff --git a/ui/tests/integration/components/task-log-test.js b/ui/tests/integration/components/task-log-test.js index 54ed13a6c..368014ac1 100644 --- a/ui/tests/integration/components/task-log-test.js +++ b/ui/tests/integration/components/task-log-test.js @@ -11,6 +11,7 @@ import hbs from 'htmlbars-inline-precompile'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; import Pretender from 'pretender'; import { logEncode } from '../../../mirage/data/logs'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; const HOST = '1.1.1.1:1111'; const allowedConnectionTime = 100; @@ -36,7 +37,22 @@ let logMode = null; module('Integration | Component | task log', function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { + hooks.beforeEach(async function () { + this.server = startMirage(); + const managementToken = this.server.create('token'); + window.localStorage.nomadTokenSecret = managementToken.secretId; + const tokenService = this.owner.lookup('service:token'); + const tokenPromise = tokenService.fetchSelfTokenAndPolicies.perform(); + const timeoutPromise = new Promise((_, reject) => { + setTimeout( + () => reject(new Error('Token fetch timed out after 3 seconds')), + 3000 + ); + }); + await Promise.race([tokenPromise, timeoutPromise]); + // ^--- TODO: noticed some flakiness in local testing; this is meant to suss it out in CI. + await settled(); + const handler = ({ queryParams }) => { let frames; let data;