diff --git a/circle.yml b/circle.yml index 70d1d365a5ab..8c9651acd944 100644 --- a/circle.yml +++ b/circle.yml @@ -50,7 +50,7 @@ executors: # Docker image with non-root "node" user non-root-docker-user: docker: - - image: cypress/base:12.0.0 + - image: cypress/browsers:node12.13.0-chrome80-ff74 user: node environment: PLATFORM: linux @@ -122,7 +122,7 @@ commands: type: string chunk: description: e2e test chunk number - type: integer + type: string steps: - attach_workspace: at: ~/ @@ -410,168 +410,174 @@ jobs: steps: - run-e2e-tests: browser: chrome - chunk: 1 + chunk: "1" "server-e2e-tests-chrome-2": <<: *defaults steps: - run-e2e-tests: browser: chrome - chunk: 2 + chunk: "2" "server-e2e-tests-chrome-3": <<: *defaults steps: - run-e2e-tests: browser: chrome - chunk: 3 + chunk: "3" "server-e2e-tests-chrome-4": <<: *defaults steps: - run-e2e-tests: browser: chrome - chunk: 4 + chunk: "4" "server-e2e-tests-chrome-5": <<: *defaults steps: - run-e2e-tests: browser: chrome - chunk: 5 + chunk: "5" "server-e2e-tests-chrome-6": <<: *defaults steps: - run-e2e-tests: browser: chrome - chunk: 6 + chunk: "6" "server-e2e-tests-chrome-7": <<: *defaults steps: - run-e2e-tests: browser: chrome - chunk: 7 + chunk: "7" "server-e2e-tests-chrome-8": <<: *defaults steps: - run-e2e-tests: browser: chrome - chunk: 8 + chunk: "8" "server-e2e-tests-electron-1": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 1 + chunk: "1" "server-e2e-tests-electron-2": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 2 + chunk: "2" "server-e2e-tests-electron-3": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 3 + chunk: "3" "server-e2e-tests-electron-4": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 4 + chunk: "4" "server-e2e-tests-electron-5": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 5 + chunk: "5" "server-e2e-tests-electron-6": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 6 + chunk: "6" "server-e2e-tests-electron-7": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 7 + chunk: "7" "server-e2e-tests-electron-8": <<: *defaults steps: - run-e2e-tests: browser: electron - chunk: 8 + chunk: "8" + + "server-e2e-tests-non-root": + <<: *defaults + steps: + - run-e2e-tests: + chunk: non_root "server-e2e-tests-firefox-1": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 1 + chunk: "1" "server-e2e-tests-firefox-2": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 2 + chunk: "2" "server-e2e-tests-firefox-3": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 3 + chunk: "3" "server-e2e-tests-firefox-4": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 4 + chunk: "4" "server-e2e-tests-firefox-5": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 5 + chunk: "5" "server-e2e-tests-firefox-6": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 6 + chunk: "6" "server-e2e-tests-firefox-7": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 7 + chunk: "7" "server-e2e-tests-firefox-8": <<: *defaults steps: - run-e2e-tests: browser: firefox - chunk: 8 + chunk: "8" "driver-integration-tests-chrome": <<: *defaults @@ -1232,6 +1238,10 @@ linux-workflow: &linux-workflow - server-e2e-tests-electron-8: requires: - build + - server-e2e-tests-non-root: + executor: non-root-docker-user + requires: + - build - server-e2e-tests-firefox-1: requires: - build diff --git a/packages/server/__snapshots__/non_root_read_only_fs_spec.ts.js b/packages/server/__snapshots__/non_root_read_only_fs_spec.ts.js new file mode 100644 index 000000000000..785a6fd7873d --- /dev/null +++ b/packages/server/__snapshots__/non_root_read_only_fs_spec.ts.js @@ -0,0 +1,82 @@ +exports['e2e readonly fs / warns when unable to write to disk'] = ` +Folder /foo/bar/.projects/read-only-project-root is not writable. + +Writing to this directory is required by Cypress in order to store screenshots and videos. + +Enable write permissions to this directory to ensure screenshots and videos are stored. + +If you don't require screenshots or videos to be stored you can safely ignore this warning. +✅ not running as root +✅ /foo/bar/.projects/read-only-project-root is not writable + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (spec.js) │ + │ Searched: cypress/integration/spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: spec.js (1 of 1) +Warning: We failed to record the video. + +This error will not alter the exit code. + +Error: EACCES: permission denied, mkdir '/foo/bar/.projects/read-only-project-root/cypress/videos' + + + 1) fails + + 0 passing + 1 failing + + 1) fails: + Error: EACCES: permission denied, mkdir '/foo/bar/.projects/read-only-project-root/cypress/screenshots' + + + + + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + +Warning: We failed processing this video. + +This error will not alter the exit code. + +Error: ffmpeg exited with code 1: /foo/bar/.projects/read-only-project-root/cypress/videos/spec.js.mp4: No such file or directory + + [stack trace lines] + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ spec.js XX:XX 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✖ 1 of 1 failed (100%) XX:XX 1 - 1 - - + + +` diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee index a43a2f012aae..f92b4264902e 100644 --- a/packages/server/lib/errors.coffee +++ b/packages/server/lib/errors.coffee @@ -944,6 +944,16 @@ getMsgByType = (type, arg1 = {}, arg2, arg3) -> To avoid this error, ensure that there are no other instances of Firefox launched by Cypress running. """ + when "FOLDER_NOT_WRITABLE" + """ + Folder #{arg1} is not writable. + + Writing to this directory is required by Cypress in order to store screenshots and videos. + + Enable write permissions to this directory to ensure screenshots and videos are stored. + + If you don't require screenshots or videos to be stored you can safely ignore this warning. + """ get = (type, arg1, arg2, arg3) -> msg = getMsgByType(type, arg1, arg2, arg3) diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js index cb82208f0cda..d59c89c870eb 100644 --- a/packages/server/lib/modes/run.js +++ b/packages/server/lib/modes/run.js @@ -645,17 +645,18 @@ const trashAssets = Promise.method((config = {}) => { const createVideoRecording = function (videoName, options = {}) { const outputDir = path.dirname(videoName) + const onError = _.once((err) => { + // catch video recording failures and log them out + // but don't let this affect the run at all + return errors.warning('VIDEO_RECORDING_FAILED', err.stack) + }) + return fs .ensureDirAsync(outputDir) + .catch(onError) .then(() => { return videoCapture - .start(videoName, _.extend({}, options, { - onError (err) { - // catch video recording failures and log them out - // but don't let this affect the run at all - return errors.warning('VIDEO_RECORDING_FAILED', err.stack) - }, - })) + .start(videoName, _.extend({}, options, { onError })) }) } diff --git a/packages/server/lib/util/settings.js b/packages/server/lib/util/settings.js index 937f8513f0ab..5ad7537c7854 100644 --- a/packages/server/lib/util/settings.js +++ b/packages/server/lib/util/settings.js @@ -121,12 +121,14 @@ module.exports = { log('cannot find file %s', file) return this._err('CONFIG_FILE_NOT_FOUND', this.configFile(options), projectRoot) + }).catch({ code: 'EACCES' }, () => { + // we cannot write due to folder permissions + return errors.warning('FOLDER_NOT_WRITABLE', projectRoot) }).catch((err) => { if (errors.isCypressErr(err)) { throw err } - // else we cannot read due to folder permissions return this._logReadErr(file, err) }) }, diff --git a/packages/server/test/e2e/non_root_read_only_fs_spec.ts b/packages/server/test/e2e/non_root_read_only_fs_spec.ts new file mode 100644 index 000000000000..3af10a47cf3f --- /dev/null +++ b/packages/server/test/e2e/non_root_read_only_fs_spec.ts @@ -0,0 +1,37 @@ +import * as fs from 'fs' +import * as path from 'path' +const e2e = require('../support/helpers/e2e') +const Fixtures = require('../support/helpers/fixtures') + +describe('e2e readonly fs', function () { + e2e.setup() + + const projectPath = Fixtures.projectPath('read-only-project-root') + + const chmodr = (p: string, mode: number) => { + const stats = fs.statSync(p) + + fs.chmodSync(p, mode) + if (stats.isDirectory()) { + fs.readdirSync(p).forEach((child) => { + chmodr(path.join(p, child), mode) + }) + } + } + + const onRun = (exec) => { + chmodr(projectPath, 0o500) + + return exec().finally(() => { + chmodr(projectPath, 0o777) + }) + } + + e2e.it('warns when unable to write to disk', { + project: projectPath, + expectedExitCode: 1, + spec: 'spec.js', + snapshot: true, + onRun, + }) +}) diff --git a/packages/server/test/support/fixtures/projects/read-only-project-root/cypress.json b/packages/server/test/support/fixtures/projects/read-only-project-root/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/read-only-project-root/cypress.json @@ -0,0 +1 @@ +{} diff --git a/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/integration/spec.js b/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/integration/spec.js new file mode 100644 index 000000000000..faa4943b745b --- /dev/null +++ b/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/integration/spec.js @@ -0,0 +1,3 @@ +it('fails', () => { + throw new Error('foo') +}) diff --git a/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/plugins/index.js new file mode 100644 index 000000000000..9ac868c37adb --- /dev/null +++ b/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/plugins/index.js @@ -0,0 +1,20 @@ +/* eslint-disable no-console */ +const fs = require('fs') +const { expect } = require('chai') + +module.exports = (on, config) => { + expect(process.geteuid()).to.not.eq(0) + console.log('✅ not running as root') + + let err + + try { + fs.accessSync(config.projectRoot, fs.constants.W_OK) + } catch (e) { + err = e + } + + expect(err).to.include({ code: 'EACCES' }) + + console.log(`✅ ${config.projectRoot} is not writable`) +} diff --git a/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/support.js b/packages/server/test/support/fixtures/projects/read-only-project-root/cypress/support.js new file mode 100644 index 000000000000..e69de29bb2d1