Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down
104 changes: 100 additions & 4 deletions lib/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

import type { Container } from 'dockerode'
import type { Stream } from 'stream'
import type { Extract, Pack } from 'tar-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'
Expand All @@ -23,6 +25,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
Expand Down Expand Up @@ -291,7 +296,7 @@ export async function configureNextcloud(apps = ['viewer'], vendoredBranch?: str
const stream = tarStreamer.pack()
stream.entry({ name: 'apps.config.php' }, appsConfig)
stream.finalize()
await container.putArchive(stream, { path: '/var/www/html/config' })
await container.putArchive(asNodeStream(stream), { path: '/var/www/html/config' })

// Build app list, only now that "apps-writable" is a known apps path so that mounted apps show up
const { stdout: json } = await runOcc(['app:list', '--output', 'json'], { container })
Expand Down Expand Up @@ -418,12 +423,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<string> {
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) => chunks.push(chunk as Buffer))
stream.on('end', () => next())
})
await pipeline(archive, asNodeStream(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<boolean> {
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 🥀')
Expand Down Expand Up @@ -693,6 +771,24 @@ export function addUser(user: User, { container, env = [], verbose = false }: Pa
)
}

/**
* Present a `tar-stream` stream as the Node.js stream its consumers expect.
*
* `tar-stream` is typed as the `streamx` streams it is built on. Those behave
* like Node.js streams at runtime, but are not structurally assignable to them, so both
* `dockerode` and `stream.pipeline` need the stream to be cast.
*
* @param stream The pack or extract stream to cast
*/
function asNodeStream(stream: Pack): NodeJS.ReadableStream
function asNodeStream(stream: Extract): NodeJS.WritableStream
/**
* @param stream The pack or extract stream to cast
*/
function asNodeStream(stream: Pack | Extract) {
return stream as unknown as NodeJS.ReadableStream & NodeJS.WritableStream
}

/**
* Pauses execution for a specified number of milliseconds.
*
Expand Down
22 changes: 1 addition & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"@nextcloud/paths": "^3.1.0",
"dockerode": "^5.0.0",
"fast-xml-parser": "^5.2.2",
"tar-stream": "^3.2.0",
"tar-stream": "^3.2.1",
"wait-on": "^9.0.1"
},
"devDependencies": {
Expand All @@ -91,7 +91,6 @@
"@playwright/test": "^1.61.1",
"@types/cypress": "^1.1.6",
"@types/dockerode": "^4.0.1",
"@types/tar-stream": "^3.1.4",
"@types/wait-on": "^5.3.4",
"cypress": "^15.18.0",
"cypress-vite": "^1.10.2",
Expand Down
2 changes: 1 addition & 1 deletion playwright/start-nextcloud-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down
Loading