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;
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 both group contents and direct get,
116 // because it displays the the cached value from
117 // 'contents' for a few moments while requesting the full
119 cy.intercept({ method: "GET", url: "**/arvados/v1/groups/*/contents?*" }, req => {
120 req.on('response', res => {
121 res.body.items.forEach(item => {
122 item.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
126 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
127 req.on('response', res => {
128 res.body.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
132 createContainerRequest(
134 `test_container_request ${Math.floor(Math.random() * 999999)}`,
136 ["echo", "hello world"],
139 ).then(function (containerRequest) {
140 cy.loginAs(activeUser);
141 cy.goToPath(`/processes/${containerRequest.uuid}`);
142 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
143 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
144 cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
148 it("should show runtime status indicators", function () {
149 // Setup running container with runtime_status error & warning messages
150 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
151 .as("containerRequest")
152 .then(function (containerRequest) {
153 expect(containerRequest.state).to.equal("Committed");
154 expect(containerRequest.container_uuid).not.to.be.equal("");
156 cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
157 expect(queuedContainer.state).to.be.equal("Queued");
159 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
161 }).then(function (lockedContainer) {
162 expect(lockedContainer.state).to.be.equal("Locked");
164 cy.updateContainer(adminUser.token, lockedContainer.uuid, {
167 error: "Something went wrong",
168 errorDetail: "Process exited with status 1",
169 warning: "Free disk space is low",
172 .as("runningContainer")
173 .then(function (runningContainer) {
174 expect(runningContainer.state).to.be.equal("Running");
175 expect(runningContainer.runtime_status).to.be.deep.equal({
176 error: "Something went wrong",
177 errorDetail: "Process exited with status 1",
178 warning: "Free disk space is low",
183 // Test that the UI shows the error and warning messages
184 cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
185 cy.loginAs(activeUser);
186 cy.goToPath(`/processes/${containerRequest.uuid}`);
187 cy.get("[data-cy=process-runtime-status-error]")
188 .should("contain", "Something went wrong")
189 .and("contain", "Process exited with status 1");
190 cy.get("[data-cy=process-runtime-status-warning]")
191 .should("contain", "Free disk space is low")
192 .and("contain", "No additional warning details available");
195 // Force container_count for testing
196 let containerCount = 2;
197 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
198 req.on('response', res => {
199 res.body.container_count = containerCount;
203 cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
204 cy.goToPath(`/processes/${containerRequest.uuid}`);
206 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time")
209 cy.getAll("@containerRequest", "@runningContainer", "@retry1").then(function ([containerRequest]) {
211 cy.goToPath(`/processes/${containerRequest.uuid}`);
213 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
217 it("allows copying processes", function () {
218 const crName = "first_container_request";
219 const copiedCrName = "copied_container_request";
220 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
221 cy.loginAs(activeUser);
222 cy.goToPath(`/processes/${containerRequest.uuid}`);
223 cy.get("[data-cy=process-details]").should("contain", crName);
225 cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
226 cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
229 cy.get("[data-cy=form-dialog]").within(() => {
230 cy.get("input[name=name]").clear().type(copiedCrName);
231 cy.get("[data-cy=projects-tree-home-tree-picker]").click();
232 cy.get("[data-cy=form-submit-btn]").click();
235 cy.get("[data-cy=process-details]").should("contain", copiedCrName);
236 cy.get("[data-cy=process-details]").find("button").contains("Run");
239 const getFakeContainer = fakeContainerUuid => ({
240 href: `/containers/${fakeContainerUuid}`,
241 kind: "arvados#container",
242 etag: "ecfosljpnxfari9a8m7e4yv06",
243 uuid: fakeContainerUuid,
244 owner_uuid: "zzzzz-tpzed-000000000000000",
245 created_at: "2023-02-13T15:55:47.308915000Z",
246 modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
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 fakeCrUuid = "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/container_requests/${containerRequest.uuid}` }, req => {
330 req.on('response', res => {
331 res.body.output_uuid = fakeCrUuid;
332 res.body.priority = 500;
333 res.body.state = "Committed";
338 const container = getFakeContainer(fakeCrUuid);
340 { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
343 body: { ...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 fakeCrLockedUuid = "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/container_requests/${containerRequest.uuid}` }, req => {
362 req.on('response', res => {
363 res.body.output_uuid = fakeCrLockedUuid;
364 res.body.priority = 500;
365 res.body.state = "Committed";
370 const container = getFakeContainer(fakeCrLockedUuid);
372 { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
375 body: { ...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 fakeCrOnHoldUuid = "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/container_requests/${containerRequest.uuid}` }, req => {
394 req.on('response', res => {
395 res.body.output_uuid = fakeCrOnHoldUuid;
396 res.body.priority = 0;
397 res.body.state = "Committed";
402 const container = getFakeContainer(fakeCrOnHoldUuid);
404 { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
407 body: { ...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 cy.intercept({ method: "GET", url: "**/arvados/v1/containers/*" }, req => {
424 req.on('response', res => {
425 res.body.state = ContainerState.RUNNING;
429 const crName = "test_container_request";
430 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
431 // Create empty log file before loading process page
432 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [""]);
434 cy.loginAs(activeUser);
435 cy.goToPath(`/processes/${containerRequest.uuid}`);
436 cy.get("[data-cy=process-details]").should("contain", crName);
437 cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
440 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", ["2023-07-18T20:14:48.128642814Z hello world"]).then(() => {
441 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
444 // Append new log line to different file
445 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:49.128642814Z hello new line"]).then(() => {
446 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello new line");
451 it("filters process logs by event type", function () {
452 const nodeInfoLogs = [
454 "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",
457 "vendor_id : GenuineIntel",
460 "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
462 const crunchRunLogs = [
463 "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
464 "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
465 "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
466 "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
467 "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
470 "2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
471 "2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
472 "2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.",
473 "2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
474 "2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.",
475 "2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
476 "2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
477 "2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
478 "2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.",
479 "2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
480 "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.",
481 "2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
482 "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.",
483 "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.",
484 "2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
485 "2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.",
486 "2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
487 "2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.",
490 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
493 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as("nodeInfoLogs");
494 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as("crunchRunLogs");
495 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as("stdoutLogs");
497 cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
498 cy.loginAs(activeUser);
499 cy.goToPath(`/processes/${containerRequest.uuid}`);
500 // Should show main logs by default
501 cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
502 cy.get("[data-cy=process-logs]")
503 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
504 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
505 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
507 cy.get("[data-cy=process-logs-filter]").click();
508 cy.get("body").contains("li", "All logs").click();
509 cy.get("[data-cy=process-logs]")
510 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
511 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
512 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
513 // Select 'node-info' logs
514 cy.get("[data-cy=process-logs-filter]").click();
515 cy.get("body").contains("li", "node-info").click();
516 cy.get("[data-cy=process-logs]")
517 .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
518 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
519 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
520 // Select 'stdout' logs
521 cy.get("[data-cy=process-logs-filter]").click();
522 cy.get("body").contains("li", "stdout").click();
523 cy.get("[data-cy=process-logs]")
524 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
525 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
526 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
531 it("sorts combined logs", function () {
532 const crName = "test_container_request";
533 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
534 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
542 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
543 "2023-07-18T20:14:48.128642814Z first",
544 "2023-07-18T20:14:49.128642814Z third",
547 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:48.528642814Z second"]).as("stderr");
549 cy.loginAs(activeUser);
550 cy.goToPath(`/processes/${containerRequest.uuid}`);
551 cy.get("[data-cy=process-details]").should("contain", crName);
552 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
554 cy.getAll("@node-info", "@stdout", "@stderr").then(() => {
555 // Verify sorted main logs
556 cy.get("[data-cy=process-logs] span > p", { timeout: 7000 }).eq(0).should("contain", "2023-07-18T20:14:48.128642814Z first");
557 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:48.528642814Z second");
558 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:49.128642814Z third");
560 // Switch to All logs
561 cy.get("[data-cy=process-logs-filter]").click();
562 cy.get("body").contains("li", "All logs").click();
563 // Verify non-sorted lines were preserved
564 cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "3: nodeinfo 1");
565 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2: nodeinfo 2");
566 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "1: nodeinfo 3");
567 cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2: nodeinfo 4");
568 cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "3: nodeinfo 5");
569 // Verify sorted logs
570 cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z first");
571 cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.528642814Z second");
572 cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:49.128642814Z third");
577 it("preserves original ordering of lines within the same log type", function () {
578 const crName = "test_container_request";
579 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
580 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
582 "2023-07-18T20:14:46.000000000Z A out 1",
583 // Comes fourth in a contiguous block
584 "2023-07-18T20:14:48.128642814Z A out 2",
585 "2023-07-18T20:14:48.128642814Z X out 3",
586 "2023-07-18T20:14:48.128642814Z A out 4",
589 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
591 "2023-07-18T20:14:47.000000000Z Z err 1",
592 // Comes third in a contiguous block
593 "2023-07-18T20:14:48.128642814Z B err 2",
594 "2023-07-18T20:14:48.128642814Z C err 3",
595 "2023-07-18T20:14:48.128642814Z Y err 4",
596 "2023-07-18T20:14:48.128642814Z Z err 5",
597 "2023-07-18T20:14:48.128642814Z A err 6",
600 cy.loginAs(activeUser);
601 cy.goToPath(`/processes/${containerRequest.uuid}`);
602 cy.get("[data-cy=process-details]").should("contain", crName);
603 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
605 cy.getAll("@stdout", "@stderr").then(() => {
606 // Switch to All logs
607 cy.get("[data-cy=process-logs-filter]").click();
608 cy.get("body").contains("li", "All logs").click();
609 // Verify sorted logs
610 cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
611 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
612 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
613 cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
614 cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
615 cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
616 cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
617 cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
618 cy.get("[data-cy=process-logs] span > p").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
619 cy.get("[data-cy=process-logs] span > p").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
624 it("correctly generates sniplines", function () {
625 const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
626 const crName = "test_container_request";
627 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
628 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
629 "X".repeat(63999) + "_" + "O".repeat(100) + "_" + "X".repeat(63999),
632 cy.loginAs(activeUser);
633 cy.goToPath(`/processes/${containerRequest.uuid}`);
634 cy.get("[data-cy=process-details]").should("contain", crName);
635 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
637 // Switch to stdout since lines are unsortable (no timestamp)
638 cy.get("[data-cy=process-logs-filter]").click();
639 cy.get("body").contains("li", "stdout").click();
641 cy.getAll("@stdout").then(() => {
642 // Verify first 64KB and snipline
643 cy.get("[data-cy=process-logs] span > p", { timeout: 7000 })
645 .should("contain", "X".repeat(63999) + "_\n" + SNIPLINE);
647 cy.get("[data-cy=process-logs] span > p")
649 .should("contain", "_" + "X".repeat(63999));
650 // Verify none of the Os got through
651 cy.get("[data-cy=process-logs] span > p").should("not.contain", "O");
656 it("correctly break long lines when no obvious line separation exists", function () {
657 function randomString(length) {
658 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
660 for (let i = 0; i < length; i++) {
661 res += chars.charAt(Math.floor(Math.random() * chars.length));
666 const logLinesQty = 10;
668 for (let i = 0; i < logLinesQty; i++) {
669 const length = Math.floor(Math.random() * 500) + 500;
670 logLines.push(randomString(length));
673 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
676 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", logLines).as("stdoutLogs");
678 cy.getAll("@stdoutLogs").then(function () {
679 cy.loginAs(activeUser);
680 cy.goToPath(`/processes/${containerRequest.uuid}`);
681 // Select 'stdout' log filter
682 cy.get("[data-cy=process-logs-filter]").click();
683 cy.get("body").contains("li", "stdout").click();
684 cy.get("[data-cy=process-logs] span > p")
685 .should('have.length', logLinesQty)
687 expect($p.text().length).to.be.greaterThan(499);
689 // This looks like an ugly hack, but I was not able
690 // to get [client|scroll]Width attributes through
691 // the usual Cypress methods.
692 const parentClientWidth = $p[0].parentElement.clientWidth;
693 const parentScrollWidth = $p[0].parentElement.scrollWidth
694 // Scrollbar should not be visible
695 expect(parentClientWidth).to.be.eq(parentScrollWidth);
702 describe("I/O panel", function () {
706 id: "#main/input_file",
707 label: "Label Description",
712 basename: "input1.tar",
714 location: "keep:00000000000000000000000000000000+01/input1.tar",
717 basename: "input1-2.txt",
719 location: "keep:00000000000000000000000000000000+01/input1-2.txt",
722 basename: "input1-3.txt",
724 location: "keep:00000000000000000000000000000000+01/input1-3.txt",
727 basename: "input1-4.txt",
729 location: "keep:00000000000000000000000000000000+01/input1-4.txt",
737 id: "#main/input_dir",
738 doc: "Doc Description",
743 basename: "11111111111111111111111111111111+01",
745 location: "keep:11111111111111111111111111111111+01",
751 id: "#main/input_bool",
752 doc: ["Doc desc 1", "Doc desc 2"],
761 id: "#main/input_int",
770 id: "#main/input_long",
779 id: "#main/input_float",
788 id: "#main/input_double",
797 id: "#main/input_string",
801 input_string: "Hello World",
806 id: "#main/input_file_array",
815 basename: "input2.tar",
817 location: "keep:00000000000000000000000000000000+02/input2.tar",
820 basename: "input3.tar",
822 location: "keep:00000000000000000000000000000000+03/input3.tar",
825 basename: "input3-2.txt",
827 location: "keep:00000000000000000000000000000000+03/input3-2.txt",
832 $import: "import_path",
839 id: "#main/input_dir_array",
848 basename: "11111111111111111111111111111111+02",
850 location: "keep:11111111111111111111111111111111+02",
853 basename: "11111111111111111111111111111111+03",
855 location: "keep:11111111111111111111111111111111+03",
858 $import: "import_path",
865 id: "#main/input_int_array",
877 $import: "import_path",
884 id: "#main/input_long_array",
895 $import: "import_path",
902 id: "#main/input_float_array",
914 $import: "import_path",
921 id: "#main/input_double_array",
928 input_double_array: [
933 $import: "import_path",
940 id: "#main/input_string_array",
947 input_string_array: [
952 $import: "import_path",
959 id: "#main/input_bool_include",
963 input_bool_include: {
964 $include: "include_path",
970 id: "#main/input_int_include",
975 $include: "include_path",
981 id: "#main/input_float_include",
985 input_float_include: {
986 $include: "include_path",
992 id: "#main/input_string_include",
996 input_string_include: {
997 $include: "include_path",
1003 id: "#main/input_file_include",
1007 input_file_include: {
1008 $include: "include_path",
1014 id: "#main/input_directory_include",
1018 input_directory_include: {
1019 $include: "include_path",
1025 id: "#main/input_file_url",
1030 basename: "index.html",
1032 location: "http://example.com/index.html",
1038 const testOutputs = [
1041 id: "#main/output_file",
1042 label: "Label Description",
1047 basename: "cat.png",
1049 location: "cat.png",
1055 id: "#main/output_file_with_secondary",
1056 doc: "Doc Description",
1060 output_file_with_secondary: {
1061 basename: "main.dat",
1063 location: "main.dat",
1066 basename: "secondary.dat",
1068 location: "secondary.dat",
1071 basename: "secondary2.dat",
1073 location: "secondary2.dat",
1081 id: "#main/output_dir",
1082 doc: ["Doc desc 1", "Doc desc 2"],
1087 basename: "outdir1",
1089 location: "outdir1",
1095 id: "#main/output_bool",
1104 id: "#main/output_int",
1113 id: "#main/output_long",
1122 id: "#main/output_float",
1126 output_float: 100.5,
1131 id: "#main/output_double",
1135 output_double: 100.3,
1140 id: "#main/output_string",
1144 output_string: "Hello output",
1149 id: "#main/output_file_array",
1156 output_file_array: [
1158 basename: "output2.tar",
1160 location: "output2.tar",
1163 basename: "output3.tar",
1165 location: "output3.tar",
1172 id: "#main/output_dir_array",
1181 basename: "outdir2",
1183 location: "outdir2",
1186 basename: "outdir3",
1188 location: "outdir3",
1195 id: "#main/output_int_array",
1202 output_int_array: [10, 11, 12],
1207 id: "#main/output_long_array",
1214 output_long_array: [51, 52],
1219 id: "#main/output_float_array",
1226 output_float_array: [100.2, 100.4, 100.6],
1231 id: "#main/output_double_array",
1238 output_double_array: [100.1, 100.2, 100.3],
1243 id: "#main/output_string_array",
1250 output_string_array: ["Hello", "Output", "!"],
1255 const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1259 .within($mainRow => {
1260 cy.get($mainRow).scrollIntoView();
1261 label && cy.contains(label);
1264 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
1266 if (Array.isArray(val)) {
1267 val.forEach(v => cy.get("@secondaryRows").contains(v));
1269 cy.get("@secondaryRows").contains(val);
1273 cy.get("@secondaryRows").contains(collection);
1277 if (Array.isArray(val)) {
1278 val.forEach(v => cy.contains(v));
1284 cy.contains(collection);
1290 const verifyIOParameterImage = (name, url) => {
1295 cy.get('[alt="Inline Preview"]')
1296 .should("be.visible")
1298 expect($img[0].naturalWidth).to.be.greaterThan(0);
1299 expect($img[0].src).contains(url);
1304 it("displays IO parameters with keep links and previews", function () {
1305 // Create output collection for real files
1306 cy.createCollection(adminUser.token, {
1307 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1308 owner_uuid: activeUser.user.uuid,
1309 }).then(testOutputCollection => {
1310 cy.loginAs(activeUser);
1312 cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1314 cy.get("[data-cy=upload-button]").click();
1316 cy.fixture("files/cat.png", "base64").then(content => {
1317 cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
1318 cy.get("[data-cy=form-submit-btn]").click();
1319 cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
1320 // Confirm final collection state.
1321 cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
1324 cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
1327 // Get updated collection pdh
1328 cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
1329 // Add output uuid and inputs to container request
1330 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1331 req.on('response', res => {
1332 res.body.output_uuid = testOutputCollection.uuid;
1333 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1334 content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
1336 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1341 inputs: testInputs.map(input => input.definition),
1342 outputs: testOutputs.map(output => output.definition),
1350 // Stub fake output collection
1352 { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
1356 uuid: testOutputCollection.uuid,
1357 portable_data_hash: testOutputCollection.portable_data_hash,
1362 // Stub fake output json
1364 { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
1367 body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
1371 // Stub webdav response, points to output json
1373 { method: "PROPFIND", url: "*" },
1375 fixture: "webdav-propfind-outputs.xml",
1380 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1384 cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
1385 cy.goToPath(`/processes/${containerRequest.uuid}`);
1386 cy.get("[data-cy=process-io-card] h6")
1387 .contains("Input Parameters")
1388 .parents("[data-cy=process-io-card]")
1390 cy.get(ctx).scrollIntoView();
1391 verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
1392 verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
1393 verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
1394 verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
1395 verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
1396 verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
1397 verifyIOParameter("input_int", null, null, "1");
1398 verifyIOParameter("input_long", null, null, "1");
1399 verifyIOParameter("input_float", null, null, "1.5");
1400 verifyIOParameter("input_double", null, null, "1.3");
1401 verifyIOParameter("input_string", null, null, "Hello World");
1402 verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
1403 verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
1404 verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
1405 verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
1406 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
1407 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
1408 verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
1409 verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
1410 verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
1411 verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1412 verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1413 verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
1414 verifyIOParameter("input_bool_include", null, null, "Cannot display value");
1415 verifyIOParameter("input_int_include", null, null, "Cannot display value");
1416 verifyIOParameter("input_float_include", null, null, "Cannot display value");
1417 verifyIOParameter("input_string_include", null, null, "Cannot display value");
1418 verifyIOParameter("input_file_include", null, null, "Cannot display value");
1419 verifyIOParameter("input_directory_include", null, null, "Cannot display value");
1420 verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
1422 cy.get("[data-cy=process-io-card] h6")
1423 .contains("Output Parameters")
1424 .parents("[data-cy=process-io-card]")
1426 cy.get(ctx).scrollIntoView();
1427 const outPdh = testOutputCollection.portable_data_hash;
1429 verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
1430 // Disabled until image preview returns
1431 // verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
1432 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
1433 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
1434 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
1435 verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
1436 verifyIOParameter("output_bool", null, null, "true");
1437 verifyIOParameter("output_int", null, null, "1");
1438 verifyIOParameter("output_long", null, null, "1");
1439 verifyIOParameter("output_float", null, null, "100.5");
1440 verifyIOParameter("output_double", null, null, "100.3");
1441 verifyIOParameter("output_string", null, null, "Hello output");
1442 verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
1443 verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
1444 verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
1445 verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
1446 verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
1447 verifyIOParameter("output_long_array", null, null, ["51", "52"]);
1448 verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
1449 verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
1450 verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
1455 it("displays IO parameters with no value", function () {
1456 const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
1457 const fakeOutputPDH = "11111111111111111111111111111111+99/";
1459 cy.loginAs(activeUser);
1461 // Add output uuid and inputs to container request
1462 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1463 req.on('response', res => {
1464 res.body.output_uuid = fakeOutputUUID;
1465 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1468 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1473 inputs: testInputs.map(input => input.definition),
1474 outputs: testOutputs.map(output => output.definition),
1482 // Stub fake output collection
1484 { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
1488 uuid: fakeOutputUUID,
1489 portable_data_hash: fakeOutputPDH,
1494 // Stub fake output json
1496 { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
1503 cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
1504 // Stub webdav response, points to output json
1506 { method: "PROPFIND", url: "*" },
1509 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
1514 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1518 cy.getAll("@containerRequest").then(function ([containerRequest]) {
1519 cy.goToPath(`/processes/${containerRequest.uuid}`);
1522 cy.get("[data-cy=process-io-card] h6")
1523 .contains("Input Parameters")
1524 .parents("[data-cy=process-io-card]")
1526 cy.get(ctx).scrollIntoView();
1530 testInputs.map((input) => {
1531 verifyIOParameter(input.definition.id.split('/').slice(-1)[0], null, null, "No value");
1534 cy.get("[data-cy=process-io-card] h6")
1535 .contains("Output Parameters")
1536 .parents("[data-cy=process-io-card]")
1538 cy.get(ctx).scrollIntoView();
1540 testOutputs.map((output) => {
1541 verifyIOParameter(output.definition.id.split('/').slice(-1)[0], null, null, "No value");