ci: add isolated ARM proposal test lane
This commit is contained in:
Generated
+880
-1031
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
|||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:mock-parity": "node scripts/mock-rpc-parity.mjs",
|
"test:mock-parity": "node scripts/mock-rpc-parity.mjs",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:mock": "concurrently --raw \"node mock-backend.js\" \"VITE_AIUI_URL=http://localhost:5173 vite\" \"cd ../../AIUI && perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV' -- pnpm dev 2>/dev/null || echo '[AIUI] Not found at ../../AIUI \u2014 chat will show placeholder'\"",
|
"dev:mock": "concurrently --raw \"node mock-backend.js\" \"VITE_AIUI_URL=http://localhost:5173 vite\" \"cd ../../AIUI && perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV' -- pnpm dev 2>/dev/null || echo '[AIUI] Not found at ../../AIUI — chat will show placeholder'\"",
|
||||||
"dev:boot": "VITE_DEV_MODE=boot concurrently --raw \"VITE_DEV_MODE=boot node mock-backend.js\" \"VITE_DEV_MODE=boot vite\"",
|
"dev:boot": "VITE_DEV_MODE=boot concurrently --raw \"VITE_DEV_MODE=boot node mock-backend.js\" \"VITE_DEV_MODE=boot vite\"",
|
||||||
"dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite",
|
"dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite",
|
||||||
"backend:mock": "node mock-backend.js",
|
"backend:mock": "node mock-backend.js",
|
||||||
@@ -51,14 +51,14 @@
|
|||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^5.0.3",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dockerode": "^4.0.9",
|
"dockerode": "^5.0.1",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.2.2",
|
"vite": "^7.2.2",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vitest": "^3.1.1",
|
"vitest": "^5.0.3",
|
||||||
"vue-tsc": "^3.1.3",
|
"vue-tsc": "^3.1.3",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ describe('MeshMap', () => {
|
|||||||
return 7
|
return 7
|
||||||
})
|
})
|
||||||
const clearWatch = vi.fn()
|
const clearWatch = vi.fn()
|
||||||
const resizeObserver = vi.fn(() => ({
|
class ResizeObserverMock {
|
||||||
observe: vi.fn(),
|
observe = vi.fn()
|
||||||
disconnect: vi.fn(),
|
disconnect = vi.fn()
|
||||||
}))
|
}
|
||||||
vi.stubGlobal('navigator', {
|
vi.stubGlobal('navigator', {
|
||||||
geolocation: { watchPosition, clearWatch },
|
geolocation: { watchPosition, clearWatch },
|
||||||
})
|
})
|
||||||
vi.stubGlobal('ResizeObserver', resizeObserver)
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
||||||
|
|
||||||
const wrapper = mount(MeshMap)
|
const wrapper = mount(MeshMap)
|
||||||
|
|
||||||
|
|||||||
@@ -79,11 +79,15 @@ describe('Mesh graphics lifecycle (Task 2): Leaflet map (MeshMap.vue)', () => {
|
|||||||
vi.useFakeTimers()
|
vi.useFakeTimers()
|
||||||
mapInstances.length = 0
|
mapInstances.length = 0
|
||||||
resizeObserverInstances = []
|
resizeObserverInstances = []
|
||||||
vi.stubGlobal('ResizeObserver', vi.fn(() => {
|
vi.stubGlobal('ResizeObserver', class {
|
||||||
const inst = { observe: vi.fn(), disconnect: vi.fn(), unobserve: vi.fn() }
|
observe = vi.fn()
|
||||||
resizeObserverInstances.push(inst)
|
disconnect = vi.fn()
|
||||||
return inst
|
unobserve = vi.fn()
|
||||||
}))
|
|
||||||
|
constructor() {
|
||||||
|
resizeObserverInstances.push(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
|
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
|
||||||
height: 200, width: 200, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, toJSON: () => undefined,
|
height: 200, width: 200, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, toJSON: () => undefined,
|
||||||
} as DOMRect)
|
} as DOMRect)
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ function createTestComponent(onCloseFn: () => void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('useModalKeyboard', () => {
|
describe('useModalKeyboard', () => {
|
||||||
let closeFn: ReturnType<typeof vi.fn>
|
let closeFn: ReturnType<typeof vi.fn<() => void>>
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
closeFn = vi.fn()
|
closeFn = vi.fn<() => void>()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls onClose when Escape is pressed and modal is open', async () => {
|
it('calls onClose when Escape is pressed and modal is open', async () => {
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ vi.mock('../discover/curatedApps', () => ({
|
|||||||
|
|
||||||
describe('Marketplace tracer tab: background refresh failure (D-07)', () => {
|
describe('Marketplace tracer tab: background refresh failure (D-07)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() })))
|
vi.stubGlobal('ResizeObserver', class {
|
||||||
|
observe = vi.fn()
|
||||||
|
disconnect = vi.fn()
|
||||||
|
})
|
||||||
routerPushMock.mockClear()
|
routerPushMock.mockClear()
|
||||||
toastErrorMock.mockClear()
|
toastErrorMock.mockClear()
|
||||||
toastInfoMock.mockClear()
|
toastInfoMock.mockClear()
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ function setGeometry(
|
|||||||
|
|
||||||
describe('OnboardingSeedGenerate scroll cue (UIFIX-03)', () => {
|
describe('OnboardingSeedGenerate scroll cue (UIFIX-03)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() })))
|
vi.stubGlobal('ResizeObserver', class {
|
||||||
|
observe = vi.fn()
|
||||||
|
disconnect = vi.fn()
|
||||||
|
})
|
||||||
vi.mocked(rpcClient.call).mockReset()
|
vi.mocked(rpcClient.call).mockReset()
|
||||||
Element.prototype.scrollIntoView = vi.fn()
|
Element.prototype.scrollIntoView = vi.fn()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ vi.mock('@/composables/useDemoIntro', () => ({ IS_DEMO: false }))
|
|||||||
// mocked at the module boundary, mirroring MarketplaceRefresh.test.ts's
|
// mocked at the module boundary, mirroring MarketplaceRefresh.test.ts's
|
||||||
// convention for isolating a view from its heavier dependencies.
|
// convention for isolating a view from its heavier dependencies.
|
||||||
vi.mock('@/services/contextBroker', () => ({
|
vi.mock('@/services/contextBroker', () => ({
|
||||||
ContextBroker: vi.fn().mockImplementation(() => ({
|
ContextBroker: class {
|
||||||
start: vi.fn(),
|
start = vi.fn()
|
||||||
stop: vi.fn(),
|
stop = vi.fn()
|
||||||
})),
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/** Mount Chat.vue behind a real <KeepAlive> so onActivated/onDeactivated fire. */
|
/** Mount Chat.vue behind a real <KeepAlive> so onActivated/onDeactivated fire. */
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
|
// Vitest 5's process-fork pool can time out while starting workers on a
|
||||||
|
// busy two-core development node. Threads retain per-file isolation and
|
||||||
|
// avoid that expensive process startup; CI itself remains containerized.
|
||||||
|
pool: 'threads',
|
||||||
|
maxWorkers: 4,
|
||||||
// Vitest's 5s default is not a statement about these tests — the whole
|
// Vitest's 5s default is not a statement about these tests — the whole
|
||||||
// 1000-test suite runs in ~70s on an idle box. It is a statement about
|
// 1000-test suite runs in ~70s on an idle box. It is a statement about
|
||||||
// the machine. This one also runs a live node, so a release gate can
|
// the machine. This one also runs a live node, so a release gate can
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { config } from '@vue/test-utils'
|
import { config } from '@vue/test-utils'
|
||||||
|
import { vi } from 'vitest'
|
||||||
import { displayVersion } from '@/utils/version'
|
import { displayVersion } from '@/utils/version'
|
||||||
|
|
||||||
// The app registers `$ver` as a global template property in main.ts
|
// The app registers `$ver` as a global template property in main.ts
|
||||||
@@ -7,3 +8,9 @@ import { displayVersion } from '@/utils/version'
|
|||||||
// component. Per-mount `global` options merge with this, so individual tests
|
// component. Per-mount `global` options merge with this, so individual tests
|
||||||
// keep their own plugins/mocks.
|
// keep their own plugins/mocks.
|
||||||
config.global.mocks = { ...(config.global.mocks ?? {}), $ver: displayVersion }
|
config.global.mocks = { ...(config.global.mocks ?? {}), $ver: displayVersion }
|
||||||
|
|
||||||
|
// jsdom intentionally leaves media playback methods unimplemented and logs a
|
||||||
|
// stack trace for every cleanup call. Components only need inert lifecycle
|
||||||
|
// behavior in unit tests; media-specific suites can still replace these spies.
|
||||||
|
HTMLMediaElement.prototype.pause = vi.fn()
|
||||||
|
HTMLMediaElement.prototype.load = vi.fn()
|
||||||
|
|||||||
@@ -2,19 +2,78 @@
|
|||||||
# Compile normally; execute unit tests away from real wallets, service buses,
|
# Compile normally; execute unit tests away from real wallets, service buses,
|
||||||
# container storage, processes and networking. Never silently fall back to host.
|
# container storage, processes and networking. Never silently fall back to host.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
SCRIPT_REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
command -v systemd-run >/dev/null
|
REPO=${ARCHY_TEST_REPO:-$SCRIPT_REPO}
|
||||||
command -v unshare >/dev/null
|
REPO=$(cd "$REPO" && pwd)
|
||||||
command -v setpriv >/dev/null
|
|
||||||
sudo -n true || { echo 'Isolated backend tests require noninteractive sudo for systemd namespaces.' >&2; exit 1; }
|
|
||||||
metadata=$(mktemp)
|
|
||||||
trap 'rm -f "$metadata"' EXIT
|
|
||||||
case "${ARCHY_TEST_PACKAGE:-archipelago}" in
|
case "${ARCHY_TEST_PACKAGE:-archipelago}" in
|
||||||
archipelago) test_target=(-p archipelago --bin archipelago) ;;
|
archipelago) test_target=(-p archipelago --bin archipelago) ;;
|
||||||
archipelago-publishing-tests) test_target=(-p archipelago-publishing-tests --lib) ;;
|
archipelago-publishing-tests) test_target=(-p archipelago-publishing-tests --lib) ;;
|
||||||
archipelago-container) test_target=(-p archipelago-container --lib) ;;
|
archipelago-container) test_target=(-p archipelago-container --lib) ;;
|
||||||
*) echo 'Unsupported isolated test package' >&2; exit 2 ;;
|
*) echo 'Unsupported isolated test package' >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ ${ARCHY_TEST_ISOLATOR:-systemd} == podman ]]; then
|
||||||
|
command -v podman >/dev/null
|
||||||
|
image=${ARCHY_TEST_IMAGE:?ARCHY_TEST_IMAGE is required for podman isolation}
|
||||||
|
cargo_home=${ARCHY_TEST_CARGO_HOME:?ARCHY_TEST_CARGO_HOME is required for podman isolation}
|
||||||
|
cargo_home=$(mkdir -p "$cargo_home" && cd "$cargo_home" && pwd)
|
||||||
|
artifacts=$(mktemp -d)
|
||||||
|
trap 'rm -rf -- "$artifacts"' EXIT
|
||||||
|
|
||||||
|
podman run --rm \
|
||||||
|
--cpus="${ARCHY_TEST_CPUS:-4}" --memory="${ARCHY_TEST_MEMORY:-4g}" --pids-limit=2048 \
|
||||||
|
--cap-drop=all --security-opt=no-new-privileges --read-only \
|
||||||
|
--tmpfs /tmp:rw,size=512m --tmpfs /root:rw,size=512m \
|
||||||
|
--network=pasta --env CARGO_HOME=/cargo-home \
|
||||||
|
--volume "$cargo_home:/cargo-home:rw,Z" \
|
||||||
|
--volume "$REPO:/workspace:rw,Z" --volume "$artifacts:/artifacts:rw,Z" \
|
||||||
|
--workdir /workspace \
|
||||||
|
"$image" \
|
||||||
|
cargo test --manifest-path core/Cargo.toml "${test_target[@]}" \
|
||||||
|
--locked --no-run --message-format=json \
|
||||||
|
--config 'profile.test.package.archipelago.opt-level=0' \
|
||||||
|
--config 'profile.test.package.archipelago.debug=0' \
|
||||||
|
>"$artifacts/metadata"
|
||||||
|
|
||||||
|
executable=$(python3 - "$artifacts/metadata" <<'PY'
|
||||||
|
import json,sys
|
||||||
|
found=[]
|
||||||
|
for line in open(sys.argv[1]):
|
||||||
|
try: item=json.loads(line)
|
||||||
|
except json.JSONDecodeError: continue
|
||||||
|
if item.get('reason')=='compiler-artifact' and item.get('profile',{}).get('test') and item.get('executable'):
|
||||||
|
found.append(item['executable'])
|
||||||
|
assert len(found)==1, f'Expected one unit test executable, got {len(found)}'
|
||||||
|
print(found[0])
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
case "$executable" in
|
||||||
|
/workspace/*) ;;
|
||||||
|
*) echo 'Compiled test executable escaped the workspace' >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
podman run --rm \
|
||||||
|
--cpus="${ARCHY_TEST_CPUS:-4}" --memory="${ARCHY_TEST_MEMORY:-4g}" --pids-limit=1024 \
|
||||||
|
--cap-drop=all --security-opt=no-new-privileges --read-only \
|
||||||
|
--tmpfs /tmp:rw,size=512m --tmpfs /run:rw,size=64m \
|
||||||
|
--tmpfs /var/lib/archipelago:rw,size=256m --tmpfs /var/lib/containers:rw,size=256m \
|
||||||
|
--tmpfs /root:rw,size=64m --network=none \
|
||||||
|
--volume "$REPO:/workspace:ro,Z" --workdir /workspace/core \
|
||||||
|
--env ARCHY_TEST_ISOLATED=1 \
|
||||||
|
"$image" "$executable" --test-threads="${ARCHY_TEST_THREADS:-4}" "$@"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ ${ARCHY_TEST_ISOLATOR:-systemd} == systemd ]] || {
|
||||||
|
echo 'ARCHY_TEST_ISOLATOR must be systemd or podman' >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
command -v systemd-run >/dev/null
|
||||||
|
command -v unshare >/dev/null
|
||||||
|
command -v setpriv >/dev/null
|
||||||
|
sudo -n true || { echo 'Isolated backend tests require noninteractive sudo for systemd namespaces.' >&2; exit 1; }
|
||||||
|
metadata=$(mktemp)
|
||||||
|
trap 'rm -f "$metadata"' EXIT
|
||||||
if ! cargo test --manifest-path "$REPO/core/Cargo.toml" "${test_target[@]}" \
|
if ! cargo test --manifest-path "$REPO/core/Cargo.toml" "${test_target[@]}" \
|
||||||
--locked --no-run --message-format=json --config 'profile.test.package.archipelago.opt-level=0' --config 'profile.test.package.archipelago.debug=0' > "$metadata"; then
|
--locked --no-run --message-format=json --config 'profile.test.package.archipelago.opt-level=0' --config 'profile.test.package.archipelago.debug=0' > "$metadata"; then
|
||||||
python3 - "$metadata" <<'PYDIAG'
|
python3 - "$metadata" <<'PYDIAG'
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
RUNNER = ROOT / "scripts" / "test-backend-isolated.sh"
|
||||||
|
|
||||||
|
|
||||||
|
class BackendIsolationRunnerTests(unittest.TestCase):
|
||||||
|
def test_podman_mode_separates_build_and_networkless_execution(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
temp = pathlib.Path(temporary)
|
||||||
|
fake_bin = temp / "bin"
|
||||||
|
fake_bin.mkdir()
|
||||||
|
log = temp / "podman.log"
|
||||||
|
podman = fake_bin / "podman"
|
||||||
|
podman.write_text(
|
||||||
|
"#!/usr/bin/env bash\n"
|
||||||
|
"printf '%s\\0' \"$@\" >> \"$PODMAN_TEST_LOG\"\n"
|
||||||
|
"printf '\\n' >> \"$PODMAN_TEST_LOG\"\n"
|
||||||
|
"if [[ \" $* \" == *' cargo test '* ]]; then\n"
|
||||||
|
" printf '%s\\n' '{\"reason\":\"compiler-artifact\",\"profile\":{\"test\":true},\"executable\":\"/workspace/core/target/debug/archy-test\"}'\n"
|
||||||
|
"fi\n"
|
||||||
|
)
|
||||||
|
podman.chmod(0o700)
|
||||||
|
cargo_home = temp / "cargo"
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(
|
||||||
|
{
|
||||||
|
"PATH": f"{fake_bin}:{env['PATH']}",
|
||||||
|
"PODMAN_TEST_LOG": str(log),
|
||||||
|
"ARCHY_TEST_ISOLATOR": "podman",
|
||||||
|
"ARCHY_TEST_REPO": str(ROOT),
|
||||||
|
"ARCHY_TEST_IMAGE": "example.invalid/ci@sha256:test",
|
||||||
|
"ARCHY_TEST_CARGO_HOME": str(cargo_home),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
subprocess.run([str(RUNNER)], env=env, check=True)
|
||||||
|
invocations = [
|
||||||
|
line.replace("\0", " ") for line in log.read_text().splitlines()
|
||||||
|
]
|
||||||
|
self.assertEqual(len(invocations), 2)
|
||||||
|
self.assertIn("--network=pasta", invocations[0])
|
||||||
|
self.assertIn(f"{ROOT}:/workspace:rw,Z", invocations[0])
|
||||||
|
self.assertIn("--network=none", invocations[1])
|
||||||
|
self.assertIn(f"{ROOT}:/workspace:ro,Z", invocations[1])
|
||||||
|
self.assertIn("ARCHY_TEST_ISOLATED=1", invocations[1])
|
||||||
|
|
||||||
|
def test_unknown_isolator_fails_closed(self):
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(RUNNER)],
|
||||||
|
env={**os.environ, "ARCHY_TEST_ISOLATOR": "unknown"},
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 2)
|
||||||
|
self.assertIn("must be systemd or podman", result.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user