1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { ContainerState } from "models/container";
7 describe("Process tests", function () {
12 // Only set up common users once. These aren't set up as aliases because
13 // aliases are cleaned up after every test. Also it doesn't make sense
14 // to set the same users on beforeEach() over and over again, so we
15 // separate a little from Cypress' 'Best Practices' here.
16 cy.getUser("admin", "Admin", "User", true, true)
19 adminUser = this.adminUser;
21 cy.getUser("user", "Active", "User", false, true)
24 activeUser = this.activeUser;
28 beforeEach(function () {
30 cy.clearLocalStorage();
33 function setupDockerImage(image_name) {
34 // Create a collection that will be used as a docker image for the tests.
35 cy.createCollection(adminUser.token, {
38 ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n",
41 .then(function (dockerImage) {
42 // Give read permissions to the active user on the docker image.
43 cy.createLink(adminUser.token, {
44 link_class: "permission",
46 tail_uuid: activeUser.user.uuid,
47 head_uuid: dockerImage.uuid,
49 .as("dockerImagePermission")
51 // Set-up docker image collection tags
52 cy.createLink(activeUser.token, {
53 link_class: "docker_image_repo+tag",
55 head_uuid: dockerImage.uuid,
56 }).as("dockerImageRepoTag");
57 cy.createLink(activeUser.token, {
58 link_class: "docker_image_hash",
59 name: "sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678",
60 head_uuid: dockerImage.uuid,
61 }).as("dockerImageHash");
64 return cy.getAll("@dockerImage", "@dockerImageRepoTag", "@dockerImageHash", "@dockerImagePermission").then(function ([dockerImage]) {
69 function createContainerRequest(user, name, docker_image, command, reuse = false, state = "Uncommitted") {
70 return setupDockerImage(docker_image).then(function (dockerImage) {
71 return cy.createContainerRequest(user.token, {
74 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
75 output_path: "stdout.txt",
77 runtime_constraints: {
93 describe("Details panel", function () {
94 it("shows process details", function () {
95 createContainerRequest(
97 `test_container_request ${Math.floor(Math.random() * 999999)}`,
99 ["echo", "hello world"],
102 ).then(function (containerRequest) {
103 cy.loginAs(activeUser);
104 cy.goToPath(`/processes/${containerRequest.uuid}`);
105 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
106 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
107 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
110 // Fake submitted by another user
111 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
113 res.body.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
117 createContainerRequest(
119 `test_container_request ${Math.floor(Math.random() * 999999)}`,
121 ["echo", "hello world"],
124 ).then(function (containerRequest) {
125 cy.loginAs(activeUser);
126 cy.goToPath(`/processes/${containerRequest.uuid}`);
127 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
128 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
129 cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
133 it("should show runtime status indicators", function () {
134 // Setup running container with runtime_status error & warning messages
135 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
136 .as("containerRequest")
137 .then(function (containerRequest) {
138 expect(containerRequest.state).to.equal("Committed");
139 expect(containerRequest.container_uuid).not.to.be.equal("");
141 cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
142 expect(queuedContainer.state).to.be.equal("Queued");
144 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
146 }).then(function (lockedContainer) {
147 expect(lockedContainer.state).to.be.equal("Locked");
149 cy.updateContainer(adminUser.token, lockedContainer.uuid, {
152 error: "Something went wrong",
153 errorDetail: "Process exited with status 1",
154 warning: "Free disk space is low",
157 .as("runningContainer")
158 .then(function (runningContainer) {
159 expect(runningContainer.state).to.be.equal("Running");
160 expect(runningContainer.runtime_status).to.be.deep.equal({
161 error: "Something went wrong",
162 errorDetail: "Process exited with status 1",
163 warning: "Free disk space is low",
168 // Test that the UI shows the error and warning messages
169 cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
170 cy.loginAs(activeUser);
171 cy.goToPath(`/processes/${containerRequest.uuid}`);
172 cy.get("[data-cy=process-runtime-status-error]")
173 .should("contain", "Something went wrong")
174 .and("contain", "Process exited with status 1");
175 cy.get("[data-cy=process-runtime-status-warning]")
176 .should("contain", "Free disk space is low")
177 .and("contain", "No additional warning details available");
180 // Force container_count for testing
181 let containerCount = 2;
182 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
184 res.body.container_count = containerCount;
188 cy.getAll("@containerRequest").then(function ([containerRequest]) {
189 cy.goToPath(`/processes/${containerRequest.uuid}`);
190 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time");
193 cy.getAll("@containerRequest").then(function ([containerRequest]) {
195 cy.goToPath(`/processes/${containerRequest.uuid}`);
196 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
200 it("allows copying processes", function () {
201 const crName = "first_container_request";
202 const copiedCrName = "copied_container_request";
203 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
204 cy.loginAs(activeUser);
205 cy.goToPath(`/processes/${containerRequest.uuid}`);
206 cy.get("[data-cy=process-details]").should("contain", crName);
208 cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
209 cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
212 cy.get("[data-cy=form-dialog]").within(() => {
213 cy.get("input[name=name]").clear().type(copiedCrName);
214 cy.get("[data-cy=projects-tree-home-tree-picker]").click();
215 cy.get("[data-cy=form-submit-btn]").click();
218 cy.get("[data-cy=process-details]").should("contain", copiedCrName);
219 cy.get("[data-cy=process-details]").find("button").contains("Run");
222 const getFakeContainer = fakeContainerUuid => ({
223 href: `/containers/${fakeContainerUuid}`,
224 kind: "arvados#container",
225 etag: "ecfosljpnxfari9a8m7e4yv06",
226 uuid: fakeContainerUuid,
227 owner_uuid: "zzzzz-tpzed-000000000000000",
228 created_at: "2023-02-13T15:55:47.308915000Z",
229 modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
230 modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
231 modified_at: "2023-02-15T19:12:45.987086000Z",
233 "arvados-cwl-runner",
236 "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
237 "/var/lib/cwl/workflow.json#main",
238 "/var/lib/cwl/cwl.input.json",
240 container_image: "4ad7d11381df349e464694762db14e04+303",
241 cwd: "/var/spool/cwl",
245 locked_by_uuid: null,
248 output_path: "/var/spool/cwl",
250 runtime_constraints: {
255 hardware_capability: "",
257 keep_cache_disk: 2147483648,
265 scheduling_parameters: {
270 runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
271 runtime_auth_scopes: ["all"],
273 gateway_address: null,
274 interactive_session_started: false,
275 output_storage_classes: ["default"],
276 output_properties: {},
278 subrequests_cost: 0.0,
281 it("shows cancel button when appropriate", function () {
282 // Ignore collection requests
284 { method: "GET", url: `**/arvados/v1/collections/*` },
291 // Uncommitted container
292 const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
293 createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
296 // Navigate to process and verify run / cancel button
297 cy.goToPath(`/processes/${containerRequest.uuid}`);
299 cy.get("[data-cy=process-details]").should("contain", crUncommitted);
300 cy.get("[data-cy=process-run-button]").should("exist");
301 cy.get("[data-cy=process-cancel-button]").should("not.exist");
305 const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
306 const fakeCrUuid = "zzzzz-dz642-000000000000001";
307 createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
310 // Fake container uuid
311 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
313 res.body.output_uuid = fakeCrUuid;
314 res.body.priority = 500;
315 res.body.state = "Committed";
320 const container = getFakeContainer(fakeCrUuid);
322 { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
325 body: { ...container, state: "Queued", priority: 500 },
329 // Navigate to process and verify cancel button
330 cy.goToPath(`/processes/${containerRequest.uuid}`);
332 cy.get("[data-cy=process-details]").should("contain", crQueued);
333 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
337 const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
338 const fakeCrLockedUuid = "zzzzz-dz642-000000000000002";
339 createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
342 // Fake container uuid
343 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
345 res.body.output_uuid = fakeCrLockedUuid;
346 res.body.priority = 500;
347 res.body.state = "Committed";
352 const container = getFakeContainer(fakeCrLockedUuid);
354 { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
357 body: { ...container, state: "Locked", priority: 500 },
361 // Navigate to process and verify cancel button
362 cy.goToPath(`/processes/${containerRequest.uuid}`);
364 cy.get("[data-cy=process-details]").should("contain", crLocked);
365 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
369 const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
370 const fakeCrOnHoldUuid = "zzzzz-dz642-000000000000003";
371 createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
374 // Fake container uuid
375 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
377 res.body.output_uuid = fakeCrOnHoldUuid;
378 res.body.priority = 0;
379 res.body.state = "Committed";
384 const container = getFakeContainer(fakeCrOnHoldUuid);
386 { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
389 body: { ...container, state: "Queued", priority: 0 },
393 // Navigate to process and verify cancel button
394 cy.goToPath(`/processes/${containerRequest.uuid}`);
396 cy.get("[data-cy=process-details]").should("contain", crOnHold);
397 cy.get("[data-cy=process-run-button]").should("exist");
398 cy.get("[data-cy=process-cancel-button]").should("not.exist");
403 describe("Logs panel", function () {
404 it("shows live process logs", function () {
405 cy.intercept({ method: "GET", url: "**/arvados/v1/containers/*" }, req => {
407 res.body.state = ContainerState.RUNNING;
411 const crName = "test_container_request";
412 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
413 // Create empty log file before loading process page
414 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [""]);
416 cy.loginAs(activeUser);
417 cy.goToPath(`/processes/${containerRequest.uuid}`);
418 cy.get("[data-cy=process-details]").should("contain", crName);
419 cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
422 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", ["2023-07-18T20:14:48.128642814Z hello world"]).then(() => {
423 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
426 // Append new log line to different file
427 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:49.128642814Z hello new line"]).then(() => {
428 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello new line");
433 it("filters process logs by event type", function () {
434 const nodeInfoLogs = [
436 "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",
439 "vendor_id : GenuineIntel",
442 "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
444 const crunchRunLogs = [
445 "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
446 "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
447 "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
448 "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
449 "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
452 "2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
453 "2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
454 "2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.",
455 "2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
456 "2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.",
457 "2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
458 "2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
459 "2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
460 "2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.",
461 "2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
462 "2022-03-22T13:56:22.542418097Z Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.",
463 "2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
464 "2022-03-22T13:56:22.542418117Z 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.",
465 "2022-03-22T13:56:22.542418127Z 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.",
466 "2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
467 "2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.",
468 "2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
469 "2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.",
472 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
475 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as("nodeInfoLogs");
476 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as("crunchRunLogs");
477 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as("stdoutLogs");
479 cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
480 cy.loginAs(activeUser);
481 cy.goToPath(`/processes/${containerRequest.uuid}`);
482 // Should show main logs by default
483 cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
484 cy.get("[data-cy=process-logs]")
485 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
486 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
487 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
489 cy.get("[data-cy=process-logs-filter]").click();
490 cy.get("body").contains("li", "All logs").click();
491 cy.get("[data-cy=process-logs]")
492 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
493 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
494 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
495 // Select 'node-info' logs
496 cy.get("[data-cy=process-logs-filter]").click();
497 cy.get("body").contains("li", "node-info").click();
498 cy.get("[data-cy=process-logs]")
499 .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
500 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
501 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
502 // Select 'stdout' logs
503 cy.get("[data-cy=process-logs-filter]").click();
504 cy.get("body").contains("li", "stdout").click();
505 cy.get("[data-cy=process-logs]")
506 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
507 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
508 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
513 it("sorts combined logs", function () {
514 const crName = "test_container_request";
515 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
516 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
524 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
525 "2023-07-18T20:14:48.128642814Z first",
526 "2023-07-18T20:14:49.128642814Z third",
529 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:48.528642814Z second"]).as("stderr");
531 cy.loginAs(activeUser);
532 cy.goToPath(`/processes/${containerRequest.uuid}`);
533 cy.get("[data-cy=process-details]").should("contain", crName);
534 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
536 cy.getAll("@node-info", "@stdout", "@stderr").then(() => {
537 // Verify sorted main logs
538 cy.get("[data-cy=process-logs] pre", { timeout: 7000 }).eq(0).should("contain", "2023-07-18T20:14:48.128642814Z first");
539 cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2023-07-18T20:14:48.528642814Z second");
540 cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "2023-07-18T20:14:49.128642814Z third");
542 // Switch to All logs
543 cy.get("[data-cy=process-logs-filter]").click();
544 cy.get("body").contains("li", "All logs").click();
545 // Verify non-sorted lines were preserved
546 cy.get("[data-cy=process-logs] pre").eq(0).should("contain", "3: nodeinfo 1");
547 cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2: nodeinfo 2");
548 cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "1: nodeinfo 3");
549 cy.get("[data-cy=process-logs] pre").eq(3).should("contain", "2: nodeinfo 4");
550 cy.get("[data-cy=process-logs] pre").eq(4).should("contain", "3: nodeinfo 5");
551 // Verify sorted logs
552 cy.get("[data-cy=process-logs] pre").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z first");
553 cy.get("[data-cy=process-logs] pre").eq(6).should("contain", "2023-07-18T20:14:48.528642814Z second");
554 cy.get("[data-cy=process-logs] pre").eq(7).should("contain", "2023-07-18T20:14:49.128642814Z third");
559 it("preserves original ordering of lines within the same log type", function () {
560 const crName = "test_container_request";
561 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
562 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
564 "2023-07-18T20:14:46.000000000Z A out 1",
565 // Comes fourth in a contiguous block
566 "2023-07-18T20:14:48.128642814Z A out 2",
567 "2023-07-18T20:14:48.128642814Z X out 3",
568 "2023-07-18T20:14:48.128642814Z A out 4",
571 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
573 "2023-07-18T20:14:47.000000000Z Z err 1",
574 // Comes third in a contiguous block
575 "2023-07-18T20:14:48.128642814Z B err 2",
576 "2023-07-18T20:14:48.128642814Z C err 3",
577 "2023-07-18T20:14:48.128642814Z Y err 4",
578 "2023-07-18T20:14:48.128642814Z Z err 5",
579 "2023-07-18T20:14:48.128642814Z A err 6",
582 cy.loginAs(activeUser);
583 cy.goToPath(`/processes/${containerRequest.uuid}`);
584 cy.get("[data-cy=process-details]").should("contain", crName);
585 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
587 cy.getAll("@stdout", "@stderr").then(() => {
588 // Switch to All logs
589 cy.get("[data-cy=process-logs-filter]").click();
590 cy.get("body").contains("li", "All logs").click();
591 // Verify sorted logs
592 cy.get("[data-cy=process-logs] pre").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
593 cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
594 cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
595 cy.get("[data-cy=process-logs] pre").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
596 cy.get("[data-cy=process-logs] pre").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
597 cy.get("[data-cy=process-logs] pre").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
598 cy.get("[data-cy=process-logs] pre").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
599 cy.get("[data-cy=process-logs] pre").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
600 cy.get("[data-cy=process-logs] pre").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
601 cy.get("[data-cy=process-logs] pre").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
606 it("correctly generates sniplines", function () {
607 const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
608 const crName = "test_container_request";
609 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
610 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
611 "X".repeat(63999) + "_" + "O".repeat(100) + "_" + "X".repeat(63999),
614 cy.loginAs(activeUser);
615 cy.goToPath(`/processes/${containerRequest.uuid}`);
616 cy.get("[data-cy=process-details]").should("contain", crName);
617 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
619 // Switch to stdout since lines are unsortable (no timestamp)
620 cy.get("[data-cy=process-logs-filter]").click();
621 cy.get("body").contains("li", "stdout").click();
623 cy.getAll("@stdout").then(() => {
624 // Verify first 64KB and snipline
625 cy.get("[data-cy=process-logs] pre", { timeout: 7000 })
627 .should("contain", "X".repeat(63999) + "_\n" + SNIPLINE);
629 cy.get("[data-cy=process-logs] pre")
631 .should("contain", "_" + "X".repeat(63999));
632 // Verify none of the Os got through
633 cy.get("[data-cy=process-logs] pre").should("not.contain", "O");
639 describe("I/O panel", function () {
643 id: "#main/input_file",
644 label: "Label Description",
649 basename: "input1.tar",
651 location: "keep:00000000000000000000000000000000+01/input1.tar",
654 basename: "input1-2.txt",
656 location: "keep:00000000000000000000000000000000+01/input1-2.txt",
659 basename: "input1-3.txt",
661 location: "keep:00000000000000000000000000000000+01/input1-3.txt",
664 basename: "input1-4.txt",
666 location: "keep:00000000000000000000000000000000+01/input1-4.txt",
674 id: "#main/input_dir",
675 doc: "Doc Description",
680 basename: "11111111111111111111111111111111+01",
682 location: "keep:11111111111111111111111111111111+01",
688 id: "#main/input_bool",
689 doc: ["Doc desc 1", "Doc desc 2"],
698 id: "#main/input_int",
707 id: "#main/input_long",
716 id: "#main/input_float",
725 id: "#main/input_double",
734 id: "#main/input_string",
738 input_string: "Hello World",
743 id: "#main/input_file_array",
752 basename: "input2.tar",
754 location: "keep:00000000000000000000000000000000+02/input2.tar",
757 basename: "input3.tar",
759 location: "keep:00000000000000000000000000000000+03/input3.tar",
762 basename: "input3-2.txt",
764 location: "keep:00000000000000000000000000000000+03/input3-2.txt",
769 $import: "import_path",
776 id: "#main/input_dir_array",
785 basename: "11111111111111111111111111111111+02",
787 location: "keep:11111111111111111111111111111111+02",
790 basename: "11111111111111111111111111111111+03",
792 location: "keep:11111111111111111111111111111111+03",
795 $import: "import_path",
802 id: "#main/input_int_array",
814 $import: "import_path",
821 id: "#main/input_long_array",
832 $import: "import_path",
839 id: "#main/input_float_array",
851 $import: "import_path",
858 id: "#main/input_double_array",
865 input_double_array: [
870 $import: "import_path",
877 id: "#main/input_string_array",
884 input_string_array: [
889 $import: "import_path",
896 id: "#main/input_bool_include",
900 input_bool_include: {
901 $include: "include_path",
907 id: "#main/input_int_include",
912 $include: "include_path",
918 id: "#main/input_float_include",
922 input_float_include: {
923 $include: "include_path",
929 id: "#main/input_string_include",
933 input_string_include: {
934 $include: "include_path",
940 id: "#main/input_file_include",
944 input_file_include: {
945 $include: "include_path",
951 id: "#main/input_directory_include",
955 input_directory_include: {
956 $include: "include_path",
962 id: "#main/input_file_url",
967 basename: "index.html",
969 location: "http://example.com/index.html",
975 const testOutputs = [
978 id: "#main/output_file",
979 label: "Label Description",
992 id: "#main/output_file_with_secondary",
993 doc: "Doc Description",
997 output_file_with_secondary: {
998 basename: "main.dat",
1000 location: "main.dat",
1003 basename: "secondary.dat",
1005 location: "secondary.dat",
1008 basename: "secondary2.dat",
1010 location: "secondary2.dat",
1018 id: "#main/output_dir",
1019 doc: ["Doc desc 1", "Doc desc 2"],
1024 basename: "outdir1",
1026 location: "outdir1",
1032 id: "#main/output_bool",
1041 id: "#main/output_int",
1050 id: "#main/output_long",
1059 id: "#main/output_float",
1063 output_float: 100.5,
1068 id: "#main/output_double",
1072 output_double: 100.3,
1077 id: "#main/output_string",
1081 output_string: "Hello output",
1086 id: "#main/output_file_array",
1093 output_file_array: [
1095 basename: "output2.tar",
1097 location: "output2.tar",
1100 basename: "output3.tar",
1102 location: "output3.tar",
1109 id: "#main/output_dir_array",
1118 basename: "outdir2",
1120 location: "outdir2",
1123 basename: "outdir3",
1125 location: "outdir3",
1132 id: "#main/output_int_array",
1139 output_int_array: [10, 11, 12],
1144 id: "#main/output_long_array",
1151 output_long_array: [51, 52],
1156 id: "#main/output_float_array",
1163 output_float_array: [100.2, 100.4, 100.6],
1168 id: "#main/output_double_array",
1175 output_double_array: [100.1, 100.2, 100.3],
1180 id: "#main/output_string_array",
1187 output_string_array: ["Hello", "Output", "!"],
1192 const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1196 .within($mainRow => {
1197 label && cy.contains(label);
1200 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
1202 if (Array.isArray(val)) {
1203 val.forEach(v => cy.get("@secondaryRows").contains(v));
1205 cy.get("@secondaryRows").contains(val);
1209 cy.get("@secondaryRows").contains(collection);
1213 if (Array.isArray(val)) {
1214 val.forEach(v => cy.contains(v));
1220 cy.contains(collection);
1226 const verifyIOParameterImage = (name, url) => {
1231 cy.get('[alt="Inline Preview"]')
1232 .should("be.visible")
1234 expect($img[0].naturalWidth).to.be.greaterThan(0);
1235 expect($img[0].src).contains(url);
1240 it("displays IO parameters with keep links and previews", function () {
1241 // Create output collection for real files
1242 cy.createCollection(adminUser.token, {
1243 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1244 owner_uuid: activeUser.user.uuid,
1245 }).then(testOutputCollection => {
1246 cy.loginAs(activeUser);
1248 cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1250 cy.get("[data-cy=upload-button]").click();
1252 cy.fixture("files/cat.png", "base64").then(content => {
1253 cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
1254 cy.get("[data-cy=form-submit-btn]").click();
1255 cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
1256 // Confirm final collection state.
1257 cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
1260 cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
1263 // Get updated collection pdh
1264 cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
1265 // Add output uuid and inputs to container request
1266 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1268 res.body.output_uuid = testOutputCollection.uuid;
1269 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1270 content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
1272 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1277 inputs: testInputs.map(input => input.definition),
1278 outputs: testOutputs.map(output => output.definition),
1286 // Stub fake output collection
1288 { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
1292 uuid: testOutputCollection.uuid,
1293 portable_data_hash: testOutputCollection.portable_data_hash,
1298 // Stub fake output json
1300 { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
1303 body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
1307 // Stub webdav response, points to output json
1309 { method: "PROPFIND", url: "*" },
1311 fixture: "webdav-propfind-outputs.xml",
1316 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1320 cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
1321 cy.goToPath(`/processes/${containerRequest.uuid}`);
1322 cy.get("[data-cy=process-io-card] h6")
1324 .parents("[data-cy=process-io-card]")
1326 verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
1327 verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
1328 verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
1329 verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
1330 verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
1331 verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
1332 verifyIOParameter("input_int", null, null, "1");
1333 verifyIOParameter("input_long", null, null, "1");
1334 verifyIOParameter("input_float", null, null, "1.5");
1335 verifyIOParameter("input_double", null, null, "1.3");
1336 verifyIOParameter("input_string", null, null, "Hello World");
1337 verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
1338 verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
1339 verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
1340 verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
1341 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
1342 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
1343 verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
1344 verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
1345 verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
1346 verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1347 verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1348 verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
1349 verifyIOParameter("input_bool_include", null, null, "Cannot display value");
1350 verifyIOParameter("input_int_include", null, null, "Cannot display value");
1351 verifyIOParameter("input_float_include", null, null, "Cannot display value");
1352 verifyIOParameter("input_string_include", null, null, "Cannot display value");
1353 verifyIOParameter("input_file_include", null, null, "Cannot display value");
1354 verifyIOParameter("input_directory_include", null, null, "Cannot display value");
1355 verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
1357 cy.get("[data-cy=process-io-card] h6")
1358 .contains("Outputs")
1359 .parents("[data-cy=process-io-card]")
1361 cy.get(ctx).scrollIntoView();
1362 cy.get('[data-cy="io-preview-image-toggle"]').click({ waitForAnimations: false });
1363 const outPdh = testOutputCollection.portable_data_hash;
1365 verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
1366 verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
1367 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
1368 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
1369 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
1370 verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
1371 verifyIOParameter("output_bool", null, null, "true");
1372 verifyIOParameter("output_int", null, null, "1");
1373 verifyIOParameter("output_long", null, null, "1");
1374 verifyIOParameter("output_float", null, null, "100.5");
1375 verifyIOParameter("output_double", null, null, "100.3");
1376 verifyIOParameter("output_string", null, null, "Hello output");
1377 verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
1378 verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
1379 verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
1380 verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
1381 verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
1382 verifyIOParameter("output_long_array", null, null, ["51", "52"]);
1383 verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
1384 verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
1385 verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
1390 it("displays IO parameters with no value", function () {
1391 const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
1392 const fakeOutputPDH = "11111111111111111111111111111111+99/";
1394 cy.loginAs(activeUser);
1396 // Add output uuid and inputs to container request
1397 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1399 res.body.output_uuid = fakeOutputUUID;
1400 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1403 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1408 inputs: testInputs.map(input => input.definition),
1409 outputs: testOutputs.map(output => output.definition),
1417 // Stub fake output collection
1419 { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
1423 uuid: fakeOutputUUID,
1424 portable_data_hash: fakeOutputPDH,
1429 // Stub fake output json
1431 { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
1438 cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
1439 // Stub webdav response, points to output json
1441 { method: "PROPFIND", url: "*" },
1444 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
1449 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1453 cy.getAll("@containerRequest").then(function ([containerRequest]) {
1454 cy.goToPath(`/processes/${containerRequest.uuid}`);
1455 cy.get("[data-cy=process-io-card] h6")
1457 .parents("[data-cy=process-io-card]")
1461 cy.get("tbody tr").each(item => {
1462 cy.wrap(item).contains("No value");
1465 cy.get("[data-cy=process-io-card] h6")
1466 .contains("Outputs")
1467 .parents("[data-cy=process-io-card]")
1469 cy.get("tbody tr").each(item => {
1470 cy.wrap(item).contains("No value");