02657b3959971583f4d5aa3ad2580903ca860fc9
[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]', {timeout: 7000})
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]', {timeout: 7000}).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]', {timeout: 7000})
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]', {timeout: 7000})
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                         "$import": "import_path"
446                     }
447                 ]
448             }
449         },
450         {
451             definition: {
452                 "id": "#main/input_dir_array",
453                 "type": {
454                   "items": "Directory",
455                   "type": "array"
456                 }
457             },
458             input: {
459                 "input_dir_array": [
460                     {
461                         "basename": "11111111111111111111111111111111+02",
462                         "class": "Directory",
463                         "location": "keep:11111111111111111111111111111111+02"
464                     },
465                     {
466                         "basename": "11111111111111111111111111111111+03",
467                         "class": "Directory",
468                         "location": "keep:11111111111111111111111111111111+03"
469                     },
470                     {
471                         "$import": "import_path"
472                     }
473                 ]
474             }
475         },
476         {
477             definition: {
478                 "id": "#main/input_int_array",
479                 "type": {
480                   "items": "int",
481                   "type": "array"
482                 }
483             },
484             input: {
485                 "input_int_array": [
486                     1,
487                     3,
488                     5,
489                     {
490                         "$import": "import_path"
491                     }
492                 ]
493             }
494         },
495         {
496             definition: {
497                 "id": "#main/input_long_array",
498                 "type": {
499                   "items": "long",
500                   "type": "array"
501                 }
502             },
503             input: {
504                 "input_long_array": [
505                     10,
506                     20,
507                     {
508                         "$import": "import_path"
509                     }
510                 ]
511             }
512         },
513         {
514             definition: {
515                 "id": "#main/input_float_array",
516                 "type": {
517                   "items": "float",
518                   "type": "array"
519                 }
520             },
521             input: {
522                 "input_float_array": [
523                     10.2,
524                     10.4,
525                     10.6,
526                     {
527                         "$import": "import_path"
528                     }
529                 ]
530             }
531         },
532         {
533             definition: {
534                 "id": "#main/input_double_array",
535                 "type": {
536                   "items": "double",
537                   "type": "array"
538                 }
539             },
540             input: {
541                 "input_double_array": [
542                     20.1,
543                     20.2,
544                     20.3,
545                     {
546                         "$import": "import_path"
547                     }
548                 ]
549             }
550         },
551         {
552             definition: {
553                 "id": "#main/input_string_array",
554                 "type": {
555                   "items": "string",
556                   "type": "array"
557                 }
558             },
559             input: {
560                 "input_string_array": [
561                     "Hello",
562                     "World",
563                     "!",
564                     {
565                         "$import": "import_path"
566                     }
567                 ]
568             }
569         },
570         {
571             definition: {
572                 "id": "#main/input_bool_include",
573                 "type": "boolean"
574             },
575             input: {
576                 "input_bool_include": {
577                     "$include": "include_path"
578                 }
579             }
580         },
581         {
582             definition: {
583                 "id": "#main/input_int_include",
584                 "type": "int"
585             },
586             input: {
587                 "input_int_include": {
588                     "$include": "include_path"
589                 }
590             }
591         },
592         {
593             definition: {
594                 "id": "#main/input_float_include",
595                 "type": "float"
596             },
597             input: {
598                 "input_float_include": {
599                     "$include": "include_path"
600                 }
601             }
602         },
603         {
604             definition: {
605                 "id": "#main/input_string_include",
606                 "type": "string"
607             },
608             input: {
609                 "input_string_include": {
610                     "$include": "include_path"
611                 }
612             }
613         },
614         {
615             definition: {
616                 "id": "#main/input_file_include",
617                 "type": "File"
618             },
619             input: {
620                 "input_file_include": {
621                     "$include": "include_path"
622                 }
623             }
624         },
625         {
626             definition: {
627                 "id": "#main/input_directory_include",
628                 "type": "Directory"
629             },
630             input: {
631                 "input_directory_include": {
632                     "$include": "include_path"
633                 }
634             }
635         },
636         {
637             definition: {
638                 "id": "#main/input_file_url",
639                 "type": "File"
640             },
641             input: {
642                 "input_file_url": {
643                     "basename": "index.html",
644                     "class": "File",
645                     "location": "http://example.com/index.html"
646                   }
647             }
648         }
649     ];
650
651     const testOutputs = [
652         {
653             definition: {
654                 "id": "#main/output_file",
655                 "label": "Label Description",
656                 "type": "File"
657             },
658             output: {
659                 "output_file": {
660                     "basename": "cat.png",
661                     "class": "File",
662                     "location": "cat.png"
663                 }
664             }
665         },
666         {
667             definition: {
668                 "id": "#main/output_file_with_secondary",
669                 "doc": "Doc Description",
670                 "type": "File"
671             },
672             output: {
673                 "output_file_with_secondary": {
674                     "basename": "main.dat",
675                     "class": "File",
676                     "location": "main.dat",
677                     "secondaryFiles": [
678                         {
679                             "basename": "secondary.dat",
680                             "class": "File",
681                             "location": "secondary.dat"
682                         },
683                         {
684                             "basename": "secondary2.dat",
685                             "class": "File",
686                             "location": "secondary2.dat"
687                         }
688                     ]
689                 }
690             }
691         },
692         {
693             definition: {
694                 "id": "#main/output_dir",
695                 "doc": ["Doc desc 1", "Doc desc 2"],
696                 "type": "Directory"
697             },
698             output: {
699                 "output_dir": {
700                     "basename": "outdir1",
701                     "class": "Directory",
702                     "location": "outdir1"
703                 }
704             }
705         },
706         {
707             definition: {
708                 "id": "#main/output_bool",
709                 "type": "boolean"
710             },
711             output: {
712                 "output_bool": true
713             }
714         },
715         {
716             definition: {
717                 "id": "#main/output_int",
718                 "type": "int"
719             },
720             output: {
721                 "output_int": 1
722             }
723         },
724         {
725             definition: {
726                 "id": "#main/output_long",
727                 "type": "long"
728             },
729             output: {
730                 "output_long": 1
731             }
732         },
733         {
734             definition: {
735                 "id": "#main/output_float",
736                 "type": "float"
737             },
738             output: {
739                 "output_float": 100.5
740             }
741         },
742         {
743             definition: {
744                 "id": "#main/output_double",
745                 "type": "double"
746             },
747             output: {
748                 "output_double": 100.3
749             }
750         },
751         {
752             definition: {
753                 "id": "#main/output_string",
754                 "type": "string"
755             },
756             output: {
757                 "output_string": "Hello output"
758             }
759         },
760         {
761             definition: {
762                 "id": "#main/output_file_array",
763                 "type": {
764                     "items": "File",
765                     "type": "array"
766                 }
767             },
768             output: {
769                 "output_file_array": [
770                     {
771                         "basename": "output2.tar",
772                         "class": "File",
773                         "location": "output2.tar"
774                     },
775                     {
776                         "basename": "output3.tar",
777                         "class": "File",
778                         "location": "output3.tar"
779                     }
780                 ]
781             }
782         },
783         {
784             definition: {
785                 "id": "#main/output_dir_array",
786                 "type": {
787                     "items": "Directory",
788                     "type": "array"
789                 }
790             },
791             output: {
792                 "output_dir_array": [
793                     {
794                         "basename": "outdir2",
795                         "class": "Directory",
796                         "location": "outdir2"
797                     },
798                     {
799                         "basename": "outdir3",
800                         "class": "Directory",
801                         "location": "outdir3"
802                     }
803                 ]
804             }
805         },
806         {
807             definition: {
808                 "id": "#main/output_int_array",
809                 "type": {
810                     "items": "int",
811                     "type": "array"
812                 }
813             },
814             output: {
815                 "output_int_array": [
816                     10,
817                     11,
818                     12
819                 ]
820             }
821         },
822         {
823             definition: {
824                 "id": "#main/output_long_array",
825                 "type": {
826                     "items": "long",
827                     "type": "array"
828                 }
829             },
830             output: {
831                 "output_long_array": [
832                     51,
833                     52
834                 ]
835             }
836         },
837         {
838             definition: {
839                 "id": "#main/output_float_array",
840                 "type": {
841                     "items": "float",
842                     "type": "array"
843                 }
844             },
845             output: {
846                 "output_float_array": [
847                     100.2,
848                     100.4,
849                     100.6
850                 ]
851             }
852         },
853         {
854             definition: {
855                 "id": "#main/output_double_array",
856                 "type": {
857                     "items": "double",
858                     "type": "array"
859                 }
860             },
861             output: {
862                 "output_double_array": [
863                     100.1,
864                     100.2,
865                     100.3
866                 ]
867             }
868         },
869         {
870             definition: {
871                 "id": "#main/output_string_array",
872                 "type": {
873                     "items": "string",
874                     "type": "array"
875                 }
876             },
877             output: {
878                 "output_string_array": [
879                     "Hello",
880                     "Output",
881                     "!"
882                 ]
883             }
884         }
885     ];
886
887     const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
888         cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
889             label && cy.contains(label);
890
891             if (multipleRows) {
892                 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
893                 if (val) {
894                     if (Array.isArray(val)) {
895                         val.forEach(v => cy.get('@secondaryRows').contains(v));
896                     } else {
897                         cy.get('@secondaryRows').contains(val);
898                     }
899                 }
900                 if (collection) {
901                     cy.get('@secondaryRows').contains(collection);
902                 }
903             } else {
904                 if (val) {
905                     if (Array.isArray(val)) {
906                         val.forEach(v => cy.contains(v));
907                     } else {
908                         cy.contains(val);
909                     }
910                 }
911                 if (collection) {
912                     cy.contains(collection);
913                 }
914             }
915
916
917         });
918     };
919
920     const verifyIOParameterImage = (name, url) => {
921         cy.get('table tr').contains(name).parents('tr').within(() => {
922             cy.get('[alt="Inline Preview"]')
923                 .should('be.visible')
924                 .and(($img) => {
925                     expect($img[0].naturalWidth).to.be.greaterThan(0);
926                     expect($img[0].src).contains(url);
927                 })
928         });
929     };
930
931     it('displays IO parameters with keep links and previews', function() {
932         // Create output collection for real files
933         cy.createCollection(adminUser.token, {
934             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
935             owner_uuid: activeUser.user.uuid,
936         }).then((testOutputCollection) => {
937                     cy.loginAs(activeUser);
938
939                     cy.goToPath(`/collections/${testOutputCollection.uuid}`);
940
941                     cy.get('[data-cy=upload-button]').click();
942
943                     cy.fixture('files/cat.png', 'base64').then(content => {
944                         cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
945                         cy.get('[data-cy=form-submit-btn]').click();
946                         cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
947                         // Confirm final collection state.
948                         cy.get('[data-cy=collection-files-panel]')
949                             .contains('cat.png').should('exist');
950                     });
951
952                     cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
953                 });
954
955         // Get updated collection pdh
956         cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
957             // Add output uuid and inputs to container request
958             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
959                 req.reply((res) => {
960                     res.body.output_uuid = testOutputCollection.uuid;
961                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
962                         content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
963                     };
964                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
965                         content: {
966                             $graph: [{
967                                 id: "#main",
968                                 inputs: testInputs.map((input) => (input.definition)),
969                                 outputs: testOutputs.map((output) => (output.definition))
970                             }]
971                         }
972                     };
973                 });
974             });
975
976             // Stub fake output collection
977             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
978                 statusCode: 200,
979                 body: {
980                     uuid: testOutputCollection.uuid,
981                     portable_data_hash: testOutputCollection.portable_data_hash,
982                 }
983             });
984
985             // Stub fake output json
986             cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
987                 statusCode: 200,
988                 body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
989             });
990
991             // Stub webdav response, points to output json
992             cy.intercept({method: 'PROPFIND', url: '*'}, {
993                 fixture: 'webdav-propfind-outputs.xml',
994             });
995         });
996
997         createContainerRequest(
998             activeUser,
999             'test_container_request',
1000             'arvados/jobs',
1001             ['echo', 'hello world'],
1002             false, 'Committed')
1003         .as('containerRequest');
1004
1005         cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
1006             cy.goToPath(`/processes/${containerRequest.uuid}`);
1007             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1008                 .parents('[data-cy=process-io-card]').within(() => {
1009                     verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
1010                     verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
1011                     verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
1012                     verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
1013                     verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
1014                     verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
1015                     verifyIOParameter('input_int', null, null, '1');
1016                     verifyIOParameter('input_long', null, null, '1');
1017                     verifyIOParameter('input_float', null, null, '1.5');
1018                     verifyIOParameter('input_double', null, null, '1.3');
1019                     verifyIOParameter('input_string', null, null, 'Hello World');
1020                     verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
1021                     verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
1022                     verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
1023                     verifyIOParameter('input_file_array', null, null, 'Cannot display value', undefined, true);
1024                     verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
1025                     verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
1026                     verifyIOParameter('input_dir_array', null, null, 'Cannot display value', undefined, true);
1027                     verifyIOParameter('input_int_array', null, null, ["1", "3", "5", "Cannot display value"]);
1028                     verifyIOParameter('input_long_array', null, null, ["10", "20", "Cannot display value"]);
1029                     verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1030                     verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1031                     verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!", "Cannot display value"]);
1032                     verifyIOParameter('input_bool_include', null, null, "Cannot display value");
1033                     verifyIOParameter('input_int_include', null, null, "Cannot display value");
1034                     verifyIOParameter('input_float_include', null, null, "Cannot display value");
1035                     verifyIOParameter('input_string_include', null, null, "Cannot display value");
1036                     verifyIOParameter('input_file_include', null, null, "Cannot display value");
1037                     verifyIOParameter('input_directory_include', null, null, "Cannot display value");
1038                     verifyIOParameter('input_file_url', null, null, "http://example.com/index.html");
1039                 });
1040             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1041                 .parents('[data-cy=process-io-card]').within((ctx) => {
1042                     cy.get(ctx).scrollIntoView();
1043                     cy.get('[data-cy="io-preview-image-toggle"]').click();
1044                     const outPdh = testOutputCollection.portable_data_hash;
1045
1046                     verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
1047                     verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
1048                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
1049                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
1050                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
1051                     verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
1052                     verifyIOParameter('output_bool', null, null, 'true');
1053                     verifyIOParameter('output_int', null, null, '1');
1054                     verifyIOParameter('output_long', null, null, '1');
1055                     verifyIOParameter('output_float', null, null, '100.5');
1056                     verifyIOParameter('output_double', null, null, '100.3');
1057                     verifyIOParameter('output_string', null, null, 'Hello output');
1058                     verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
1059                     verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
1060                     verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
1061                     verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
1062                     verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
1063                     verifyIOParameter('output_long_array', null, null, ["51", "52"]);
1064                     verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
1065                     verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
1066                     verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
1067                 });
1068         });
1069     });
1070
1071     it('displays IO parameters with no value', function() {
1072
1073         const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
1074         const fakeOutputPDH = '11111111111111111111111111111111+99/';
1075
1076         cy.loginAs(activeUser);
1077
1078         // Add output uuid and inputs to container request
1079         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1080             req.reply((res) => {
1081                 res.body.output_uuid = fakeOutputUUID;
1082                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1083                     content: {}
1084                 };
1085                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1086                     content: {
1087                         $graph: [{
1088                             id: "#main",
1089                             inputs: testInputs.map((input) => (input.definition)),
1090                             outputs: testOutputs.map((output) => (output.definition))
1091                         }]
1092                     }
1093                 };
1094             });
1095         });
1096
1097         // Stub fake output collection
1098         cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
1099             statusCode: 200,
1100             body: {
1101                 uuid: fakeOutputUUID,
1102                 portable_data_hash: fakeOutputPDH,
1103             }
1104         });
1105
1106         // Stub fake output json
1107         cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
1108             statusCode: 200,
1109             body: {}
1110         });
1111
1112         cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
1113             // Stub webdav response, points to output json
1114             cy.intercept({method: 'PROPFIND', url: '*'}, {
1115                 statusCode: 200,
1116                 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
1117             });
1118         });
1119
1120         createContainerRequest(
1121             activeUser,
1122             'test_container_request',
1123             'arvados/jobs',
1124             ['echo', 'hello world'],
1125             false, 'Committed')
1126         .as('containerRequest');
1127
1128         cy.getAll('@containerRequest').then(function([containerRequest]) {
1129             cy.goToPath(`/processes/${containerRequest.uuid}`);
1130             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1131                 .parents('[data-cy=process-io-card]').within(() => {
1132                     cy.wait(2000);
1133                     cy.waitForDom();
1134                     cy.get('tbody tr').each((item) => {
1135                         cy.wrap(item).contains('No value');
1136                     });
1137                 });
1138             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1139                 .parents('[data-cy=process-io-card]').within(() => {
1140                     cy.get('tbody tr').each((item) => {
1141                         cy.wrap(item).contains('No value');
1142                     });
1143                 });
1144         });
1145     });
1146
1147
1148     it('allows copying processes', function() {
1149         const crName = 'first_container_request';
1150         const copiedCrName = 'copied_container_request';
1151         createContainerRequest(
1152             activeUser,
1153             crName,
1154             'arvados/jobs',
1155             ['echo', 'hello world'],
1156             false, 'Committed')
1157         .then(function(containerRequest) {
1158             cy.loginAs(activeUser);
1159             cy.goToPath(`/processes/${containerRequest.uuid}`);
1160             cy.get('[data-cy=process-details]').should('contain', crName);
1161
1162             cy.get('[data-cy=process-details]').find('button[title="More options"]').click();
1163             cy.get('ul[data-cy=context-menu]').contains("Copy and re-run process").click();
1164         });
1165
1166         cy.get('[data-cy=form-dialog]').within(() => {
1167             cy.get('input[name=name]').clear().type(copiedCrName);
1168             cy.get('[data-cy=projects-tree-home-tree-picker]').click();
1169             cy.get('[data-cy=form-submit-btn]').click();
1170         });
1171
1172         cy.get('[data-cy=process-details]').should('contain', copiedCrName);
1173         cy.get('[data-cy=process-details]').find('button').contains('Run Process');
1174     });
1175
1176 });