15768: adjusted project spec Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox...
[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")
16             .then(function () {
17                 adminUser = this.adminUser;
18             });
19         cy.getUser("user", "Active", "User", false, true)
20             .as("activeUser")
21             .then(function () {
22                 activeUser = this.activeUser;
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:
36                 ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n",
37         })
38             .as("dockerImage")
39             .then(function (dockerImage) {
40                 // Give read permissions to the active user on the docker image.
41                 cy.createLink(adminUser.token, {
42                     link_class: "permission",
43                     name: "can_read",
44                     tail_uuid: activeUser.user.uuid,
45                     head_uuid: dockerImage.uuid,
46                 })
47                     .as("dockerImagePermission")
48                     .then(function () {
49                         // Set-up docker image collection tags
50                         cy.createLink(activeUser.token, {
51                             link_class: "docker_image_repo+tag",
52                             name: image_name,
53                             head_uuid: dockerImage.uuid,
54                         }).as("dockerImageRepoTag");
55                         cy.createLink(activeUser.token, {
56                             link_class: "docker_image_hash",
57                             name: "sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678",
58                             head_uuid: dockerImage.uuid,
59                         }).as("dockerImageHash");
60                     });
61             });
62         return cy.getAll("@dockerImage", "@dockerImageRepoTag", "@dockerImageHash", "@dockerImagePermission").then(function ([dockerImage]) {
63             return dockerImage;
64         });
65     }
66
67     function createContainerRequest(user, name, docker_image, command, reuse = false, state = "Uncommitted") {
68         return setupDockerImage(docker_image).then(function (dockerImage) {
69             return cy.createContainerRequest(user.token, {
70                 name: name,
71                 command: command,
72                 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
73                 output_path: "stdout.txt",
74                 priority: 1,
75                 runtime_constraints: {
76                     vcpus: 1,
77                     ram: 1,
78                 },
79                 use_existing: reuse,
80                 state: state,
81                 mounts: {
82                     foo: {
83                         kind: "tmp",
84                         path: "/tmp/foo",
85                     },
86                 },
87             });
88         });
89     }
90
91     it("shows process logs", function () {
92         const crName = "test_container_request";
93         createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
94             cy.loginAs(activeUser);
95             cy.goToPath(`/processes/${containerRequest.uuid}`);
96             cy.get("[data-cy=process-details]").should("contain", crName);
97             cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
98             cy.createLog(activeUser.token, {
99                 object_uuid: containerRequest.container_uuid,
100                 properties: {
101                     text: "hello world",
102                 },
103                 event_type: "stdout",
104             }).then(function (log) {
105                 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
106             });
107         });
108     });
109
110     it("shows process details", function () {
111         createContainerRequest(
112             activeUser,
113             `test_container_request ${Math.floor(Math.random() * 999999)}`,
114             "arvados/jobs",
115             ["echo", "hello world"],
116             false,
117             "Committed"
118         ).then(function (containerRequest) {
119             cy.loginAs(activeUser);
120             cy.goToPath(`/processes/${containerRequest.uuid}`);
121             cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
122             cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
123             cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
124         });
125
126         // Fake submitted by another user
127         cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
128             req.reply(res => {
129                 res.body.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
130             });
131         });
132
133         createContainerRequest(
134             activeUser,
135             `test_container_request ${Math.floor(Math.random() * 999999)}`,
136             "arvados/jobs",
137             ["echo", "hello world"],
138             false,
139             "Committed"
140         ).then(function (containerRequest) {
141             cy.loginAs(activeUser);
142             cy.goToPath(`/processes/${containerRequest.uuid}`);
143             cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
144             cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
145             cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
146         });
147     });
148
149     it("filters process logs by event type", function () {
150         const nodeInfoLogs = [
151             "Host Information",
152             "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",
153             "CPU Information",
154             "processor  : 0",
155             "vendor_id  : GenuineIntel",
156             "cpu family : 6",
157             "model      : 79",
158             "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
159         ];
160         const crunchRunLogs = [
161             "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
162             "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
163             "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
164             "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
165             "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
166         ];
167         const stdoutLogs = [
168             "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
169             "Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
170             "In hac habitasse platea dictumst.",
171             "Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
172             "Interdum et malesuada fames ac ante ipsum primis in faucibus.",
173             "Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
174             "Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
175             "Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
176             "Donec vitae leo id augue gravida bibendum.",
177             "Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
178             "Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.",
179             "Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
180             "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.",
181             "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.",
182             "Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
183             "Duis tristique semper dolor, vitae pulvinar risus.",
184             "Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
185             "Nulla eget mollis ipsum.",
186         ];
187
188         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
189             containerRequest
190         ) {
191             cy.logsForContainer(activeUser.token, containerRequest.container_uuid, "node-info", nodeInfoLogs).as("nodeInfoLogs");
192             cy.logsForContainer(activeUser.token, containerRequest.container_uuid, "crunch-run", crunchRunLogs).as("crunchRunLogs");
193             cy.logsForContainer(activeUser.token, containerRequest.container_uuid, "stdout", stdoutLogs).as("stdoutLogs");
194             cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
195                 cy.loginAs(activeUser);
196                 cy.goToPath(`/processes/${containerRequest.uuid}`);
197                 // Should show main logs by default
198                 cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
199                 cy.get("[data-cy=process-logs]")
200                     .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
201                     .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
202                     .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
203                 // Select 'All logs'
204                 cy.get("[data-cy=process-logs-filter]").click();
205                 cy.get("body").contains("li", "All logs").click();
206                 cy.get("[data-cy=process-logs]")
207                     .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
208                     .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
209                     .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
210                 // Select 'node-info' logs
211                 cy.get("[data-cy=process-logs-filter]").click();
212                 cy.get("body").contains("li", "node-info").click();
213                 cy.get("[data-cy=process-logs]")
214                     .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
215                     .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
216                     .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
217                 // Select 'stdout' logs
218                 cy.get("[data-cy=process-logs-filter]").click();
219                 cy.get("body").contains("li", "stdout").click();
220                 cy.get("[data-cy=process-logs]")
221                     .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
222                     .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
223                     .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
224             });
225         });
226     });
227
228     it("should show runtime status indicators", function () {
229         // Setup running container with runtime_status error & warning messages
230         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
231             .as("containerRequest")
232             .then(function (containerRequest) {
233                 expect(containerRequest.state).to.equal("Committed");
234                 expect(containerRequest.container_uuid).not.to.be.equal("");
235
236                 cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
237                     expect(queuedContainer.state).to.be.equal("Queued");
238                 });
239                 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
240                     state: "Locked",
241                 }).then(function (lockedContainer) {
242                     expect(lockedContainer.state).to.be.equal("Locked");
243
244                     cy.updateContainer(adminUser.token, lockedContainer.uuid, {
245                         state: "Running",
246                         runtime_status: {
247                             error: "Something went wrong",
248                             errorDetail: "Process exited with status 1",
249                             warning: "Free disk space is low",
250                         },
251                     })
252                         .as("runningContainer")
253                         .then(function (runningContainer) {
254                             expect(runningContainer.state).to.be.equal("Running");
255                             expect(runningContainer.runtime_status).to.be.deep.equal({
256                                 error: "Something went wrong",
257                                 errorDetail: "Process exited with status 1",
258                                 warning: "Free disk space is low",
259                             });
260                         });
261                 });
262             });
263         // Test that the UI shows the error and warning messages
264         cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
265             cy.loginAs(activeUser);
266             cy.goToPath(`/processes/${containerRequest.uuid}`);
267             cy.get("[data-cy=process-runtime-status-error]").should("contain", "Something went wrong").and("contain", "Process exited with status 1");
268             cy.get("[data-cy=process-runtime-status-warning]")
269                 .should("contain", "Free disk space is low")
270                 .and("contain", "No additional warning details available");
271         });
272
273         // Force container_count for testing
274         let containerCount = 2;
275         cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
276             req.reply(res => {
277                 res.body.container_count = containerCount;
278             });
279         });
280
281         cy.getAll("@containerRequest").then(function ([containerRequest]) {
282             cy.goToPath(`/processes/${containerRequest.uuid}`);
283             cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time");
284         });
285
286         cy.getAll("@containerRequest").then(function ([containerRequest]) {
287             containerCount = 3;
288             cy.goToPath(`/processes/${containerRequest.uuid}`);
289             cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
290         });
291     });
292
293     const testInputs = [
294         {
295             definition: {
296                 id: "#main/input_file",
297                 label: "Label Description",
298                 type: "File",
299             },
300             input: {
301                 input_file: {
302                     basename: "input1.tar",
303                     class: "File",
304                     location: "keep:00000000000000000000000000000000+01/input1.tar",
305                     secondaryFiles: [
306                         {
307                             basename: "input1-2.txt",
308                             class: "File",
309                             location: "keep:00000000000000000000000000000000+01/input1-2.txt",
310                         },
311                         {
312                             basename: "input1-3.txt",
313                             class: "File",
314                             location: "keep:00000000000000000000000000000000+01/input1-3.txt",
315                         },
316                         {
317                             basename: "input1-4.txt",
318                             class: "File",
319                             location: "keep:00000000000000000000000000000000+01/input1-4.txt",
320                         },
321                     ],
322                 },
323             },
324         },
325         {
326             definition: {
327                 id: "#main/input_dir",
328                 doc: "Doc Description",
329                 type: "Directory",
330             },
331             input: {
332                 input_dir: {
333                     basename: "11111111111111111111111111111111+01",
334                     class: "Directory",
335                     location: "keep:11111111111111111111111111111111+01",
336                 },
337             },
338         },
339         {
340             definition: {
341                 id: "#main/input_bool",
342                 doc: ["Doc desc 1", "Doc desc 2"],
343                 type: "boolean",
344             },
345             input: {
346                 input_bool: true,
347             },
348         },
349         {
350             definition: {
351                 id: "#main/input_int",
352                 type: "int",
353             },
354             input: {
355                 input_int: 1,
356             },
357         },
358         {
359             definition: {
360                 id: "#main/input_long",
361                 type: "long",
362             },
363             input: {
364                 input_long: 1,
365             },
366         },
367         {
368             definition: {
369                 id: "#main/input_float",
370                 type: "float",
371             },
372             input: {
373                 input_float: 1.5,
374             },
375         },
376         {
377             definition: {
378                 id: "#main/input_double",
379                 type: "double",
380             },
381             input: {
382                 input_double: 1.3,
383             },
384         },
385         {
386             definition: {
387                 id: "#main/input_string",
388                 type: "string",
389             },
390             input: {
391                 input_string: "Hello World",
392             },
393         },
394         {
395             definition: {
396                 id: "#main/input_file_array",
397                 type: {
398                     items: "File",
399                     type: "array",
400                 },
401             },
402             input: {
403                 input_file_array: [
404                     {
405                         basename: "input2.tar",
406                         class: "File",
407                         location: "keep:00000000000000000000000000000000+02/input2.tar",
408                     },
409                     {
410                         basename: "input3.tar",
411                         class: "File",
412                         location: "keep:00000000000000000000000000000000+03/input3.tar",
413                         secondaryFiles: [
414                             {
415                                 basename: "input3-2.txt",
416                                 class: "File",
417                                 location: "keep:00000000000000000000000000000000+03/input3-2.txt",
418                             },
419                         ],
420                     },
421                     {
422                         $import: "import_path",
423                     },
424                 ],
425             },
426         },
427         {
428             definition: {
429                 id: "#main/input_dir_array",
430                 type: {
431                     items: "Directory",
432                     type: "array",
433                 },
434             },
435             input: {
436                 input_dir_array: [
437                     {
438                         basename: "11111111111111111111111111111111+02",
439                         class: "Directory",
440                         location: "keep:11111111111111111111111111111111+02",
441                     },
442                     {
443                         basename: "11111111111111111111111111111111+03",
444                         class: "Directory",
445                         location: "keep:11111111111111111111111111111111+03",
446                     },
447                     {
448                         $import: "import_path",
449                     },
450                 ],
451             },
452         },
453         {
454             definition: {
455                 id: "#main/input_int_array",
456                 type: {
457                     items: "int",
458                     type: "array",
459                 },
460             },
461             input: {
462                 input_int_array: [
463                     1,
464                     3,
465                     5,
466                     {
467                         $import: "import_path",
468                     },
469                 ],
470             },
471         },
472         {
473             definition: {
474                 id: "#main/input_long_array",
475                 type: {
476                     items: "long",
477                     type: "array",
478                 },
479             },
480             input: {
481                 input_long_array: [
482                     10,
483                     20,
484                     {
485                         $import: "import_path",
486                     },
487                 ],
488             },
489         },
490         {
491             definition: {
492                 id: "#main/input_float_array",
493                 type: {
494                     items: "float",
495                     type: "array",
496                 },
497             },
498             input: {
499                 input_float_array: [
500                     10.2,
501                     10.4,
502                     10.6,
503                     {
504                         $import: "import_path",
505                     },
506                 ],
507             },
508         },
509         {
510             definition: {
511                 id: "#main/input_double_array",
512                 type: {
513                     items: "double",
514                     type: "array",
515                 },
516             },
517             input: {
518                 input_double_array: [
519                     20.1,
520                     20.2,
521                     20.3,
522                     {
523                         $import: "import_path",
524                     },
525                 ],
526             },
527         },
528         {
529             definition: {
530                 id: "#main/input_string_array",
531                 type: {
532                     items: "string",
533                     type: "array",
534                 },
535             },
536             input: {
537                 input_string_array: [
538                     "Hello",
539                     "World",
540                     "!",
541                     {
542                         $import: "import_path",
543                     },
544                 ],
545             },
546         },
547         {
548             definition: {
549                 id: "#main/input_bool_include",
550                 type: "boolean",
551             },
552             input: {
553                 input_bool_include: {
554                     $include: "include_path",
555                 },
556             },
557         },
558         {
559             definition: {
560                 id: "#main/input_int_include",
561                 type: "int",
562             },
563             input: {
564                 input_int_include: {
565                     $include: "include_path",
566                 },
567             },
568         },
569         {
570             definition: {
571                 id: "#main/input_float_include",
572                 type: "float",
573             },
574             input: {
575                 input_float_include: {
576                     $include: "include_path",
577                 },
578             },
579         },
580         {
581             definition: {
582                 id: "#main/input_string_include",
583                 type: "string",
584             },
585             input: {
586                 input_string_include: {
587                     $include: "include_path",
588                 },
589             },
590         },
591         {
592             definition: {
593                 id: "#main/input_file_include",
594                 type: "File",
595             },
596             input: {
597                 input_file_include: {
598                     $include: "include_path",
599                 },
600             },
601         },
602         {
603             definition: {
604                 id: "#main/input_directory_include",
605                 type: "Directory",
606             },
607             input: {
608                 input_directory_include: {
609                     $include: "include_path",
610                 },
611             },
612         },
613         {
614             definition: {
615                 id: "#main/input_file_url",
616                 type: "File",
617             },
618             input: {
619                 input_file_url: {
620                     basename: "index.html",
621                     class: "File",
622                     location: "http://example.com/index.html",
623                 },
624             },
625         },
626     ];
627
628     const testOutputs = [
629         {
630             definition: {
631                 id: "#main/output_file",
632                 label: "Label Description",
633                 type: "File",
634             },
635             output: {
636                 output_file: {
637                     basename: "cat.png",
638                     class: "File",
639                     location: "cat.png",
640                 },
641             },
642         },
643         {
644             definition: {
645                 id: "#main/output_file_with_secondary",
646                 doc: "Doc Description",
647                 type: "File",
648             },
649             output: {
650                 output_file_with_secondary: {
651                     basename: "main.dat",
652                     class: "File",
653                     location: "main.dat",
654                     secondaryFiles: [
655                         {
656                             basename: "secondary.dat",
657                             class: "File",
658                             location: "secondary.dat",
659                         },
660                         {
661                             basename: "secondary2.dat",
662                             class: "File",
663                             location: "secondary2.dat",
664                         },
665                     ],
666                 },
667             },
668         },
669         {
670             definition: {
671                 id: "#main/output_dir",
672                 doc: ["Doc desc 1", "Doc desc 2"],
673                 type: "Directory",
674             },
675             output: {
676                 output_dir: {
677                     basename: "outdir1",
678                     class: "Directory",
679                     location: "outdir1",
680                 },
681             },
682         },
683         {
684             definition: {
685                 id: "#main/output_bool",
686                 type: "boolean",
687             },
688             output: {
689                 output_bool: true,
690             },
691         },
692         {
693             definition: {
694                 id: "#main/output_int",
695                 type: "int",
696             },
697             output: {
698                 output_int: 1,
699             },
700         },
701         {
702             definition: {
703                 id: "#main/output_long",
704                 type: "long",
705             },
706             output: {
707                 output_long: 1,
708             },
709         },
710         {
711             definition: {
712                 id: "#main/output_float",
713                 type: "float",
714             },
715             output: {
716                 output_float: 100.5,
717             },
718         },
719         {
720             definition: {
721                 id: "#main/output_double",
722                 type: "double",
723             },
724             output: {
725                 output_double: 100.3,
726             },
727         },
728         {
729             definition: {
730                 id: "#main/output_string",
731                 type: "string",
732             },
733             output: {
734                 output_string: "Hello output",
735             },
736         },
737         {
738             definition: {
739                 id: "#main/output_file_array",
740                 type: {
741                     items: "File",
742                     type: "array",
743                 },
744             },
745             output: {
746                 output_file_array: [
747                     {
748                         basename: "output2.tar",
749                         class: "File",
750                         location: "output2.tar",
751                     },
752                     {
753                         basename: "output3.tar",
754                         class: "File",
755                         location: "output3.tar",
756                     },
757                 ],
758             },
759         },
760         {
761             definition: {
762                 id: "#main/output_dir_array",
763                 type: {
764                     items: "Directory",
765                     type: "array",
766                 },
767             },
768             output: {
769                 output_dir_array: [
770                     {
771                         basename: "outdir2",
772                         class: "Directory",
773                         location: "outdir2",
774                     },
775                     {
776                         basename: "outdir3",
777                         class: "Directory",
778                         location: "outdir3",
779                     },
780                 ],
781             },
782         },
783         {
784             definition: {
785                 id: "#main/output_int_array",
786                 type: {
787                     items: "int",
788                     type: "array",
789                 },
790             },
791             output: {
792                 output_int_array: [10, 11, 12],
793             },
794         },
795         {
796             definition: {
797                 id: "#main/output_long_array",
798                 type: {
799                     items: "long",
800                     type: "array",
801                 },
802             },
803             output: {
804                 output_long_array: [51, 52],
805             },
806         },
807         {
808             definition: {
809                 id: "#main/output_float_array",
810                 type: {
811                     items: "float",
812                     type: "array",
813                 },
814             },
815             output: {
816                 output_float_array: [100.2, 100.4, 100.6],
817             },
818         },
819         {
820             definition: {
821                 id: "#main/output_double_array",
822                 type: {
823                     items: "double",
824                     type: "array",
825                 },
826             },
827             output: {
828                 output_double_array: [100.1, 100.2, 100.3],
829             },
830         },
831         {
832             definition: {
833                 id: "#main/output_string_array",
834                 type: {
835                     items: "string",
836                     type: "array",
837                 },
838             },
839             output: {
840                 output_string_array: ["Hello", "Output", "!"],
841             },
842         },
843     ];
844
845     const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
846         cy.get("table tr")
847             .contains(name)
848             .parents("tr")
849             .within($mainRow => {
850                 label && cy.contains(label);
851
852                 if (multipleRows) {
853                     cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
854                     if (val) {
855                         if (Array.isArray(val)) {
856                             val.forEach(v => cy.get("@secondaryRows").contains(v));
857                         } else {
858                             cy.get("@secondaryRows").contains(val);
859                         }
860                     }
861                     if (collection) {
862                         cy.get("@secondaryRows").contains(collection);
863                     }
864                 } else {
865                     if (val) {
866                         if (Array.isArray(val)) {
867                             val.forEach(v => cy.contains(v));
868                         } else {
869                             cy.contains(val);
870                         }
871                     }
872                     if (collection) {
873                         cy.contains(collection);
874                     }
875                 }
876             });
877     };
878
879     const verifyIOParameterImage = (name, url) => {
880         cy.get("table tr")
881             .contains(name)
882             .parents("tr")
883             .within(() => {
884                 cy.get('[alt="Inline Preview"]')
885                     .should("be.visible")
886                     .and($img => {
887                         expect($img[0].naturalWidth).to.be.greaterThan(0);
888                         expect($img[0].src).contains(url);
889                     });
890             });
891     };
892
893     it("displays IO parameters with keep links and previews", function () {
894         // Create output collection for real files
895         cy.createCollection(adminUser.token, {
896             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
897             owner_uuid: activeUser.user.uuid,
898         }).then(testOutputCollection => {
899             cy.loginAs(activeUser);
900
901             cy.goToPath(`/collections/${testOutputCollection.uuid}`);
902
903             cy.get("[data-cy=upload-button]").click();
904
905             cy.fixture("files/cat.png", "base64").then(content => {
906                 cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
907                 cy.get("[data-cy=form-submit-btn]").click();
908                 cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
909                 // Confirm final collection state.
910                 cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
911             });
912
913             cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
914         });
915
916         // Get updated collection pdh
917         cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
918             // Add output uuid and inputs to container request
919             cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
920                 req.reply(res => {
921                     res.body.output_uuid = testOutputCollection.uuid;
922                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
923                         content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
924                     };
925                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
926                         content: {
927                             $graph: [
928                                 {
929                                     id: "#main",
930                                     inputs: testInputs.map(input => input.definition),
931                                     outputs: testOutputs.map(output => output.definition),
932                                 },
933                             ],
934                         },
935                     };
936                 });
937             });
938
939             // Stub fake output collection
940             cy.intercept(
941                 { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
942                 {
943                     statusCode: 200,
944                     body: {
945                         uuid: testOutputCollection.uuid,
946                         portable_data_hash: testOutputCollection.portable_data_hash,
947                     },
948                 }
949             );
950
951             // Stub fake output json
952             cy.intercept(
953                 { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
954                 {
955                     statusCode: 200,
956                     body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
957                 }
958             );
959
960             // Stub webdav response, points to output json
961             cy.intercept(
962                 { method: "PROPFIND", url: "*" },
963                 {
964                     fixture: "webdav-propfind-outputs.xml",
965                 }
966             );
967         });
968
969         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
970             "containerRequest"
971         );
972
973         cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
974             cy.goToPath(`/processes/${containerRequest.uuid}`);
975             cy.get("[data-cy=process-io-card] h6")
976                 .contains("Inputs")
977                 .parents("[data-cy=process-io-card]")
978                 .within(() => {
979                     verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
980                     verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
981                     verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
982                     verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
983                     verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
984                     verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
985                     verifyIOParameter("input_int", null, null, "1");
986                     verifyIOParameter("input_long", null, null, "1");
987                     verifyIOParameter("input_float", null, null, "1.5");
988                     verifyIOParameter("input_double", null, null, "1.3");
989                     verifyIOParameter("input_string", null, null, "Hello World");
990                     verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
991                     verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
992                     verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
993                     verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
994                     verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
995                     verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
996                     verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
997                     verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
998                     verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
999                     verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1000                     verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1001                     verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
1002                     verifyIOParameter("input_bool_include", null, null, "Cannot display value");
1003                     verifyIOParameter("input_int_include", null, null, "Cannot display value");
1004                     verifyIOParameter("input_float_include", null, null, "Cannot display value");
1005                     verifyIOParameter("input_string_include", null, null, "Cannot display value");
1006                     verifyIOParameter("input_file_include", null, null, "Cannot display value");
1007                     verifyIOParameter("input_directory_include", null, null, "Cannot display value");
1008                     verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
1009                 });
1010             cy.get("[data-cy=process-io-card] h6")
1011                 .contains("Outputs")
1012                 .parents("[data-cy=process-io-card]")
1013                 .within(ctx => {
1014                     cy.get(ctx).scrollIntoView();
1015                     cy.get('[data-cy="io-preview-image-toggle"]').click({ waitForAnimations: false });
1016                     const outPdh = testOutputCollection.portable_data_hash;
1017                     verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
1018                     verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
1019                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
1020                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
1021                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
1022                     verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
1023                     verifyIOParameter("output_bool", null, null, "true");
1024                     verifyIOParameter("output_int", null, null, "1");
1025                     verifyIOParameter("output_long", null, null, "1");
1026                     verifyIOParameter("output_float", null, null, "100.5");
1027                     verifyIOParameter("output_double", null, null, "100.3");
1028                     verifyIOParameter("output_string", null, null, "Hello output");
1029                     verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
1030                     verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
1031                     verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
1032                     verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
1033                     verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
1034                     verifyIOParameter("output_long_array", null, null, ["51", "52"]);
1035                     verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
1036                     verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
1037                     verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
1038                 });
1039         });
1040     });
1041
1042     it("displays IO parameters with no value", function () {
1043         const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
1044         const fakeOutputPDH = "11111111111111111111111111111111+99/";
1045
1046         cy.loginAs(activeUser);
1047
1048         // Add output uuid and inputs to container request
1049         cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1050             req.reply(res => {
1051                 res.body.output_uuid = fakeOutputUUID;
1052                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1053                     content: {},
1054                 };
1055                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1056                     content: {
1057                         $graph: [
1058                             {
1059                                 id: "#main",
1060                                 inputs: testInputs.map(input => input.definition),
1061                                 outputs: testOutputs.map(output => output.definition),
1062                             },
1063                         ],
1064                     },
1065                 };
1066             });
1067         });
1068
1069         // Stub fake output collection
1070         cy.intercept(
1071             { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
1072             {
1073                 statusCode: 200,
1074                 body: {
1075                     uuid: fakeOutputUUID,
1076                     portable_data_hash: fakeOutputPDH,
1077                 },
1078             }
1079         );
1080
1081         // Stub fake output json
1082         cy.intercept(
1083             { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
1084             {
1085                 statusCode: 200,
1086                 body: {},
1087             }
1088         );
1089
1090         cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
1091             // Stub webdav response, points to output json
1092             cy.intercept(
1093                 { method: "PROPFIND", url: "*" },
1094                 {
1095                     statusCode: 200,
1096                     body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
1097                 }
1098             );
1099         });
1100
1101         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1102             "containerRequest"
1103         );
1104
1105         cy.getAll("@containerRequest").then(function ([containerRequest]) {
1106             cy.goToPath(`/processes/${containerRequest.uuid}`);
1107             cy.get("[data-cy=process-io-card] h6")
1108                 .contains("Inputs")
1109                 .parents("[data-cy=process-io-card]")
1110                 .within(() => {
1111                     cy.wait(2000);
1112                     cy.waitForDom();
1113                     cy.get("tbody tr").each(item => {
1114                         cy.wrap(item).contains("No value");
1115                     });
1116                 });
1117             cy.get("[data-cy=process-io-card] h6")
1118                 .contains("Outputs")
1119                 .parents("[data-cy=process-io-card]")
1120                 .within(() => {
1121                     cy.get("tbody tr").each(item => {
1122                         cy.wrap(item).contains("No value");
1123                     });
1124                 });
1125         });
1126     });
1127
1128     it("allows copying processes", function () {
1129         const crName = "first_container_request";
1130         const copiedCrName = "copied_container_request";
1131         createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1132             cy.loginAs(activeUser);
1133             cy.goToPath(`/processes/${containerRequest.uuid}`);
1134             cy.get("[data-cy=process-details]").should("contain", crName);
1135
1136             cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
1137             cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
1138         });
1139
1140         cy.get("[data-cy=form-dialog]").within(() => {
1141             cy.get("input[name=name]").clear().type(copiedCrName);
1142             cy.get("[data-cy=projects-tree-home-tree-picker]").click();
1143             cy.get("[data-cy=form-submit-btn]").click();
1144         });
1145
1146         cy.get("[data-cy=process-details]").should("contain", copiedCrName);
1147         cy.get("[data-cy=process-details]").find("button").contains("Run");
1148     });
1149
1150     const getFakeContainer = fakeContainerUuid => ({
1151         href: `/containers/${fakeContainerUuid}`,
1152         kind: "arvados#container",
1153         etag: "ecfosljpnxfari9a8m7e4yv06",
1154         uuid: fakeContainerUuid,
1155         owner_uuid: "zzzzz-tpzed-000000000000000",
1156         created_at: "2023-02-13T15:55:47.308915000Z",
1157         modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
1158         modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
1159         modified_at: "2023-02-15T19:12:45.987086000Z",
1160         command: [
1161             "arvados-cwl-runner",
1162             "--api=containers",
1163             "--local",
1164             "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
1165             "/var/lib/cwl/workflow.json#main",
1166             "/var/lib/cwl/cwl.input.json",
1167         ],
1168         container_image: "4ad7d11381df349e464694762db14e04+303",
1169         cwd: "/var/spool/cwl",
1170         environment: {},
1171         exit_code: null,
1172         finished_at: null,
1173         locked_by_uuid: null,
1174         log: null,
1175         output: null,
1176         output_path: "/var/spool/cwl",
1177         progress: null,
1178         runtime_constraints: {
1179             API: true,
1180             cuda: {
1181                 device_count: 0,
1182                 driver_version: "",
1183                 hardware_capability: "",
1184             },
1185             keep_cache_disk: 2147483648,
1186             keep_cache_ram: 0,
1187             ram: 1342177280,
1188             vcpus: 1,
1189         },
1190         runtime_status: {},
1191         started_at: null,
1192         auth_uuid: null,
1193         scheduling_parameters: {
1194             max_run_time: 0,
1195             partitions: [],
1196             preemptible: false,
1197         },
1198         runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
1199         runtime_auth_scopes: ["all"],
1200         lock_count: 2,
1201         gateway_address: null,
1202         interactive_session_started: false,
1203         output_storage_classes: ["default"],
1204         output_properties: {},
1205         cost: 0.0,
1206         subrequests_cost: 0.0,
1207     });
1208
1209     it("shows cancel button when appropriate", function () {
1210         // Ignore collection requests
1211         cy.intercept(
1212             { method: "GET", url: `**/arvados/v1/collections/*` },
1213             {
1214                 statusCode: 200,
1215                 body: {},
1216             }
1217         );
1218
1219         // Uncommitted container
1220         const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
1221         createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
1222             containerRequest
1223         ) {
1224             // Navigate to process and verify run / cancel button
1225             cy.goToPath(`/processes/${containerRequest.uuid}`);
1226             cy.waitForDom();
1227             cy.get("[data-cy=process-details]").should("contain", crUncommitted);
1228             cy.get("[data-cy=process-run-button]").should("exist");
1229             cy.get("[data-cy=process-cancel-button]").should("not.exist");
1230         });
1231
1232         // Queued container
1233         const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
1234         const fakeCrUuid = "zzzzz-dz642-000000000000001";
1235         createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1236             // Fake container uuid
1237             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
1238                 req.reply(res => {
1239                     res.body.output_uuid = fakeCrUuid;
1240                     res.body.priority = 500;
1241                     res.body.state = "Committed";
1242                 });
1243             });
1244
1245             // Fake container
1246             const container = getFakeContainer(fakeCrUuid);
1247             cy.intercept(
1248                 { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
1249                 {
1250                     statusCode: 200,
1251                     body: { ...container, state: "Queued", priority: 500 },
1252                 }
1253             );
1254
1255             // Navigate to process and verify cancel button
1256             cy.goToPath(`/processes/${containerRequest.uuid}`);
1257             cy.waitForDom();
1258             cy.get("[data-cy=process-details]").should("contain", crQueued);
1259             cy.get("[data-cy=process-cancel-button]").contains("Cancel");
1260         });
1261
1262         // Locked container
1263         const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
1264         const fakeCrLockedUuid = "zzzzz-dz642-000000000000002";
1265         createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1266             // Fake container uuid
1267             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
1268                 req.reply(res => {
1269                     res.body.output_uuid = fakeCrLockedUuid;
1270                     res.body.priority = 500;
1271                     res.body.state = "Committed";
1272                 });
1273             });
1274
1275             // Fake container
1276             const container = getFakeContainer(fakeCrLockedUuid);
1277             cy.intercept(
1278                 { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
1279                 {
1280                     statusCode: 200,
1281                     body: { ...container, state: "Locked", priority: 500 },
1282                 }
1283             );
1284
1285             // Navigate to process and verify cancel button
1286             cy.goToPath(`/processes/${containerRequest.uuid}`);
1287             cy.waitForDom();
1288             cy.get("[data-cy=process-details]").should("contain", crLocked);
1289             cy.get("[data-cy=process-cancel-button]").contains("Cancel");
1290         });
1291
1292         // On Hold container
1293         const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
1294         const fakeCrOnHoldUuid = "zzzzz-dz642-000000000000003";
1295         createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1296             // Fake container uuid
1297             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
1298                 req.reply(res => {
1299                     res.body.output_uuid = fakeCrOnHoldUuid;
1300                     res.body.priority = 0;
1301                     res.body.state = "Committed";
1302                 });
1303             });
1304
1305             // Fake container
1306             const container = getFakeContainer(fakeCrOnHoldUuid);
1307             cy.intercept(
1308                 { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
1309                 {
1310                     statusCode: 200,
1311                     body: { ...container, state: "Queued", priority: 0 },
1312                 }
1313             );
1314
1315             // Navigate to process and verify cancel button
1316             cy.goToPath(`/processes/${containerRequest.uuid}`);
1317             cy.waitForDom();
1318             cy.get("[data-cy=process-details]").should("contain", crOnHold);
1319             cy.get("[data-cy=process-run-button]").should("exist");
1320             cy.get("[data-cy=process-cancel-button]").should("not.exist");
1321         });
1322     });
1323 });