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
85 changes: 85 additions & 0 deletions .github/workflows/vdsm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: vdsm

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
# Pins JavaScript-action Node runtime to Node 24 across all JS-based
# GitHub Actions in this workflow. Matches the pattern used by the
# other workflows in .github/workflows/ so CI behavior is consistent
# across runners during GitHub's staged Node runtime rollouts.
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
DSM_VERSION: "7.2.2"
# Default admin credentials used inside vdsm setup. NOT secrets — the
# VM is ephemeral and exposed only to the test runner.
VDSM_ADMIN_USER: mcpadmin
VDSM_ADMIN_PASSWORD: "McpTest123!"

jobs:
vdsm:
name: vdsm integration tests
runs-on: ubuntu-24.04
# Non-blocking while the job stabilizes. Promote to required check
# after the job is green on main for several consecutive runs.
continue-on-error: true
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Verify /dev/kvm is available
run: |
ls -la /dev/kvm
sudo chmod 666 /dev/kvm
echo "KVM available"

- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"

- name: Install project with vdsm extras
run: uv sync --extra dev --extra vdsm

- name: Install Playwright Chromium
run: uv run playwright install --with-deps chromium

# Cache key deliberately hashes only the DSM version and the Python
# files that drive image construction. `pyproject.toml` and `uv.lock`
# are NOT in the key because the image content is DSM-determined — a
# testcontainers or playwright dependency upgrade doesn't change the
# bits inside the saved tarball. If that assumption ever changes
# (e.g., a dep upgrade affects bind-mount format or timing), bump the
# cache key manually or add the manifest to hashFiles.
- name: Restore golden image cache
id: cache-golden
uses: actions/cache@v4
with:
path: |
.vdsm/golden/dsm-${{ env.DSM_VERSION }}.tar.gz
.vdsm/golden/dsm-${{ env.DSM_VERSION }}.meta.json
key: vdsm-golden-${{ env.DSM_VERSION }}-${{ hashFiles('scripts/vdsm_setup.py', 'tests/vdsm/setup_dsm.py', 'tests/vdsm/config.py', 'tests/vdsm/golden_image.py', 'tests/vdsm/container.py') }}

- name: Build golden image (cache miss)
if: steps.cache-golden.outputs.cache-hit != 'true'
run: |
uv run python scripts/vdsm_setup.py \
--version "${DSM_VERSION}" \
--admin-user "${VDSM_ADMIN_USER}" \
--admin-password "${VDSM_ADMIN_PASSWORD}" \
--yes

- name: Run vdsm integration tests
run: uv run pytest -m vdsm -v --no-cov --log-cli-level=INFO

- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: vdsm-failure-logs
path: |
.vdsm/storage/**/log/
/tmp/vdsm-*.log
if-no-files-found: ignore
retention-days: 7
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

### Added

