071224323f160d217fb923b860d64ea7a5dd7108
[arvados-workbench2.git] / cypress / integration / process.spec.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 describe('Process tests', function() {
6     let activeUser;
7     let adminUser;
8
9     before(function() {
10         // Only set up common users once. These aren't set up as aliases because
11         // aliases are cleaned up after every test. Also it doesn't make sense
12         // to set the same users on beforeEach() over and over again, so we
13         // separate a little from Cypress' 'Best Practices' here.
14         cy.getUser('admin', 'Admin', 'User', true, true)
15             .as('adminUser').then(function() {
16                 adminUser = this.adminUser;
17             }
18         );
19         cy.getUser('user', 'Active', 'User', false, true)
20             .as('activeUser').then(function() {
21                 activeUser = this.activeUser;
22             }
23         );
24     });
25
26     beforeEach(function() {
27         cy.clearCookies();
28         cy.clearLocalStorage();
29     });
30
31     function setupDockerImage(image_name) {
32         // Create a collection that will be used as a docker image for the tests.
33         cy.createCollection(adminUser.token, {
34             name: 'docker_image',
35             manifest_text: ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n"
36         }).as('dockerImage').then(function(dockerImage) {
37             // Give read permissions to the active user on the docker image.
38             cy.createLink(adminUser.token, {
39                 link_class: 'permission',
40                 name: 'can_read',
41                 tail_uuid: activeUser.user.uuid,
42                 head_uuid: dockerImage.uuid
43             }).as('dockerImagePermission').then(function() {
44                 // Set-up docker image collection tags
45                 cy.createLink(activeUser.token, {
46                     link_class: 'docker_image_repo+tag',
47                     name: image_name,
48                     head_uuid: dockerImage.uuid,
49                 }).as('dockerImageRepoTag');
50                 cy.createLink(activeUser.token, {
51                     link_class: 'docker_image_hash',
52                     name: 'sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678',
53                     head_uuid: dockerImage.uuid,
54                 }).as('dockerImageHash');
55             })
56         });
57         return cy.getAll('@dockerImage', '@dockerImageRepoTag', '@dockerImageHash',
58             '@dockerImagePermission').then(function([dockerImage]) {
59                 return dockerImage;
60             });
61     }
62
63     function createContainerRequest(user, name, docker_image, command, reuse = false, state = 'Uncommitted') {
64         return setupDockerImage(docker_image).then(function(dockerImage) {
65             return cy.createContainerRequest(user.token, {
66                 name: name,
67                 command: command,
68                 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
69                 output_path: 'stdout.txt',
70                 priority: 1,
71                 runtime_constraints: {
72                     vcpus: 1,
73                     ram: 1,
74                 },
75                 use_existing: reuse,
76                 state: state,
77                 mounts: {
78                     foo: {
79                         kind: 'tmp',
80                         path: '/tmp/foo',
81                     }
82                 }
83             });
84         });
85     }
86
87     it('shows process logs', function() {
88         const crName = 'test_container_request';
89         createContainerRequest(
90             activeUser,
91             crName,
92             'arvados/jobs',
93             ['echo', 'hello world'],
94             false, 'Committed')
95         .then(function(containerRequest) {
96             cy.loginAs(activeUser);
97             cy.goToPath(`/processes/${containerRequest.uuid}`);
98             cy.get('[data-cy=process-details]').should('contain', crName);
99             cy.get('[data-cy=process-logs]')
100                 .should('contain', 'No logs yet')
101                 .and('not.contain', 'hello world');
102             cy.createLog(activeUser.token, {
103                 object_uuid: containerRequest.container_uuid,
104                 properties: {
105                     text: 'hello world'
106                 },
107                 event_type: 'stdout'
108             }).then(function(log) {
109                 cy.get('[data-cy=process-logs]')
110                     .should('not.contain', 'No logs yet')
111                     .and('contain', 'hello world');
112             })
113         });
114     });
115
116     it('shows process details', function() {
117         createContainerRequest(
118             activeUser,
119             `test_container_request ${Math.floor(Math.random() * 999999)}`,
120             'arvados/jobs',
121             ['echo', 'hello world'],
122             false, 'Committed')
123         .then(function(containerRequest) {
124             cy.loginAs(activeUser);
125             cy.goToPath(`/processes/${containerRequest.uuid}`);
126             cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
127             cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
128             cy.get('[data-cy=process-details-attributes-runtime-user]').should('not.exist');
129         });
130
131         // Fake submitted by another user
132         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
133             req.reply((res) => {
134                 res.body.modified_by_user_uuid = 'zzzzz-tpzed-000000000000000';
135             });
136         });
137
138         createContainerRequest(
139             activeUser,
140             `test_container_request ${Math.floor(Math.random() * 999999)}`,
141             'arvados/jobs',
142             ['echo', 'hello world'],
143             false, 'Committed')
144         .then(function(containerRequest) {
145             cy.loginAs(activeUser);
146             cy.goToPath(`/processes/${containerRequest.uuid}`);
147             cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
148             cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`zzzzz-tpzed-000000000000000`);
149             cy.get('[data-cy=process-details-attributes-runtime-user]').contains(`Active User (${activeUser.user.uuid})`);
150         });
151     });
152
153     it('filters process logs by event type', function() {
154         const nodeInfoLogs = [
155             'Host Information',
156             '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',
157             'CPU Information',
158             'processor  : 0',
159             'vendor_id  : GenuineIntel',
160             'cpu family : 6',
161             'model      : 79',
162             'model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz'
163         ];
164         const crunchRunLogs = [
165             '2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection',
166             '2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started',
167             '2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)',
168             '2022-03-22T13:56:26.244862836Z Executing container \'zzzzz-dz642-1wokwvcct9s9du3\' using docker runtime',
169             '2022-03-22T13:56:26.245037738Z Executing on host \'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p\'',
170         ];
171         const stdoutLogs = [
172             'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.',
173             'Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.',
174             'In hac habitasse platea dictumst.',
175             'Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.',
176             'Interdum et malesuada fames ac ante ipsum primis in faucibus.',
177             'Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.',
178             'Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.',
179             'Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.',
180             'Donec vitae leo id augue gravida bibendum.',
181             'Nam libero libero, pretium ac faucibus elementum, mattis nec ex.',
182             'Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.',
183             'Aliquam viverra nisi nulla, et efficitur dolor mattis in.',
184             '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.',
185             '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.',
186             'Phasellus non ex quis arcu tempus faucibus molestie in sapien.',
187             'Duis tristique semper dolor, vitae pulvinar risus.',
188             'Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.',
189             'Nulla eget mollis ipsum.',
190         ];
191
192         createContainerRequest(
193             activeUser,
194             'test_container_request',
195             'arvados/jobs',
196             ['echo', 'hello world'],
197             false, 'Committed')
198         .then(function(containerRequest) {
199             cy.logsForContainer(activeUser.token, containerRequest.container_uuid,
200                 'node-info', nodeInfoLogs).as('nodeInfoLogs');
201             cy.logsForContainer(activeUser.token, containerRequest.container_uuid,
202                 'crunch-run', crunchRunLogs).as('crunchRunLogs');
203             cy.logsForContainer(activeUser.token, containerRequest.container_uuid,
204                 'stdout', stdoutLogs).as('stdoutLogs');
205             cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
206                 cy.loginAs(activeUser);
207                 cy.goToPath(`/processes/${containerRequest.uuid}`);
208                 // Should show main logs by default
209                 cy.get('[data-cy=process-logs-filter]').should('contain', 'Main logs');
210                 cy.get('[data-cy=process-logs]')
211                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
212                     .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
213                     .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
214                 // Select 'All logs'
215                 cy.get('[data-cy=process-logs-filter]').click();
216                 cy.get('body').contains('li', 'All logs').click();
217                 cy.get('[data-cy=process-logs]')
218                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
219                     .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
220                     .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
221                 // Select 'node-info' logs
222                 cy.get('[data-cy=process-logs-filter]').click();
223                 cy.get('body').contains('li', 'node-info').click();
224                 cy.get('[data-cy=process-logs]')
225                     .should('not.contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
226                     .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
227                     .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
228                 // Select 'stdout' logs
229                 cy.get('[data-cy=process-logs-filter]').click();
230                 cy.get('body').contains('li', 'stdout').click();
231                 cy.get('[data-cy=process-logs]')
232                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
233                     .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
234                     .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
235             });
236         });
237     });
238
239     it('should show runtime status indicators', function() {
240         // Setup running container with runtime_status error & warning messages
241         createContainerRequest(
242             activeUser,
243             'test_container_request',
244             'arvados/jobs',
245             ['echo', 'hello world'],
246             false, 'Committed')
247         .as('containerRequest')
248         .then(function(containerRequest) {
249             expect(containerRequest.state).to.equal('Committed');
250             expect(containerRequest.container_uuid).not.to.be.equal('');
251
252             cy.getContainer(activeUser.token, containerRequest.container_uuid)
253             .then(function(queuedContainer) {
254                 expect(queuedContainer.state).to.be.equal('Queued');
255             });
256             cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
257                 state: 'Locked'
258             }).then(function(lockedContainer) {
259                 expect(lockedContainer.state).to.be.equal('Locked');
260
261                 cy.updateContainer(adminUser.token, lockedContainer.uuid, {
262                     state: 'Running',
263                     runtime_status: {
264                         error: 'Something went wrong',
265                         errorDetail: 'Process exited with status 1',
266                         warning: 'Free disk space is low',
267                     }
268                 })
269                 .as('runningContainer')
270                 .then(function(runningContainer) {
271                     expect(runningContainer.state).to.be.equal('Running');
272                     expect(runningContainer.runtime_status).to.be.deep.equal({
273                         'error': 'Something went wrong',
274                         'errorDetail': 'Process exited with status 1',
275                         'warning': 'Free disk space is low',
276                     });
277                 });
278             })
279         });
280         // Test that the UI shows the error and warning messages
281         cy.getAll('@containerRequest', '@runningContainer').then(function([containerRequest]) {
282             cy.loginAs(activeUser);
283             cy.goToPath(`/processes/${containerRequest.uuid}`);
284             cy.get('[data-cy=process-runtime-status-error]')
285                 .should('contain', 'Something went wrong')
286                 .and('contain', 'Process exited with status 1');
287             cy.get('[data-cy=process-runtime-status-warning]')
288                 .should('contain', 'Free disk space is low')
289                 .and('contain', 'No additional warning details available');
290         });
291
292
293         // Force container_count for testing
294         let containerCount = 2;
295         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
296             req.reply((res) => {
297                 res.body.container_count = containerCount;
298             });
299         });
300
301         cy.getAll('@containerRequest').then(function([containerRequest]) {
302             cy.goToPath(`/processes/${containerRequest.uuid}`);
303             cy.get('[data-cy=process-runtime-status-retry-warning]')
304                 .should('contain', 'Process retried 1 time');
305         });
306
307         cy.getAll('@containerRequest').then(function([containerRequest]) {
308             containerCount = 3;
309             cy.goToPath(`/processes/${containerRequest.uuid}`);
310             cy.get('[data-cy=process-runtime-status-retry-warning]')
311                 .should('contain', 'Process retried 2 times');
312         });
313     });
314
315
316     const testInputs = [
317         {
318             definition: {
319                 "id": "#main/input_file",
320                 "label": "Label Description",
321                 "type": "File"
322             },
323             input: {
324                 "input_file": {
325                     "basename": "input1.tar",
326                     "class": "File",
327                     "location": "keep:00000000000000000000000000000000+01/input1.tar",
328                     "secondaryFiles": [
329                         {
330                             "basename": "input1-2.txt",
331                             "class": "File",
332                             "location": "keep:00000000000000000000000000000000+01/input1-2.txt"
333                         },
334                         {
335                             "basename": "input1-3.txt",
336                             "class": "File",
337                             "location": "keep:00000000000000000000000000000000+01/input1-3.txt"
338                         },
339                         {
340                             "basename": "input1-4.txt",
341                             "class": "File",
342                             "location": "keep:00000000000000000000000000000000+01/input1-4.txt"
343                         }
344                     ]
345                 }
346             }
347         },
348         {
349             definition: {
350                 "id": "#main/input_dir",
351                 "doc": "Doc Description",
352                 "type": "Directory"
353             },
354             input: {
355                 "input_dir": {
356                     "basename": "11111111111111111111111111111111+01",
357                     "class": "Directory",
358                     "location": "keep:11111111111111111111111111111111+01"
359                 }
360             }
361         },
362         {
363             definition: {
364                 "id": "#main/input_bool",
365                 "doc": ["Doc desc 1", "Doc desc 2"],
366                 "type": "boolean"
367             },
368             input: {
369                 "input_bool": true,
370             }
371         },
372         {
373             definition: {
374                 "id": "#main/input_int",
375                 "type": "int"
376             },
377             input: {
378                 "input_int": 1,
379             }
380         },
381         {
382             definition: {
383                 "id": "#main/input_long",
384                 "type": "long"
385             },
386             input: {
387                 "input_long" : 1,
388             }
389         },
390         {
391             definition: {
392                 "id": "#main/input_float",
393                 "type": "float"
394             },
395             input: {
396                 "input_float": 1.5,
397             }
398         },
399         {
400             definition: {
401                 "id": "#main/input_double",
402                 "type": "double"
403             },
404             input: {
405                 "input_double": 1.3,
406             }
407         },
408         {
409             definition: {
410                 "id": "#main/input_string",
411                 "type": "string"
412             },
413             input: {
414                 "input_string": "Hello World",
415             }
416         },
417         {
418             definition: {
419                 "id": "#main/input_file_array",
420                 "type": {
421                   "items": "File",
422                   "type": "array"
423                 }
424             },
425             input: {
426                 "input_file_array": [
427                     {
428                         "basename": "input2.tar",
429                         "class": "File",
430                         "location": "keep:00000000000000000000000000000000+02/input2.tar"
431                     },
432                     {
433                         "basename": "input3.tar",
434                         "class": "File",
435                         "location": "keep:00000000000000000000000000000000+03/input3.tar",
436                         "secondaryFiles": [
437                             {
438                                 "basename": "input3-2.txt",
439                                 "class": "File",
440                                 "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
441                             }
442                         ]
443                     }
444                 ]
445             }
446         },
447         {
448             definition: {
449                 "id": "#main/input_dir_array",
450                 "type": {
451                   "items": "Directory",
452                   "type": "array"
453                 }
454             },
455             input: {
456                 "input_dir_array": [
457                     {
458                         "basename": "11111111111111111111111111111111+02",
459                         "class": "Directory",
460                         "location": "keep:11111111111111111111111111111111+02"
461                     },
462                     {
463                         "basename": "11111111111111111111111111111111+03",
464                         "class": "Directory",
465                         "location": "keep:11111111111111111111111111111111+03"
466                     }
467                 ]
468             }
469         },
470         {
471             definition: {
472                 "id": "#main/input_int_array",
473                 "type": {
474                   "items": "int",
475                   "type": "array"
476                 }
477             },
478             input: {
479                 "input_int_array": [
480                     1,
481                     3,
482                     5
483                 ]
484             }
485         },
486         {
487             definition: {
488                 "id": "#main/input_long_array",
489                 "type": {
490                   "items": "long",
491                   "type": "array"
492                 }
493             },
494             input: {
495                 "input_long_array": [
496                     10,
497                     20
498                 ]
499             }
500         },
501         {
502             definition: {
503                 "id": "#main/input_float_array",
504                 "type": {
505                   "items": "float",
506                   "type": "array"
507                 }
508             },
509             input: {
510                 "input_float_array": [
511                     10.2,
512                     10.4,
513                     10.6
514                 ]
515             }
516         },
517         {
518             definition: {
519                 "id": "#main/input_double_array",
520                 "type": {
521                   "items": "double",
522                   "type": "array"
523                 }
524             },
525             input: {
526                 "input_double_array": [
527                     20.1,
528                     20.2,
529                     20.3
530                 ]
531             }
532         },
533         {
534             definition: {
535                 "id": "#main/input_string_array",
536                 "type": {
537                   "items": "string",
538                   "type": "array"
539                 }
540             },
541             input: {
542                 "input_string_array": [
543                     "Hello",
544                     "World",
545                     "!"
546                 ]
547             }
548         }
549     ];
550
551     const testOutputs = [
552         {
553             definition: {
554                 "id": "#main/output_file",
555                 "label": "Label Description",
556                 "type": "File"
557             },
558             output: {
559                 "output_file": {
560                     "basename": "cat.png",
561                     "class": "File",
562                     "location": "cat.png"
563                 }
564             }
565         },
566         {
567             definition: {
568                 "id": "#main/output_file_with_secondary",
569                 "doc": "Doc Description",
570                 "type": "File"
571             },
572             output: {
573                 "output_file_with_secondary": {
574                     "basename": "main.dat",
575                     "class": "File",
576                     "location": "main.dat",
577                     "secondaryFiles": [
578                         {
579                             "basename": "secondary.dat",
580                             "class": "File",
581                             "location": "secondary.dat"
582                         },
583                         {
584                             "basename": "secondary2.dat",
585                             "class": "File",
586                             "location": "secondary2.dat"
587                         }
588                     ]
589                 }
590             }
591         },
592         {
593             definition: {
594                 "id": "#main/output_dir",
595                 "doc": ["Doc desc 1", "Doc desc 2"],
596                 "type": "Directory"
597             },
598             output: {
599                 "output_dir": {
600                     "basename": "outdir1",
601                     "class": "Directory",
602                     "location": "outdir1"
603                 }
604             }
605         },
606         {
607             definition: {
608                 "id": "#main/output_bool",
609                 "type": "boolean"
610             },
611             output: {
612                 "output_bool": true
613             }
614         },
615         {
616             definition: {
617                 "id": "#main/output_int",
618                 "type": "int"
619             },
620             output: {
621                 "output_int": 1
622             }
623         },
624         {
625             definition: {
626                 "id": "#main/output_long",
627                 "type": "long"
628             },
629             output: {
630                 "output_long": 1
631             }
632         },
633         {
634             definition: {
635                 "id": "#main/output_float",
636                 "type": "float"
637             },
638             output: {
639                 "output_float": 100.5
640             }
641         },
642         {
643             definition: {
644                 "id": "#main/output_double",
645                 "type": "double"
646             },
647             output: {
648                 "output_double": 100.3
649             }
650         },
651         {
652             definition: {
653                 "id": "#main/output_string",
654                 "type": "string"
655             },
656             output: {
657                 "output_string": "Hello output"
658             }
659         },
660         {
661             definition: {
662                 "id": "#main/output_file_array",
663                 "type": {
664                     "items": "File",
665                     "type": "array"
666                 }
667             },
668             output: {
669                 "output_file_array": [
670                     {
671                         "basename": "output2.tar",
672                         "class": "File",
673                         "location": "output2.tar"
674                     },
675                     {
676                         "basename": "output3.tar",
677                         "class": "File",
678                         "location": "output3.tar"
679                     }
680                 ]
681             }
682         },
683         {
684             definition: {
685                 "id": "#main/output_dir_array",
686                 "type": {
687                     "items": "Directory",
688                     "type": "array"
689                 }
690             },
691             output: {
692                 "output_dir_array": [
693                     {
694                         "basename": "outdir2",
695                         "class": "Directory",
696                         "location": "outdir2"
697                     },
698                     {
699                         "basename": "outdir3",
700                         "class": "Directory",
701                         "location": "outdir3"
702                     }
703                 ]
704             }
705         },
706         {
707             definition: {
708                 "id": "#main/output_int_array",
709                 "type": {
710                     "items": "int",
711                     "type": "array"
712                 }
713             },
714             output: {
715                 "output_int_array": [
716                     10,
717                     11,
718                     12
719                 ]
720             }
721         },
722         {
723             definition: {
724                 "id": "#main/output_long_array",
725                 "type": {
726                     "items": "long",
727                     "type": "array"
728                 }
729             },
730             output: {
731                 "output_long_array": [
732                     51,
733                     52
734                 ]
735             }
736         },
737         {
738             definition: {
739                 "id": "#main/output_float_array",
740                 "type": {
741                     "items": "float",
742                     "type": "array"
743                 }
744             },
745             output: {
746                 "output_float_array": [
747                     100.2,
748                     100.4,
749                     100.6
750                 ]
751             }
752         },
753         {
754             definition: {
755                 "id": "#main/output_double_array",
756                 "type": {
757                     "items": "double",
758                     "type": "array"
759                 }
760             },
761             output: {
762                 "output_double_array": [
763                     100.1,
764                     100.2,
765                     100.3
766                 ]
767             }
768         },
769         {
770             definition: {
771                 "id": "#main/output_string_array",
772                 "type": {
773                     "items": "string",
774                     "type": "array"
775                 }
776             },
777             output: {
778                 "output_string_array": [
779                     "Hello",
780                     "Output",
781                     "!"
782                 ]
783             }
784         }
785     ];
786
787     const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
788         cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
789             label && cy.contains(label);
790
791             if (multipleRows) {
792                 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
793                 if (val) {
794                     if (Array.isArray(val)) {
795                         val.forEach(v => cy.get('@secondaryRows').contains(v));
796                     } else {
797                         cy.get('@secondaryRows').contains(val);
798                     }
799                 }
800                 if (collection) {
801                     cy.get('@secondaryRows').contains(collection);
802                 }
803             } else {
804                 if (val) {
805                     if (Array.isArray(val)) {
806                         val.forEach(v => cy.contains(v));
807                     } else {
808                         cy.contains(val);
809                     }
810                 }
811                 if (collection) {
812                     cy.contains(collection);
813                 }
814             }
815
816
817         });
818     };
819
820     const verifyIOParameterImage = (name, url) => {
821         cy.get('table tr').contains(name).parents('tr').within(() => {
822             cy.get('[alt="Inline Preview"]')
823                 .should('be.visible')
824                 .and(($img) => {
825                     expect($img[0].naturalWidth).to.be.greaterThan(0);
826                     expect($img[0].src).contains(url);
827                 })
828         });
829     };
830
831     it('displays IO parameters with keep links and previews', function() {
832         // Create output collection for real files
833         cy.createCollection(adminUser.token, {
834             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
835             owner_uuid: activeUser.user.uuid,
836         }).then((testOutputCollection) => {
837                     cy.loginAs(activeUser);
838
839                     cy.goToPath(`/collections/${testOutputCollection.uuid}`);
840
841                     cy.get('[data-cy=upload-button]').click();
842
843                     cy.fixture('files/cat.png', 'base64').then(content => {
844                         cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
845                         cy.get('[data-cy=form-submit-btn]').click();
846                         cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
847                         // Confirm final collection state.
848                         cy.get('[data-cy=collection-files-panel]')
849                             .contains('cat.png').should('exist');
850                     });
851
852                     cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
853                 });
854
855         // Get updated collection pdh
856         cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
857             // Add output uuid and inputs to container request
858             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
859                 req.reply((res) => {
860                     res.body.output_uuid = testOutputCollection.uuid;
861                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
862                         content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
863                     };
864                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
865                         content: {
866                             $graph: [{
867                                 id: "#main",
868                                 inputs: testInputs.map((input) => (input.definition)),
869                                 outputs: testOutputs.map((output) => (output.definition))
870                             }]
871                         }
872                     };
873                 });
874             });
875
876             // Stub fake output collection
877             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
878                 statusCode: 200,
879                 body: {
880                     uuid: testOutputCollection.uuid,
881                     portable_data_hash: testOutputCollection.portable_data_hash,
882                 }
883             });
884
885             // Stub fake output json
886             cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
887                 statusCode: 200,
888                 body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
889             });
890
891             // Stub webdav response, points to output json
892             cy.intercept({method: 'PROPFIND', url: '*'}, {
893                 fixture: 'webdav-propfind-outputs.xml',
894             });
895         });
896
897         createContainerRequest(
898             activeUser,
899             'test_container_request',
900             'arvados/jobs',
901             ['echo', 'hello world'],
902             false, 'Committed')
903         .as('containerRequest');
904
905         cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
906             cy.goToPath(`/processes/${containerRequest.uuid}`);
907             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
908                 .parents('[data-cy=process-io-card]').within(() => {
909                     verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
910                     verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
911                     verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
912                     verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
913                     verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
914                     verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
915                     verifyIOParameter('input_int', null, null, '1');
916                     verifyIOParameter('input_long', null, null, '1');
917                     verifyIOParameter('input_float', null, null, '1.5');
918                     verifyIOParameter('input_double', null, null, '1.3');
919                     verifyIOParameter('input_string', null, null, 'Hello World');
920                     verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
921                     verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
922                     verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
923                     verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
924                     verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
925                     verifyIOParameter('input_int_array', null, null, ["1", "3", "5"]);
926                     verifyIOParameter('input_long_array', null, null, ["10", "20"]);
927                     verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6"]);
928                     verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3"]);
929                     verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!"]);
930                 });
931             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
932                 .parents('[data-cy=process-io-card]').within((ctx) => {
933                     cy.get(ctx).scrollIntoView();
934                     cy.get('[data-cy="io-preview-image-toggle"]').click();
935                     const outPdh = testOutputCollection.portable_data_hash;
936
937                     verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
938                     verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
939                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
940                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
941                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
942                     verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
943                     verifyIOParameter('output_bool', null, null, 'true');
944                     verifyIOParameter('output_int', null, null, '1');
945                     verifyIOParameter('output_long', null, null, '1');
946                     verifyIOParameter('output_float', null, null, '100.5');
947                     verifyIOParameter('output_double', null, null, '100.3');
948                     verifyIOParameter('output_string', null, null, 'Hello output');
949                     verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
950                     verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
951                     verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
952                     verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
953                     verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
954                     verifyIOParameter('output_long_array', null, null, ["51", "52"]);
955                     verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
956                     verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
957                     verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
958                 });
959         });
960     });
961
962     it('displays IO parameters with no value', function() {
963
964         const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
965         const fakeOutputPDH = '11111111111111111111111111111111+99/';
966
967         cy.loginAs(activeUser);
968
969         // Add output uuid and inputs to container request
970         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
971             req.reply((res) => {
972                 res.body.output_uuid = fakeOutputUUID;
973                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
974                     content: {}
975                 };
976                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
977                     content: {
978                         $graph: [{
979                             id: "#main",
980                             inputs: testInputs.map((input) => (input.definition)),
981                             outputs: testOutputs.map((output) => (output.definition))
982                         }]
983                     }
984                 };
985             });
986         });
987
988         // Stub fake output collection
989         cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
990             statusCode: 200,
991             body: {
992                 uuid: fakeOutputUUID,
993                 portable_data_hash: fakeOutputPDH,
994             }
995         });
996
997         // Stub fake output json
998         cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
999             statusCode: 200,
1000             body: {}
1001         });
1002
1003         cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
1004             // Stub webdav response, points to output json
1005             cy.intercept({method: 'PROPFIND', url: '*'}, {
1006                 statusCode: 200,
1007                 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
1008             });
1009         });
1010
1011         createContainerRequest(
1012             activeUser,
1013             'test_container_request',
1014             'arvados/jobs',
1015             ['echo', 'hello world'],
1016             false, 'Committed')
1017         .as('containerRequest');
1018
1019         cy.getAll('@containerRequest').then(function([containerRequest]) {
1020             cy.goToPath(`/processes/${containerRequest.uuid}`);
1021             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1022                 .parents('[data-cy=process-io-card]').within(() => {
1023                     cy.wait(2000);
1024                     cy.waitForDom();
1025                     cy.get('tbody tr').each((item) => {
1026                         cy.wrap(item).contains('No value');
1027                     });
1028                 });
1029             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1030                 .parents('[data-cy=process-io-card]').within(() => {
1031                     cy.get('tbody tr').each((item) => {
1032                         cy.wrap(item).contains('No value');
1033                     });
1034                 });
1035         });
1036     });
1037
1038 });