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("activeuser", "Active", "User", false, true)
24 activeUser = this.activeUser;
29 function createContainerRequest(user, name, docker_image, command, reuse = false, state = "Uncommitted") {
30 return cy.setupDockerImage(docker_image).then(function (dockerImage) {
31 return cy.createContainerRequest(user.token, {
34 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
35 output_path: "stdout.txt",
37 runtime_constraints: {
53 describe('Multiselect Toolbar', () => {
54 it('shows the appropriate buttons in the toolbar', () => {
56 const msButtonTooltips = [
62 'Copy and re-run process',
68 createContainerRequest(
70 `test_container_request ${Math.floor(Math.random() * 999999)}`,
72 ["echo", "hello world"],
75 ).then(function (containerRequest) {
76 cy.loginAs(activeUser);
77 cy.goToPath(`/processes/${containerRequest.uuid}`);
78 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
79 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
80 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
81 cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
83 cy.get('[data-cy=mpv-tabs]').contains("Workflow Runs").click();
84 cy.get('[data-cy=data-table-row]').contains(containerRequest.name).should('exist').parents('[data-cy=data-table-row]').click()
86 cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
87 for (let i = 0; i < msButtonTooltips.length; i++) {
88 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
89 cy.get('body').contains(msButtonTooltips[i]).should('exist')
90 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
96 describe("Details panel", function () {
97 it("shows process details", function () {
98 createContainerRequest(
100 `test_container_request ${Math.floor(Math.random() * 999999)}`,
102 ["echo", "hello world"],
105 ).then(function (containerRequest) {
106 cy.loginAs(activeUser);
107 cy.goToPath(`/processes/${containerRequest.uuid}`);
108 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
109 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
110 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
113 // Fake submitted by another user to test "runtime user" field.
115 // Need to override group contents because we use group
116 // contents to fetch both the container_request and
117 // container record in a single API call.
118 cy.intercept({ method: "GET", url: "**/arvados/v1/groups/contents?*" }, req => {
119 req.on('response', res => {
120 if (!res.body.items) {
123 res.body.items.forEach(item => {
124 item.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
129 createContainerRequest(
131 `test_container_request ${Math.floor(Math.random() * 999999)}`,
133 ["echo", "hello world"],
136 ).then(function (containerRequest) {
137 cy.loginAs(activeUser);
138 cy.goToPath(`/processes/${containerRequest.uuid}`);
139 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
140 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
141 cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
145 it("should show runtime status indicators", function () {
146 // Setup running container with runtime_status error & warning messages
147 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
148 .as("containerRequest")
149 .then(function (containerRequest) {
150 expect(containerRequest.state).to.equal("Committed");
151 expect(containerRequest.container_uuid).not.to.be.equal("");
153 cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
154 expect(queuedContainer.state).to.be.equal("Queued");
156 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
158 }).then(function (lockedContainer) {
159 expect(lockedContainer.state).to.be.equal("Locked");
161 cy.updateContainer(adminUser.token, lockedContainer.uuid, {
164 error: "Something went wrong",
165 errorDetail: "Process exited with status 1",
166 warning: "Free disk space is low",
169 .as("runningContainer")
170 .then(function (runningContainer) {
171 expect(runningContainer.state).to.be.equal("Running");
172 expect(runningContainer.runtime_status).to.be.deep.equal({
173 error: "Something went wrong",
174 errorDetail: "Process exited with status 1",
175 warning: "Free disk space is low",
180 // Test that the UI shows the error and warning messages
181 cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
182 cy.loginAs(activeUser);
183 cy.goToPath(`/processes/${containerRequest.uuid}`);
184 cy.get("[data-cy=process-runtime-status-error]")
185 .should("contain", "Something went wrong")
186 .and("contain", "Process exited with status 1");
187 cy.get("[data-cy=process-runtime-status-warning]")
188 .should("contain", "Free disk space is low")
189 .and("contain", "No additional warning details available");
192 // Force container_count for testing
193 let containerCount = 2;
194 cy.intercept({ method: "GET", url: "**/arvados/v1/groups/contents?*" }, req => {
195 req.on('response', res => {
196 if (!res.body.items) {
199 res.body.items.forEach(item => {
200 item.container_count = containerCount;
205 cy.getAll("@containerRequest", "@runningContainer", "@intercept1").then(function ([containerRequest]) {
206 cy.goToPath(`/processes/${containerRequest.uuid}`);
208 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time")
211 cy.getAll("@containerRequest", "@runningContainer", "@retry1").then(function ([containerRequest]) {
213 cy.goToPath(`/processes/${containerRequest.uuid}`);
215 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
219 it("allows copying processes", function () {
220 const crName = "first_container_request";
221 const copiedCrName = "copied_container_request";
222 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
223 cy.loginAs(activeUser);
224 cy.goToPath(`/processes/${containerRequest.uuid}`);
225 cy.get("[data-cy=process-details]").should("contain", crName);
226 cy.get("[data-cy=process-details]").find('button[aria-label="More options"]').click();
227 cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
230 cy.get("[data-cy=form-dialog]").within(() => {
231 cy.get("input[name=name]").clear().type(copiedCrName);
232 cy.get("[data-cy=projects-tree-home-tree-picker]").click();
233 cy.get("[data-cy=form-submit-btn]").click();
236 cy.get("[data-cy=process-details]").should("contain", copiedCrName);
237 cy.get("[data-cy=process-details]").find("button").contains("Run");
240 const getFakeContainer = fakeContainerUuid => ({
241 href: `/containers/${fakeContainerUuid}`,
242 kind: "arvados#container",
243 etag: "ecfosljpnxfari9a8m7e4yv06",
244 uuid: fakeContainerUuid,
245 owner_uuid: "zzzzz-tpzed-000000000000000",
246 created_at: "2023-02-13T15:55:47.308915000Z",
247 modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
248 modified_at: "2023-02-15T19:12:45.987086000Z",
250 "arvados-cwl-runner",
253 "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
254 "/var/lib/cwl/workflow.json#main",
255 "/var/lib/cwl/cwl.input.json",
257 container_image: "4ad7d11381df349e464694762db14e04+303",
258 cwd: "/var/spool/cwl",
262 locked_by_uuid: null,
265 output_path: "/var/spool/cwl",
267 runtime_constraints: {
272 hardware_capability: "",
274 keep_cache_disk: 2147483648,
282 scheduling_parameters: {
287 runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
288 runtime_auth_scopes: ["all"],
290 gateway_address: null,
291 interactive_session_started: false,
292 output_storage_classes: ["default"],
293 output_properties: {},
295 subrequests_cost: 0.0,
298 it("shows cancel button when appropriate", function () {
299 // Ignore collection requests
301 { method: "GET", url: `**/arvados/v1/collections/*` },
308 // Uncommitted container
309 const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
310 createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
313 cy.loginAs(activeUser);
314 // Navigate to process and verify run / cancel button
315 cy.goToPath(`/processes/${containerRequest.uuid}`);
317 cy.get("[data-cy=process-details]").should("contain", crUncommitted);
318 cy.get("[data-cy=process-run-button]").should("exist");
319 cy.get("[data-cy=process-cancel-button]").should("not.exist");
323 const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
324 const fakeCtrUuid = "zzzzz-dz642-000000000000001";
325 createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
328 // Fake container uuid
329 cy.intercept({ method: "GET", url: `**/arvados/v1/groups/contents?*${containerRequest.uuid}*` }, req => {
330 req.on('response', res => {
331 if (!res.body.items) {
334 res.body.items.forEach(item => {
335 item.container_uuid = fakeCtrUuid;
337 item.state = "Committed";
339 if (!res.body.included) {
342 const container = getFakeContainer(fakeCtrUuid);
343 res.body.included = [{ ...container, state: "Queued", priority: 500 }];
347 // Navigate to process and verify cancel button
348 cy.goToPath(`/processes/${containerRequest.uuid}`);
350 cy.get("[data-cy=process-details]").should("contain", crQueued);
351 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
355 const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
356 const fakeCtrLockedUuid = "zzzzz-dz642-000000000000002";
357 createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
360 // Fake container uuid
361 cy.intercept({ method: "GET", url: `**/arvados/v1/groups/contents?*${containerRequest.uuid}*` }, req => {
362 req.on('response', res => {
363 if (!res.body.items) {
366 res.body.items.forEach(item => {
367 item.container_uuid = fakeCtrLockedUuid;
369 item.state = "Committed";
371 if (!res.body.included) {
374 const container = getFakeContainer(fakeCtrLockedUuid);
375 res.body.included = [{ ...container, state: "Locked", priority: 500 }];
379 // Navigate to process and verify cancel button
380 cy.goToPath(`/processes/${containerRequest.uuid}`);
382 cy.get("[data-cy=process-details]").should("contain", crLocked);
383 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
387 const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
388 const fakeCtrOnHoldUuid = "zzzzz-dz642-000000000000003";
389 createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
392 // Fake container uuid
393 cy.intercept({ method: "GET", url: `**/arvados/v1/groups/contents?*${containerRequest.uuid}*` }, req => {
394 req.on('response', res => {
395 if (!res.body.items) {
398 res.body.items.forEach(item => {
399 item.container_uuid = fakeCtrOnHoldUuid;
401 item.state = "Committed";
403 if (!res.body.included) {
406 const container = getFakeContainer(fakeCtrOnHoldUuid);
407 res.body.included = [{ ...container, state: "Queued", priority: 0 }];
411 // Navigate to process and verify cancel button
412 cy.goToPath(`/processes/${containerRequest.uuid}`);
414 cy.get("[data-cy=process-details]").should("contain", crOnHold);
415 cy.get("[data-cy=process-run-button]").should("exist");
416 cy.get("[data-cy=process-cancel-button]").should("not.exist");
421 describe("Logs panel", function () {
422 it("shows live process logs", function () {
423 // Fake container uuid
424 cy.intercept({ method: "GET", url: `**/arvados/v1/groups/contents?*` }, req => {
425 req.on('response', res => {
426 if (!res.body.included || res.body.included.length === 0) {
429 res.body.included[0].state = ContainerState.RUNNING;
433 const crName = "test_container_request";
434 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
435 // Create empty log file before loading process page
436 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [""]);
438 cy.loginAs(activeUser);
439 cy.goToPath(`/processes/${containerRequest.uuid}`);
440 cy.get("[data-cy=process-details]").should("contain", crName);
441 cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
444 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", ["2023-07-18T20:14:48.128642814Z hello world"]).then(() => {
445 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
448 // Append new log line to different file
449 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:49.128642814Z hello new line"]).then(() => {
450 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello new line");
455 it("filters process logs by event type", function () {
456 const nodeInfoLogs = [
458 "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",
461 "vendor_id : GenuineIntel",
464 "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
466 const crunchRunLogs = [
467 "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
468 "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
469 "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
470 "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
471 "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
474 "2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
475 "2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
476 "2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.",
477 "2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
478 "2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.",
479 "2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
480 "2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
481 "2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
482 "2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.",
483 "2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
484 "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.",
485 "2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
486 "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.",
487 "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.",
488 "2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
489 "2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.",
490 "2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
491 "2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.",
494 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
497 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as("nodeInfoLogs");
498 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as("crunchRunLogs");
499 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as("stdoutLogs");
501 cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
502 cy.loginAs(activeUser);
503 cy.goToPath(`/processes/${containerRequest.uuid}`);
505 // Should show main logs by default
506 cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
507 cy.get("[data-cy=process-logs]")
508 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
509 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
510 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
512 cy.get("[data-cy=process-logs-filter]").click();
513 cy.get("body").contains("li", "All logs").click();
514 cy.get("[data-cy=process-logs]")
515 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
516 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
517 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
518 // Select 'node-info' logs
519 cy.get("[data-cy=process-logs-filter]").click();
520 cy.get("body").contains("li", "node-info").click();
521 cy.get("[data-cy=process-logs]")
522 .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
523 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
524 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
525 // Select 'stdout' logs
526 cy.get("[data-cy=process-logs-filter]").click();
527 cy.get("body").contains("li", "stdout").click();
528 cy.get("[data-cy=process-logs]")
529 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
530 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
531 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
536 it("sorts combined logs", function () {
537 const crName = "test_container_request";
538 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
539 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
547 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
548 "2023-07-18T20:14:48.128642814Z first",
549 "2023-07-18T20:14:49.128642814Z third",
552 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:48.528642814Z second"]).as("stderr");
554 cy.loginAs(activeUser);
555 cy.goToPath(`/processes/${containerRequest.uuid}`);
556 cy.get("[data-cy=process-details]").should("contain", crName);
557 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
559 cy.getAll("@node-info", "@stdout", "@stderr").then(() => {
560 // Verify sorted main logs
561 cy.get("[data-cy=process-logs] span > p", { timeout: 7000 }).eq(0).should("contain", "2023-07-18T20:14:48.128642814Z first");
562 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:48.528642814Z second");
563 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:49.128642814Z third");
565 // Switch to All logs
566 cy.get("[data-cy=process-logs-filter]").click();
567 cy.get("body").contains("li", "All logs").click();
568 // Verify non-sorted lines were preserved
569 cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "3: nodeinfo 1");
570 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2: nodeinfo 2");
571 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "1: nodeinfo 3");
572 cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2: nodeinfo 4");
573 cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "3: nodeinfo 5");
574 // Verify sorted logs
575 cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z first");
576 cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.528642814Z second");
577 cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:49.128642814Z third");
582 it("preserves original ordering of lines within the same log type", function () {
583 const crName = "test_container_request";
584 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
585 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
587 "2023-07-18T20:14:46.000000000Z A out 1",
588 // Comes fourth in a contiguous block
589 "2023-07-18T20:14:48.128642814Z A out 2",
590 "2023-07-18T20:14:48.128642814Z X out 3",
591 "2023-07-18T20:14:48.128642814Z A out 4",
594 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
596 "2023-07-18T20:14:47.000000000Z Z err 1",
597 // Comes third in a contiguous block
598 "2023-07-18T20:14:48.128642814Z B err 2",
599 "2023-07-18T20:14:48.128642814Z C err 3",
600 "2023-07-18T20:14:48.128642814Z Y err 4",
601 "2023-07-18T20:14:48.128642814Z Z err 5",
602 "2023-07-18T20:14:48.128642814Z A err 6",
605 cy.loginAs(activeUser);
606 cy.goToPath(`/processes/${containerRequest.uuid}`);
607 cy.get("[data-cy=process-details]").should("contain", crName);
608 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
610 cy.getAll("@stdout", "@stderr").then(() => {
611 // Switch to All logs
612 cy.get("[data-cy=process-logs-filter]").click();
613 cy.get("body").contains("li", "All logs").click();
614 // Verify sorted logs
615 cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
616 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
617 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
618 cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
619 cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
620 cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
621 cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
622 cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
623 cy.get("[data-cy=process-logs] span > p").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
624 cy.get("[data-cy=process-logs] span > p").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
629 it("correctly generates sniplines", function () {
630 const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
631 const crName = "test_container_request";
632 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
633 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
634 "X".repeat(63999) + "_" + "O".repeat(100) + "_" + "X".repeat(63999),
637 cy.loginAs(activeUser);
638 cy.goToPath(`/processes/${containerRequest.uuid}`);
639 cy.get("[data-cy=process-details]").should("contain", crName);
640 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
642 // Switch to stdout since lines are unsortable (no timestamp)
643 cy.get("[data-cy=process-logs-filter]").click();
644 cy.get("body").contains("li", "stdout").click();
646 cy.getAll("@stdout").then(() => {
647 // Verify first 64KB and snipline
648 cy.get("[data-cy=process-logs] span > p", { timeout: 7000 })
650 .should("contain", "X".repeat(63999) + "_\n" + SNIPLINE);
652 cy.get("[data-cy=process-logs] span > p")
654 .should("contain", "_" + "X".repeat(63999));
655 // Verify none of the Os got through
656 cy.get("[data-cy=process-logs] span > p").should("not.contain", "O");
661 it("correctly break long lines when no obvious line separation exists", function () {
662 function randomString(length) {
663 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
665 for (let i = 0; i < length; i++) {
666 res += chars.charAt(Math.floor(Math.random() * chars.length));
671 const logLinesQty = 10;
673 for (let i = 0; i < logLinesQty; i++) {
674 const length = Math.floor(Math.random() * 500) + 500;
675 logLines.push(randomString(length));
678 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
681 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", logLines).as("stdoutLogs");
683 cy.getAll("@stdoutLogs").then(function () {
684 cy.loginAs(activeUser);
685 cy.goToPath(`/processes/${containerRequest.uuid}`);
686 // Select 'stdout' log filter
687 cy.get("[data-cy=process-logs-filter]").click();
688 cy.get("body").contains("li", "stdout").click();
689 cy.get("[data-cy=process-logs] span > p")
690 .should('have.length', logLinesQty)
692 expect($p.text().length).to.be.greaterThan(499);
694 // This looks like an ugly hack, but I was not able
695 // to get [client|scroll]Width attributes through
696 // the usual Cypress methods.
697 const parentClientWidth = $p[0].parentElement.clientWidth;
698 const parentScrollWidth = $p[0].parentElement.scrollWidth
699 // Scrollbar should not be visible
700 expect(parentClientWidth).to.be.eq(parentScrollWidth);
707 describe("I/O panel", function () {
711 id: "#main/input_file",
712 label: "Label Description",
717 basename: "input1.tar",
719 location: "keep:00000000000000000000000000000000+01/input1.tar",
722 basename: "input1-2.txt",
724 location: "keep:00000000000000000000000000000000+01/input1-2.txt",
727 basename: "input1-3.txt",
729 location: "keep:00000000000000000000000000000000+01/input1-3.txt",
732 basename: "input1-4.txt",
734 location: "keep:00000000000000000000000000000000+01/input1-4.txt",
742 id: "#main/input_dir",
743 doc: "Doc Description",
748 basename: "11111111111111111111111111111111+01",
750 location: "keep:11111111111111111111111111111111+01",
756 id: "#main/input_bool",
757 doc: ["Doc desc 1", "Doc desc 2"],
766 id: "#main/input_int",
775 id: "#main/input_long",
784 id: "#main/input_float",
793 id: "#main/input_double",
802 id: "#main/input_string",
806 input_string: "Hello World",
811 id: "#main/input_file_array",
820 basename: "input2.tar",
822 location: "keep:00000000000000000000000000000000+02/input2.tar",
825 basename: "input3.tar",
827 location: "keep:00000000000000000000000000000000+03/input3.tar",
830 basename: "input3-2.txt",
832 location: "keep:00000000000000000000000000000000+03/input3-2.txt",
837 $import: "import_path",
844 id: "#main/input_dir_array",
853 basename: "11111111111111111111111111111111+02",
855 location: "keep:11111111111111111111111111111111+02",
858 basename: "11111111111111111111111111111111+03",
860 location: "keep:11111111111111111111111111111111+03",
863 $import: "import_path",
870 id: "#main/input_int_array",
882 $import: "import_path",
889 id: "#main/input_long_array",
900 $import: "import_path",
907 id: "#main/input_float_array",
919 $import: "import_path",
926 id: "#main/input_double_array",
933 input_double_array: [
938 $import: "import_path",
945 id: "#main/input_string_array",
952 input_string_array: [
957 $import: "import_path",
964 id: "#main/input_bool_include",
968 input_bool_include: {
969 $include: "include_path",
975 id: "#main/input_int_include",
980 $include: "include_path",
986 id: "#main/input_float_include",
990 input_float_include: {
991 $include: "include_path",
997 id: "#main/input_string_include",
1001 input_string_include: {
1002 $include: "include_path",
1008 id: "#main/input_file_include",
1012 input_file_include: {
1013 $include: "include_path",
1019 id: "#main/input_directory_include",
1023 input_directory_include: {
1024 $include: "include_path",
1030 id: "#main/input_file_url",
1035 basename: "index.html",
1037 location: "http://example.com/index.html",
1043 const testOutputs = [
1046 id: "#main/output_file",
1047 label: "Label Description",
1052 basename: "cat.png",
1054 location: "cat.png",
1060 id: "#main/output_file_with_secondary",
1061 doc: "Doc Description",
1065 output_file_with_secondary: {
1066 basename: "main.dat",
1068 location: "main.dat",
1071 basename: "secondary.dat",
1073 location: "secondary.dat",
1076 basename: "secondary2.dat",
1078 location: "secondary2.dat",
1086 id: "#main/output_dir",
1087 doc: ["Doc desc 1", "Doc desc 2"],
1092 basename: "outdir1",
1094 location: "outdir1",
1100 id: "#main/output_bool",
1109 id: "#main/output_int",
1118 id: "#main/output_long",
1127 id: "#main/output_float",
1131 output_float: 100.5,
1136 id: "#main/output_double",
1140 output_double: 100.3,
1145 id: "#main/output_string",
1149 output_string: "Hello output",
1154 id: "#main/output_file_array",
1161 output_file_array: [
1163 basename: "output2.tar",
1165 location: "output2.tar",
1168 basename: "output3.tar",
1170 location: "output3.tar",
1177 id: "#main/output_dir_array",
1186 basename: "outdir2",
1188 location: "outdir2",
1191 basename: "outdir3",
1193 location: "outdir3",
1200 id: "#main/output_int_array",
1207 output_int_array: [10, 11, 12],
1212 id: "#main/output_long_array",
1219 output_long_array: [51, 52],
1224 id: "#main/output_float_array",
1231 output_float_array: [100.2, 100.4, 100.6],
1236 id: "#main/output_double_array",
1243 output_double_array: [100.1, 100.2, 100.3],
1248 id: "#main/output_string_array",
1255 output_string_array: ["Hello", "Output", "!"],
1260 const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1264 .within($mainRow => {
1265 cy.get($mainRow).scrollIntoView();
1266 label && cy.contains(label);
1269 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
1271 if (Array.isArray(val)) {
1272 val.forEach(v => cy.get("@secondaryRows").contains(v));
1274 cy.get("@secondaryRows").contains(val);
1278 cy.get("@secondaryRows").contains(collection);
1282 if (Array.isArray(val)) {
1283 val.forEach(v => cy.contains(v));
1289 cy.contains(collection);
1295 const verifyIOParameterImage = (name, url) => {
1300 cy.get('[alt="Inline Preview"]')
1301 .should("be.visible")
1303 expect($img[0].naturalWidth).to.be.greaterThan(0);
1304 expect($img[0].src).contains(url);
1309 it("displays IO parameters with keep links and previews", function () {
1310 // Create output collection for real files
1311 cy.createCollection(adminUser.token, {
1312 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1313 owner_uuid: activeUser.user.uuid,
1314 }).then(testOutputCollection => {
1315 cy.loginAs(activeUser);
1317 cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1319 cy.get("[data-cy=upload-button]").click();
1321 cy.fixture("files/cat.png", "base64").then(content => {
1322 cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
1323 cy.get("[data-cy=form-submit-btn]").click();
1324 cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
1325 // Confirm final collection state.
1326 cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
1329 cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
1332 // Get updated collection pdh
1333 cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
1334 // Add output uuid and inputs to container request
1335 cy.intercept({ method: "GET", url: `**/arvados/v1/groups/contents?*` }, req => {
1336 req.on('response', res => {
1337 if (res.body.included && res.body.included.length > 0) {
1338 res.body.included[0].state = ContainerState.RUNNING;
1340 const body = res.body.items ? res.body.items[0] : res.body;
1341 if (!body || !body.mounts) {
1344 body.output_uuid = testOutputCollection.uuid;
1345 body.mounts["/var/lib/cwl/cwl.input.json"] = {
1346 content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
1348 body.mounts["/var/lib/cwl/workflow.json"] = {
1353 inputs: testInputs.map(input => input.definition),
1354 outputs: testOutputs.map(output => output.definition),
1362 // Stub fake output collection
1364 { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
1368 uuid: testOutputCollection.uuid,
1369 portable_data_hash: testOutputCollection.portable_data_hash,
1374 // Stub fake output json
1376 { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
1379 body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
1383 // Stub webdav response, points to output json
1385 { method: "PROPFIND", url: "*" },
1387 fixture: "webdav-propfind-outputs.xml",
1392 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1396 cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
1397 cy.goToPath(`/processes/${containerRequest.uuid}`);
1398 cy.get("[data-cy=process-io-card] h6")
1399 .contains("Input Parameters")
1400 .parents("[data-cy=process-io-card]")
1402 cy.get(ctx).scrollIntoView();
1403 verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
1404 verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
1405 verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
1406 verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
1407 verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
1408 verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
1409 verifyIOParameter("input_int", null, null, "1");
1410 verifyIOParameter("input_long", null, null, "1");
1411 verifyIOParameter("input_float", null, null, "1.5");
1412 verifyIOParameter("input_double", null, null, "1.3");
1413 verifyIOParameter("input_string", null, null, "Hello World");
1414 verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
1415 verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
1416 verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
1417 verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
1418 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
1419 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
1420 verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
1421 verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
1422 verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
1423 verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1424 verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1425 verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
1426 verifyIOParameter("input_bool_include", null, null, "Cannot display value");
1427 verifyIOParameter("input_int_include", null, null, "Cannot display value");
1428 verifyIOParameter("input_float_include", null, null, "Cannot display value");
1429 verifyIOParameter("input_string_include", null, null, "Cannot display value");
1430 verifyIOParameter("input_file_include", null, null, "Cannot display value");
1431 verifyIOParameter("input_directory_include", null, null, "Cannot display value");
1432 verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
1434 cy.get("[data-cy=process-io-card] h6")
1435 .contains("Output Parameters")
1436 .parents("[data-cy=process-io-card]")
1438 cy.get(ctx).scrollIntoView();
1439 const outPdh = testOutputCollection.portable_data_hash;
1441 verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
1442 // Disabled until image preview returns
1443 // verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
1444 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
1445 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
1446 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
1447 verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
1448 verifyIOParameter("output_bool", null, null, "true");
1449 verifyIOParameter("output_int", null, null, "1");
1450 verifyIOParameter("output_long", null, null, "1");
1451 verifyIOParameter("output_float", null, null, "100.5");
1452 verifyIOParameter("output_double", null, null, "100.3");
1453 verifyIOParameter("output_string", null, null, "Hello output");
1454 verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
1455 verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
1456 verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
1457 verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
1458 verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
1459 verifyIOParameter("output_long_array", null, null, ["51", "52"]);
1460 verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
1461 verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
1462 verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
1467 it("displays IO parameters with no value", function () {
1468 const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
1469 const fakeOutputPDH = "11111111111111111111111111111111+99/";
1471 cy.loginAs(activeUser);
1473 // Add output uuid and inputs to container request
1474 cy.intercept({ method: "GET", url: `**/arvados/v1/groups/contents?*` }, req => {
1475 req.on('response', res => {
1476 const body = res.body.items ? res.body.items[0] : res.body;
1477 if (!body || !body.mounts) {
1480 body.output_uuid = fakeOutputUUID;
1481 body.mounts["/var/lib/cwl/cwl.input.json"] = {
1484 body.mounts["/var/lib/cwl/workflow.json"] = {
1489 inputs: testInputs.map(input => input.definition),
1490 outputs: testOutputs.map(output => output.definition),
1498 // Stub fake output collection
1500 { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
1504 uuid: fakeOutputUUID,
1505 portable_data_hash: fakeOutputPDH,
1510 // Stub fake output json
1512 { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
1519 cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
1520 // Stub webdav response, points to output json
1522 { method: "PROPFIND", url: "*" },
1525 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
1530 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1534 cy.getAll("@containerRequest").then(function ([containerRequest]) {
1535 cy.goToPath(`/processes/${containerRequest.uuid}`);
1538 cy.get("[data-cy=process-io-card] h6")
1539 .contains("Input Parameters")
1540 .parents("[data-cy=process-io-card]")
1542 cy.get(ctx).scrollIntoView();
1546 testInputs.map((input) => {
1547 verifyIOParameter(input.definition.id.split('/').slice(-1)[0], null, null, "No value");
1550 cy.get("[data-cy=process-io-card] h6")
1551 .contains("Output Parameters")
1552 .parents("[data-cy=process-io-card]")
1554 cy.get(ctx).scrollIntoView();
1556 testOutputs.map((output) => {
1557 verifyIOParameter(output.definition.id.split('/').slice(-1)[0], null, null, "No value");