19684: Catch object values in primitive parameters and external values in files/direc...
[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
638     const testOutputs = [
639         {
640             definition: {
641                 "id": "#main/output_file",
642                 "label": "Label Description",
643                 "type": "File"
644             },
645             output: {
646                 "output_file": {
647                     "basename": "cat.png",
648                     "class": "File",
649                     "location": "cat.png"
650                 }
651             }
652         },
653         {
654             definition: {
655                 "id": "#main/output_file_with_secondary",
656                 "doc": "Doc Description",
657                 "type": "File"
658             },
659             output: {
660                 "output_file_with_secondary": {
661                     "basename": "main.dat",
662                     "class": "File",
663                     "location": "main.dat",
664                     "secondaryFiles": [
665                         {
666                             "basename": "secondary.dat",
667                             "class": "File",
668                             "location": "secondary.dat"
669                         },
670                         {
671                             "basename": "secondary2.dat",
672                             "class": "File",
673                             "location": "secondary2.dat"
674                         }
675                     ]
676                 }
677             }
678         },
679         {
680             definition: {
681                 "id": "#main/output_dir",
682                 "doc": ["Doc desc 1", "Doc desc 2"],
683                 "type": "Directory"
684             },
685             output: {
686                 "output_dir": {
687                     "basename": "outdir1",
688                     "class": "Directory",
689                     "location": "outdir1"
690                 }
691             }
692         },
693         {
694             definition: {
695                 "id": "#main/output_bool",
696                 "type": "boolean"
697             },
698             output: {
699                 "output_bool": true
700             }
701         },
702         {
703             definition: {
704                 "id": "#main/output_int",
705                 "type": "int"
706             },
707             output: {
708                 "output_int": 1
709             }
710         },
711         {
712             definition: {
713                 "id": "#main/output_long",
714                 "type": "long"
715             },
716             output: {
717                 "output_long": 1
718             }
719         },
720         {
721             definition: {
722                 "id": "#main/output_float",
723                 "type": "float"
724             },
725             output: {
726                 "output_float": 100.5
727             }
728         },
729         {
730             definition: {
731                 "id": "#main/output_double",
732                 "type": "double"
733             },
734             output: {
735                 "output_double": 100.3
736             }
737         },
738         {
739             definition: {
740                 "id": "#main/output_string",
741                 "type": "string"
742             },
743             output: {
744                 "output_string": "Hello output"
745             }
746         },
747         {
748             definition: {
749                 "id": "#main/output_file_array",
750                 "type": {
751                     "items": "File",
752                     "type": "array"
753                 }
754             },
755             output: {
756                 "output_file_array": [
757                     {
758                         "basename": "output2.tar",
759                         "class": "File",
760                         "location": "output2.tar"
761                     },
762                     {
763                         "basename": "output3.tar",
764                         "class": "File",
765                         "location": "output3.tar"
766                     }
767                 ]
768             }
769         },
770         {
771             definition: {
772                 "id": "#main/output_dir_array",
773                 "type": {
774                     "items": "Directory",
775                     "type": "array"
776                 }
777             },
778             output: {
779                 "output_dir_array": [
780                     {
781                         "basename": "outdir2",
782                         "class": "Directory",
783                         "location": "outdir2"
784                     },
785                     {
786                         "basename": "outdir3",
787                         "class": "Directory",
788                         "location": "outdir3"
789                     }
790                 ]
791             }
792         },
793         {
794             definition: {
795                 "id": "#main/output_int_array",
796                 "type": {
797                     "items": "int",
798                     "type": "array"
799                 }
800             },
801             output: {
802                 "output_int_array": [
803                     10,
804                     11,
805                     12
806                 ]
807             }
808         },
809         {
810             definition: {
811                 "id": "#main/output_long_array",
812                 "type": {
813                     "items": "long",
814                     "type": "array"
815                 }
816             },
817             output: {
818                 "output_long_array": [
819                     51,
820                     52
821                 ]
822             }
823         },
824         {
825             definition: {
826                 "id": "#main/output_float_array",
827                 "type": {
828                     "items": "float",
829                     "type": "array"
830                 }
831             },
832             output: {
833                 "output_float_array": [
834                     100.2,
835                     100.4,
836                     100.6
837                 ]
838             }
839         },
840         {
841             definition: {
842                 "id": "#main/output_double_array",
843                 "type": {
844                     "items": "double",
845                     "type": "array"
846                 }
847             },
848             output: {
849                 "output_double_array": [
850                     100.1,
851                     100.2,
852                     100.3
853                 ]
854             }
855         },
856         {
857             definition: {
858                 "id": "#main/output_string_array",
859                 "type": {
860                     "items": "string",
861                     "type": "array"
862                 }
863             },
864             output: {
865                 "output_string_array": [
866                     "Hello",
867                     "Output",
868                     "!"
869                 ]
870             }
871         }
872     ];
873
874     const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
875         cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
876             label && cy.contains(label);
877
878             if (multipleRows) {
879                 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
880                 if (val) {
881                     if (Array.isArray(val)) {
882                         val.forEach(v => cy.get('@secondaryRows').contains(v));
883                     } else {
884                         cy.get('@secondaryRows').contains(val);
885                     }
886                 }
887                 if (collection) {
888                     cy.get('@secondaryRows').contains(collection);
889                 }
890             } else {
891                 if (val) {
892                     if (Array.isArray(val)) {
893                         val.forEach(v => cy.contains(v));
894                     } else {
895                         cy.contains(val);
896                     }
897                 }
898                 if (collection) {
899                     cy.contains(collection);
900                 }
901             }
902
903
904         });
905     };
906
907     const verifyIOParameterImage = (name, url) => {
908         cy.get('table tr').contains(name).parents('tr').within(() => {
909             cy.get('[alt="Inline Preview"]')
910                 .should('be.visible')
911                 .and(($img) => {
912                     expect($img[0].naturalWidth).to.be.greaterThan(0);
913                     expect($img[0].src).contains(url);
914                 })
915         });
916     };
917
918     it('displays IO parameters with keep links and previews', function() {
919         // Create output collection for real files
920         cy.createCollection(adminUser.token, {
921             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
922             owner_uuid: activeUser.user.uuid,
923         }).then((testOutputCollection) => {
924                     cy.loginAs(activeUser);
925
926                     cy.goToPath(`/collections/${testOutputCollection.uuid}`);
927
928                     cy.get('[data-cy=upload-button]').click();
929
930                     cy.fixture('files/cat.png', 'base64').then(content => {
931                         cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
932                         cy.get('[data-cy=form-submit-btn]').click();
933                         cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
934                         // Confirm final collection state.
935                         cy.get('[data-cy=collection-files-panel]')
936                             .contains('cat.png').should('exist');
937                     });
938
939                     cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
940                 });
941
942         // Get updated collection pdh
943         cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
944             // Add output uuid and inputs to container request
945             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
946                 req.reply((res) => {
947                     res.body.output_uuid = testOutputCollection.uuid;
948                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
949                         content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
950                     };
951                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
952                         content: {
953                             $graph: [{
954                                 id: "#main",
955                                 inputs: testInputs.map((input) => (input.definition)),
956                                 outputs: testOutputs.map((output) => (output.definition))
957                             }]
958                         }
959                     };
960                 });
961             });
962
963             // Stub fake output collection
964             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
965                 statusCode: 200,
966                 body: {
967                     uuid: testOutputCollection.uuid,
968                     portable_data_hash: testOutputCollection.portable_data_hash,
969                 }
970             });
971
972             // Stub fake output json
973             cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
974                 statusCode: 200,
975                 body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
976             });
977
978             // Stub webdav response, points to output json
979             cy.intercept({method: 'PROPFIND', url: '*'}, {
980                 fixture: 'webdav-propfind-outputs.xml',
981             });
982         });
983
984         createContainerRequest(
985             activeUser,
986             'test_container_request',
987             'arvados/jobs',
988             ['echo', 'hello world'],
989             false, 'Committed')
990         .as('containerRequest');
991
992         cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
993             cy.goToPath(`/processes/${containerRequest.uuid}`);
994             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
995                 .parents('[data-cy=process-io-card]').within(() => {
996                     verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
997                     verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
998                     verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
999                     verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
1000                     verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
1001                     verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
1002                     verifyIOParameter('input_int', null, null, '1');
1003                     verifyIOParameter('input_long', null, null, '1');
1004                     verifyIOParameter('input_float', null, null, '1.5');
1005                     verifyIOParameter('input_double', null, null, '1.3');
1006                     verifyIOParameter('input_string', null, null, 'Hello World');
1007                     verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
1008                     verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
1009                     verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
1010                     verifyIOParameter('input_file_array', null, null, 'Cannot display value', undefined, true);
1011                     verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
1012                     verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
1013                     verifyIOParameter('input_dir_array', null, null, 'Cannot display value', undefined, true);
1014                     verifyIOParameter('input_int_array', null, null, ["1", "3", "5", "Cannot display value"]);
1015                     verifyIOParameter('input_long_array', null, null, ["10", "20", "Cannot display value"]);
1016                     verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1017                     verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1018                     verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!", "Cannot display value"]);
1019                     verifyIOParameter('input_bool_include', null, null, "Cannot display value");
1020                     verifyIOParameter('input_int_include', null, null, "Cannot display value");
1021                     verifyIOParameter('input_float_include', null, null, "Cannot display value");
1022                     verifyIOParameter('input_string_include', null, null, "Cannot display value");
1023                     verifyIOParameter('input_file_include', null, null, "Cannot display value");
1024                     verifyIOParameter('input_directory_include', null, null, "Cannot display value");
1025                 });
1026             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1027                 .parents('[data-cy=process-io-card]').within((ctx) => {
1028                     cy.get(ctx).scrollIntoView();
1029                     cy.get('[data-cy="io-preview-image-toggle"]').click();
1030                     const outPdh = testOutputCollection.portable_data_hash;
1031
1032                     verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
1033                     verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
1034                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
1035                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
1036                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
1037                     verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
1038                     verifyIOParameter('output_bool', null, null, 'true');
1039                     verifyIOParameter('output_int', null, null, '1');
1040                     verifyIOParameter('output_long', null, null, '1');
1041                     verifyIOParameter('output_float', null, null, '100.5');
1042                     verifyIOParameter('output_double', null, null, '100.3');
1043                     verifyIOParameter('output_string', null, null, 'Hello output');
1044                     verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
1045                     verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
1046                     verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
1047                     verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
1048                     verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
1049                     verifyIOParameter('output_long_array', null, null, ["51", "52"]);
1050                     verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
1051                     verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
1052                     verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
1053                 });
1054         });
1055     });
1056
1057     it('displays IO parameters with no value', function() {
1058
1059         const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
1060         const fakeOutputPDH = '11111111111111111111111111111111+99/';
1061
1062         cy.loginAs(activeUser);
1063
1064         // Add output uuid and inputs to container request
1065         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1066             req.reply((res) => {
1067                 res.body.output_uuid = fakeOutputUUID;
1068                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1069                     content: {}
1070                 };
1071                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1072                     content: {
1073                         $graph: [{
1074                             id: "#main",
1075                             inputs: testInputs.map((input) => (input.definition)),
1076                             outputs: testOutputs.map((output) => (output.definition))
1077                         }]
1078                     }
1079                 };
1080             });
1081         });
1082
1083         // Stub fake output collection
1084         cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
1085             statusCode: 200,
1086             body: {
1087                 uuid: fakeOutputUUID,
1088                 portable_data_hash: fakeOutputPDH,
1089             }
1090         });
1091
1092         // Stub fake output json
1093         cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
1094             statusCode: 200,
1095             body: {}
1096         });
1097
1098         cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
1099             // Stub webdav response, points to output json
1100             cy.intercept({method: 'PROPFIND', url: '*'}, {
1101                 statusCode: 200,
1102                 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
1103             });
1104         });
1105
1106         createContainerRequest(
1107             activeUser,
1108             'test_container_request',
1109             'arvados/jobs',
1110             ['echo', 'hello world'],
1111             false, 'Committed')
1112         .as('containerRequest');
1113
1114         cy.getAll('@containerRequest').then(function([containerRequest]) {
1115             cy.goToPath(`/processes/${containerRequest.uuid}`);
1116             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1117                 .parents('[data-cy=process-io-card]').within(() => {
1118                     cy.wait(2000);
1119                     cy.waitForDom();
1120                     cy.get('tbody tr').each((item) => {
1121                         cy.wrap(item).contains('No value');
1122                     });
1123                 });
1124             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1125                 .parents('[data-cy=process-io-card]').within(() => {
1126                     cy.get('tbody tr').each((item) => {
1127                         cy.wrap(item).contains('No value');
1128                     });
1129                 });
1130         });
1131     });
1132
1133 });