15768: fixed rerun bug Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii...
[arvados.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
1018                     verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
1019                     verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
1020                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
1021                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
1022                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
1023                     verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
1024                     verifyIOParameter("output_bool", null, null, "true");
1025                     verifyIOParameter("output_int", null, null, "1");
1026                     verifyIOParameter("output_long", null, null, "1");
1027                     verifyIOParameter("output_float", null, null, "100.5");
1028                     verifyIOParameter("output_double", null, null, "100.3");
1029                     verifyIOParameter("output_string", null, null, "Hello output");
1030                     verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
1031                     verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
1032                     verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
1033                     verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
1034                     verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
1035                     verifyIOParameter("output_long_array", null, null, ["51", "52"]);
1036                     verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
1037                     verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
1038                     verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
1039                 });
1040         });
1041     });
1042
1043     it("displays IO parameters with no value", function () {
1044         const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
1045         const fakeOutputPDH = "11111111111111111111111111111111+99/";
1046
1047         cy.loginAs(activeUser);
1048
1049         // Add output uuid and inputs to container request
1050         cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1051             req.reply(res => {
1052                 res.body.output_uuid = fakeOutputUUID;
1053                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1054                     content: {},
1055                 };
1056                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1057                     content: {
1058                         $graph: [
1059                             {
1060                                 id: "#main",
1061                                 inputs: testInputs.map(input => input.definition),
1062                                 outputs: testOutputs.map(output => output.definition),
1063                             },
1064                         ],
1065                     },
1066                 };
1067             });
1068         });
1069
1070         // Stub fake output collection
1071         cy.intercept(
1072             { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
1073             {
1074                 statusCode: 200,
1075                 body: {
1076                     uuid: fakeOutputUUID,
1077                     portable_data_hash: fakeOutputPDH,
1078                 },
1079             }
1080         );
1081
1082         // Stub fake output json
1083         cy.intercept(
1084             { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
1085             {
1086                 statusCode: 200,
1087                 body: {},
1088             }
1089         );
1090
1091         cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
1092             // Stub webdav response, points to output json
1093             cy.intercept(
1094                 { method: "PROPFIND", url: "*" },
1095                 {
1096                     statusCode: 200,
1097                     body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
1098                 }
1099             );
1100         });
1101
1102         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1103             "containerRequest"
1104         );
1105
1106         cy.getAll("@containerRequest").then(function ([containerRequest]) {
1107             cy.goToPath(`/processes/${containerRequest.uuid}`);
1108             cy.get("[data-cy=process-io-card] h6")
1109                 .contains("Inputs")
1110                 .parents("[data-cy=process-io-card]")
1111                 .within(() => {
1112                     cy.wait(2000);
1113                     cy.waitForDom();
1114                     cy.get("tbody tr").each(item => {
1115                         cy.wrap(item).contains("No value");
1116                     });
1117                 });
1118             cy.get("[data-cy=process-io-card] h6")
1119                 .contains("Outputs")
1120                 .parents("[data-cy=process-io-card]")
1121                 .within(() => {
1122                     cy.get("tbody tr").each(item => {
1123                         cy.wrap(item).contains("No value");
1124                     });
1125                 });
1126         });
1127     });
1128
1129     it("allows copying processes", function () {
1130         const crName = "first_container_request";
1131         const copiedCrName = "copied_container_request";
1132         createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1133             cy.loginAs(activeUser);
1134             cy.goToPath(`/processes/${containerRequest.uuid}`);
1135             cy.get("[data-cy=process-details]").should("contain", crName);
1136
1137             cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
1138             cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
1139         });
1140
1141         cy.get("[data-cy=form-dialog]").within(() => {
1142             cy.get("input[name=name]").clear().type(copiedCrName);
1143             cy.get("[data-cy=projects-tree-home-tree-picker]").click();
1144             cy.get("[data-cy=form-submit-btn]").click();
1145         });
1146
1147         cy.get("[data-cy=process-details]").should("contain", copiedCrName);
1148         cy.get("[data-cy=process-details]").find("button").contains("Run");
1149     });
1150
1151     const getFakeContainer = fakeContainerUuid => ({
1152         href: `/containers/${fakeContainerUuid}`,
1153         kind: "arvados#container",
1154         etag: "ecfosljpnxfari9a8m7e4yv06",
1155         uuid: fakeContainerUuid,
1156         owner_uuid: "zzzzz-tpzed-000000000000000",
1157         created_at: "2023-02-13T15:55:47.308915000Z",
1158         modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
1159         modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
1160         modified_at: "2023-02-15T19:12:45.987086000Z",
1161         command: [
1162             "arvados-cwl-runner",
1163             "--api=containers",
1164             "--local",
1165             "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
1166             "/var/lib/cwl/workflow.json#main",
1167             "/var/lib/cwl/cwl.input.json",
1168         ],
1169         container_image: "4ad7d11381df349e464694762db14e04+303",
1170         cwd: "/var/spool/cwl",
1171         environment: {},
1172         exit_code: null,
1173         finished_at: null,
1174         locked_by_uuid: null,
1175         log: null,
1176         output: null,
1177         output_path: "/var/spool/cwl",
1178         progress: null,
1179         runtime_constraints: {
1180             API: true,
1181             cuda: {
1182                 device_count: 0,
1183                 driver_version: "",
1184                 hardware_capability: "",
1185             },
1186             keep_cache_disk: 2147483648,
1187             keep_cache_ram: 0,
1188             ram: 1342177280,
1189             vcpus: 1,
1190         },
1191         runtime_status: {},
1192         started_at: null,
1193         auth_uuid: null,
1194         scheduling_parameters: {
1195             max_run_time: 0,
1196             partitions: [],
1197             preemptible: false,
1198         },
1199         runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
1200         runtime_auth_scopes: ["all"],
1201         lock_count: 2,
1202         gateway_address: null,
1203         interactive_session_started: false,
1204         output_storage_classes: ["default"],
1205         output_properties: {},
1206         cost: 0.0,
1207         subrequests_cost: 0.0,
1208     });
1209
1210     it("shows cancel button when appropriate", function () {
1211         // Ignore collection requests
1212         cy.intercept(
1213             { method: "GET", url: `**/arvados/v1/collections/*` },
1214             {
1215                 statusCode: 200,
1216                 body: {},
1217             }
1218         );
1219
1220         // Uncommitted container
1221         const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
1222         createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
1223             containerRequest
1224         ) {
1225             // Navigate to process and verify run / cancel button
1226             cy.goToPath(`/processes/${containerRequest.uuid}`);
1227             cy.waitForDom();
1228             cy.get("[data-cy=process-details]").should("contain", crUncommitted);
1229             cy.get("[data-cy=process-run-button]").should("exist");
1230             cy.get("[data-cy=process-cancel-button]").should("not.exist");
1231         });
1232
1233         // Queued container
1234         const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
1235         const fakeCrUuid = "zzzzz-dz642-000000000000001";
1236         createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1237             // Fake container uuid
1238             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
1239                 req.reply(res => {
1240                     res.body.output_uuid = fakeCrUuid;
1241                     res.body.priority = 500;
1242                     res.body.state = "Committed";
1243                 });
1244             });
1245
1246             // Fake container
1247             const container = getFakeContainer(fakeCrUuid);
1248             cy.intercept(
1249                 { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
1250                 {
1251                     statusCode: 200,
1252                     body: { ...container, state: "Queued", priority: 500 },
1253                 }
1254             );
1255
1256             // Navigate to process and verify cancel button
1257             cy.goToPath(`/processes/${containerRequest.uuid}`);
1258             cy.waitForDom();
1259             cy.get("[data-cy=process-details]").should("contain", crQueued);
1260             cy.get("[data-cy=process-cancel-button]").contains("Cancel");
1261         });
1262
1263         // Locked container
1264         const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
1265         const fakeCrLockedUuid = "zzzzz-dz642-000000000000002";
1266         createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1267             // Fake container uuid
1268             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
1269                 req.reply(res => {
1270                     res.body.output_uuid = fakeCrLockedUuid;
1271                     res.body.priority = 500;
1272                     res.body.state = "Committed";
1273                 });
1274             });
1275
1276             // Fake container
1277             const container = getFakeContainer(fakeCrLockedUuid);
1278             cy.intercept(
1279                 { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
1280                 {
1281                     statusCode: 200,
1282                     body: { ...container, state: "Locked", priority: 500 },
1283                 }
1284             );
1285
1286             // Navigate to process and verify cancel button
1287             cy.goToPath(`/processes/${containerRequest.uuid}`);
1288             cy.waitForDom();
1289             cy.get("[data-cy=process-details]").should("contain", crLocked);
1290             cy.get("[data-cy=process-cancel-button]").contains("Cancel");
1291         });
1292
1293         // On Hold container
1294         const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
1295         const fakeCrOnHoldUuid = "zzzzz-dz642-000000000000003";
1296         createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
1297             // Fake container uuid
1298             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
1299                 req.reply(res => {
1300                     res.body.output_uuid = fakeCrOnHoldUuid;
1301                     res.body.priority = 0;
1302                     res.body.state = "Committed";
1303                 });
1304             });
1305
1306             // Fake container
1307             const container = getFakeContainer(fakeCrOnHoldUuid);
1308             cy.intercept(
1309                 { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
1310                 {
1311                     statusCode: 200,
1312                     body: { ...container, state: "Queued", priority: 0 },
1313                 }
1314             );
1315
1316             // Navigate to process and verify cancel button
1317             cy.goToPath(`/processes/${containerRequest.uuid}`);
1318             cy.waitForDom();
1319             cy.get("[data-cy=process-details]").should("contain", crOnHold);
1320             cy.get("[data-cy=process-run-button]").should("exist");
1321             cy.get("[data-cy=process-cancel-button]").should("not.exist");
1322         });
1323     });
1324 });