Merge pull request #518 from BCNelson/bcn/playwright-e2e-ci
This commit is contained in:
@@ -246,7 +246,10 @@ jobs:
|
|||||||
- name: Build SPA (Vite -> static-dist/)
|
- name: Build SPA (Vite -> static-dist/)
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
- run: cargo build --release
|
# --features plugins so the e2e Playwright job (which sets
|
||||||
|
# OXICLOUD_ENABLE_PLUGINS) can exercise the admin Plugins tab. The api/webdav
|
||||||
|
# and unit jobs reuse this binary but leave plugins disabled at runtime.
|
||||||
|
- run: cargo build --release --features plugins
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: oxicloud-release
|
name: oxicloud-release
|
||||||
@@ -331,20 +334,23 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
# The release binary serves the SPA from ./static-dist on disk (not
|
# The release binary serves the SPA from ./static-dist on disk (not
|
||||||
# embedded), and the Build job builds it WITHOUT VITE_E2E so it lacks the
|
# embedded). Build the instrumented SPA here with COVERAGE=1 (Istanbul, for
|
||||||
# `data-testid` hooks the specs target. Build it here with VITE_E2E=1 so
|
# the coverage report) and VITE_E2E=1 (keeps the `data-testid` hooks the
|
||||||
# the server actually serves the e2e SPA the scenarios drive.
|
# specs target). start-server-spa.sh points OXICLOUD_STATIC_PATH here.
|
||||||
- name: Build SPA for e2e (VITE_E2E keeps data-testid hooks)
|
- name: Build instrumented SPA for e2e (COVERAGE + VITE_E2E)
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: npm ci && VITE_E2E=1 npm run build
|
run: npm ci && COVERAGE=1 VITE_E2E=1 npm run build
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
working-directory: tests/e2e
|
working-directory: tests/e2e
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
- name: Run Playwright tests (spawns DB via pretest hook)
|
# Drives this PR's SvelteKit SPA specs (tests/e2e/spa) via the coverage
|
||||||
|
# config + start-server-spa.sh. (The legacy `npm test` scenarios targeted
|
||||||
|
# the removed vanilla `static/` frontend and are no longer exercised.)
|
||||||
|
- name: Run SPA e2e coverage suite
|
||||||
working-directory: tests/e2e
|
working-directory: tests/e2e
|
||||||
run: npm test
|
run: npm run test:coverage
|
||||||
env:
|
env:
|
||||||
BUILD_TARGET: release
|
BUILD_TARGET: release
|
||||||
|
|
||||||
|
|||||||
@@ -47,13 +47,20 @@ use oxicloud::application::ports::blob_storage_ports::BlobStorageBackend;
|
|||||||
use oxicloud::infrastructure::services::local_blob_backend::LocalBlobBackend;
|
use oxicloud::infrastructure::services::local_blob_backend::LocalBlobBackend;
|
||||||
|
|
||||||
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_list_usize(key: &str, default: &[usize]) -> Vec<usize> {
|
fn env_list_usize(key: &str, default: &[usize]) -> Vec<usize> {
|
||||||
env::var(key)
|
env::var(key)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|s| s.split(',').filter_map(|x| x.trim().parse().ok()).collect::<Vec<_>>())
|
.map(|s| {
|
||||||
|
s.split(',')
|
||||||
|
.filter_map(|x| x.trim().parse().ok())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
.filter(|v: &Vec<usize>| !v.is_empty())
|
.filter(|v: &Vec<usize>| !v.is_empty())
|
||||||
.unwrap_or_else(|| default.to_vec())
|
.unwrap_or_else(|| default.to_vec())
|
||||||
}
|
}
|
||||||
@@ -130,7 +137,11 @@ async fn run_once(
|
|||||||
// Coarse token-bucket: only sleep once the accumulated owed time clears a
|
// Coarse token-bucket: only sleep once the accumulated owed time clears a
|
||||||
// 2 ms floor, so the throttle models a rate-limited socket without drowning
|
// 2 ms floor, so the throttle models a rate-limited socket without drowning
|
||||||
// the measurement in sub-ms timer noise.
|
// the measurement in sub-ms timer noise.
|
||||||
let per_byte_secs = if throttle_bps > 0.0 { 1.0 / throttle_bps } else { 0.0 };
|
let per_byte_secs = if throttle_bps > 0.0 {
|
||||||
|
1.0 / throttle_bps
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
let mut owed = Duration::ZERO;
|
let mut owed = Duration::ZERO;
|
||||||
|
|
||||||
while let Some(item) = byte_stream.next().await {
|
while let Some(item) = byte_stream.next().await {
|
||||||
@@ -196,15 +207,18 @@ async fn main() {
|
|||||||
"# production LocalBlobBackend.read_prefetch() = {}",
|
"# production LocalBlobBackend.read_prefetch() = {}",
|
||||||
backend.read_prefetch()
|
backend.read_prefetch()
|
||||||
);
|
);
|
||||||
println!("# reps/cell: {reps} (median MB/s reported) cold-cache: {}", {
|
println!(
|
||||||
if !COLD_SUPPORTED {
|
"# reps/cell: {reps} (median MB/s reported) cold-cache: {}",
|
||||||
"unsupported (non-Linux) → warm only"
|
{
|
||||||
} else if want_cold {
|
if !COLD_SUPPORTED {
|
||||||
"yes (posix_fadvise DONTNEED, best-effort)"
|
"unsupported (non-Linux) → warm only"
|
||||||
} else {
|
} else if want_cold {
|
||||||
"disabled (BENCH_COLD=0)"
|
"yes (posix_fadvise DONTNEED, best-effort)"
|
||||||
|
} else {
|
||||||
|
"disabled (BENCH_COLD=0)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
println!("# N=1 is current production ('antes'); higher N is the candidate ('después')");
|
println!("# N=1 is current production ('antes'); higher N is the candidate ('después')");
|
||||||
println!("############################################################\n");
|
println!("############################################################\n");
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ use oxicloud::infrastructure::services::thumbnail_service::ThumbnailService;
|
|||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
std::env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -181,7 +184,9 @@ fn main() {
|
|||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"# available_parallelism = {} effective_parallelism = {} (= the 'after' permit count)",
|
"# available_parallelism = {} effective_parallelism = {} (= the 'after' permit count)",
|
||||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0),
|
std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(0),
|
||||||
eff
|
eff
|
||||||
);
|
);
|
||||||
println!("# run under `taskset -c 0,1` to model a 2-core quota");
|
println!("# run under `taskset -c 0,1` to model a 2-core quota");
|
||||||
@@ -190,7 +195,10 @@ fn main() {
|
|||||||
"| {:>8} | {:>9} | {:>10} | {:>9} | {:>9} |",
|
"| {:>8} | {:>9} | {:>10} | {:>9} | {:>9} |",
|
||||||
"permits", "renders", "renders/s", "p50 ms", "p99 ms"
|
"permits", "renders", "renders/s", "p50 ms", "p99 ms"
|
||||||
);
|
);
|
||||||
println!("|{:-<10}|{:-<11}|{:-<12}|{:-<11}|{:-<11}|", "", "", "", "", "");
|
println!(
|
||||||
|
"|{:-<10}|{:-<11}|{:-<12}|{:-<11}|{:-<11}|",
|
||||||
|
"", "", "", "", ""
|
||||||
|
);
|
||||||
|
|
||||||
// Warm up (also triggers corpus generation / codec init).
|
// Warm up (also triggers corpus generation / codec init).
|
||||||
let _ = bench_k(&rt, img.clone(), 2, producers, 1);
|
let _ = bench_k(&rt, img.clone(), 2, producers, 1);
|
||||||
@@ -206,7 +214,10 @@ fn main() {
|
|||||||
|
|
||||||
// ── Part B: peak RSS for K concurrent decodes (the real over-permit cost) ──
|
// ── Part B: peak RSS for K concurrent decodes (the real over-permit cost) ──
|
||||||
println!("\n[B] Peak RSS with K concurrent decodes (one wave)\n");
|
println!("\n[B] Peak RSS with K concurrent decodes (one wave)\n");
|
||||||
println!("| {:>8} | {:>14} | {:>12} |", "permits", "peak RSS MiB", "vs effective");
|
println!(
|
||||||
|
"| {:>8} | {:>14} | {:>12} |",
|
||||||
|
"permits", "peak RSS MiB", "vs effective"
|
||||||
|
);
|
||||||
println!("|{:-<10}|{:-<16}|{:-<14}|", "", "", "");
|
println!("|{:-<10}|{:-<16}|{:-<14}|", "", "", "");
|
||||||
let mut eff_rss: Option<u64> = None;
|
let mut eff_rss: Option<u64> = None;
|
||||||
for &k in &k_list {
|
for &k in &k_list {
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ use std::time::{Duration, Instant};
|
|||||||
use oxicloud::common::runtime::{cgroup_cpu_quota, effective_parallelism, runtime_pool_sizes};
|
use oxicloud::common::runtime::{cgroup_cpu_quota, effective_parallelism, runtime_pool_sizes};
|
||||||
|
|
||||||
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||||
env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -204,9 +207,7 @@ fn main() {
|
|||||||
|
|
||||||
// ── Part A ──────────────────────────────────────────────────────────────
|
// ── Part A ──────────────────────────────────────────────────────────────
|
||||||
println!("\n[A] Worker over-subscription under CPU contention");
|
println!("\n[A] Worker over-subscription under CPU contention");
|
||||||
println!(
|
println!(" workload: {concurrency} concurrent requests, {burn_kb} KiB BLAKE3 each, {secs}s");
|
||||||
" workload: {concurrency} concurrent requests, {burn_kb} KiB BLAKE3 each, {secs}s"
|
|
||||||
);
|
|
||||||
println!(" (run under `taskset -c 0,1` to model a 2-core quota)\n");
|
println!(" (run under `taskset -c 0,1` to model a 2-core quota)\n");
|
||||||
println!(
|
println!(
|
||||||
"| {:<26} | {:>8} | {:>10} | {:>8} | {:>8} |",
|
"| {:<26} | {:>8} | {:>10} | {:>8} | {:>8} |",
|
||||||
@@ -227,7 +228,13 @@ fn main() {
|
|||||||
before.2,
|
before.2,
|
||||||
before.3
|
before.3
|
||||||
);
|
);
|
||||||
let after = bench_workers(workers_after, def_max_blocking, concurrency, secs, burn_bytes);
|
let after = bench_workers(
|
||||||
|
workers_after,
|
||||||
|
def_max_blocking,
|
||||||
|
concurrency,
|
||||||
|
secs,
|
||||||
|
burn_bytes,
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"| {:<26} | {:>8.0} | {:>10} | {:>8} | {:>8} |",
|
"| {:<26} | {:>8.0} | {:>10} | {:>8} | {:>8} |",
|
||||||
format!("after: {workers_after} workers"),
|
format!("after: {workers_after} workers"),
|
||||||
@@ -259,7 +266,8 @@ fn main() {
|
|||||||
"| {:<26} | {:>12} | {:>12} |",
|
"| {:<26} | {:>12} | {:>12} |",
|
||||||
"before: 512 (tokio default)", peak_def, "—"
|
"before: 512 (tokio default)", peak_def, "—"
|
||||||
);
|
);
|
||||||
let (peak_cap, _base_cap) = bench_blocking_rss(max_blocking_after, blocking_tasks, alloc_mb, hold_ms);
|
let (peak_cap, _base_cap) =
|
||||||
|
bench_blocking_rss(max_blocking_after, blocking_tasks, alloc_mb, hold_ms);
|
||||||
let saved = peak_def as i64 - peak_cap as i64;
|
let saved = peak_def as i64 - peak_cap as i64;
|
||||||
println!(
|
println!(
|
||||||
"| {:<26} | {:>12} | {:>12} |",
|
"| {:<26} | {:>12} | {:>12} |",
|
||||||
|
|||||||
@@ -699,17 +699,26 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// viewer → URL: when closed from within (X / Esc / backdrop), drop the param
|
// viewer → URL: when the user closes the viewer (X / Esc / backdrop), drop the
|
||||||
// (replaceState, so closing doesn't add a history entry).
|
// `?file=` param (replaceState, so closing doesn't add a history entry). Only
|
||||||
|
// act on a genuine open→closed transition: on a cold deep link the viewer
|
||||||
|
// starts closed *with* the param while the listing is still loading, and
|
||||||
|
// stripping it there would race the URL→viewer effect above and the preview
|
||||||
|
// would never open.
|
||||||
|
let viewerWasOpen = false;
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
const open = viewerOpen;
|
||||||
const hasParam = page.url.searchParams.get('file') !== null;
|
const hasParam = page.url.searchParams.get('file') !== null;
|
||||||
if (!viewerOpen && hasParam) {
|
untrack(() => {
|
||||||
const url = new URL(page.url);
|
if (viewerWasOpen && !open && hasParam) {
|
||||||
url.searchParams.delete('file');
|
const url = new URL(page.url);
|
||||||
// Same-origin URL object (see note above); resolve() can't type it.
|
url.searchParams.delete('file');
|
||||||
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
// Same-origin URL object (see note above); resolve() can't type it.
|
||||||
void goto(url, { keepFocus: true, noScroll: true, replaceState: true });
|
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
||||||
}
|
void goto(url, { keepFocus: true, noScroll: true, replaceState: true });
|
||||||
|
}
|
||||||
|
viewerWasOpen = open;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1607,26 +1616,31 @@
|
|||||||
</ListToolbar>
|
</ListToolbar>
|
||||||
|
|
||||||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||||||
|
<!-- Persistent home link → the root listing (bare /files canonicalizes to
|
||||||
|
the user's drive root). `buildCrumbs` returns only the path folders,
|
||||||
|
so this is the single always-present "go home" affordance. -->
|
||||||
|
<a
|
||||||
|
href={resolve('/files')}
|
||||||
|
class="breadcrumb-item breadcrumb-home breadcrumb-link"
|
||||||
|
title={t('breadcrumb.home', 'Home')}
|
||||||
|
data-testid="files-breadcrumb-home-link"
|
||||||
|
ondragover={(e) => e.dataTransfer?.types.includes(DRAG_TYPE) && e.preventDefault()}
|
||||||
|
ondrop={(e) => session.homeFolderId && onCrumbDrop(e, session.homeFolderId)}
|
||||||
|
>
|
||||||
|
<Icon name={rootIcon} />
|
||||||
|
</a>
|
||||||
{#each crumbs as c, i (c.id)}
|
{#each crumbs as c, i (c.id)}
|
||||||
{#if i > 0}
|
<span class="breadcrumb-separator">></span>
|
||||||
<span class="breadcrumb-separator">></span>
|
|
||||||
{/if}
|
|
||||||
{#if i === crumbs.length - 1}
|
{#if i === crumbs.length - 1}
|
||||||
<span class="breadcrumb-item breadcrumb-current" class:breadcrumb-home={i === 0}>
|
<span class="breadcrumb-item breadcrumb-current">{c.name}</span>
|
||||||
{#if i === 0}<Icon name={rootIcon} />{/if}
|
|
||||||
{c.name}
|
|
||||||
</span>
|
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
href={resolve(`/files/${pathSegments.slice(0, i + 1).join('/')}`)}
|
href={resolve(`/files/${pathSegments.slice(0, i + 1).join('/')}`)}
|
||||||
class="breadcrumb-item breadcrumb-link"
|
class="breadcrumb-item breadcrumb-link"
|
||||||
class:breadcrumb-home={i === 0}
|
data-testid={`files-breadcrumb-${c.id}`}
|
||||||
title={i === 0 ? t('breadcrumb.home', 'Home') : undefined}
|
|
||||||
data-testid={i === 0 ? 'files-breadcrumb-home-link' : `files-breadcrumb-${c.id}`}
|
|
||||||
ondragover={(e) => e.dataTransfer?.types.includes(DRAG_TYPE) && e.preventDefault()}
|
ondragover={(e) => e.dataTransfer?.types.includes(DRAG_TYPE) && e.preventDefault()}
|
||||||
ondrop={(e) => onCrumbDrop(e, c.id)}
|
ondrop={(e) => onCrumbDrop(e, c.id)}
|
||||||
>
|
>
|
||||||
{#if i === 0}<Icon name={rootIcon} />{/if}
|
|
||||||
{c.name}
|
{c.name}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default defineConfig({
|
|||||||
globalTeardown: require.resolve('./global-teardown'),
|
globalTeardown: require.resolve('./global-teardown'),
|
||||||
|
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:8088',
|
baseURL: 'http://127.0.0.1:8088',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
headless: true,
|
headless: true,
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
@@ -50,7 +50,7 @@ export default defineConfig({
|
|||||||
command: process.env.BUILD_TARGET
|
command: process.env.BUILD_TARGET
|
||||||
? `bash "${startScript}" "${workspace}/target/${process.env.BUILD_TARGET}/oxicloud"`
|
? `bash "${startScript}" "${workspace}/target/${process.env.BUILD_TARGET}/oxicloud"`
|
||||||
: `bash "${startScript}" cargo run --features plugins`,
|
: `bash "${startScript}" cargo run --features plugins`,
|
||||||
url: 'http://localhost:8088',
|
url: 'http://127.0.0.1:8088/ready',
|
||||||
timeout: 600_000,
|
timeout: 600_000,
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
cwd: '../..',
|
cwd: '../..',
|
||||||
|
|||||||
@@ -202,9 +202,11 @@ test('breadcrumb navigates back to home', async ({ page }) => {
|
|||||||
const folderName = uniq('Crumb');
|
const folderName = uniq('Crumb');
|
||||||
const folder = await apiCreateFolder(page, folderName);
|
const folder = await apiCreateFolder(page, folderName);
|
||||||
await page.goto(`/files/${folder.id}`);
|
await page.goto(`/files/${folder.id}`);
|
||||||
// Breadcrumb home link returns to the root listing.
|
// Breadcrumb home link leaves the subfolder for the root listing. Bare /files
|
||||||
|
// canonicalizes to the user's drive root, where the just-created folder lives.
|
||||||
await page.getByTestId('files-breadcrumb-home-link').click();
|
await page.getByTestId('files-breadcrumb-home-link').click();
|
||||||
await expect(page).toHaveURL(/\/files\/?$/);
|
await expect(page).not.toHaveURL(new RegExp(folder.id));
|
||||||
|
await expect(page.getByTestId(folderName)).toBeVisible({ timeout: 15_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('open an image in the viewer and use the zoom controls', async ({ page }) => {
|
test('open an image in the viewer and use the zoom controls', async ({ page }) => {
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ export default async function globalSetup() {
|
|||||||
fs.rmSync(nycDir, { recursive: true, force: true });
|
fs.rmSync(nycDir, { recursive: true, force: true });
|
||||||
fs.mkdirSync(nycDir, { recursive: true });
|
fs.mkdirSync(nycDir, { recursive: true });
|
||||||
|
|
||||||
await seedAdmin('http://localhost:8088');
|
await seedAdmin('http://127.0.0.1:8088');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,30 @@ set -euo pipefail
|
|||||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
SPA_STORAGE_PATH="$REPO_ROOT/tests/e2e/storage-spa"
|
SPA_STORAGE_PATH="$REPO_ROOT/tests/e2e/storage-spa"
|
||||||
|
|
||||||
|
# Mirror markers + the server's stdout/stderr to a log file as well as the
|
||||||
|
# console; CI surfaces it via the "Print server startup log" step (Playwright's
|
||||||
|
# own webServer capture isn't always shown there). The final `exec "$@"`
|
||||||
|
# inherits these fds, so the server's output is tee'd while it still replaces
|
||||||
|
# this shell (Playwright tracks the PID for teardown).
|
||||||
|
SERVER_LOG="$REPO_ROOT/tests/e2e/server-startup.log"
|
||||||
|
exec > >(tee "$SERVER_LOG") 2>&1
|
||||||
|
|
||||||
|
mark() { echo "[start-server-spa $(date -u +%H:%M:%S)] $*"; }
|
||||||
|
mark "repo_root=$REPO_ROOT server args: $*"
|
||||||
|
if [[ -n "${1:-}" && "$1" != "cargo" ]]; then
|
||||||
|
ls -la "$1" 2>&1 || mark "WARNING: server binary '$1' not found"
|
||||||
|
fi
|
||||||
|
mark "DATABASE_URL=${DATABASE_URL:-<unset>} PORT=${OXICLOUD_SERVER_PORT:-<unset>} STATIC=${OXICLOUD_STATIC_PATH:-<unset>}"
|
||||||
|
|
||||||
# ensure storage is empty before starting
|
# ensure storage is empty before starting
|
||||||
echo "Wipe $SPA_STORAGE_PATH to ensure clean startup"
|
mark "wiping $SPA_STORAGE_PATH to ensure clean startup"
|
||||||
rm -rf "$SPA_STORAGE_PATH"
|
rm -rf "$SPA_STORAGE_PATH"
|
||||||
mkdir -p "$SPA_STORAGE_PATH"
|
mkdir -p "$SPA_STORAGE_PATH"
|
||||||
|
|
||||||
# Spawn database (idempotent — reuses the running test postgres if present).
|
# Spawn database (idempotent — reuses the running test postgres if present).
|
||||||
|
mark "spawning test database…"
|
||||||
bash "$REPO_ROOT/tests/common/spawn-db.sh"
|
bash "$REPO_ROOT/tests/common/spawn-db.sh"
|
||||||
|
mark "database ready; starting server…"
|
||||||
|
|
||||||
# Replace the shell with the server process so Playwright's PID tracking works.
|
# Replace the shell with the server process so Playwright's PID tracking works.
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user