- **vdsm CI workflow** (#24) — adds `.github/workflows/vdsm.yml` that runs the 47 vdsm integration tests on every PR using GitHub Actions' `ubuntu-24.04` runner with `/dev/kvm` access. Golden image is cached via `actions/cache@v4`, keyed on DSM version + hash of the setup scripts (`scripts/vdsm_setup.py`, `tests/vdsm/setup_dsm.py`, `tests/vdsm/config.py`, `tests/vdsm/golden_image.py`, `tests/vdsm/container.py`). Cache miss path invokes `scripts/vdsm_setup.py --yes` to build a fresh golden image. The new workflow is independent from `ci.yml` so a vdsm flake never blocks unit-test merges, and starts with `continue-on-error: true` until it has a track record of stability. Also adds a `--yes/-y` flag to `vdsm_setup.py` for non-interactive CI use.

### Fixed

- **Remove `synoshare --setopt` recycle bin enablement** (#24) — reverts the recycle bin enablement added in #23. DSM 7.2.2's `synoshare` CLI has no `--setopt` subcommand, so the call always fails (only hidden locally because the pre-existing golden image was built before the change). The revert is safe because `list_recycle_bin` in production code already handles a disabled recycle bin gracefully (returns a friendly "not enabled" message), which is exactly the path `test_02_list_recycle_bin` exercises. `TestRecycleBin` docstring updated to reflect that the test is tolerant of both states.
- **vdsm 47/47: fix all 5 remaining virtual-dsm test failures** (#23) — fixes the 5 vdsm-specific test failures identified in the #22 handoff. Production code improvements: `get_dir_size` now handles DSM error 599 (task completed before status poll) gracefully instead of crashing, returning a best-effort result; `list_recycle_bin` catches error 408 on the `#recycle` path and returns a friendly "recycle bin not enabled" message instead of raising. Test fixes: `test_get_system_info` makes Temperature assertion conditional on non-virtual hardware; `test_search_keyword_finds_directory` creates a "Bambu Studio" directory via the API (DSM search matches names, not content) and searches from the share root with retries; `test_utilization_before_and_during_load` tolerates DirSize failure since it's only a load generator. Setup fix: `setup_dsm.py` enables recycle bin via `synoshare --setopt` after share creation. New unit test for the error 599 path.

- **vdsm full automation: SSH + synoshare for shared folders** (#22) — completes the vdsm golden image setup by SSH-ing into the DSM guest VM (not the QEMU host container) to run `/usr/syno/sbin/synoshare --add` for proper DSM shared folder registration. Exposes SSH port 22, enables SSH via `SYNO.Core.Terminal` API, creates test data without sudo. Fixes FileStation API error 119 by adding `session` parameter to login. 42/47 vdsm tests pass; 5 remaining failures are virtual-dsm behavioral differences (no temp sensor, background task timing, search indexing, recycle bin config).
Expand Down
15 changes: 12 additions & 3 deletions scripts/vdsm_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,15 @@ def _check_prerequisites() -> None:
confirmation_prompt=True,
help="Admin password for DSM setup wizard.",
)
def setup(dsm_version: str, admin_user: str, admin_password: str) -> None:
@click.option(
"--yes",
"-y",
"assume_yes",
is_flag=True,
default=False,
help="Skip confirmation prompts (e.g. overwrite existing golden image). Required for CI use.",
)
def setup(dsm_version: str, admin_user: str, admin_password: str, assume_yes: bool) -> None:
"""Create a golden image for virtual-dsm testing.

This command starts a virtual-dsm container, waits for you to complete
Expand All @@ -86,8 +94,9 @@ def setup(dsm_version: str, admin_user: str, admin_password: str) -> None:
click.echo(" Docker: OK")

# 2. Check if golden image already exists
if has_golden_image(dsm_version) and not click.confirm(
f"\nGolden image for DSM {dsm_version} already exists. Overwrite?"
if has_golden_image(dsm_version) and not (
assume_yes
or click.confirm(f"\nGolden image for DSM {dsm_version} already exists. Overwrite?")
):
click.echo("Aborted.")
sys.exit(0)
Expand Down
8 changes: 6 additions & 2 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,8 +628,12 @@ async def test_10_verify_deleted(self, nas_client: Any) -> None:
class TestRecycleBin:
"""Test recycle bin listing after delete.

Requires the writable_folder's share to have recycle bin enabled.
Creates a folder, deletes it, checks recycle bin, then cleans up.
Works whether or not the writable_folder's share has recycle bin enabled:
when enabled, the deleted folder shows up in `#recycle`; when disabled,
`list_recycle_bin` returns a friendly "not enabled" message. Both paths
are valid and the test verifies `list_recycle_bin` returns a string in
either case. Creates a folder, deletes it, checks recycle bin, then
cleans up.
"""

_RECYCLE_TEST = "_recycle_bin_test"
Expand Down
15 changes: 6 additions & 9 deletions tests/vdsm/setup_dsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,15 +485,12 @@ def ssh(cmd: str, *, sudo: bool = True) -> tuple[int, str]:
msg = f"synoshare --add {name} failed (rc={rc}): {out}"
raise RuntimeError(msg)

# Enable recycle bin on shares (synoshare --add creates them disabled).
# This is a prerequisite for test_02_list_recycle_bin — failure is fatal.
for name in ["testshare", "writable"]:
rc, out = ssh(f"{_SYNOSHARE} --setopt {name} enable_recycle_bin=yes")
if rc == 0:
print(f" Recycle bin enabled on {name}")
else:
msg = f"synoshare --setopt {name} enable_recycle_bin=yes failed (rc={rc}): {out}"
raise RuntimeError(msg)
# Note: DSM 7.2.2's `synoshare` CLI has no supported command to toggle the
# per-share recycle bin. Shares created via `synoshare --add` have recycle
# bin disabled. This is fine — `list_recycle_bin` in production code
# handles the disabled case gracefully (returns a friendly
# "Recycle bin is not enabled" message), and `test_02_list_recycle_bin`
# exercises exactly that path.

# Verify shares registered
rc, out = ssh(f"{_SYNOSHARE} --enum ALL")
Expand Down
Loading