From a0e8d23e5734ee7e0f53e46dd38c1e5569ada15b Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 08:53:16 -0800 Subject: [PATCH 01/10] switch to smol-toml package instead of toml package, as we needed ability to load and save config file, even in cases of TOML parsing errors. --- test/e2e/package-lock.json | 22 ++++++++++++++-------- test/e2e/package.json | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json index c5d747ecc..ba8b612aa 100644 --- a/test/e2e/package-lock.json +++ b/test/e2e/package-lock.json @@ -18,7 +18,7 @@ "eslint-plugin-mocha": "^10.5.0", "lint-staged": "^15.4.3", "prettier": "^3.4.2", - "toml": "^3.0.0", + "smol-toml": "^1.3.1", "wait-on": "^8.0.2" } }, @@ -3434,6 +3434,19 @@ "node": ">=8" } }, + "node_modules/smol-toml": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.1.tgz", + "integrity": "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -3581,13 +3594,6 @@ "node": ">=8.0" } }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "dev": true, - "license": "MIT" - }, "node_modules/tough-cookie": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.0.tgz", diff --git a/test/e2e/package.json b/test/e2e/package.json index 6d9b39cf0..c3a86d5c1 100644 --- a/test/e2e/package.json +++ b/test/e2e/package.json @@ -20,7 +20,7 @@ "eslint-plugin-mocha": "^10.5.0", "lint-staged": "^15.4.3", "prettier": "^3.4.2", - "toml": "^3.0.0", + "smol-toml": "^1.3.1", "wait-on": "^8.0.2" } } From d0ce398d6a7f703c1718881ccd1fc2e42e92b34e Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 08:57:21 -0800 Subject: [PATCH 02/10] Additional cypress commands to support tests. Deployment sequence, switch to new toml package and wildcard expansion to open specific files. --- test/e2e/support/commands.js | 70 ++++++++++++++++++-- test/e2e/support/sequences.js | 121 ++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 test/e2e/support/sequences.js diff --git a/test/e2e/support/commands.js b/test/e2e/support/commands.js index afccc8293..0bd882659 100644 --- a/test/e2e/support/commands.js +++ b/test/e2e/support/commands.js @@ -1,6 +1,9 @@ +// Copyright (C) 2025 by Posit Software, PBC. + import "@testing-library/cypress/add-commands"; -import toml from "toml"; +import { parse, stringify } from "smol-toml"; import "./selectors"; +import "./sequences"; const connectManagerServer = Cypress.env("CONNECT_MANAGER_URL"); @@ -130,18 +133,58 @@ EOF`, ); }); -Cypress.Commands.add("clearupDeployments", () => { - cy.exec(`rm -rf content-workspace/static/.posit`); +Cypress.Commands.add("clearupDeployments", (subdir) => { + cy.exec(`rm -rf content-workspace/${subdir}/.posit`); +}); + +Cypress.Commands.add("expandWildcardFile", (projectName, wildCardPath) => { + const workingDir = `content-workspace/${projectName}/.posit/publish`; + return cy + .exec("pwd") + .then((result) => { + return cy.log("CWD", result.stdout); + }) + .then(() => { + const cmd = `cd ${workingDir} && file=$(echo ${wildCardPath}) && echo $file`; + return cy.exec(cmd); + }) + .then((result) => { + if (result.code === 0 && result.stdout) { + return result.stdout; + } + throw new Error(`Could not expandWildcardFile. ${result.stderr}`); + }); +}); + +Cypress.Commands.add("savePublisherFile", (filePath, jsonObject) => { + const projectFilePath = `content-workspace/${filePath}`; + return cy + .exec("pwd") + .then((result) => { + return cy + .log("savePublisherFile CWD", result.stdout) + .log("filePath", projectFilePath); + }) + .then(() => { + const tomlString = stringify(jsonObject); + return cy.writeFile(projectFilePath, tomlString); + }); }); -Cypress.Commands.add("loadProjectConfigFile", (projectName) => { - const projectConfigPath = `content-workspace/${projectName}/.posit/publish/static-*.toml`; +Cypress.Commands.add("loadProjectConfigFile", (projectName, configName) => { + const projectConfigPath = `content-workspace/${projectName}/.posit/publish/${configName}`; // Do not fail on non-zero exit this time, we can provide a better error return cy + .exec("pwd") + .then((result) => { + return cy + .log("projectConfigPath", projectConfigPath) + .log("loadProjectConfigFile CWD", result.stdout); + }) .exec(`cat ${projectConfigPath}`, { failOnNonZeroExit: false }) .then((result) => { if (result.code === 0 && result.stdout) { - return toml.parse(result.stdout); + return parse(result.stdout); } throw new Error(`Could not load project configuration. ${result.stderr}`); }); @@ -154,7 +197,20 @@ Cypress.Commands.add("loadProjectDeploymentFile", (projectName) => { .exec(`cat ${projectDeploymentPath}`, { failOnNonZeroExit: false }) .then((result) => { if (result.code === 0 && result.stdout) { - return toml.parse(result.stdout); + return parse(result.stdout); + } + throw new Error(`Could not load project deployment. ${result.stderr}`); + }); +}); + +Cypress.Commands.add("loadProjectDeploymentFile", (projectName) => { + const projectDeploymentPath = `content-workspace/${projectName}/.posit/publish/deployments/deployment-*.toml`; + // Do not fail on non-zero exit this time, we can provide a better error + return cy + .exec(`cat ${projectDeploymentPath}`, { failOnNonZeroExit: false }) + .then((result) => { + if (result.code === 0 && result.stdout) { + return parse(result.stdout); } throw new Error(`Could not load project deployment. ${result.stderr}`); }); diff --git a/test/e2e/support/sequences.js b/test/e2e/support/sequences.js new file mode 100644 index 000000000..450157925 --- /dev/null +++ b/test/e2e/support/sequences.js @@ -0,0 +1,121 @@ +// Copyright (C) 2025 by Posit Software, PBC. + +Cypress.Commands.add( + "createDeployment", + ( + projectDir, // string + entrypointFile, // string + title, // string + verifyConfigCallback, // func({ fileName: configFileName, contents: contents}) + ) => { + // Temporarily ignore uncaught exception due to a vscode worker being cancelled at some point. + cy.on("uncaught:exception", () => false); + + // Open the entrypoint ahead of time for easier selection later. + // expand the subdirectory + if (projectDir !== ".") { + cy.get(".explorer-viewlet").find(`[aria-label="${projectDir}"]`).click(); + } + + // open the entrypoint file + cy.get(".explorer-viewlet") + .find(`[aria-label="${entrypointFile}"]`) + .should("be.visible") + .dblclick(); + + // confirm that the file got opened in a tab + cy.get(".tabs-container") + .find(`[aria-label="${entrypointFile}"]`) + .should("be.visible"); + + // activate the publisher extension + cy.getPublisherSidebarIcon() + .should("be.visible", { timeout: 10000 }) + .click(); + + // Create a new deployment via the select-deployment button + cy.publisherWebview() + .findByTestId("select-deployment") + .then((dplyPicker) => { + Cypress.$(dplyPicker).trigger("click"); + }); + + // Ux displayed via quick input + cy.get(".quick-input-widget").should("be.visible"); + + // confirm we've got the correct sequence + cy.get(".quick-input-titlebar").should("have.text", "Select Deployment"); + + // Create a new deployment + cy.get(".quick-input-list") + .find( + '[aria-label="Create a New Deployment, (or pick one of the existing deployments below), New"]', + ) + .should("be.visible") + .click(); + + // TODO - Need to specifically select and press enter for creating a new deployment. + // cy.get(".quickInput_list").get("div").get("div.monaco-list-rows") + // cy.get(".quickInput_list").find('[aria-label="fastapi - base directory, Missing Credential for http://connect-publisher-e2e:3939 • simple.py, Existing"').click() + // cy.get(".quickInput_list").find('[aria-label="simple.py, Open Files"]').click() + + // cy.get(".quick-input-widget").type("{enter}") + + // prompt for select entrypoint + let targetLabel = `${projectDir}/${entrypointFile}, Open Files`; + if (projectDir === ".") { + targetLabel = `${entrypointFile}, Open Files`; + } + + cy.get(".quick-input-widget") + .find(`[aria-label="${targetLabel}"]`) + .should("be.visible") + .click(); + + cy.get(".quick-input-widget") + .find(".quick-input-filter input") + .type(`${title}{enter}`); + + cy.get(".quick-input-widget") + .find( + '[aria-label="admin-code-server, http://connect-publisher-e2e:3939"]', + ) + .should("be.visible") + .click(); + + return cy + .expandWildcardFile(projectDir, `${title}-*.toml`) + .then((configFileName) => { + cy.loadProjectConfigFile(projectDir, configFileName).then( + (contents) => { + return { + fileName: configFileName, + contents: contents, + }; + }, + ); + }) + .then((configFile) => { + return verifyConfigCallback(configFile); + }); + }, +); + +Cypress.Commands.add("deployCurrentlySelected", () => { + cy.publisherWebview() + .findByTestId("deploy-button") + .should("be.visible") + .then((dplyBtn) => { + Cypress.$(dplyBtn).trigger("click"); + }); + + // Wait for deploying message to finish + cy.get(".notifications-toasts") + .should("be.visible") + .findByText("Deploying your project: Starting to Deploy...") + .should("not.exist"); + + cy.findByText("Deployment was successful", { timeout: 60000 }).should( + "be.visible", + ); +}); From 9ffaece6c8bcda526f6d62b5a2e9e6bc61a18f16 Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 08:58:31 -0800 Subject: [PATCH 03/10] test for invalid configurations --- .../config-errors/.gitignore | 1 + .../config-errors/_quarto.yml | 2 + .../config-errors/quarto-project.Rproj | 13 + .../config-errors/quarto-project.qmd | 12 + .../content-workspace/config-errors/renv.lock | 335 ++++++++++++++++++ .../config-errors/requirements.txt | 0 test/e2e/tests/error-err-config.cy.js | 88 +++++ 7 files changed, 451 insertions(+) create mode 100644 test/e2e/content-workspace/config-errors/.gitignore create mode 100644 test/e2e/content-workspace/config-errors/_quarto.yml create mode 100644 test/e2e/content-workspace/config-errors/quarto-project.Rproj create mode 100644 test/e2e/content-workspace/config-errors/quarto-project.qmd create mode 100644 test/e2e/content-workspace/config-errors/renv.lock create mode 100644 test/e2e/content-workspace/config-errors/requirements.txt create mode 100644 test/e2e/tests/error-err-config.cy.js diff --git a/test/e2e/content-workspace/config-errors/.gitignore b/test/e2e/content-workspace/config-errors/.gitignore new file mode 100644 index 000000000..075b2542a --- /dev/null +++ b/test/e2e/content-workspace/config-errors/.gitignore @@ -0,0 +1 @@ +/.quarto/ diff --git a/test/e2e/content-workspace/config-errors/_quarto.yml b/test/e2e/content-workspace/config-errors/_quarto.yml new file mode 100644 index 000000000..8ea2bc624 --- /dev/null +++ b/test/e2e/content-workspace/config-errors/_quarto.yml @@ -0,0 +1,2 @@ +project: + title: "quarto-project" diff --git a/test/e2e/content-workspace/config-errors/quarto-project.Rproj b/test/e2e/content-workspace/config-errors/quarto-project.Rproj new file mode 100644 index 000000000..8e3c2ebc9 --- /dev/null +++ b/test/e2e/content-workspace/config-errors/quarto-project.Rproj @@ -0,0 +1,13 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX diff --git a/test/e2e/content-workspace/config-errors/quarto-project.qmd b/test/e2e/content-workspace/config-errors/quarto-project.qmd new file mode 100644 index 000000000..bfd673382 --- /dev/null +++ b/test/e2e/content-workspace/config-errors/quarto-project.qmd @@ -0,0 +1,12 @@ +--- +title: "quarto-project" +jupyter: python3 +--- + +## Quarto + +Quarto enables you to weave together content and executable code into a finished document. To learn more about Quarto see . + +```{python} +1 + 1 +``` diff --git a/test/e2e/content-workspace/config-errors/renv.lock b/test/e2e/content-workspace/config-errors/renv.lock new file mode 100644 index 000000000..f5d6fd1f3 --- /dev/null +++ b/test/e2e/content-workspace/config-errors/renv.lock @@ -0,0 +1,335 @@ +{ + "R": { + "Version": "4.3.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cloud.r-project.org" + } + ] + }, + "Packages": { + "R6": { + "Package": "R6", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "470851b6d5d0ac559e9d01bb352b4021" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "543776ae6848fde2f48ff3816d0628bc" + }, + "bslib": { + "Package": "bslib", + "Version": "0.8.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "cachem", + "fastmap", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "lifecycle", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "b299c6741ca9746fb227debcb0f9fb6c" + }, + "cachem": { + "Package": "cachem", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "cd9a672193789068eb5a2aad65a0dedf" + }, + "cli": { + "Package": "cli", + "Version": "3.6.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "b21916dd77a27642b447374a5d30ecf3" + }, + "digest": { + "Package": "digest", + "Version": "0.6.37", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "33698c4b3127fc9f506654607fb73676" + }, + "evaluate": { + "Package": "evaluate", + "Version": "0.23", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "daf4a1246be12c1fa8c7705a0935c1a0" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "aa5e1cd11c2d15497494c5292d7ffcc8" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "bd1297f9b5b1fc1372d19e2c4cd82215" + }, + "fs": { + "Package": "fs", + "Version": "1.6.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "7f48af39fa27711ea5fbd183b399920d" + }, + "glue": { + "Package": "glue", + "Version": "1.8.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "5899f1eaa825580172bb56c08266f37c" + }, + "highr": { + "Package": "highr", + "Version": "0.10", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "xfun" + ], + "Hash": "06230136b2d2b9ba5805e1963fa6e890" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.8.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "digest", + "fastmap", + "grDevices", + "rlang", + "utils" + ], + "Hash": "81d371a9cc60640e74e4ab6ac46dcedc" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "1.8.9", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods" + ], + "Hash": "4e993b65c2c3ffbffce7bb3e2c6f832b" + }, + "knitr": { + "Package": "knitr", + "Version": "1.45", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "evaluate", + "highr", + "methods", + "tools", + "xfun", + "yaml" + ], + "Hash": "1ec462871063897135c1bcbe0fc8f07d" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "rlang" + ], + "Hash": "b8552d117e1b808b09a832f589b79035" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cachem", + "rlang" + ], + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" + }, + "mime": { + "Package": "mime", + "Version": "0.12", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "tools" + ], + "Hash": "18e9c28c1d3ca1560ce30658b22ce104" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "5e3c5dc0b071b21fa128676560dbe94d" + }, + "renv": { + "Package": "renv", + "Version": "1.0.11", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "47623f66b4e80b3b0587bc5d7b309888" + }, + "rlang": { + "Package": "rlang", + "Version": "1.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "3eec01f8b1dee337674b2e34ab1f9bc1" + }, + "rmarkdown": { + "Package": "rmarkdown", + "Version": "2.29", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "bslib", + "evaluate", + "fontawesome", + "htmltools", + "jquerylib", + "jsonlite", + "knitr", + "methods", + "tinytex", + "tools", + "utils", + "xfun", + "yaml" + ], + "Hash": "df99277f63d01c34e95e3d2f06a79736" + }, + "sass": { + "Package": "sass", + "Version": "0.4.9", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "d53dbfddf695303ea4ad66f86e99b95d" + }, + "tinytex": { + "Package": "tinytex", + "Version": "0.54", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "xfun" + ], + "Hash": "3ec7e3ddcacc2d34a9046941222bf94d" + }, + "xfun": { + "Package": "xfun", + "Version": "0.49", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "stats", + "tools" + ], + "Hash": "8687398773806cfff9401a2feca96298" + }, + "yaml": { + "Package": "yaml", + "Version": "2.3.10", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "51dab85c6c98e50a18d7551e9d49f76c" + } + } +} diff --git a/test/e2e/content-workspace/config-errors/requirements.txt b/test/e2e/content-workspace/config-errors/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test/e2e/tests/error-err-config.cy.js b/test/e2e/tests/error-err-config.cy.js new file mode 100644 index 000000000..68e43296f --- /dev/null +++ b/test/e2e/tests/error-err-config.cy.js @@ -0,0 +1,88 @@ +// Copyright (C) 2025 by Posit Software, PBC. + +// NOTE:: The error cases are created here by using pre-created files. +// Because of this, they are not suitable for deployment (due to their hard-coded values) + +describe("Detect error in config", () => { + beforeEach(() => { + cy.resetConnect(); + cy.setAdminCredentials(); + cy.visit("/").debug(); + + // Select the publisher extension + cy.getPublisherSidebarIcon() + .should("be.visible", { timeout: 10000 }) + .click(); + }); + + it("Show errors when Config is invalid", () => { + // click on the select deployment button + cy.publisherWebview() + .findByTestId("select-deployment") + .then((dplyPicker) => { + Cypress.$(dplyPicker).trigger("click"); + }); + + cy.get(".quick-input-widget").should("be.visible"); + + cy.get(".quick-input-titlebar").should("have.text", "Select Deployment"); + + // select our error case. This confirms that we have it. + cy.get(".quick-input-widget") + .contains("Unknown Title • Error in quarto-project-8G2B") + .click(); + + // confirm that the selector shows the error + cy.publisherWebview() + .findByTestId("publisher-deployment-section") + .find(".deployment-control") + .find(".quick-pick-option") + .find(".quick-pick-row") + .find(".quick-pick-label-container") + .find(".quick-pick-label") + .should("have.text", "Unknown Title • Error in quarto-project-8G2B"); + + // confirm that we also have an error section + cy.publisherWebview() + .findByTestId("publisher-deployment-section") + .find('p:contains("The selected Configuration has an error.")'); + }); + + it("Show errors when Config is missing", () => { + // click on the select deployment button + cy.publisherWebview() + .findByTestId("select-deployment") + .then((dplyPicker) => { + Cypress.$(dplyPicker).trigger("click"); + }); + + cy.get(".quick-input-widget").should("be.visible"); + + cy.get(".quick-input-titlebar").should("have.text", "Select Deployment"); + + // select our error case. This confirms that we have it. + cy.get(".quick-input-widget") + .contains("Unknown Title Due to Missing Config fastapi-simple-DHJL") + .click(); + + // confirm that the selector shows the error + cy.publisherWebview() + .findByTestId("publisher-deployment-section") + .find(".deployment-control") + .find(".quick-pick-option") + .find(".quick-pick-row") + .find(".quick-pick-label-container") + .find(".quick-pick-label") + .should( + "have.text", + "Unknown Title Due to Missing Config fastapi-simple-DHJL", + ); + + // confirm that we also have an error section + cy.publisherWebview() + .findByTestId("publisher-deployment-section") + .find( + 'p:contains("The last Configuration used for this Deployment was not found.")', + ); + }); +}); From 69a416c7e733d26c9d840c8b74cce7dc2558041b Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 08:59:38 -0800 Subject: [PATCH 04/10] test for embedded deployments --- test/e2e/config/connect.gcfg | 6 +- .../fastapi-simple/fastapi-main.py | 17 ++++ .../fastapi-simple/requirements.txt | 3 + test/e2e/content-workspace/requirements.txt | 3 + test/e2e/content-workspace/simple.py | 18 +++++ test/e2e/tests/embedded-deployments.cy.js | 81 +++++++++++++++++++ 6 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 test/e2e/content-workspace/fastapi-simple/fastapi-main.py create mode 100644 test/e2e/content-workspace/fastapi-simple/requirements.txt create mode 100644 test/e2e/content-workspace/requirements.txt create mode 100644 test/e2e/content-workspace/simple.py create mode 100644 test/e2e/tests/embedded-deployments.cy.js diff --git a/test/e2e/config/connect.gcfg b/test/e2e/config/connect.gcfg index ad994593f..432fe726c 100644 --- a/test/e2e/config/connect.gcfg +++ b/test/e2e/config/connect.gcfg @@ -28,7 +28,7 @@ APIKeyBcryptCost = 4 [Python] Enabled = true Executable = /opt/python/cpython-3.11.3-linux-x86_64-gnu/bin/python3.11 -EnvironmentManagement = false +EnvironmentManagement = true [Metrics] Enabled = false @@ -37,4 +37,6 @@ Enabled = false URL = "https://packagemanager.posit.co/cran/__linux__/jammy/latest" [R] -EnvironmentManagement = false +Enabled = true +ExecutableScanning = true +EnvironmentManagement = true diff --git a/test/e2e/content-workspace/fastapi-simple/fastapi-main.py b/test/e2e/content-workspace/fastapi-simple/fastapi-main.py new file mode 100644 index 000000000..4a1c4d0de --- /dev/null +++ b/test/e2e/content-workspace/fastapi-simple/fastapi-main.py @@ -0,0 +1,17 @@ +from typing import List + +from fastapi import FastAPI + +app = FastAPI() + + +@app.post("/capitalize") +def capitalize(text: List[str]) -> List[str]: + capitalized = [t.upper() for t in text] + return capitalized + + +@app.post("/paste") +def paste(first: List[str], second: List[str]) -> List[str]: + result = [a + " " + b for a, b in zip(first, second)] + return result diff --git a/test/e2e/content-workspace/fastapi-simple/requirements.txt b/test/e2e/content-workspace/fastapi-simple/requirements.txt new file mode 100644 index 000000000..615f99743 --- /dev/null +++ b/test/e2e/content-workspace/fastapi-simple/requirements.txt @@ -0,0 +1,3 @@ +# requirements.txt auto-generated by Posit Publisher +# using /Users/billsager/.pyenv/shims/python3 +fastapi==0.115.7 diff --git a/test/e2e/content-workspace/requirements.txt b/test/e2e/content-workspace/requirements.txt new file mode 100644 index 000000000..8be53f211 --- /dev/null +++ b/test/e2e/content-workspace/requirements.txt @@ -0,0 +1,3 @@ +# requirements.txt auto-generated by Posit Publisher +# using /usr/bin/python3 +fastapi==0.115.7 diff --git a/test/e2e/content-workspace/simple.py b/test/e2e/content-workspace/simple.py new file mode 100644 index 000000000..dc8d62f20 --- /dev/null +++ b/test/e2e/content-workspace/simple.py @@ -0,0 +1,18 @@ + +from typing import List + +from fastapi import FastAPI + +app = FastAPI() + + +@app.post("/capitalize") +def capitalize(text: List[str]) -> List[str]: + capitalized = [t.upper() for t in text] + return capitalized + + +@app.post("/paste") +def paste(first: List[str], second: List[str]) -> List[str]: + result = [a + " " + b for a, b in zip(first, second)] + return result diff --git a/test/e2e/tests/embedded-deployments.cy.js b/test/e2e/tests/embedded-deployments.cy.js new file mode 100644 index 000000000..d0554a76e --- /dev/null +++ b/test/e2e/tests/embedded-deployments.cy.js @@ -0,0 +1,81 @@ +// Copyright (C) 2025 by Posit Software, PBC. + +describe("Create Deployments", () => { + beforeEach(() => { + cy.resetConnect(); + cy.setAdminCredentials(); + cy.clearupDeployments("."); + cy.clearupDeployments("fastapi-simple"); + cy.visit("/"); + }); + + it("fastapi at top of workspace", () => { + cy.createDeployment( + ".", + "simple.py", + "fastapi-base-directory", + (configFile) => { + expect(configFile.contents.title).to.equal("fastapi-base-directory"); + expect(configFile.contents.type).to.equal("python-fastapi"); + expect(configFile.contents.entrypoint).to.equal("simple.py"); + expect(configFile.contents.files[0]).to.equal("/simple.py"); + expect(configFile.contents.files[1]).to.equal("/requirements.txt"); + expect(configFile.contents.files[2]).to.equal( + `/.posit/publish/${configFile.fileName}`, + ); + // /\/.posit\/publish\/fastapi-base-directory-[A-Z0-9]{4}\.toml/, + // ); + expect(configFile.contents.files[3]).to.match( + /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, + ); + return configFile; + }, + ) + .then((configFile) => { + configFile.contents.python.version = "3.11.3"; + return cy.savePublisherFile( + `.posit/publish/${configFile.fileName}`, + configFile.contents, + ); + }) + .then(() => { + return cy.log("File saved."); + }) + .deployCurrentlySelected(); + }); + + it("fastAPI in subdirectory of workspace", () => { + cy.createDeployment( + "fastapi-simple", + "fastapi-main.py", + "fastapi-sub-directory", + (configFile) => { + expect(configFile.contents.title).to.equal("fastapi-sub-directory"); + expect(configFile.contents.type).to.equal("python-fastapi"); + expect(configFile.contents.entrypoint).to.equal("fastapi-main.py"); + expect(configFile.contents.files[0]).to.equal("/fastapi-main.py"); + expect(configFile.contents.files[1]).to.equal("/requirements.txt"); + expect(configFile.contents.files[2]).to.equal( + `/.posit/publish/${configFile.fileName}`, + ); + // /\/.posit\/publish\/fastapi-base-directory-[A-Z0-9]{4}\.toml/, + // ); + expect(configFile.contents.files[3]).to.match( + /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, + ); + return configFile; + }, + ) + .then((configFile) => { + configFile.contents.python.version = "3.11.3"; + return cy.savePublisherFile( + `.posit/publish/${configFile.fileName}`, + configFile.contents, + ); + }) + .then(() => { + return cy.log("File saved."); + }) + .deployCurrentlySelected(); + }); +}); From e849f5b43d4537dbefc27f15f4328c1a5f7d7cce Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 09:02:27 -0800 Subject: [PATCH 05/10] prepare for shinyapp deployment - but unable to deploy as of yet - missing R on code server docker image --- test/e2e/content-workspace/shinyapp/.Rprofile | 1 + test/e2e/content-workspace/shinyapp/app.R | 49 + test/e2e/content-workspace/shinyapp/index.htm | 25 + test/e2e/content-workspace/shinyapp/renv.lock | 568 +++++++++ .../shinyapp/renv/.gitignore | 7 + .../shinyapp/renv/activate.R | 1032 +++++++++++++++++ .../shinyapp/renv/settings.dcf | 8 + .../shinyapp/renv/settings.json | 13 + 8 files changed, 1703 insertions(+) create mode 100644 test/e2e/content-workspace/shinyapp/.Rprofile create mode 100644 test/e2e/content-workspace/shinyapp/app.R create mode 100644 test/e2e/content-workspace/shinyapp/index.htm create mode 100644 test/e2e/content-workspace/shinyapp/renv.lock create mode 100644 test/e2e/content-workspace/shinyapp/renv/.gitignore create mode 100644 test/e2e/content-workspace/shinyapp/renv/activate.R create mode 100644 test/e2e/content-workspace/shinyapp/renv/settings.dcf create mode 100644 test/e2e/content-workspace/shinyapp/renv/settings.json diff --git a/test/e2e/content-workspace/shinyapp/.Rprofile b/test/e2e/content-workspace/shinyapp/.Rprofile new file mode 100644 index 000000000..81b960f5c --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/.Rprofile @@ -0,0 +1 @@ +source("renv/activate.R") diff --git a/test/e2e/content-workspace/shinyapp/app.R b/test/e2e/content-workspace/shinyapp/app.R new file mode 100644 index 000000000..284848e45 --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/app.R @@ -0,0 +1,49 @@ +# +# This is a Shiny web application. You can run the application by clicking +# the 'Run App' button above. +# +# Find out more about building applications with Shiny here: +# +# http://shiny.rstudio.com/ +# + +library(shiny) + +# Define UI for application that draws a histogram +ui <- fluidPage( + + # Application title + titlePanel("Old Faithful Geyser Data"), + + # Sidebar with a slider input for number of bins + sidebarLayout( + sidebarPanel( + sliderInput("bins", + "Number of bins:", + min = 1, + max = 50, + value = 30) + ), + + # Show a plot of the generated distribution + mainPanel( + plotOutput("distPlot") + ) + ) +) + +# Define server logic required to draw a histogram +server <- function(input, output) { + + output$distPlot <- renderPlot({ + # generate bins based on input$bins from ui.R + x <- faithful[, 2] + bins <- seq(min(x), max(x), length.out = input$bins + 1) + + # draw the histogram with the specified number of bins + hist(x, breaks = bins, col = 'darkgray', border = 'white') + }) +} + +# Run the application +shinyApp(ui = ui, server = server) diff --git a/test/e2e/content-workspace/shinyapp/index.htm b/test/e2e/content-workspace/shinyapp/index.htm new file mode 100644 index 000000000..f5fa342a4 --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/index.htm @@ -0,0 +1,25 @@ + + + + + + + +

