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 function setupDockerImage(image_name) {
29 // Create a collection that will be used as a docker image for the tests.
30 cy.createCollection(adminUser.token, {
33 ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n",
36 .then(function (dockerImage) {
37 // Give read permissions to the active user on the docker image.
38 cy.createLink(adminUser.token, {
39 link_class: "permission",
41 tail_uuid: activeUser.user.uuid,
42 head_uuid: dockerImage.uuid,
44 .as("dockerImagePermission")
46 // Set-up docker image collection tags
47 cy.createLink(activeUser.token, {
48 link_class: "docker_image_repo+tag",
50 head_uuid: dockerImage.uuid,
51 }).as("dockerImageRepoTag");
52 cy.createLink(activeUser.token, {
53 link_class: "docker_image_hash",
54 name: "sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678",
55 head_uuid: dockerImage.uuid,
56 }).as("dockerImageHash");
59 return cy.getAll("@dockerImage", "@dockerImageRepoTag", "@dockerImageHash", "@dockerImagePermission").then(function ([dockerImage]) {
64 function createContainerRequest(user, name, docker_image, command, reuse = false, state = "Uncommitted") {
65 return setupDockerImage(docker_image).then(function (dockerImage) {
66 return cy.createContainerRequest(user.token, {
69 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
70 output_path: "stdout.txt",
72 runtime_constraints: {
88 describe('Multiselect Toolbar', () => {
89 it('shows the appropriate buttons in the toolbar', () => {
91 const msButtonTooltips = [
95 'Copy and re-run process',
105 createContainerRequest(
107 `test_container_request ${Math.floor(Math.random() * 999999)}`,
109 ["echo", "hello world"],
112 ).then(function (containerRequest) {
113 cy.loginAs(activeUser);
114 cy.goToPath(`/processes/${containerRequest.uuid}`);
115 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
116 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
117 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
118 cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
120 cy.get('[data-cy=data-table-row]').contains(containerRequest.name).should('exist').parent().parent().parent().parent().click()
122 cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
123 for (let i = 0; i < msButtonTooltips.length; i++) {
124 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
125 cy.get('body').contains(msButtonTooltips[i]).should('exist')
126 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
132 describe("Details panel", function () {
133 it("shows process details", function () {
134 createContainerRequest(
136 `test_container_request ${Math.floor(Math.random() * 999999)}`,
138 ["echo", "hello world"],
141 ).then(function (containerRequest) {
142 cy.loginAs(activeUser);
143 cy.goToPath(`/processes/${containerRequest.uuid}`);
144 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
145 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
146 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
149 // Fake submitted by another user
150 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
152 res.body.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
156 createContainerRequest(
158 `test_container_request ${Math.floor(Math.random() * 999999)}`,
160 ["echo", "hello world"],
163 ).then(function (containerRequest) {
164 cy.loginAs(activeUser);
165 cy.goToPath(`/processes/${containerRequest.uuid}`);
166 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
167 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
168 cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
172 it("should show runtime status indicators", function () {
173 // Setup running container with runtime_status error & warning messages
174 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
175 .as("containerRequest")
176 .then(function (containerRequest) {
177 expect(containerRequest.state).to.equal("Committed");
178 expect(containerRequest.container_uuid).not.to.be.equal("");
180 cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
181 expect(queuedContainer.state).to.be.equal("Queued");
183 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
185 }).then(function (lockedContainer) {
186 expect(lockedContainer.state).to.be.equal("Locked");
188 cy.updateContainer(adminUser.token, lockedContainer.uuid, {
191 error: "Something went wrong",
192 errorDetail: "Process exited with status 1",
193 warning: "Free disk space is low",
196 .as("runningContainer")
197 .then(function (runningContainer) {
198 expect(runningContainer.state).to.be.equal("Running");
199 expect(runningContainer.runtime_status).to.be.deep.equal({
200 error: "Something went wrong",
201 errorDetail: "Process exited with status 1",
202 warning: "Free disk space is low",
207 // Test that the UI shows the error and warning messages
208 cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
209 cy.loginAs(activeUser);
210 cy.goToPath(`/processes/${containerRequest.uuid}`);
211 cy.get("[data-cy=process-runtime-status-error]")
212 .should("contain", "Something went wrong")
213 .and("contain", "Process exited with status 1");
214 cy.get("[data-cy=process-runtime-status-warning]")
215 .should("contain", "Free disk space is low")
216 .and("contain", "No additional warning details available");
219 // Force container_count for testing
220 let containerCount = 2;
221 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
223 res.body.container_count = containerCount;
227 cy.getAll("@containerRequest").then(function ([containerRequest]) {
228 cy.goToPath(`/processes/${containerRequest.uuid}`);
229 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time");
232 cy.getAll("@containerRequest").then(function ([containerRequest]) {
234 cy.goToPath(`/processes/${containerRequest.uuid}`);
235 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
239 it("allows copying processes", function () {
240 const crName = "first_container_request";
241 const copiedCrName = "copied_container_request";
242 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
243 cy.loginAs(activeUser);
244 cy.goToPath(`/processes/${containerRequest.uuid}`);
245 cy.get("[data-cy=process-details]").should("contain", crName);
247 cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
248 cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
251 cy.get("[data-cy=form-dialog]").within(() => {
252 cy.get("input[name=name]").clear().type(copiedCrName);
253 cy.get("[data-cy=projects-tree-home-tree-picker]").click();
254 cy.get("[data-cy=form-submit-btn]").click();
257 cy.get("[data-cy=process-details]").should("contain", copiedCrName);
258 cy.get("[data-cy=process-details]").find("button").contains("Run");
261 const getFakeContainer = fakeContainerUuid => ({
262 href: `/containers/${fakeContainerUuid}`,
263 kind: "arvados#container",
264 etag: "ecfosljpnxfari9a8m7e4yv06",
265 uuid: fakeContainerUuid,
266 owner_uuid: "zzzzz-tpzed-000000000000000",
267 created_at: "2023-02-13T15:55:47.308915000Z",
268 modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
269 modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
270 modified_at: "2023-02-15T19:12:45.987086000Z",
272 "arvados-cwl-runner",
275 "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
276 "/var/lib/cwl/workflow.json#main",
277 "/var/lib/cwl/cwl.input.json",
279 container_image: "4ad7d11381df349e464694762db14e04+303",
280 cwd: "/var/spool/cwl",
284 locked_by_uuid: null,
287 output_path: "/var/spool/cwl",
289 runtime_constraints: {
294 hardware_capability: "",
296 keep_cache_disk: 2147483648,
304 scheduling_parameters: {
309 runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
310 runtime_auth_scopes: ["all"],
312 gateway_address: null,
313 interactive_session_started: false,
314 output_storage_classes: ["default"],
315 output_properties: {},
317 subrequests_cost: 0.0,
320 it("shows cancel button when appropriate", function () {
321 // Ignore collection requests
323 { method: "GET", url: `**/arvados/v1/collections/*` },
330 // Uncommitted container
331 const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
332 createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
335 cy.loginAs(activeUser);
336 // Navigate to process and verify run / cancel button
337 cy.goToPath(`/processes/${containerRequest.uuid}`);
339 cy.get("[data-cy=process-details]").should("contain", crUncommitted);
340 cy.get("[data-cy=process-run-button]").should("exist");
341 cy.get("[data-cy=process-cancel-button]").should("not.exist");
345 const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
346 const fakeCrUuid = "zzzzz-dz642-000000000000001";
347 createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
350 // Fake container uuid
351 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
353 res.body.output_uuid = fakeCrUuid;
354 res.body.priority = 500;
355 res.body.state = "Committed";
360 const container = getFakeContainer(fakeCrUuid);
362 { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
365 body: { ...container, state: "Queued", priority: 500 },
369 // Navigate to process and verify cancel button
370 cy.goToPath(`/processes/${containerRequest.uuid}`);
372 cy.get("[data-cy=process-details]").should("contain", crQueued);
373 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
377 const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
378 const fakeCrLockedUuid = "zzzzz-dz642-000000000000002";
379 createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
382 // Fake container uuid
383 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
385 res.body.output_uuid = fakeCrLockedUuid;
386 res.body.priority = 500;
387 res.body.state = "Committed";
392 const container = getFakeContainer(fakeCrLockedUuid);
394 { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
397 body: { ...container, state: "Locked", priority: 500 },
401 // Navigate to process and verify cancel button
402 cy.goToPath(`/processes/${containerRequest.uuid}`);
404 cy.get("[data-cy=process-details]").should("contain", crLocked);
405 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
409 const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
410 const fakeCrOnHoldUuid = "zzzzz-dz642-000000000000003";
411 createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
414 // Fake container uuid
415 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
417 res.body.output_uuid = fakeCrOnHoldUuid;
418 res.body.priority = 0;
419 res.body.state = "Committed";
424 const container = getFakeContainer(fakeCrOnHoldUuid);
426 { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
429 body: { ...container, state: "Queued", priority: 0 },
433 // Navigate to process and verify cancel button
434 cy.goToPath(`/processes/${containerRequest.uuid}`);
436 cy.get("[data-cy=process-details]").should("contain", crOnHold);
437 cy.get("[data-cy=process-run-button]").should("exist");
438 cy.get("[data-cy=process-cancel-button]").should("not.exist");
443 describe("Logs panel", function () {
444 it("shows live process logs", function () {
445 cy.intercept({ method: "GET", url: "**/arvados/v1/containers/*" }, req => {
447 res.body.state = ContainerState.RUNNING;
451 const crName = "test_container_request";
452 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
453 // Create empty log file before loading process page
454 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [""]);
456 cy.loginAs(activeUser);
457 cy.goToPath(`/processes/${containerRequest.uuid}`);
458 cy.get("[data-cy=process-details]").should("contain", crName);
459 cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
462 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", ["2023-07-18T20:14:48.128642814Z hello world"]).then(() => {
463 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
466 // Append new log line to different file
467 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:49.128642814Z hello new line"]).then(() => {
468 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello new line");
473 it("filters process logs by event type", function () {
474 const nodeInfoLogs = [
476 "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",
479 "vendor_id : GenuineIntel",
482 "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
484 const crunchRunLogs = [
485 "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
486 "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
487 "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
488 "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
489 "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
492 "2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
493 "2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
494 "2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.",
495 "2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
496 "2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.",
497 "2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
498 "2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
499 "2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
500 "2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.",
501 "2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
502 "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.",
503 "2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
504 "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.",
505 "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.",
506 "2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
507 "2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.",
508 "2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
509 "2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.",
512 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
515 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as("nodeInfoLogs");
516 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as("crunchRunLogs");
517 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as("stdoutLogs");
519 cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
520 cy.loginAs(activeUser);
521 cy.goToPath(`/processes/${containerRequest.uuid}`);
522 // Should show main logs by default
523 cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
524 cy.get("[data-cy=process-logs]")
525 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
526 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
527 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
529 cy.get("[data-cy=process-logs-filter]").click();
530 cy.get("body").contains("li", "All logs").click();
531 cy.get("[data-cy=process-logs]")
532 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
533 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
534 .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
535 // Select 'node-info' logs
536 cy.get("[data-cy=process-logs-filter]").click();
537 cy.get("body").contains("li", "node-info").click();
538 cy.get("[data-cy=process-logs]")
539 .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
540 .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
541 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
542 // Select 'stdout' logs
543 cy.get("[data-cy=process-logs-filter]").click();
544 cy.get("body").contains("li", "stdout").click();
545 cy.get("[data-cy=process-logs]")
546 .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
547 .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
548 .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
553 it("sorts combined logs", function () {
554 const crName = "test_container_request";
555 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
556 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
564 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
565 "2023-07-18T20:14:48.128642814Z first",
566 "2023-07-18T20:14:49.128642814Z third",
569 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:48.528642814Z second"]).as("stderr");
571 cy.loginAs(activeUser);
572 cy.goToPath(`/processes/${containerRequest.uuid}`);
573 cy.get("[data-cy=process-details]").should("contain", crName);
574 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
576 cy.getAll("@node-info", "@stdout", "@stderr").then(() => {
577 // Verify sorted main logs
578 cy.get("[data-cy=process-logs] span > p", { timeout: 7000 }).eq(0).should("contain", "2023-07-18T20:14:48.128642814Z first");
579 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:48.528642814Z second");
580 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:49.128642814Z third");
582 // Switch to All logs
583 cy.get("[data-cy=process-logs-filter]").click();
584 cy.get("body").contains("li", "All logs").click();
585 // Verify non-sorted lines were preserved
586 cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "3: nodeinfo 1");
587 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2: nodeinfo 2");
588 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "1: nodeinfo 3");
589 cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2: nodeinfo 4");
590 cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "3: nodeinfo 5");
591 // Verify sorted logs
592 cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z first");
593 cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.528642814Z second");
594 cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:49.128642814Z third");
599 it("preserves original ordering of lines within the same log type", function () {
600 const crName = "test_container_request";
601 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
602 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
604 "2023-07-18T20:14:46.000000000Z A out 1",
605 // Comes fourth in a contiguous block
606 "2023-07-18T20:14:48.128642814Z A out 2",
607 "2023-07-18T20:14:48.128642814Z X out 3",
608 "2023-07-18T20:14:48.128642814Z A out 4",
611 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
613 "2023-07-18T20:14:47.000000000Z Z err 1",
614 // Comes third in a contiguous block
615 "2023-07-18T20:14:48.128642814Z B err 2",
616 "2023-07-18T20:14:48.128642814Z C err 3",
617 "2023-07-18T20:14:48.128642814Z Y err 4",
618 "2023-07-18T20:14:48.128642814Z Z err 5",
619 "2023-07-18T20:14:48.128642814Z A err 6",
622 cy.loginAs(activeUser);
623 cy.goToPath(`/processes/${containerRequest.uuid}`);
624 cy.get("[data-cy=process-details]").should("contain", crName);
625 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
627 cy.getAll("@stdout", "@stderr").then(() => {
628 // Switch to All logs
629 cy.get("[data-cy=process-logs-filter]").click();
630 cy.get("body").contains("li", "All logs").click();
631 // Verify sorted logs
632 cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
633 cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
634 cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
635 cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
636 cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
637 cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
638 cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
639 cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
640 cy.get("[data-cy=process-logs] span > p").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
641 cy.get("[data-cy=process-logs] span > p").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
646 it("correctly generates sniplines", function () {
647 const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
648 const crName = "test_container_request";
649 createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
650 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
651 "X".repeat(63999) + "_" + "O".repeat(100) + "_" + "X".repeat(63999),
654 cy.loginAs(activeUser);
655 cy.goToPath(`/processes/${containerRequest.uuid}`);
656 cy.get("[data-cy=process-details]").should("contain", crName);
657 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
659 // Switch to stdout since lines are unsortable (no timestamp)
660 cy.get("[data-cy=process-logs-filter]").click();
661 cy.get("body").contains("li", "stdout").click();
663 cy.getAll("@stdout").then(() => {
664 // Verify first 64KB and snipline
665 cy.get("[data-cy=process-logs] span > p", { timeout: 7000 })
667 .should("contain", "X".repeat(63999) + "_\n" + SNIPLINE);
669 cy.get("[data-cy=process-logs] span > p")
671 .should("contain", "_" + "X".repeat(63999));
672 // Verify none of the Os got through
673 cy.get("[data-cy=process-logs] span > p").should("not.contain", "O");
678 it("correctly break long lines when no obvious line separation exists", function () {
679 function randomString(length) {
680 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
682 for (let i = 0; i < length; i++) {
683 res += chars.charAt(Math.floor(Math.random() * chars.length));
688 const logLinesQty = 10;
690 for (let i = 0; i < logLinesQty; i++) {
691 const length = Math.floor(Math.random() * 500) + 500;
692 logLines.push(randomString(length));
695 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
698 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", logLines).as("stdoutLogs");
700 cy.getAll("@stdoutLogs").then(function () {
701 cy.loginAs(activeUser);
702 cy.goToPath(`/processes/${containerRequest.uuid}`);
703 // Select 'stdout' log filter
704 cy.get("[data-cy=process-logs-filter]").click();
705 cy.get("body").contains("li", "stdout").click();
706 cy.get("[data-cy=process-logs] span > p")
707 .should('have.length', logLinesQty)
709 expect($p.text().length).to.be.greaterThan(499);
711 // This looks like an ugly hack, but I was not able
712 // to get [client|scroll]Width attributes through
713 // the usual Cypress methods.
714 const parentClientWidth = $p[0].parentElement.clientWidth;
715 const parentScrollWidth = $p[0].parentElement.scrollWidth
716 // Scrollbar should not be visible
717 expect(parentClientWidth).to.be.eq(parentScrollWidth);
724 describe("I/O panel", function () {
728 id: "#main/input_file",
729 label: "Label Description",
734 basename: "input1.tar",
736 location: "keep:00000000000000000000000000000000+01/input1.tar",
739 basename: "input1-2.txt",
741 location: "keep:00000000000000000000000000000000+01/input1-2.txt",
744 basename: "input1-3.txt",
746 location: "keep:00000000000000000000000000000000+01/input1-3.txt",
749 basename: "input1-4.txt",
751 location: "keep:00000000000000000000000000000000+01/input1-4.txt",
759 id: "#main/input_dir",
760 doc: "Doc Description",
765 basename: "11111111111111111111111111111111+01",
767 location: "keep:11111111111111111111111111111111+01",
773 id: "#main/input_bool",
774 doc: ["Doc desc 1", "Doc desc 2"],
783 id: "#main/input_int",
792 id: "#main/input_long",
801 id: "#main/input_float",
810 id: "#main/input_double",
819 id: "#main/input_string",
823 input_string: "Hello World",
828 id: "#main/input_file_array",
837 basename: "input2.tar",
839 location: "keep:00000000000000000000000000000000+02/input2.tar",
842 basename: "input3.tar",
844 location: "keep:00000000000000000000000000000000+03/input3.tar",
847 basename: "input3-2.txt",
849 location: "keep:00000000000000000000000000000000+03/input3-2.txt",
854 $import: "import_path",
861 id: "#main/input_dir_array",
870 basename: "11111111111111111111111111111111+02",
872 location: "keep:11111111111111111111111111111111+02",
875 basename: "11111111111111111111111111111111+03",
877 location: "keep:11111111111111111111111111111111+03",
880 $import: "import_path",
887 id: "#main/input_int_array",
899 $import: "import_path",
906 id: "#main/input_long_array",
917 $import: "import_path",
924 id: "#main/input_float_array",
936 $import: "import_path",
943 id: "#main/input_double_array",
950 input_double_array: [
955 $import: "import_path",
962 id: "#main/input_string_array",
969 input_string_array: [
974 $import: "import_path",
981 id: "#main/input_bool_include",
985 input_bool_include: {
986 $include: "include_path",
992 id: "#main/input_int_include",
997 $include: "include_path",
1003 id: "#main/input_float_include",
1007 input_float_include: {
1008 $include: "include_path",
1014 id: "#main/input_string_include",
1018 input_string_include: {
1019 $include: "include_path",
1025 id: "#main/input_file_include",
1029 input_file_include: {
1030 $include: "include_path",
1036 id: "#main/input_directory_include",
1040 input_directory_include: {
1041 $include: "include_path",
1047 id: "#main/input_file_url",
1052 basename: "index.html",
1054 location: "http://example.com/index.html",
1060 const testOutputs = [
1063 id: "#main/output_file",
1064 label: "Label Description",
1069 basename: "cat.png",
1071 location: "cat.png",
1077 id: "#main/output_file_with_secondary",
1078 doc: "Doc Description",
1082 output_file_with_secondary: {
1083 basename: "main.dat",
1085 location: "main.dat",
1088 basename: "secondary.dat",
1090 location: "secondary.dat",
1093 basename: "secondary2.dat",
1095 location: "secondary2.dat",
1103 id: "#main/output_dir",
1104 doc: ["Doc desc 1", "Doc desc 2"],
1109 basename: "outdir1",
1111 location: "outdir1",
1117 id: "#main/output_bool",
1126 id: "#main/output_int",
1135 id: "#main/output_long",
1144 id: "#main/output_float",
1148 output_float: 100.5,
1153 id: "#main/output_double",
1157 output_double: 100.3,
1162 id: "#main/output_string",
1166 output_string: "Hello output",
1171 id: "#main/output_file_array",
1178 output_file_array: [
1180 basename: "output2.tar",
1182 location: "output2.tar",
1185 basename: "output3.tar",
1187 location: "output3.tar",
1194 id: "#main/output_dir_array",
1203 basename: "outdir2",
1205 location: "outdir2",
1208 basename: "outdir3",
1210 location: "outdir3",
1217 id: "#main/output_int_array",
1224 output_int_array: [10, 11, 12],
1229 id: "#main/output_long_array",
1236 output_long_array: [51, 52],
1241 id: "#main/output_float_array",
1248 output_float_array: [100.2, 100.4, 100.6],
1253 id: "#main/output_double_array",
1260 output_double_array: [100.1, 100.2, 100.3],
1265 id: "#main/output_string_array",
1272 output_string_array: ["Hello", "Output", "!"],
1277 const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1281 .within($mainRow => {
1282 label && cy.contains(label);
1285 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
1287 if (Array.isArray(val)) {
1288 val.forEach(v => cy.get("@secondaryRows").contains(v));
1290 cy.get("@secondaryRows").contains(val);
1294 cy.get("@secondaryRows").contains(collection);
1298 if (Array.isArray(val)) {
1299 val.forEach(v => cy.contains(v));
1305 cy.contains(collection);
1311 const verifyIOParameterImage = (name, url) => {
1316 cy.get('[alt="Inline Preview"]')
1317 .should("be.visible")
1319 expect($img[0].naturalWidth).to.be.greaterThan(0);
1320 expect($img[0].src).contains(url);
1325 it("displays IO parameters with keep links and previews", function () {
1326 // Create output collection for real files
1327 cy.createCollection(adminUser.token, {
1328 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1329 owner_uuid: activeUser.user.uuid,
1330 }).then(testOutputCollection => {
1331 cy.loginAs(activeUser);
1333 cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1335 cy.get("[data-cy=upload-button]").click();
1337 cy.fixture("files/cat.png", "base64").then(content => {
1338 cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
1339 cy.get("[data-cy=form-submit-btn]").click();
1340 cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
1341 // Confirm final collection state.
1342 cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
1345 cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
1348 // Get updated collection pdh
1349 cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
1350 // Add output uuid and inputs to container request
1351 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1353 res.body.output_uuid = testOutputCollection.uuid;
1354 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1355 content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
1357 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1362 inputs: testInputs.map(input => input.definition),
1363 outputs: testOutputs.map(output => output.definition),
1371 // Stub fake output collection
1373 { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
1377 uuid: testOutputCollection.uuid,
1378 portable_data_hash: testOutputCollection.portable_data_hash,
1383 // Stub fake output json
1385 { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
1388 body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
1392 // Stub webdav response, points to output json
1394 { method: "PROPFIND", url: "*" },
1396 fixture: "webdav-propfind-outputs.xml",
1401 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1405 cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
1406 cy.goToPath(`/processes/${containerRequest.uuid}`);
1407 cy.get("[data-cy=process-io-card] h6")
1408 .contains("Input Parameters")
1409 .parents("[data-cy=process-io-card]")
1411 verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
1412 verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
1413 verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
1414 verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
1415 verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
1416 verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
1417 verifyIOParameter("input_int", null, null, "1");
1418 verifyIOParameter("input_long", null, null, "1");
1419 verifyIOParameter("input_float", null, null, "1.5");
1420 verifyIOParameter("input_double", null, null, "1.3");
1421 verifyIOParameter("input_string", null, null, "Hello World");
1422 verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
1423 verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
1424 verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
1425 verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
1426 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
1427 verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
1428 verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
1429 verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
1430 verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
1431 verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1432 verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1433 verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
1434 verifyIOParameter("input_bool_include", null, null, "Cannot display value");
1435 verifyIOParameter("input_int_include", null, null, "Cannot display value");
1436 verifyIOParameter("input_float_include", null, null, "Cannot display value");
1437 verifyIOParameter("input_string_include", null, null, "Cannot display value");
1438 verifyIOParameter("input_file_include", null, null, "Cannot display value");
1439 verifyIOParameter("input_directory_include", null, null, "Cannot display value");
1440 verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
1442 cy.get("[data-cy=process-io-card] h6")
1443 .contains("Output Parameters")
1444 .parents("[data-cy=process-io-card]")
1446 cy.get(ctx).scrollIntoView();
1447 cy.get('[data-cy="io-preview-image-toggle"]').click({ waitForAnimations: false });
1448 const outPdh = testOutputCollection.portable_data_hash;
1450 verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
1451 verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
1452 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
1453 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
1454 verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
1455 verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
1456 verifyIOParameter("output_bool", null, null, "true");
1457 verifyIOParameter("output_int", null, null, "1");
1458 verifyIOParameter("output_long", null, null, "1");
1459 verifyIOParameter("output_float", null, null, "100.5");
1460 verifyIOParameter("output_double", null, null, "100.3");
1461 verifyIOParameter("output_string", null, null, "Hello output");
1462 verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
1463 verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
1464 verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
1465 verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
1466 verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
1467 verifyIOParameter("output_long_array", null, null, ["51", "52"]);
1468 verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
1469 verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
1470 verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
1475 it("displays IO parameters with no value", function () {
1476 const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
1477 const fakeOutputPDH = "11111111111111111111111111111111+99/";
1479 cy.loginAs(activeUser);
1481 // Add output uuid and inputs to container request
1482 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1484 res.body.output_uuid = fakeOutputUUID;
1485 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1488 res.body.mounts["/var/lib/cwl/workflow.json"] = {
1493 inputs: testInputs.map(input => input.definition),
1494 outputs: testOutputs.map(output => output.definition),
1502 // Stub fake output collection
1504 { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
1508 uuid: fakeOutputUUID,
1509 portable_data_hash: fakeOutputPDH,
1514 // Stub fake output json
1516 { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
1523 cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
1524 // Stub webdav response, points to output json
1526 { method: "PROPFIND", url: "*" },
1529 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
1534 createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1538 cy.getAll("@containerRequest").then(function ([containerRequest]) {
1539 cy.goToPath(`/processes/${containerRequest.uuid}`);
1542 cy.get("[data-cy=process-io-card] h6")
1543 .contains("Input Parameters")
1544 .parents("[data-cy=process-io-card]")
1548 cy.get("tbody tr").each(item => {
1549 cy.wrap(item).contains("No value");
1552 cy.get("[data-cy=process-io-card] h6")
1553 .contains("Output Parameters")
1554 .parents("[data-cy=process-io-card]")
1556 cy.get("tbody tr").each(item => {
1557 cy.wrap(item).contains("No value");