From a3de96405fa3548f740e4fa06a5efd8c1771e68d Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sat, 29 Aug 2026 16:35:41 +0200 Subject: [PATCH] feat: allow to store log on teardown Signed-off-by: Ferdinand Thiessen --- README.md | 23 ++++++++ cypress.config.ts | 3 +- lib/docker.ts | 83 ++++++++++++++++++++++++++- playwright/start-nextcloud-server.mjs | 2 +- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6280d47c..a5d05b34 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,29 @@ export default defineConfig({ }) ``` +## Getting the server log + +The server's `data` directory is mounted as a tmpfs and the container is removed after the run, +so `data/nextcloud.log` is gone once the tests have finished. +To keep it, save it while the container still exists — either by passing `saveLogTo` to `stopNextcloud`, +or by setting the `NEXTCLOUD_E2E_LOG_FILE` environment variable (both take a path on your machine, relative paths are resolved from the current working directory): + +```js +import { stopNextcloud } from '@nextcloud/e2e-test-server' + +// Writes `data/nextcloud.log` to `cypress/logs/nextcloud.log`, then removes the container +await stopNextcloud({ saveLogTo: 'cypress/logs/nextcloud.log' }) +``` + +If you need the log during a run — e.g. to attach it to a failing test — use `getNextcloudLog()`, +which resolves with the log contents, or `saveNextcloudLog(path)` to write it out. + +```js +import { getNextcloudLog } from '@nextcloud/e2e-test-server' + +const log = await getNextcloudLog() +``` + ## Cypress commands You can import individual commands or all at once diff --git a/cypress.config.ts b/cypress.config.ts index 3f3be67a..5251686f 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -40,7 +40,8 @@ export default defineConfig({ // Remove container after run on('after:run', async () => { - await stopNextcloud() + // The data directory is a tmpfs, so grab the server log before the container goes away + await stopNextcloud({ saveLogTo: 'cypress/logs/nextcloud.log' }) await docker.getVolume('apps_writable').remove() }) diff --git a/lib/docker.ts b/lib/docker.ts index 4fc4921c..1762c39f 100644 --- a/lib/docker.ts +++ b/lib/docker.ts @@ -8,9 +8,10 @@ import type { Stream } from 'stream' import Docker from 'dockerode' import { XMLParser } from 'fast-xml-parser' -import { existsSync, readFileSync } from 'fs' -import { basename, join, resolve, sep } from 'path' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' +import { basename, dirname, join, resolve, sep } from 'path' import { PassThrough } from 'stream' +import { pipeline } from 'stream/promises' import tarStreamer from 'tar-stream' import waitOn from 'wait-on' import { User } from './User.ts' @@ -23,6 +24,9 @@ const COMPOSER_PHAR = '/tmp/composer.phar' /** `COMPOSER_HOME` used inside the server container, must be writable by `www-data` */ const COMPOSER_HOME = '/tmp/composer-home' +/** Path of the server log inside the container, it lives on the tmpfs mounted data directory */ +const NEXTCLOUD_LOG = '/var/www/html/data/nextcloud.log' + export const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET ?? '/var/run/docker.sock' }) // Store the container name, different names are used to prevent conflicts when testing multiple apps locally @@ -418,12 +422,85 @@ export async function restoreSnapshot(snapshot = 'init', container?: Container) console.log('└─ Done') } +/** + * Read the server log (`data/nextcloud.log`) from the container. + * + * The data directory is a tmpfs and the container is removed after the run, + * so the log has to be fetched while the container still exists. + * + * @param container Optional server container to use (defaults to current container) + * @return The log contents, or an empty string if the server has not written a log + */ +export async function getNextcloudLog(container?: Container): Promise { + container = container ?? getContainer() + + let archive: NodeJS.ReadableStream + try { + archive = await container.getArchive({ path: NEXTCLOUD_LOG }) + } catch { + // No log written (yet), or the container is already gone + return '' + } + + // `getArchive` always answers with a tar stream, containing the single log entry + const extract = tarStreamer.extract() + const chunks: Buffer[] = [] + extract.on('entry', (_header, stream, next) => { + stream.on('data', (chunk: Buffer) => chunks.push(chunk)) + stream.on('end', next) + }) + await pipeline(archive, extract) + + return Buffer.concat(chunks).toString('utf8') +} + +/** + * Save the server log (`data/nextcloud.log`) from the container to a local file. + * + * Must be called before {@link stopNextcloud}, the log is lost with the container. + * + * @param targetPath Local path to write the log to (default 'nextcloud.log' in the current directory) + * @param container Optional server container to use (defaults to current container) + * @return Whether a log was found and written + */ +export async function saveNextcloudLog(targetPath = 'nextcloud.log', container?: Container): Promise { + const log = await getNextcloudLog(container) + if (log === '') { + console.log('└─ No server log found in the container') + return false + } + + const target = resolve(targetPath) + mkdirSync(dirname(target), { recursive: true }) + writeFileSync(target, log) + console.log(`└─ Server log saved to ${target} 📝`) + return true +} + +interface StopOptions { + /** + * Local path to save the server log (`data/nextcloud.log`) to before the container is removed. + * + * @default process.env.NEXTCLOUD_E2E_LOG_FILE (disabled if unset) + */ + saveLogTo?: string +} + /** * Force stop the testing container + * + * @param options Optional parameters to configure the container removal */ -export async function stopNextcloud() { +export async function stopNextcloud(options: StopOptions = {}) { try { const container = getContainer() + + const logTarget = options.saveLogTo ?? process.env.NEXTCLOUD_E2E_LOG_FILE + if (logTarget) { + console.log('\nSaving Nextcloud server log…') + await saveNextcloudLog(logTarget, container) + } + console.log('Stopping Nextcloud container…') await container.remove({ force: true }) console.log('└─ Nextcloud container removed 🥀') diff --git a/playwright/start-nextcloud-server.mjs b/playwright/start-nextcloud-server.mjs index 41685aab..7bc44fac 100644 --- a/playwright/start-nextcloud-server.mjs +++ b/playwright/start-nextcloud-server.mjs @@ -34,7 +34,7 @@ function getBranch() { await start() // Listen for process to exit (tests done) and shut down the docker container process.on('beforeExit', async () => { - await stopNextcloud() + await stopNextcloud({ saveLogTo: 'playwright-report/nextcloud.log' }) await docker.getVolume('apps_writable').remove() })