testapp

+
+ + diff --git a/test/e2e/content-workspace/shinyapp/renv.lock b/test/e2e/content-workspace/shinyapp/renv.lock new file mode 100644 index 000000000..091997fcb --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/renv.lock @@ -0,0 +1,568 @@ +{ + "R": { + "Version": "4.3.3", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://cloud.r-project.org" + } + ] + }, + "Packages": { + "R6": { + "Package": "R6", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "R6", + "RemoteRef": "R6", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "2.5.1", + "Requirements": [ + "R" + ], + "Hash": "470851b6d5d0ac559e9d01bb352b4021" + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.0.14", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods", + "utils" + ], + "Hash": "e7bdd9ee90e96921ca8a0f1972d66682" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-3", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "base64enc", + "RemoteRef": "base64enc", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.1-3", + "Requirements": [ + "R" + ], + "Hash": "543776ae6848fde2f48ff3816d0628bc" + }, + "bslib": { + "Package": "bslib", + "Version": "0.9.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "cachem", + "fastmap", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "lifecycle", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "70a6489cc254171fb9b4a7f130f44dca" + }, + "cachem": { + "Package": "cachem", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "cachem", + "RemoteRef": "cachem", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.1.0", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "cd9a672193789068eb5a2aad65a0dedf" + }, + "cli": { + "Package": "cli", + "Version": "3.6.3", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "cli", + "RemoteRef": "cli", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "3.6.3", + "Requirements": [ + "R", + "utils" + ], + "Hash": "b21916dd77a27642b447374a5d30ecf3" + }, + "commonmark": { + "Package": "commonmark", + "Version": "1.9.2", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "commonmark", + "RemoteRef": "commonmark", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.9.2", + "Hash": "14eb0596f987c71535d07c3aff814742" + }, + "crayon": { + "Package": "crayon", + "Version": "1.5.3", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "crayon", + "RemoteRef": "crayon", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.5.3", + "Requirements": [ + "grDevices", + "methods", + "utils" + ], + "Hash": "859d96e65ef198fd43e82b9628d593ef" + }, + "digest": { + "Package": "digest", + "Version": "0.6.37", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "digest", + "RemoteRef": "digest", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.6.37", + "Requirements": [ + "R", + "utils" + ], + "Hash": "33698c4b3127fc9f506654607fb73676" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "fastmap", + "RemoteRef": "fastmap", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.2.0", + "Hash": "aa5e1cd11c2d15497494c5292d7ffcc8" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.3", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "fontawesome", + "RemoteRef": "fontawesome", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.5.3", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "bd1297f9b5b1fc1372d19e2c4cd82215" + }, + "fs": { + "Package": "fs", + "Version": "1.6.5", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "fs", + "RemoteRef": "fs", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.6.5", + "Requirements": [ + "R", + "methods" + ], + "Hash": "7f48af39fa27711ea5fbd183b399920d" + }, + "glue": { + "Package": "glue", + "Version": "1.8.0", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "glue", + "RemoteRef": "glue", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.8.0", + "Requirements": [ + "R", + "methods" + ], + "Hash": "5899f1eaa825580172bb56c08266f37c" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.8.1", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "htmltools", + "RemoteRef": "htmltools", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.5.8.1", + "Requirements": [ + "R", + "base64enc", + "digest", + "fastmap", + "grDevices", + "rlang", + "utils" + ], + "Hash": "81d371a9cc60640e74e4ab6ac46dcedc" + }, + "httpuv": { + "Package": "httpuv", + "Version": "1.6.15", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "httpuv", + "RemoteRef": "httpuv", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.6.15", + "Requirements": [ + "R", + "R6", + "Rcpp", + "later", + "promises", + "utils" + ], + "Hash": "d55aa087c47a63ead0f6fc10f8fa1ee0" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "jquerylib", + "RemoteRef": "jquerylib", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.1.4", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "1.8.9", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "jsonlite", + "RemoteRef": "jsonlite", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.8.9", + "Requirements": [ + "methods" + ], + "Hash": "4e993b65c2c3ffbffce7bb3e2c6f832b" + }, + "later": { + "Package": "later", + "Version": "1.4.1", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "later", + "RemoteRef": "later", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.4.1", + "Requirements": [ + "Rcpp", + "rlang" + ], + "Hash": "501744395cac0bab0fbcfab9375ae92c" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "glue", + "rlang" + ], + "Hash": "b8552d117e1b808b09a832f589b79035" + }, + "magrittr": { + "Package": "magrittr", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "magrittr", + "RemoteRef": "magrittr", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "2.0.3", + "Requirements": [ + "R" + ], + "Hash": "7ce2733a9826b3aeb1775d56fd305472" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "memoise", + "RemoteRef": "memoise", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "2.0.1", + "Requirements": [ + "cachem", + "rlang" + ], + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" + }, + "mime": { + "Package": "mime", + "Version": "0.12", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "mime", + "RemoteRef": "mime", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.12", + "Requirements": [ + "tools" + ], + "Hash": "18e9c28c1d3ca1560ce30658b22ce104" + }, + "promises": { + "Package": "promises", + "Version": "1.3.2", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "promises", + "RemoteRef": "promises", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.3.2", + "Requirements": [ + "R6", + "Rcpp", + "fastmap", + "later", + "magrittr", + "rlang", + "stats" + ], + "Hash": "c84fd4f75ea1f5434735e08b7f50fbca" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.3", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "rappdirs", + "RemoteRef": "rappdirs", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.3.3", + "Requirements": [ + "R" + ], + "Hash": "5e3c5dc0b071b21fa128676560dbe94d" + }, + "renv": { + "Package": "renv", + "Version": "0.17.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "4543b8cd233ae25c6aba8548be9e747e" + }, + "rlang": { + "Package": "rlang", + "Version": "1.1.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "724dcc1490cd7071ee75ca2994a5446e" + }, + "sass": { + "Package": "sass", + "Version": "0.4.9", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "sass", + "RemoteRef": "sass", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.4.9", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "d53dbfddf695303ea4ad66f86e99b95d" + }, + "shiny": { + "Package": "shiny", + "Version": "1.10.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "bslib", + "cachem", + "commonmark", + "crayon", + "fastmap", + "fontawesome", + "glue", + "grDevices", + "htmltools", + "httpuv", + "jsonlite", + "later", + "lifecycle", + "methods", + "mime", + "promises", + "rlang", + "sourcetools", + "tools", + "utils", + "withr", + "xtable" + ], + "Hash": "4b4477baa9a939c5577e5ddb4bf01f28" + }, + "sourcetools": { + "Package": "sourcetools", + "Version": "0.1.7-1", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "sourcetools", + "RemoteRef": "sourcetools", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "0.1.7-1", + "Requirements": [ + "R" + ], + "Hash": "5f5a7629f956619d519205ec475fe647" + }, + "withr": { + "Package": "withr", + "Version": "3.0.2", + "Source": "Repository", + "Repository": "CRAN", + "RemoteType": "standard", + "RemotePkgRef": "withr", + "RemoteRef": "withr", + "RemoteRepos": "https://cloud.r-project.org", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "3.0.2", + "Requirements": [ + "R", + "grDevices", + "graphics" + ], + "Hash": "cc2d62c76458d425210d1eb1478b30b4" + }, + "xtable": { + "Package": "xtable", + "Version": "1.8-4", + "Source": "Repository", + "Repository": "RSPM", + "RemoteType": "standard", + "RemotePkgRef": "xtable", + "RemoteRef": "xtable", + "RemoteRepos": "https://packagemanager.posit.co/cran/latest", + "RemoteReposName": "CRAN", + "RemotePkgPlatform": "x86_64-apple-darwin20", + "RemoteSha": "1.8-4", + "Requirements": [ + "R", + "stats", + "utils" + ], + "Hash": "b8acdf8af494d9ec19ccb2481a9b11c2" + } + } +} diff --git a/test/e2e/content-workspace/shinyapp/renv/.gitignore b/test/e2e/content-workspace/shinyapp/renv/.gitignore new file mode 100644 index 000000000..6ae4167d4 --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/renv/.gitignore @@ -0,0 +1,7 @@ +cellar/ +sandbox/ +library/ +local/ +lock/ +python/ +staging/ diff --git a/test/e2e/content-workspace/shinyapp/renv/activate.R b/test/e2e/content-workspace/shinyapp/renv/activate.R new file mode 100644 index 000000000..a8fdc3201 --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/renv/activate.R @@ -0,0 +1,1032 @@ + +local({ + + # the requested version of renv + version <- "0.17.3" + + # the project directory + project <- getwd() + + # figure out whether the autoloader is enabled + enabled <- local({ + + # first, check config option + override <- getOption("renv.config.autoloader.enabled") + if (!is.null(override)) + return(override) + + # next, check environment variables + # TODO: prefer using the configuration one in the future + envvars <- c( + "RENV_CONFIG_AUTOLOADER_ENABLED", + "RENV_AUTOLOADER_ENABLED", + "RENV_ACTIVATE_PROJECT" + ) + + for (envvar in envvars) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(tolower(envval) %in% c("true", "t", "1")) + } + + # enable by default + TRUE + + }) + + if (!enabled) + return(FALSE) + + # avoid recursion + if (identical(getOption("renv.autoloader.running"), TRUE)) { + warning("ignoring recursive attempt to run renv autoloader") + return(invisible(TRUE)) + } + + # signal that we're loading renv during R startup + options(renv.autoloader.running = TRUE) + on.exit(options(renv.autoloader.running = NULL), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # unload renv if it's already been loaded + if ("renv" %in% loadedNamespaces()) + unloadNamespace("renv") + + # load bootstrap tools + `%||%` <- function(x, y) { + if (is.environment(x) || length(x)) x else y + } + + `%??%` <- function(x, y) { + if (is.null(x)) y else x + } + + bootstrap <- function(version, library) { + + # attempt to download renv + tarball <- tryCatch(renv_bootstrap_download(version), error = identity) + if (inherits(tarball, "error")) + stop("failed to download renv ", version) + + # now attempt to install + status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity) + if (inherits(status, "error")) + stop("failed to install renv ", version) + + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # get CRAN repository + cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) { + + # check for RSPM; if set, use a fallback repository for renv + rspm <- Sys.getenv("RSPM", unset = NA) + if (identical(rspm, repos)) + repos <- c(RSPM = rspm, CRAN = cran) + + return(repos) + + } + + # check for lockfile repositories + repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) + if (!inherits(repos, "error") && length(repos)) + return(repos) + + # if we're testing, re-use the test repositories + if (renv_bootstrap_tests_running()) { + repos <- getOption("renv.tests.repos") + if (!is.null(repos)) + return(repos) + } + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- cran + + # add in renv.bootstrap.repos if set + default <- c(FALLBACK = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_repos_lockfile <- function() { + + lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") + if (!file.exists(lockpath)) + return(NULL) + + lockfile <- tryCatch(renv_json_read(lockpath), error = identity) + if (inherits(lockfile, "error")) { + warning(lockfile) + return(NULL) + } + + repos <- lockfile$R$Repositories + if (length(repos) == 0) + return(NULL) + + keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) + vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) + names(vals) <- keys + + return(vals) + + } + + renv_bootstrap_download <- function(version) { + + # if the renv version number has 4 components, assume it must + # be retrieved via github + nv <- numeric_version(version) + components <- unclass(nv)[[1]] + + # if this appears to be a development version of 'renv', we'll + # try to restore from github + dev <- length(components) == 4L + + # begin collecting different methods for finding renv + methods <- c( + renv_bootstrap_download_tarball, + if (dev) + renv_bootstrap_download_github + else c( + renv_bootstrap_download_cran_latest, + renv_bootstrap_download_cran_archive + ) + ) + + for (method in methods) { + path <- tryCatch(method(version), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("failed to download renv ", version) + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + args <- list( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + if ("headers" %in% names(formals(utils::download.file))) + args$headers <- renv_bootstrap_download_custom_headers(url) + + do.call(utils::download.file, args) + + } + + renv_bootstrap_download_custom_headers <- function(url) { + + headers <- getOption("renv.download.headers") + if (is.null(headers)) + return(character()) + + if (!is.function(headers)) + stopf("'renv.download.headers' is not a function") + + headers <- headers(url) + if (length(headers) == 0L) + return(character()) + + if (is.list(headers)) + headers <- unlist(headers, recursive = FALSE, use.names = TRUE) + + ok <- + is.character(headers) && + is.character(names(headers)) && + all(nzchar(names(headers))) + + if (!ok) + stop("invocation of 'renv.download.headers' did not return a named character vector") + + headers + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + spec <- renv_bootstrap_download_cran_latest_find(version) + type <- spec$type + repos <- spec$repos + + message("* Downloading renv ", version, " ... ", appendLF = FALSE) + + baseurl <- utils::contrib.url(repos = repos, type = type) + ext <- if (identical(type, "source")) + ".tar.gz" + else if (Sys.info()[["sysname"]] == "Windows") + ".zip" + else + ".tgz" + name <- sprintf("renv_%s%s", version, ext) + url <- paste(baseurl, name, sep = "/") + + destfile <- file.path(tempdir(), name) + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (inherits(status, "condition")) { + message("FAILED") + return(FALSE) + } + + # report success and return + message("OK (downloaded ", type, ")") + destfile + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + # check whether binaries are supported on this system + binary <- + getOption("renv.bootstrap.binary", default = TRUE) && + !identical(.Platform$pkgType, "source") && + !identical(getOption("pkgType"), "source") && + Sys.info()[["sysname"]] %in% c("Darwin", "Windows") + + types <- c(if (binary) "binary", "source") + + # iterate over types + repositories + for (type in types) { + for (repos in renv_bootstrap_repos()) { + + # retrieve package database + db <- tryCatch( + as.data.frame( + utils::available.packages(type = type, repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + # check for compatible entry + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + # found it; return spec to caller + spec <- list(entry = entry, type = type, repos = repos) + return(spec) + + } + } + + # if we got here, we failed to find renv + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + message("* Downloading renv ", version, " ... ", appendLF = FALSE) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) { + message("OK") + return(destfile) + } + + } + + message("FAILED") + return(FALSE) + + } + + renv_bootstrap_download_tarball <- function(version) { + + # if the user has provided the path to a tarball via + # an environment variable, then use it + tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) + if (is.na(tarball)) + return() + + # allow directories + if (dir.exists(tarball)) { + name <- sprintf("renv_%s.tar.gz", version) + tarball <- file.path(tarball, name) + } + + # bail if it doesn't exist + if (!file.exists(tarball)) { + + # let the user know we weren't able to honour their request + fmt <- "* RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." + msg <- sprintf(fmt, tarball) + warning(msg) + + # bail + return() + + } + + fmt <- "* Bootstrapping with tarball at path '%s'." + msg <- sprintf(fmt, tarball) + message(msg) + + tarball + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE) + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) { + message("FAILED") + return(FALSE) + } + + message("OK") + return(destfile) + + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + message("* Installing renv ", version, " ... ", appendLF = FALSE) + dir.create(library, showWarnings = FALSE, recursive = TRUE) + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + r <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + output <- system2(r, args, stdout = TRUE, stderr = TRUE) + message("Done!") + + # check for successful install + status <- attr(output, "status") + if (is.numeric(status) && !identical(status, 0L)) { + header <- "Error installing renv:" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- c(header, lines, output) + writeLines(text, con = stderr()) + } + + status + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version) { + + loadedversion <- utils::packageDescription("renv", fields = "Version") + if (version == loadedversion) + return(TRUE) + + # assume four-component versions are from GitHub; + # three-component versions are from CRAN + components <- strsplit(loadedversion, "[.-]")[[1]] + remote <- if (length(components) == 4L) + paste("rstudio/renv", loadedversion, sep = "@") + else + paste("renv", loadedversion, sep = "@") + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + + msg <- sprintf(fmt, loadedversion, version, remote) + warning(msg, call. = FALSE) + + FALSE + + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warning) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- catch(renv_json_read_jsonlite(file, text)) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- catch(renv_json_read_default(file, text)) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% read(file), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # find strings in the JSON + text <- paste(text %||% read(file), collapse = "\n") + pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + locs <- gregexpr(pattern, text, perl = TRUE)[[1]] + + # if any are found, replace them with placeholders + replaced <- text + strings <- character() + replacements <- character() + + if (!identical(c(locs), -1L)) { + + # get the string values + starts <- locs + ends <- locs + attr(locs, "match.length") - 1L + strings <- substring(text, starts, ends) + + # only keep those requiring escaping + strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) + + # compute replacements + replacements <- sprintf('"\032%i\032"', seq_along(strings)) + + # replace the strings + mapply(function(string, replacement) { + replaced <<- sub(string, replacement, replaced, fixed = TRUE) + }, strings, replacements) + + } + + # transform the JSON into something the R parser understands + transformed <- replaced + transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) + transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) + transformed <- gsub("[]}]", ")", transformed, perl = TRUE) + transformed <- gsub(":", "=", transformed, fixed = TRUE) + text <- paste(transformed, collapse = "\n") + + # parse it + json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] + + # construct map between source strings, replaced strings + map <- as.character(parse(text = strings)) + names(map) <- as.character(parse(text = replacements)) + + # convert to list + map <- as.list(map) + + # remap strings in object + remapped <- renv_json_remap(json, map) + + # evaluate + eval(remapped, envir = baseenv()) + + } + + renv_json_remap <- function(json, map) { + + # fix names + if (!is.null(names(json))) { + lhs <- match(names(json), names(map), nomatch = 0L) + rhs <- match(names(map), names(json), nomatch = 0L) + names(json)[rhs] <- map[lhs] + } + + # fix values + if (is.character(json)) + return(map[[json]] %||% json) + + # handle true, false, null + if (is.name(json)) { + text <- as.character(json) + if (text == "true") + return(TRUE) + else if (text == "false") + return(FALSE) + else if (text == "null") + return(NULL) + } + + # recurse + if (is.recursive(json)) { + for (i in seq_along(json)) { + json[i] <- list(renv_json_remap(json[[i]], map)) + } + } + + json + + } + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # attempt to load + if (renv_bootstrap_load(project, libpath, version)) + return(TRUE) + + # load failed; inform user we're about to bootstrap + prefix <- paste("# Bootstrapping renv", version) + postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "") + header <- paste(prefix, postfix) + message(header) + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + message("* Successfully installed and loaded renv ", version, ".") + return(renv::load()) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + +}) diff --git a/test/e2e/content-workspace/shinyapp/renv/settings.dcf b/test/e2e/content-workspace/shinyapp/renv/settings.dcf new file mode 100644 index 000000000..fc4e47900 --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/renv/settings.dcf @@ -0,0 +1,8 @@ +external.libraries: +ignored.packages: +package.dependency.fields: Imports, Depends, LinkingTo +r.version: +snapshot.type: implicit +use.cache: TRUE +vcs.ignore.library: TRUE +vcs.ignore.local: TRUE diff --git a/test/e2e/content-workspace/shinyapp/renv/settings.json b/test/e2e/content-workspace/shinyapp/renv/settings.json new file mode 100644 index 000000000..76d77c705 --- /dev/null +++ b/test/e2e/content-workspace/shinyapp/renv/settings.json @@ -0,0 +1,13 @@ +{ + "bioconductor.version": null, + "external.libraries": [], + "ignored.packages": [], + "package.dependency.fields": ["Imports", "Depends", "LinkingTo"], + "r.version": null, + "snapshot.type": "implicit", + "use.cache": true, + "vcs.ignore.cellar": true, + "vcs.ignore.library": true, + "vcs.ignore.local": true, + "vcs.manage.ignores": true +} From 3635642b1df2a15cbad3415863b9b1e9f4ce003f Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 09:03:12 -0800 Subject: [PATCH 06/10] switch deployment test to use new sequence helper command --- test/e2e/tests/deployments.cy.js | 97 +++++++++----------------------- 1 file changed, 26 insertions(+), 71 deletions(-) diff --git a/test/e2e/tests/deployments.cy.js b/test/e2e/tests/deployments.cy.js index e0ccf7843..9e0840afd 100644 --- a/test/e2e/tests/deployments.cy.js +++ b/test/e2e/tests/deployments.cy.js @@ -4,84 +4,39 @@ describe("Deployments Section", () => { beforeEach(() => { cy.resetConnect(); cy.setAdminCredentials(); + cy.clearupDeployments("static"); cy.visit("/"); }); it("Static Content Deployment", () => { - // Temporarily ignore uncaught exception due to a vscode worker being cancelled at some point. - cy.on("uncaught:exception", () => false); - - // Open the entrypoint ahead of time for easier selection later. - cy.get(".explorer-viewlet").find('[aria-label="static"]').click(); - - cy.get(".explorer-viewlet") - .find('[aria-label="index.html"]') - .should("be.visible") - .dblclick(); - - cy.get(".tabs-container") - .find('[aria-label="index.html"]') - .should("be.visible"); - - cy.getPublisherSidebarIcon() - .should("be.visible", { timeout: 10000 }) - .click(); - - cy.publisherWebview() - .findByTestId("select-deployment") - .then((dplyPicker) => { - Cypress.$(dplyPicker).trigger("click"); - }); - - cy.get(".quick-input-widget").should("be.visible"); - - cy.get(".quick-input-titlebar").should("have.text", "Select Deployment"); - - cy.get(".quick-input-widget").type("{enter}"); - - cy.get(".quick-input-widget") - .find('[aria-label="static/index.html, Open Files"]') - .should("be.visible") - .click(); - - cy.get(".quick-input-widget") - .find(".quick-input-filter input") - .should("have.value", "static") - .type("{enter}"); - - cy.get(".quick-input-widget") - .find( - '[aria-label="admin-code-server, http://connect-publisher-e2e:3939"]', - ) - .should("be.visible") - .click(); - - cy.loadProjectConfigFile("static").then((config) => { - expect(config.title).to.equal("static"); - expect(config.type).to.equal("html"); - expect(config.entrypoint).to.equal("index.html"); - expect(config.files[0]).to.equal("/index.html"); - expect(config.files[1]).to.match( - /\/.posit\/publish\/static-[A-Z0-9]{4}\.toml/, + cy.createDeployment("static", "index.html", "static", (configFile) => { + expect(configFile.contents.title).to.equal("static"); + expect(configFile.contents.type).to.equal("html"); + expect(configFile.contents.entrypoint).to.equal("index.html"); + expect(configFile.contents.files[0]).to.equal("/index.html"); + expect(configFile.contents.files[1]).to.equal( + `/.posit/publish/${configFile.fileName}`, ); - expect(config.files[2]).to.match( + expect(configFile.contents.files[2]).to.match( /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, ); - }); - - cy.publisherWebview() - .findByTestId("deploy-button") - .should("be.visible") - .then((dplyBtn) => { - Cypress.$(dplyBtn).trigger("click"); - }); - - // Wait for deploying message to finish - cy.get(".notifications-toasts") - .should("be.visible") - .findByText("Deploying your project: Starting to Deploy...") - .should("not.exist"); + }).deployCurrentlySelected(); + }); - cy.findByText("Deployment was successful").should("be.visible"); + // Unable to run this, as the docker image for the code server does not have R installed. + it.skip("ShinyApp Content Deployment", () => { + cy.createDeployment("shinyapp", "app.R", "ShinyApp", (configFile) => { + expect(configFile.contents.title).to.equal("ShinyApp"); + expect(configFile.contents.type).to.equal("r-shiny"); + expect(configFile.contents.entrypoint).to.equal("app.R"); + expect(configFile.contents.files[0]).to.equal("/app.R"); + expect(configFile.contents.files[1]).to.equal("/renv.lock"); + expect(configFile.contents.files[2]).to.equal( + `/.posit/publish/${configFile.fileName}`, + ); + expect(configFile.contents.files[3]).to.match( + /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, + ); + }).deployCurrentlySelected(); }); }); From 196fd4cadce29f004448f31802a90ed13e1a5e29 Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 09:03:54 -0800 Subject: [PATCH 07/10] change required to deploy fastapi correctly on connect server --- test/sample-content/fastapi-simple/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/sample-content/fastapi-simple/requirements.txt b/test/sample-content/fastapi-simple/requirements.txt index c44fd61dc..5d9af448f 100644 --- a/test/sample-content/fastapi-simple/requirements.txt +++ b/test/sample-content/fastapi-simple/requirements.txt @@ -1,3 +1,3 @@ # requirements.txt auto-generated by Posit Publisher -# using /Users/billsager/.pyenv/shims/python3 -fastapi +# using /Users/billsager/dev/publishing-client/test/sample-content/.venv/bin/python +fastapi==0.115.7 From 01af3798994558f0c4b36c0058bc7f373297496a Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 10:07:01 -0800 Subject: [PATCH 08/10] fix embedded subdirectory test - did not have proper path to config file --- test/e2e/tests/embedded-deployments.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/tests/embedded-deployments.cy.js b/test/e2e/tests/embedded-deployments.cy.js index d0554a76e..670732f9f 100644 --- a/test/e2e/tests/embedded-deployments.cy.js +++ b/test/e2e/tests/embedded-deployments.cy.js @@ -69,7 +69,7 @@ describe("Create Deployments", () => { .then((configFile) => { configFile.contents.python.version = "3.11.3"; return cy.savePublisherFile( - `.posit/publish/${configFile.fileName}`, + `fastapi-simple/.posit/publish/${configFile.fileName}`, configFile.contents, ); }) From 2250cb17fa70fa93493e561998cd6e84ae4a4c74 Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Wed, 19 Feb 2025 11:30:17 -0800 Subject: [PATCH 09/10] add specific check on content record filename and refactor toml file methods --- test/e2e/support/commands.js | 90 ++++++++++++----------- test/e2e/support/sequences.js | 53 ++++++++++--- test/e2e/tests/deployments.cy.js | 40 +++++----- test/e2e/tests/embedded-deployments.cy.js | 66 ++++++++--------- 4 files changed, 142 insertions(+), 107 deletions(-) diff --git a/test/e2e/support/commands.js b/test/e2e/support/commands.js index 0bd882659..e46682761 100644 --- a/test/e2e/support/commands.js +++ b/test/e2e/support/commands.js @@ -137,15 +137,55 @@ Cypress.Commands.add("clearupDeployments", (subdir) => { cy.exec(`rm -rf content-workspace/${subdir}/.posit`); }); -Cypress.Commands.add("expandWildcardFile", (projectName, wildCardPath) => { - const workingDir = `content-workspace/${projectName}/.posit/publish`; +// returns +// config: { +// name: string, +// path: string, +// }, +// contentRecord: { +// name: string, +// path: string, +// } +Cypress.Commands.add("getPublisherTomlFilePaths", (projectDir) => { + let configTargetDir = `content-workspace/${projectDir}/.posit/publish`; + let configFileName = ""; + let configFilePath = ""; + let contentRecordTargetDir = `content-workspace/${projectDir}/.posit/publish/deployments`; + let contentRecordFileName = ""; + let contentRecordFilePath = ""; + + cy.expandWildcardFile(configTargetDir, "*.toml") + .then((configFile) => { + configFileName = configFile; + configFilePath = `${configTargetDir}/${configFile}`; + }) + .expandWildcardFile(contentRecordTargetDir, "*.toml") + .then((contentRecordFile) => { + contentRecordFileName = contentRecordFile; + contentRecordFilePath = `${contentRecordTargetDir}/${contentRecordFile}`; + }) + .then(() => { + return { + config: { + name: configFileName, + path: configFilePath, + }, + contentRecord: { + name: contentRecordFileName, + path: contentRecordFilePath, + }, + }; + }); +}); + +Cypress.Commands.add("expandWildcardFile", (targetDir, wildCardPath) => { return cy .exec("pwd") .then((result) => { return cy.log("CWD", result.stdout); }) .then(() => { - const cmd = `cd ${workingDir} && file=$(echo ${wildCardPath}) && echo $file`; + const cmd = `cd ${targetDir} && file=$(echo ${wildCardPath}) && echo $file`; return cy.exec(cmd); }) .then((result) => { @@ -157,31 +197,23 @@ Cypress.Commands.add("expandWildcardFile", (projectName, wildCardPath) => { }); Cypress.Commands.add("savePublisherFile", (filePath, jsonObject) => { - const projectFilePath = `content-workspace/${filePath}`; return cy .exec("pwd") .then((result) => { return cy .log("savePublisherFile CWD", result.stdout) - .log("filePath", projectFilePath); + .log("filePath", filePath); }) .then(() => { const tomlString = stringify(jsonObject); - return cy.writeFile(projectFilePath, tomlString); + return cy.writeFile(filePath, tomlString); }); }); -Cypress.Commands.add("loadProjectConfigFile", (projectName, configName) => { - const projectConfigPath = `content-workspace/${projectName}/.posit/publish/${configName}`; - // Do not fail on non-zero exit this time, we can provide a better error +Cypress.Commands.add("loadTomlFile", (filePath) => { return cy - .exec("pwd") - .then((result) => { - return cy - .log("projectConfigPath", projectConfigPath) - .log("loadProjectConfigFile CWD", result.stdout); - }) - .exec(`cat ${projectConfigPath}`, { failOnNonZeroExit: false }) + .log("filePath", filePath) + .exec(`cat ${filePath}`, { failOnNonZeroExit: false }) .then((result) => { if (result.code === 0 && result.stdout) { return parse(result.stdout); @@ -190,32 +222,6 @@ Cypress.Commands.add("loadProjectConfigFile", (projectName, configName) => { }); }); -Cypress.Commands.add("loadProjectDeploymentFile", (projectName) => { - const projectDeploymentPath = `content-workspace/${projectName}/.posit/publish/deployments/deployment-*.toml`; - // Do not fail on non-zero exit this time, we can provide a better error - return cy - .exec(`cat ${projectDeploymentPath}`, { failOnNonZeroExit: false }) - .then((result) => { - if (result.code === 0 && result.stdout) { - return parse(result.stdout); - } - throw new Error(`Could not load project deployment. ${result.stderr}`); - }); -}); - -Cypress.Commands.add("loadProjectDeploymentFile", (projectName) => { - const projectDeploymentPath = `content-workspace/${projectName}/.posit/publish/deployments/deployment-*.toml`; - // Do not fail on non-zero exit this time, we can provide a better error - return cy - .exec(`cat ${projectDeploymentPath}`, { failOnNonZeroExit: false }) - .then((result) => { - if (result.code === 0 && result.stdout) { - return parse(result.stdout); - } - throw new Error(`Could not load project deployment. ${result.stderr}`); - }); -}); - // Performs the full set of reset commands we typically use before executing our tests Cypress.Commands.add("resetConnect", () => { cy.clearupDeployments(); diff --git a/test/e2e/support/sequences.js b/test/e2e/support/sequences.js index 450157925..12824d258 100644 --- a/test/e2e/support/sequences.js +++ b/test/e2e/support/sequences.js @@ -1,12 +1,26 @@ // Copyright (C) 2025 by Posit Software, PBC. +// tomlCallback interface = func( +// { +// config: { +// name: string, +// path: string, +// contents: {}, +// }, +// contentRecord: { +// name: string, +// path: string, +// contents: {}, +// } +// }) + Cypress.Commands.add( "createDeployment", ( projectDir, // string entrypointFile, // string title, // string - verifyConfigCallback, // func({ fileName: configFileName, contents: contents}) + verifyTomlCallback, // func({config: { filename: string, contents: {},}, contentRecord: { filename: string, contents: {}}) ) => { // Temporarily ignore uncaught exception due to a vscode worker being cancelled at some point. cy.on("uncaught:exception", () => false); @@ -84,19 +98,34 @@ Cypress.Commands.add( .click(); return cy - .expandWildcardFile(projectDir, `${title}-*.toml`) - .then((configFileName) => { - cy.loadProjectConfigFile(projectDir, configFileName).then( - (contents) => { - return { - fileName: configFileName, - contents: contents, - }; + .getPublisherTomlFilePaths(projectDir) + .then((filePaths) => { + let result = { + config: { + name: filePaths.config.name, + path: filePaths.config.path, + contents: {}, + }, + contentRecord: { + name: filePaths.contentRecord.name, + path: filePaths.contentRecord.path, + contents: {}, }, - ); + }; + cy.loadTomlFile(filePaths.config.path) + .then((config) => { + result.config.contents = config; + }) + .loadTomlFile(filePaths.contentRecord.path) + .then((contentRecord) => { + result.contentRecord.contents = contentRecord; + }) + .then(() => { + return result; + }); }) - .then((configFile) => { - return verifyConfigCallback(configFile); + .then((tomlFiles) => { + return verifyTomlCallback(tomlFiles); }); }, ); diff --git a/test/e2e/tests/deployments.cy.js b/test/e2e/tests/deployments.cy.js index 9e0840afd..fb1eafac4 100644 --- a/test/e2e/tests/deployments.cy.js +++ b/test/e2e/tests/deployments.cy.js @@ -9,33 +9,35 @@ describe("Deployments Section", () => { }); it("Static Content Deployment", () => { - cy.createDeployment("static", "index.html", "static", (configFile) => { - expect(configFile.contents.title).to.equal("static"); - expect(configFile.contents.type).to.equal("html"); - expect(configFile.contents.entrypoint).to.equal("index.html"); - expect(configFile.contents.files[0]).to.equal("/index.html"); - expect(configFile.contents.files[1]).to.equal( - `/.posit/publish/${configFile.fileName}`, + cy.createDeployment("static", "index.html", "static", (tomlFiles) => { + const config = tomlFiles.config.contents; + expect(config.title).to.equal("static"); + expect(config.type).to.equal("html"); + expect(config.entrypoint).to.equal("index.html"); + expect(config.files[0]).to.equal("/index.html"); + expect(config.files[1]).to.equal( + `/.posit/publish/${tomlFiles.config.name}`, ); - expect(configFile.contents.files[2]).to.match( - /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, + expect(config.files[2]).to.equal( + `/.posit/publish/deployments/${tomlFiles.contentRecord.name}`, ); }).deployCurrentlySelected(); }); // Unable to run this, as the docker image for the code server does not have R installed. it.skip("ShinyApp Content Deployment", () => { - cy.createDeployment("shinyapp", "app.R", "ShinyApp", (configFile) => { - expect(configFile.contents.title).to.equal("ShinyApp"); - expect(configFile.contents.type).to.equal("r-shiny"); - expect(configFile.contents.entrypoint).to.equal("app.R"); - expect(configFile.contents.files[0]).to.equal("/app.R"); - expect(configFile.contents.files[1]).to.equal("/renv.lock"); - expect(configFile.contents.files[2]).to.equal( - `/.posit/publish/${configFile.fileName}`, + cy.createDeployment("shinyapp", "app.R", "ShinyApp", (tomlFiles) => { + const config = tomlFiles.config.contents; + expect(config.title).to.equal("ShinyApp"); + expect(config.type).to.equal("r-shiny"); + expect(config.entrypoint).to.equal("app.R"); + expect(config.files[0]).to.equal("/app.R"); + expect(config.files[1]).to.equal("/renv.lock"); + expect(config.files[2]).to.equal( + `/.posit/publish/${tomlFiles.config.name}`, ); - expect(configFile.contents.files[3]).to.match( - /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, + expect(config.files[3]).to.equal( + `/.posit/publish/deployments/${tomlFiles.contentRecord.name}`, ); }).deployCurrentlySelected(); }); diff --git a/test/e2e/tests/embedded-deployments.cy.js b/test/e2e/tests/embedded-deployments.cy.js index 670732f9f..b24753cae 100644 --- a/test/e2e/tests/embedded-deployments.cy.js +++ b/test/e2e/tests/embedded-deployments.cy.js @@ -14,28 +14,27 @@ describe("Create Deployments", () => { ".", "simple.py", "fastapi-base-directory", - (configFile) => { - expect(configFile.contents.title).to.equal("fastapi-base-directory"); - expect(configFile.contents.type).to.equal("python-fastapi"); - expect(configFile.contents.entrypoint).to.equal("simple.py"); - expect(configFile.contents.files[0]).to.equal("/simple.py"); - expect(configFile.contents.files[1]).to.equal("/requirements.txt"); - expect(configFile.contents.files[2]).to.equal( - `/.posit/publish/${configFile.fileName}`, + (tomlFiles) => { + const config = tomlFiles.config.contents; + expect(config.title).to.equal("fastapi-base-directory"); + expect(config.type).to.equal("python-fastapi"); + expect(config.entrypoint).to.equal("simple.py"); + expect(config.files[0]).to.equal("/simple.py"); + expect(config.files[1]).to.equal("/requirements.txt"); + expect(config.files[2]).to.equal( + `/.posit/publish/${tomlFiles.config.name}`, ); - // /\/.posit\/publish\/fastapi-base-directory-[A-Z0-9]{4}\.toml/, - // ); - expect(configFile.contents.files[3]).to.match( - /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, + expect(config.files[3]).to.equal( + `/.posit/publish/deployments/${tomlFiles.contentRecord.name}`, ); - return configFile; + return tomlFiles; }, ) - .then((configFile) => { - configFile.contents.python.version = "3.11.3"; + .then((tomlFiles) => { + tomlFiles.config.contents.python.version = "3.11.3"; return cy.savePublisherFile( - `.posit/publish/${configFile.fileName}`, - configFile.contents, + tomlFiles.config.path, + tomlFiles.config.contents, ); }) .then(() => { @@ -49,28 +48,27 @@ describe("Create Deployments", () => { "fastapi-simple", "fastapi-main.py", "fastapi-sub-directory", - (configFile) => { - expect(configFile.contents.title).to.equal("fastapi-sub-directory"); - expect(configFile.contents.type).to.equal("python-fastapi"); - expect(configFile.contents.entrypoint).to.equal("fastapi-main.py"); - expect(configFile.contents.files[0]).to.equal("/fastapi-main.py"); - expect(configFile.contents.files[1]).to.equal("/requirements.txt"); - expect(configFile.contents.files[2]).to.equal( - `/.posit/publish/${configFile.fileName}`, + (tomlFiles) => { + const config = tomlFiles.config.contents; + expect(config.title).to.equal("fastapi-sub-directory"); + expect(config.type).to.equal("python-fastapi"); + expect(config.entrypoint).to.equal("fastapi-main.py"); + expect(config.files[0]).to.equal("/fastapi-main.py"); + expect(config.files[1]).to.equal("/requirements.txt"); + expect(config.files[2]).to.equal( + `/.posit/publish/${tomlFiles.config.name}`, ); - // /\/.posit\/publish\/fastapi-base-directory-[A-Z0-9]{4}\.toml/, - // ); - expect(configFile.contents.files[3]).to.match( - /\/.posit\/publish\/deployments\/deployment-[A-Z0-9]{4}\.toml/, + expect(config.files[3]).to.equal( + `/.posit/publish/deployments/${tomlFiles.contentRecord.name}`, ); - return configFile; + return tomlFiles; }, ) - .then((configFile) => { - configFile.contents.python.version = "3.11.3"; + .then((tomlFiles) => { + tomlFiles.config.contents.python.version = "3.11.3"; return cy.savePublisherFile( - `fastapi-simple/.posit/publish/${configFile.fileName}`, - configFile.contents, + tomlFiles.config.path, + tomlFiles.config.contents, ); }) .then(() => { From 7bde31a8ecfbc968b0fdfa2e6ebcb41f682e504f Mon Sep 17 00:00:00 2001 From: Bill Sager Date: Fri, 21 Feb 2025 11:54:41 -0800 Subject: [PATCH 10/10] need to add fixed test deployments/config --- .../config-errors/.gitignore | 1 + .../publish/deployments/deployment-4QPM.toml | 43 +++ .../publish/deployments/deployment-P869.toml | 278 ++++++++++++++++++ .../.posit/publish/quarto-project-8G2B.toml | 24 ++ 4 files changed, 346 insertions(+) create mode 100644 test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-4QPM.toml create mode 100644 test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-P869.toml create mode 100644 test/e2e/content-workspace/config-errors/.posit/publish/quarto-project-8G2B.toml diff --git a/test/e2e/content-workspace/config-errors/.gitignore b/test/e2e/content-workspace/config-errors/.gitignore index 075b2542a..9141b5b23 100644 --- a/test/e2e/content-workspace/config-errors/.gitignore +++ b/test/e2e/content-workspace/config-errors/.gitignore @@ -1 +1,2 @@ /.quarto/ +!.posit diff --git a/test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-4QPM.toml b/test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-4QPM.toml new file mode 100644 index 000000000..d1dc99468 --- /dev/null +++ b/test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-4QPM.toml @@ -0,0 +1,43 @@ +# This file is automatically generated by Posit Publisher; do not edit. +'$schema' = 'https://cdn.posit.co/publisher/schemas/posit-publishing-record-schema-v3.json' +server_type = 'connect' +server_url = 'http://localhost:3939' +client_version = '1.10.0-21-gf65d45573' +created_at = '2025-01-22T10:45:45-08:00' +dismissed_at = '' +type = 'python-fastapi' +configuration_name = 'fastapi-simple-DHJL' +id = '913f357e-1060-41f6-a619-2515a959250d' +dashboard_url = 'http://localhost:3939/connect/#/apps/913f357e-1060-41f6-a619-2515a959250d' +direct_url = 'http://localhost:3939/content/913f357e-1060-41f6-a619-2515a959250d/' +logs_url = 'http://localhost:3939/connect/#/apps/913f357e-1060-41f6-a619-2515a959250d/logs' +deployed_at = '2025-02-10T12:27:54-08:00' +bundle_id = '402' +bundle_url = 'http://localhost:3939/__api__/v1/content/913f357e-1060-41f6-a619-2515a959250d/bundles/402/download' +files = [ + '.posit/publish/deployments/deployment-4QPM.toml', + '.posit/publish/fastapi-simple-DHJL.toml', + 'requirements.txt', + 'simple.py' +] +requirements = [ + 'fastapi' +] + +[configuration] +'$schema' = 'https://cdn.posit.co/publisher/schemas/posit-publishing-schema-v3.json' +type = 'python-fastapi' +entrypoint = 'simple.py' +validate = true +files = [ + '/simple.py', + '/requirements.txt', + '/.posit/publish/fastapi-simple-DHJL.toml', + '/.posit/publish/deployments/deployment-4QPM.toml' +] +title = 'fastapi-simple' + +[configuration.python] +version = '3.11.9' +package_file = 'requirements.txt' +package_manager = 'pip' diff --git a/test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-P869.toml b/test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-P869.toml new file mode 100644 index 000000000..4f0ab923b --- /dev/null +++ b/test/e2e/content-workspace/config-errors/.posit/publish/deployments/deployment-P869.toml @@ -0,0 +1,278 @@ +# This file is automatically generated by Posit Publisher; do not edit. +'$schema' = 'https://cdn.posit.co/publisher/schemas/posit-publishing-record-schema-v3.json' +server_type = 'connect' +server_url = 'http://localhost:3939' +client_version = '' +created_at = '2025-01-17T11:27:38-08:00' +type = 'quarto-static' +configuration_name = 'quarto-project-8G2B' +id = '4e230f05-c52f-4f04-8a06-98ddc6a93cea' +dashboard_url = 'http://localhost:3939/connect/#/apps/4e230f05-c52f-4f04-8a06-98ddc6a93cea' +direct_url = 'http://localhost:3939/content/4e230f05-c52f-4f04-8a06-98ddc6a93cea/' +logs_url = 'http://localhost:3939/connect/#/apps/4e230f05-c52f-4f04-8a06-98ddc6a93cea/logs' +deployed_at = '2025-01-17T16:16:48-08:00' +bundle_id = '175' +bundle_url = 'http://localhost:3939/__api__/v1/content/4e230f05-c52f-4f04-8a06-98ddc6a93cea/bundles/175/download' +files = [ + '_quarto.yml', + 'quarto-project.qmd', + 'requirements.txt' +] + +[deployment_error] +code = 'requirementsFileReadingError' +message = 'Missing dependency file requirements.txt. This file must be included in the deployment.' +operation = 'publish/checkCapabilities' + +[deployment_error.data] +RequirementsFile = '/Users/billsager/dev/publishing-client/test/sample-content/quarto-project/requirements.txt' + +[configuration] +'$schema' = 'https://cdn.posit.co/publisher/schemas/posit-publishing-schema-v3.json' +type = 'quarto-static' +entrypoint = 'quarto-project.qmd' +validate = true +files = [ + '/quarto-project.qmd', + '/_quarto.yml', + '/', + '/' +] +title = 'quarto-project' + +[configuration.python] +version = '3.8.2' +package_file = 'requirements.txt' +package_manager = 'pip' + +[configuration.r] +version = '4.3.3' +package_file = 'renv.lock' +package_manager = 'renv' + +[configuration.quarto] +version = '1.6.39' +engines = ['jupyter', 'knitr'] + +[renv] +[renv.r] +version = '4.3.3' + +[[renv.r.repositories]] +name = 'CRAN' +url = 'https://cloud.r-project.org' + +[renv.packages] +[renv.packages.R6] +package = 'R6' +version = '2.5.1' +source = 'Repository' +repository = 'CRAN' +requirements = ['R'] +hash = '470851b6d5d0ac559e9d01bb352b4021' + +[renv.packages.base64enc] +package = 'base64enc' +version = '0.1-3' +source = 'Repository' +repository = 'CRAN' +requirements = ['R'] +hash = '543776ae6848fde2f48ff3816d0628bc' + +[renv.packages.bslib] +package = 'bslib' +version = '0.8.0' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'base64enc', 'cachem', 'fastmap', 'grDevices', 'htmltools', 'jquerylib', 'jsonlite', 'lifecycle', 'memoise', 'mime', 'rlang', 'sass'] +hash = 'b299c6741ca9746fb227debcb0f9fb6c' + +[renv.packages.cachem] +package = 'cachem' +version = '1.1.0' +source = 'Repository' +repository = 'CRAN' +requirements = ['fastmap', 'rlang'] +hash = 'cd9a672193789068eb5a2aad65a0dedf' + +[renv.packages.cli] +package = 'cli' +version = '3.6.3' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'utils'] +hash = 'b21916dd77a27642b447374a5d30ecf3' + +[renv.packages.digest] +package = 'digest' +version = '0.6.37' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'utils'] +hash = '33698c4b3127fc9f506654607fb73676' + +[renv.packages.evaluate] +package = 'evaluate' +version = '0.23' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'methods'] +hash = 'daf4a1246be12c1fa8c7705a0935c1a0' + +[renv.packages.fastmap] +package = 'fastmap' +version = '1.2.0' +source = 'Repository' +repository = 'CRAN' +hash = 'aa5e1cd11c2d15497494c5292d7ffcc8' + +[renv.packages.fontawesome] +package = 'fontawesome' +version = '0.5.3' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'htmltools', 'rlang'] +hash = 'bd1297f9b5b1fc1372d19e2c4cd82215' + +[renv.packages.fs] +package = 'fs' +version = '1.6.5' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'methods'] +hash = '7f48af39fa27711ea5fbd183b399920d' + +[renv.packages.glue] +package = 'glue' +version = '1.8.0' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'methods'] +hash = '5899f1eaa825580172bb56c08266f37c' + +[renv.packages.highr] +package = 'highr' +version = '0.10' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'xfun'] +hash = '06230136b2d2b9ba5805e1963fa6e890' + +[renv.packages.htmltools] +package = 'htmltools' +version = '0.5.8.1' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'base64enc', 'digest', 'fastmap', 'grDevices', 'rlang', 'utils'] +hash = '81d371a9cc60640e74e4ab6ac46dcedc' + +[renv.packages.jquerylib] +package = 'jquerylib' +version = '0.1.4' +source = 'Repository' +repository = 'CRAN' +requirements = ['htmltools'] +hash = '5aab57a3bd297eee1c1d862735972182' + +[renv.packages.jsonlite] +package = 'jsonlite' +version = '1.8.9' +source = 'Repository' +repository = 'CRAN' +requirements = ['methods'] +hash = '4e993b65c2c3ffbffce7bb3e2c6f832b' + +[renv.packages.knitr] +package = 'knitr' +version = '1.45' +source = 'Repository' +repository = 'RSPM' +requirements = ['R', 'evaluate', 'highr', 'methods', 'tools', 'xfun', 'yaml'] +hash = '1ec462871063897135c1bcbe0fc8f07d' + +[renv.packages.lifecycle] +package = 'lifecycle' +version = '1.0.4' +source = 'Repository' +repository = 'RSPM' +requirements = ['R', 'cli', 'glue', 'rlang'] +hash = 'b8552d117e1b808b09a832f589b79035' + +[renv.packages.memoise] +package = 'memoise' +version = '2.0.1' +source = 'Repository' +repository = 'CRAN' +requirements = ['cachem', 'rlang'] +hash = 'e2817ccf4a065c5d9d7f2cfbe7c1d78c' + +[renv.packages.mime] +package = 'mime' +version = '0.12' +source = 'Repository' +repository = 'CRAN' +requirements = ['tools'] +hash = '18e9c28c1d3ca1560ce30658b22ce104' + +[renv.packages.rappdirs] +package = 'rappdirs' +version = '0.3.3' +source = 'Repository' +repository = 'CRAN' +requirements = ['R'] +hash = '5e3c5dc0b071b21fa128676560dbe94d' + +[renv.packages.renv] +package = 'renv' +version = '1.0.11' +source = 'Repository' +repository = 'CRAN' +requirements = ['utils'] +hash = '47623f66b4e80b3b0587bc5d7b309888' + +[renv.packages.rlang] +package = 'rlang' +version = '1.1.4' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'utils'] +hash = '3eec01f8b1dee337674b2e34ab1f9bc1' + +[renv.packages.rmarkdown] +package = 'rmarkdown' +version = '2.29' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'bslib', 'evaluate', 'fontawesome', 'htmltools', 'jquerylib', 'jsonlite', 'knitr', 'methods', 'tinytex', 'tools', 'utils', 'xfun', 'yaml'] +hash = 'df99277f63d01c34e95e3d2f06a79736' + +[renv.packages.sass] +package = 'sass' +version = '0.4.9' +source = 'Repository' +repository = 'CRAN' +requirements = ['R6', 'fs', 'htmltools', 'rappdirs', 'rlang'] +hash = 'd53dbfddf695303ea4ad66f86e99b95d' + +[renv.packages.tinytex] +package = 'tinytex' +version = '0.54' +source = 'Repository' +repository = 'CRAN' +requirements = ['xfun'] +hash = '3ec7e3ddcacc2d34a9046941222bf94d' + +[renv.packages.xfun] +package = 'xfun' +version = '0.49' +source = 'Repository' +repository = 'CRAN' +requirements = ['R', 'grDevices', 'stats', 'tools'] +hash = '8687398773806cfff9401a2feca96298' + +[renv.packages.yaml] +package = 'yaml' +version = '2.3.10' +source = 'Repository' +repository = 'CRAN' +hash = '51dab85c6c98e50a18d7551e9d49f76c' diff --git a/test/e2e/content-workspace/config-errors/.posit/publish/quarto-project-8G2B.toml b/test/e2e/content-workspace/config-errors/.posit/publish/quarto-project-8G2B.toml new file mode 100644 index 000000000..006a74774 --- /dev/null +++ b/test/e2e/content-workspace/config-errors/.posit/publish/quarto-project-8G2B.toml @@ -0,0 +1,24 @@ +# Configuration file generated by Posit Publisher. +# Please review and modify as needed. See the documentation for more options: +# https://github.com/posit-dev/publisher/blob/main/docs/configuration.md +'$schema' = 'https://cdn.posit.co/publisher/schemas/posit-publishing-schema-v3.json' +type = 'quarto-static' +entrypoint = 'quarto-project.qmd' +validate = true +files = [ + '/quarto-project.qmd', + '/_quarto.yml', + '/requirements.txt', + '/renv.lock', + '/.posit/publish/quarto-project-8G2B.toml', + '/.posit/publish/deployments/deployment-P869.toml' +] +title = 'quarto-project' + +[python] + +[r] + +[quarto] +version = '1.6.39' +engines = ['jupyter']