1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { ContainerState } from 'models/container';
7 describe('Process tests', function() {
12 // Only set up common users once. These aren't set up as aliases because
13 // aliases are cleaned up after every test. Also it doesn't make sense
14 // to set the same users on beforeEach() over and over again, so we
15 // separate a little from Cypress' 'Best Practices' here.
16 cy.getUser('admin', 'Admin', 'User', true, true)
17 .as('adminUser').then(function() {
18 adminUser = this.adminUser;
21 cy.getUser('user', 'Active', 'User', false, true)
22 .as('activeUser').then(function() {
23 activeUser = this.activeUser;
28 beforeEach(function() {
30 cy.clearLocalStorage();
33 function setupDockerImage(image_name) {
34 // Create a collection that will be used as a docker image for the tests.
35 cy.createCollection(adminUser.token, {
37 manifest_text: ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n"
38 }).as('dockerImage').then(function(dockerImage) {
39 // Give read permissions to the active user on the docker image.
40 cy.createLink(adminUser.token, {
41 link_class: 'permission',
43 tail_uuid: activeUser.user.uuid,
44 head_uuid: dockerImage.uuid
45 }).as('dockerImagePermission').then(function() {
46 // Set-up docker image collection tags
47 cy.createLink(activeUser.token, {
48 link_class: 'docker_image_repo+tag',
50 head_uuid: dockerImage.uuid,
51 }).as('dockerImageRepoTag');
52 cy.createLink(activeUser.token, {
53 link_class: 'docker_image_hash',
54 name: 'sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678',
55 head_uuid: dockerImage.uuid,
56 }).as('dockerImageHash');
59 return cy.getAll('@dockerImage', '@dockerImageRepoTag', '@dockerImageHash',
60 '@dockerImagePermission').then(function([dockerImage]) {
65 function createContainerRequest(user, name, docker_image, command, reuse = false, state = 'Uncommitted') {
66 return setupDockerImage(docker_image).then(function(dockerImage) {
67 return cy.createContainerRequest(user.token, {
70 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
71 output_path: 'stdout.txt',
73 runtime_constraints: {
89 describe('Details panel', function() {
90 it('shows process details', function() {
91 createContainerRequest(
93 `test_container_request ${Math.floor(Math.random() * 999999)}`,
95 ['echo', 'hello world'],
97 .then(function(containerRequest) {
98 cy.loginAs(activeUser);
99 cy.goToPath(`/processes/${containerRequest.uuid}`);
100 cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
101 cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
102 cy.get('[data-cy=process-details-attributes-runtime-user]').should('not.exist');
105 // Fake submitted by another user
106 cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
108 res.body.modified_by_user_uuid = 'zzzzz-tpzed-000000000000000';
112 createContainerRequest(
114 `test_container_request ${Math.floor(Math.random() * 999999)}`,
116 ['echo', 'hello world'],
118 .then(function(containerRequest) {
119 cy.loginAs(activeUser);
120 cy.goToPath(`/processes/${containerRequest.uuid}`);
121 cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
122 cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`zzzzz-tpzed-000000000000000`);
123 cy.get('[data-cy=process-details-attributes-runtime-user]').contains(`Active User (${activeUser.user.uuid})`);
127 it('should show runtime status indicators', function() {
128 // Setup running container with runtime_status error & warning messages
129 createContainerRequest(
131 'test_container_request',
133 ['echo', 'hello world'],
135 .as('containerRequest')
136 .then(function(containerRequest) {
137 expect(containerRequest.state).to.equal('Committed');
138 expect(containerRequest.container_uuid).not.to.be.equal('');
140 cy.getContainer(activeUser.token, containerRequest.container_uuid)
141 .then(function(queuedContainer) {
142 expect(queuedContainer.state).to.be.equal('Queued');
144 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
146 }).then(function(lockedContainer) {
147 expect(lockedContainer.state).to.be.equal('Locked');
149 cy.updateContainer(adminUser.token, lockedContainer.uuid, {
152 error: 'Something went wrong',
153 errorDetail: 'Process exited with status 1',
154 warning: 'Free disk space is low',
157 .as('runningContainer')
158 .then(function(runningContainer) {
159 expect(runningContainer.state).to.be.equal('Running');
160 expect(runningContainer.runtime_status).to.be.deep.equal({
161 'error': 'Something went wrong',
162 'errorDetail': 'Process exited with status 1',
163 'warning': 'Free disk space is low',
168 // Test that the UI shows the error and warning messages
169 cy.getAll('@containerRequest', '@runningContainer').then(function([containerRequest]) {
170 cy.loginAs(activeUser);
171 cy.goToPath(`/processes/${containerRequest.uuid}`);
172 cy.get('[data-cy=process-runtime-status-error]')
173 .should('contain', 'Something went wrong')
174 .and('contain', 'Process exited with status 1');
175 cy.get('[data-cy=process-runtime-status-warning]')
176 .should('contain', 'Free disk space is low')
177 .and('contain', 'No additional warning details available');
181 // Force container_count for testing
182 let containerCount = 2;
183 cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
185 res.body.container_count = containerCount;
189 cy.getAll('@containerRequest').then(function([containerRequest]) {
190 cy.goToPath(`/processes/${containerRequest.uuid}`);
191 cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
192 .should('contain', 'Process retried 1 time');
195 cy.getAll('@containerRequest').then(function([containerRequest]) {
197 cy.goToPath(`/processes/${containerRequest.uuid}`);
198 cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
199 .should('contain', 'Process retried 2 times');
203 it('allows copying processes', function() {
204 const crName = 'first_container_request';
205 const copiedCrName = 'copied_container_request';
206 createContainerRequest(
210 ['echo', 'hello world'],
212 .then(function(containerRequest) {
213 cy.loginAs(activeUser);
214 cy.goToPath(`/processes/${containerRequest.uuid}`);
215 cy.get('[data-cy=process-details]').should('contain', crName);
217 cy.get('[data-cy=process-details]').find('button[title="More options"]').click();
218 cy.get('ul[data-cy=context-menu]').contains("Copy and re-run process").click();
221 cy.get('[data-cy=form-dialog]').within(() => {
222 cy.get('input[name=name]').clear().type(copiedCrName);
223 cy.get('[data-cy=projects-tree-home-tree-picker]').click();
224 cy.get('[data-cy=form-submit-btn]').click();
227 cy.get('[data-cy=process-details]').should('contain', copiedCrName);
228 cy.get('[data-cy=process-details]').find('button').contains('Run');
231 const getFakeContainer = (fakeContainerUuid) => ({
232 href: `/containers/${fakeContainerUuid}`,
233 kind: "arvados#container",
234 etag: "ecfosljpnxfari9a8m7e4yv06",
235 uuid: fakeContainerUuid,
236 owner_uuid: "zzzzz-tpzed-000000000000000",
237 created_at: "2023-02-13T15:55:47.308915000Z",
238 modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
239 modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
240 modified_at: "2023-02-15T19:12:45.987086000Z",
242 "arvados-cwl-runner",
245 "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
246 "/var/lib/cwl/workflow.json#main",
247 "/var/lib/cwl/cwl.input.json",
249 container_image: "4ad7d11381df349e464694762db14e04+303",
250 cwd: "/var/spool/cwl",
254 locked_by_uuid: null,
257 output_path: "/var/spool/cwl",
259 runtime_constraints: {
264 hardware_capability: "",
266 keep_cache_disk: 2147483648,
274 scheduling_parameters: {
279 runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
280 runtime_auth_scopes: ["all"],
282 gateway_address: null,
283 interactive_session_started: false,
284 output_storage_classes: ["default"],
285 output_properties: {},
287 subrequests_cost: 0.0,
290 it('shows cancel button when appropriate', function() {
291 // Ignore collection requests
292 cy.intercept({method: 'GET', url: `**/arvados/v1/collections/*`}, {
297 // Uncommitted container
298 const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
299 createContainerRequest(
303 ['echo', 'hello world'],
304 false, 'Uncommitted')
305 .then(function(containerRequest) {
306 // Navigate to process and verify run / cancel button
307 cy.goToPath(`/processes/${containerRequest.uuid}`);
309 cy.get('[data-cy=process-details]').should('contain', crUncommitted);
310 cy.get('[data-cy=process-run-button]').should('exist');
311 cy.get('[data-cy=process-cancel-button]').should('not.exist');
315 const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
316 const fakeCrUuid = 'zzzzz-dz642-000000000000001';
317 createContainerRequest(
321 ['echo', 'hello world'],
323 .then(function(containerRequest) {
324 // Fake container uuid
325 cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
327 res.body.output_uuid = fakeCrUuid;
328 res.body.priority = 500;
329 res.body.state = "Committed";
334 const container = getFakeContainer(fakeCrUuid);
335 cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrUuid}`}, {
337 body: {...container, state: "Queued", priority: 500}
340 // Navigate to process and verify cancel button
341 cy.goToPath(`/processes/${containerRequest.uuid}`);
343 cy.get('[data-cy=process-details]').should('contain', crQueued);
344 cy.get('[data-cy=process-cancel-button]').contains('Cancel');
348 const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
349 const fakeCrLockedUuid = 'zzzzz-dz642-000000000000002';
350 createContainerRequest(
354 ['echo', 'hello world'],
356 .then(function(containerRequest) {
357 // Fake container uuid
358 cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
360 res.body.output_uuid = fakeCrLockedUuid;
361 res.body.priority = 500;
362 res.body.state = "Committed";
367 const container = getFakeContainer(fakeCrLockedUuid);
368 cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrLockedUuid}`}, {
370 body: {...container, state: "Locked", priority: 500}
373 // Navigate to process and verify cancel button
374 cy.goToPath(`/processes/${containerRequest.uuid}`);
376 cy.get('[data-cy=process-details]').should('contain', crLocked);
377 cy.get('[data-cy=process-cancel-button]').contains('Cancel');
381 const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
382 const fakeCrOnHoldUuid = 'zzzzz-dz642-000000000000003';
383 createContainerRequest(
387 ['echo', 'hello world'],
389 .then(function(containerRequest) {
390 // Fake container uuid
391 cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
393 res.body.output_uuid = fakeCrOnHoldUuid;
394 res.body.priority = 0;
395 res.body.state = "Committed";
400 const container = getFakeContainer(fakeCrOnHoldUuid);
401 cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrOnHoldUuid}`}, {
403 body: {...container, state: "Queued", priority: 0}
406 // Navigate to process and verify cancel button
407 cy.goToPath(`/processes/${containerRequest.uuid}`);
409 cy.get('[data-cy=process-details]').should('contain', crOnHold);
410 cy.get('[data-cy=process-run-button]').should('exist');
411 cy.get('[data-cy=process-cancel-button]').should('not.exist');
418 describe('Logs panel', function() {
419 it('shows live process logs', function() {
420 cy.intercept({method: 'GET', url: '**/arvados/v1/containers/*'}, (req) => {
422 res.body.state = ContainerState.RUNNING;
426 const crName = 'test_container_request';
427 createContainerRequest(
431 ['echo', 'hello world'],
433 .then(function(containerRequest) {
434 // Create empty log file before loading process page
435 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
439 cy.loginAs(activeUser);
440 cy.goToPath(`/processes/${containerRequest.uuid}`);
441 cy.get('[data-cy=process-details]').should('contain', crName);
442 cy.get('[data-cy=process-logs]')
443 .should('contain', 'No logs yet')
444 .and('not.contain', 'hello world');
447 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
448 "2023-07-18T20:14:48.128642814Z hello world"
450 cy.get('[data-cy=process-logs]', {timeout: 7000})
451 .should('not.contain', 'No logs yet')
452 .and('contain', 'hello world');
455 // Append new log line to different file
456 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
457 "2023-07-18T20:14:49.128642814Z hello new line"
459 cy.get('[data-cy=process-logs]', {timeout: 7000})
460 .should('not.contain', 'No logs yet')
461 .and('contain', 'hello new line');
466 it('filters process logs by event type', function() {
467 const nodeInfoLogs = [
469 'Linux compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p 5.4.0-1059-azure #62~18.04.1-Ubuntu SMP Tue Sep 14 17:53:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux',
472 'vendor_id : GenuineIntel',
475 'model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz'
477 const crunchRunLogs = [
478 '2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection',
479 '2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started',
480 '2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)',
481 '2022-03-22T13:56:26.244862836Z Executing container \'zzzzz-dz642-1wokwvcct9s9du3\' using docker runtime',
482 '2022-03-22T13:56:26.245037738Z Executing on host \'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p\'',
485 '2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.',
486 '2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.',
487 '2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.',
488 '2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.',
489 '2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.',
490 '2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.',
491 '2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.',
492 '2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.',
493 '2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.',
494 '2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.',
495 '2022-03-22T13:56:22.542418097Z Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.',
496 '2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.',
497 '2022-03-22T13:56:22.542418117Z Sed at enim sit amet nulla tincidunt mattis. Aenean eget aliquet ex, non ultrices ex. Nulla ex tortor, vestibulum aliquam tempor ac, aliquam vel est.',
498 '2022-03-22T13:56:22.542418127Z Fusce auctor faucibus libero id venenatis. Etiam sodales, odio eu cursus efficitur, quam sem blandit ex, quis porttitor enim dui quis lectus. In id tincidunt felis.',
499 '2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.',
500 '2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.',
501 '2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.',
502 '2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.',
505 createContainerRequest(
507 'test_container_request',
509 ['echo', 'hello world'],
511 .then(function(containerRequest) {
512 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as('nodeInfoLogs');
513 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as('crunchRunLogs');
514 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as('stdoutLogs');
516 cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
517 cy.loginAs(activeUser);
518 cy.goToPath(`/processes/${containerRequest.uuid}`);
519 // Should show main logs by default
520 cy.get('[data-cy=process-logs-filter]', {timeout: 7000}).should('contain', 'Main logs');
521 cy.get('[data-cy=process-logs]')
522 .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
523 .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
524 .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
526 cy.get('[data-cy=process-logs-filter]').click();
527 cy.get('body').contains('li', 'All logs').click();
528 cy.get('[data-cy=process-logs]')
529 .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
530 .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
531 .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
532 // Select 'node-info' logs
533 cy.get('[data-cy=process-logs-filter]').click();
534 cy.get('body').contains('li', 'node-info').click();
535 cy.get('[data-cy=process-logs]')
536 .should('not.contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
537 .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
538 .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
539 // Select 'stdout' logs
540 cy.get('[data-cy=process-logs-filter]').click();
541 cy.get('body').contains('li', 'stdout').click();
542 cy.get('[data-cy=process-logs]')
543 .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
544 .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
545 .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
550 it('sorts combined logs', function() {
551 const crName = 'test_container_request';
552 createContainerRequest(
556 ['echo', 'hello world'],
558 .then(function(containerRequest) {
559 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
567 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
568 "2023-07-18T20:14:48.128642814Z first",
569 "2023-07-18T20:14:49.128642814Z third"
572 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
573 "2023-07-18T20:14:48.528642814Z second"
576 cy.loginAs(activeUser);
577 cy.goToPath(`/processes/${containerRequest.uuid}`);
578 cy.get('[data-cy=process-details]').should('contain', crName);
579 cy.get('[data-cy=process-logs]')
580 .should('contain', 'No logs yet');
582 cy.getAll('@node-info', '@stdout', '@stderr').then(() => {
583 // Verify sorted main logs
584 cy.get('[data-cy=process-logs] pre', {timeout: 7000})
585 .eq(0).should('contain', '2023-07-18T20:14:48.128642814Z first');
586 cy.get('[data-cy=process-logs] pre')
587 .eq(1).should('contain', '2023-07-18T20:14:48.528642814Z second');
588 cy.get('[data-cy=process-logs] pre')
589 .eq(2).should('contain', '2023-07-18T20:14:49.128642814Z third');
591 // Switch to All logs
592 cy.get('[data-cy=process-logs-filter]').click();
593 cy.get('body').contains('li', 'All logs').click();
594 // Verify non-sorted lines were preserved
595 cy.get('[data-cy=process-logs] pre')
596 .eq(0).should('contain', '3: nodeinfo 1');
597 cy.get('[data-cy=process-logs] pre')
598 .eq(1).should('contain', '2: nodeinfo 2');
599 cy.get('[data-cy=process-logs] pre')
600 .eq(2).should('contain', '1: nodeinfo 3');
601 cy.get('[data-cy=process-logs] pre')
602 .eq(3).should('contain', '2: nodeinfo 4');
603 cy.get('[data-cy=process-logs] pre')
604 .eq(4).should('contain', '3: nodeinfo 5');
605 // Verify sorted logs
606 cy.get('[data-cy=process-logs] pre')
607 .eq(5).should('contain', '2023-07-18T20:14:48.128642814Z first');
608 cy.get('[data-cy=process-logs] pre')
609 .eq(6).should('contain', '2023-07-18T20:14:48.528642814Z second');
610 cy.get('[data-cy=process-logs] pre')
611 .eq(7).should('contain', '2023-07-18T20:14:49.128642814Z third');
616 it('correctly generates sniplines', function() {
617 const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
618 const crName = 'test_container_request';
619 createContainerRequest(
623 ['echo', 'hello world'],
625 .then(function(containerRequest) {
627 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
628 'X'.repeat(63999) + '_' +
630 '_' + 'X'.repeat(63999)
633 cy.loginAs(activeUser);
634 cy.goToPath(`/processes/${containerRequest.uuid}`);
635 cy.get('[data-cy=process-details]').should('contain', crName);
636 cy.get('[data-cy=process-logs]')
637 .should('contain', 'No logs yet');
639 // Switch to stdout since lines are unsortable (no timestamp)
640 cy.get('[data-cy=process-logs-filter]').click();
641 cy.get('body').contains('li', 'stdout').click();
643 cy.getAll('@stdout').then(() => {
644 // Verify first 64KB and snipline
645 cy.get('[data-cy=process-logs] pre', {timeout: 7000})
646 .eq(0).should('contain', 'X'.repeat(63999) + '_\n' + SNIPLINE);
648 cy.get('[data-cy=process-logs] pre')
649 .eq(1).should('contain', '_' + 'X'.repeat(63999));
650 // Verify none of the Os got through
651 cy.get('[data-cy=process-logs] pre')
652 .should('not.contain', 'O');
659 describe('I/O panel', function() {
663 "id": "#main/input_file",
664 "label": "Label Description",
669 "basename": "input1.tar",
671 "location": "keep:00000000000000000000000000000000+01/input1.tar",
674 "basename": "input1-2.txt",
676 "location": "keep:00000000000000000000000000000000+01/input1-2.txt"
679 "basename": "input1-3.txt",
681 "location": "keep:00000000000000000000000000000000+01/input1-3.txt"
684 "basename": "input1-4.txt",
686 "location": "keep:00000000000000000000000000000000+01/input1-4.txt"
694 "id": "#main/input_dir",
695 "doc": "Doc Description",
700 "basename": "11111111111111111111111111111111+01",
701 "class": "Directory",
702 "location": "keep:11111111111111111111111111111111+01"
708 "id": "#main/input_bool",
709 "doc": ["Doc desc 1", "Doc desc 2"],
718 "id": "#main/input_int",
727 "id": "#main/input_long",
736 "id": "#main/input_float",
745 "id": "#main/input_double",
754 "id": "#main/input_string",
758 "input_string": "Hello World",
763 "id": "#main/input_file_array",
770 "input_file_array": [
772 "basename": "input2.tar",
774 "location": "keep:00000000000000000000000000000000+02/input2.tar"
777 "basename": "input3.tar",
779 "location": "keep:00000000000000000000000000000000+03/input3.tar",
782 "basename": "input3-2.txt",
784 "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
789 "$import": "import_path"
796 "id": "#main/input_dir_array",
798 "items": "Directory",
805 "basename": "11111111111111111111111111111111+02",
806 "class": "Directory",
807 "location": "keep:11111111111111111111111111111111+02"
810 "basename": "11111111111111111111111111111111+03",
811 "class": "Directory",
812 "location": "keep:11111111111111111111111111111111+03"
815 "$import": "import_path"
822 "id": "#main/input_int_array",
834 "$import": "import_path"
841 "id": "#main/input_long_array",
848 "input_long_array": [
852 "$import": "import_path"
859 "id": "#main/input_float_array",
866 "input_float_array": [
871 "$import": "import_path"
878 "id": "#main/input_double_array",
885 "input_double_array": [
890 "$import": "import_path"
897 "id": "#main/input_string_array",
904 "input_string_array": [
909 "$import": "import_path"
916 "id": "#main/input_bool_include",
920 "input_bool_include": {
921 "$include": "include_path"
927 "id": "#main/input_int_include",
931 "input_int_include": {
932 "$include": "include_path"
938 "id": "#main/input_float_include",
942 "input_float_include": {
943 "$include": "include_path"
949 "id": "#main/input_string_include",
953 "input_string_include": {
954 "$include": "include_path"
960 "id": "#main/input_file_include",
964 "input_file_include": {
965 "$include": "include_path"
971 "id": "#main/input_directory_include",
975 "input_directory_include": {
976 "$include": "include_path"
982 "id": "#main/input_file_url",
987 "basename": "index.html",
989 "location": "http://example.com/index.html"
995 const testOutputs = [
998 "id": "#main/output_file",
999 "label": "Label Description",
1004 "basename": "cat.png",
1006 "location": "cat.png"
1012 "id": "#main/output_file_with_secondary",
1013 "doc": "Doc Description",
1017 "output_file_with_secondary": {
1018 "basename": "main.dat",
1020 "location": "main.dat",
1023 "basename": "secondary.dat",
1025 "location": "secondary.dat"
1028 "basename": "secondary2.dat",
1030 "location": "secondary2.dat"
1038 "id": "#main/output_dir",
1039 "doc": ["Doc desc 1", "Doc desc 2"],
1044 "basename": "outdir1",
1045 "class": "Directory",
1046 "location": "outdir1"
1052 "id": "#main/output_bool",
1061 "id": "#main/output_int",
1070 "id": "#main/output_long",
1079 "id": "#main/output_float",
1083 "output_float": 100.5
1088 "id": "#main/output_double",
1092 "output_double": 100.3
1097 "id": "#main/output_string",
1101 "output_string": "Hello output"
1106 "id": "#main/output_file_array",
1113 "output_file_array": [
1115 "basename": "output2.tar",
1117 "location": "output2.tar"
1120 "basename": "output3.tar",
1122 "location": "output3.tar"
1129 "id": "#main/output_dir_array",
1131 "items": "Directory",
1136 "output_dir_array": [
1138 "basename": "outdir2",
1139 "class": "Directory",
1140 "location": "outdir2"
1143 "basename": "outdir3",
1144 "class": "Directory",
1145 "location": "outdir3"
1152 "id": "#main/output_int_array",
1159 "output_int_array": [
1168 "id": "#main/output_long_array",
1175 "output_long_array": [
1183 "id": "#main/output_float_array",
1190 "output_float_array": [
1199 "id": "#main/output_double_array",
1206 "output_double_array": [
1215 "id": "#main/output_string_array",
1222 "output_string_array": [
1231 const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1232 cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
1233 label && cy.contains(label);
1236 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
1238 if (Array.isArray(val)) {
1239 val.forEach(v => cy.get('@secondaryRows').contains(v));
1241 cy.get('@secondaryRows').contains(val);
1245 cy.get('@secondaryRows').contains(collection);
1249 if (Array.isArray(val)) {
1250 val.forEach(v => cy.contains(v));
1256 cy.contains(collection);
1264 const verifyIOParameterImage = (name, url) => {
1265 cy.get('table tr').contains(name).parents('tr').within(() => {
1266 cy.get('[alt="Inline Preview"]')
1267 .should('be.visible')
1269 expect($img[0].naturalWidth).to.be.greaterThan(0);
1270 expect($img[0].src).contains(url);
1275 it('displays IO parameters with keep links and previews', function() {
1276 // Create output collection for real files
1277 cy.createCollection(adminUser.token, {
1278 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1279 owner_uuid: activeUser.user.uuid,
1280 }).then((testOutputCollection) => {
1281 cy.loginAs(activeUser);
1283 cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1285 cy.get('[data-cy=upload-button]').click();
1287 cy.fixture('files/cat.png', 'base64').then(content => {
1288 cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
1289 cy.get('[data-cy=form-submit-btn]').click();
1290 cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
1291 // Confirm final collection state.
1292 cy.get('[data-cy=collection-files-panel]')
1293 .contains('cat.png').should('exist');
1296 cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
1299 // Get updated collection pdh
1300 cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
1301 // Add output uuid and inputs to container request
1302 cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1303 req.reply((res) => {
1304 res.body.output_uuid = testOutputCollection.uuid;
1305 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1306 content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
1308 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1312 inputs: testInputs.map((input) => (input.definition)),
1313 outputs: testOutputs.map((output) => (output.definition))
1320 // Stub fake output collection
1321 cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
1324 uuid: testOutputCollection.uuid,
1325 portable_data_hash: testOutputCollection.portable_data_hash,
1329 // Stub fake output json
1330 cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
1332 body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
1335 // Stub webdav response, points to output json
1336 cy.intercept({method: 'PROPFIND', url: '*'}, {
1337 fixture: 'webdav-propfind-outputs.xml',
1341 createContainerRequest(
1343 'test_container_request',
1345 ['echo', 'hello world'],
1347 .as('containerRequest');
1349 cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
1350 cy.goToPath(`/processes/${containerRequest.uuid}`);
1351 cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1352 .parents('[data-cy=process-io-card]').within(() => {
1353 verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
1354 verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
1355 verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
1356 verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
1357 verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
1358 verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
1359 verifyIOParameter('input_int', null, null, '1');
1360 verifyIOParameter('input_long', null, null, '1');
1361 verifyIOParameter('input_float', null, null, '1.5');
1362 verifyIOParameter('input_double', null, null, '1.3');
1363 verifyIOParameter('input_string', null, null, 'Hello World');
1364 verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
1365 verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
1366 verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
1367 verifyIOParameter('input_file_array', null, null, 'Cannot display value', undefined, true);
1368 verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
1369 verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
1370 verifyIOParameter('input_dir_array', null, null, 'Cannot display value', undefined, true);
1371 verifyIOParameter('input_int_array', null, null, ["1", "3", "5", "Cannot display value"]);
1372 verifyIOParameter('input_long_array', null, null, ["10", "20", "Cannot display value"]);
1373 verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1374 verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1375 verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!", "Cannot display value"]);
1376 verifyIOParameter('input_bool_include', null, null, "Cannot display value");
1377 verifyIOParameter('input_int_include', null, null, "Cannot display value");
1378 verifyIOParameter('input_float_include', null, null, "Cannot display value");
1379 verifyIOParameter('input_string_include', null, null, "Cannot display value");
1380 verifyIOParameter('input_file_include', null, null, "Cannot display value");
1381 verifyIOParameter('input_directory_include', null, null, "Cannot display value");
1382 verifyIOParameter('input_file_url', null, null, "http://example.com/index.html");
1384 cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1385 .parents('[data-cy=process-io-card]').within((ctx) => {
1386 cy.get(ctx).scrollIntoView();
1387 cy.get('[data-cy="io-preview-image-toggle"]').click({waitForAnimations: false});
1388 const outPdh = testOutputCollection.portable_data_hash;
1390 verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
1391 verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
1392 verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
1393 verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
1394 verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
1395 verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
1396 verifyIOParameter('output_bool', null, null, 'true');
1397 verifyIOParameter('output_int', null, null, '1');
1398 verifyIOParameter('output_long', null, null, '1');
1399 verifyIOParameter('output_float', null, null, '100.5');
1400 verifyIOParameter('output_double', null, null, '100.3');
1401 verifyIOParameter('output_string', null, null, 'Hello output');
1402 verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
1403 verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
1404 verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
1405 verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
1406 verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
1407 verifyIOParameter('output_long_array', null, null, ["51", "52"]);
1408 verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
1409 verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
1410 verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
1415 it('displays IO parameters with no value', function() {
1417 const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
1418 const fakeOutputPDH = '11111111111111111111111111111111+99/';
1420 cy.loginAs(activeUser);
1422 // Add output uuid and inputs to container request
1423 cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1424 req.reply((res) => {
1425 res.body.output_uuid = fakeOutputUUID;
1426 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1429 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1433 inputs: testInputs.map((input) => (input.definition)),
1434 outputs: testOutputs.map((output) => (output.definition))
1441 // Stub fake output collection
1442 cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
1445 uuid: fakeOutputUUID,
1446 portable_data_hash: fakeOutputPDH,
1450 // Stub fake output json
1451 cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
1456 cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
1457 // Stub webdav response, points to output json
1458 cy.intercept({method: 'PROPFIND', url: '*'}, {
1460 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
1464 createContainerRequest(
1466 'test_container_request',
1468 ['echo', 'hello world'],
1470 .as('containerRequest');
1472 cy.getAll('@containerRequest').then(function([containerRequest]) {
1473 cy.goToPath(`/processes/${containerRequest.uuid}`);
1474 cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1475 .parents('[data-cy=process-io-card]').within(() => {
1478 cy.get('tbody tr').each((item) => {
1479 cy.wrap(item).contains('No value');
1482 cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1483 .parents('[data-cy=process-io-card]').within(() => {
1484 cy.get('tbody tr').each((item) => {
1485 cy.wrap(item).contains('No value');