You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/09/30 12:33:13 UTC

(tooling-trusted-releases) branch arm updated: #1504 - fix e2e tests and create workflow to run them

This is an automated email from the ASF dual-hosted git repository.

alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/arm by this push:
     new 1339a0fa #1504 - fix e2e tests and create workflow to run them
1339a0fa is described below

commit 1339a0fa346273ee5780a040560cde330b103e41
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Wed Sep 30 13:32:15 2026 +0100

    #1504 - fix e2e tests and create workflow to run them
---
 .github/workflows/e2e.yml              | 84 ++++++++++++++++++++++++++++++++++
 atr/docs/running-and-creating-tests.md |  2 +
 atr/svn/__init__.py                    | 10 ++--
 tests/e2e/announce/test_get.py         |  5 +-
 tests/e2e/helpers.py                   |  7 +++
 tests/run-e2e-ci.sh                    | 22 +++++++++
 tests/unit/test_svn_export.py          | 42 +++++++++++++++++
 7 files changed, 166 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 00000000..307fb1a6
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,84 @@
+name: End-to-end tests
+
+# The full e2e suite is too slow to run on every commit, so we run it nightly
+# against main instead. A failure raises a tracking issue rather than blocking
+# anything.
+
+on:
+  schedule:
+    # Nightly at 01:00 UTC.
+    - cron: '0 1 * * *'
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+# One run at a time - they share a compose project on the runner.
+concurrency:
+  group: e2e
+  cancel-in-progress: false
+
+jobs:
+  # Skip the night if this commit already passed, e.g. nothing landed since.
+  dedup:
+    uses: $/.github/workflows/dedup.yml
+    permissions:
+      actions: read
+    with:
+      workflow_file: e2e.yml
+
+  e2e:
+    needs: dedup
+    permissions:
+      contents: read
+      issues: write
+    # Only runs on schedule / dispatch, so always trusted - use our runner.
+    runs-on: self-hosted
+    # Manual dispatch always runs so we can force a re-run.
+    if: needs.dedup.outputs.already_built != 'true' || github.event_name == 'workflow_dispatch'
+    timeout-minutes: 60
+    env:
+      # Keep clear of the compose project the Playwright step in build.yml uses,
+      # as its teardown would otherwise take our containers with it.
+      COMPOSE_PROJECT_NAME: atr-e2e-nightly
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          persist-credentials: false
+
+      - name: Run end-to-end tests
+        run: |
+          sh tests/run-e2e-ci.sh
+
+      - name: Tear down any leftover containers
+        if: always()
+        run: |
+          cd tests && docker compose down -v --remove-orphans
+
+      - name: Open an issue on e2e failure
+        if: failure()
+        env:
+          GH_TOKEN: ${{ github.token }}
+          REPO: ${{ github.repository }}
+          BRANCH: ${{ github.ref_name }}
+          SHA: ${{ github.sha }}
+          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+        run: |
+          set -euo pipefail
+          api="https://api.github.com/repos/${REPO}"
+          title="Nightly e2e tests are failing [${BRANCH}]"
+          existing=$(curl -fsS \
+            -H "Authorization: Bearer ${GH_TOKEN}" \
+            -H "Accept: application/vnd.github+json" \
+            "${api}/issues?state=open&labels=testing&per_page=100")
+          if printf '%s' "$existing" | grep -Fq "\"${title}\""; then
+            # TODO(human): decide what a repeat failure should do.
+            echo "An open e2e failure issue already exists for ${BRANCH}; doing nothing."
+            exit 0
+          fi
+          body="The nightly e2e run failed on ${BRANCH} at commit ${SHA}. Run: ${RUN_URL}"
+          data=$(printf '{"title":"%s","body":"%s","labels":["testing"]}' "$title" "$body")
+          curl -fsS -X POST \
+            -H "Authorization: Bearer ${GH_TOKEN}" \
+            -H "Accept: application/vnd.github+json" \
+            "${api}/issues" -d "$data" >/dev/null
diff --git a/atr/docs/running-and-creating-tests.md b/atr/docs/running-and-creating-tests.md
index 19a818af..f9ddf5a5 100644
--- a/atr/docs/running-and-creating-tests.md
+++ b/atr/docs/running-and-creating-tests.md
@@ -70,6 +70,8 @@ The actual test cases themselves tend to use helpers such as [`go_to_path`](/ref
 
 To run ATR end-to-end (e2e) tests, you must first have an OCI container runtime with Compose functionality, such as Docker or Podman, installed. You will also need a POSIX shell. You can then run `tests/run-e2e.sh` to run the entire e2e test suite.
 
+The e2e suite does not run on every commit. Instead, the `End-to-end tests` workflow runs it nightly against `main`, using `tests/run-e2e-ci.sh` to test the built image rather than the dev container, and opens an issue if it fails. The workflow can also be started manually from the Actions tab.
+
 ### Running unit tests
 
 Unit tests can be run separately from e2e tests:
diff --git a/atr/svn/__init__.py b/atr/svn/__init__.py
index 8a5e48a9..920a4fa5 100644
--- a/atr/svn/__init__.py
+++ b/atr/svn/__init__.py
@@ -261,14 +261,16 @@ async def list_files(url: str, revision: int | None = None) -> list[str]:
     then hand the same revision to svnmucc as its baseline, so the commit fails out of
     date rather than acting on a tree that drifted between the listing and the change.
     """
-    arguments = ["list"]
+    target = url
+    arguments: list[str] = []
     if revision is not None:
         # Peg and operate at the one revision, the same shape export uses
-        arguments.extend(["-r", str(revision), f"{url}@{revision}"])
-    else:
-        arguments.append(url)
+        target = f"{url}@{revision}"
+        arguments.extend(["-r", str(revision)])
     arguments.extend(["--recursive", "--username", ASF_TOOL, "--password-from-stdin", "--non-interactive"])
     output = await _run_svn_command(
+        "list",
+        target,
         *arguments,
         timeout_seconds=LIST_TIMEOUT_SECONDS,
         stdin_bytes=_authentication(url),
diff --git a/tests/e2e/announce/test_get.py b/tests/e2e/announce/test_get.py
index ed76cdf1..d675a2a7 100644
--- a/tests/e2e/announce/test_get.py
+++ b/tests/e2e/announce/test_get.py
@@ -15,10 +15,10 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import pathlib
 import re
 
 import e2e.announce.helpers as helpers  # type: ignore[reportMissingImports]
+import e2e.helpers as root_helpers  # type: ignore[reportMissingImports]
 import pytest
 from playwright.sync_api import Page, expect
 
@@ -58,7 +58,8 @@ def test_finish_refreshes_svn_and_announcement_status(page: Page, published: boo
         '<div id="finish-svn-publish"><div id="finish-svn-publishing">Being published</div></div>'
     )
     page.clock.install()
-    page.add_script_tag(path=pathlib.Path(__file__).parents[3] / "atr/static/js/src/finish-downloads.js")
+    # Fetched from the server, as the e2e image holds the tests but not the ATR source
+    page.add_script_tag(content=root_helpers.static_text(page.request, "js/src/finish-downloads.js"))
     page.evaluate('document.dispatchEvent(new Event("DOMContentLoaded"))')
     button = page.locator("#finish-announce")
     expect(button).to_have_attribute("aria-disabled", "true")
diff --git a/tests/e2e/helpers.py b/tests/e2e/helpers.py
index a39a86a1..4c41ae71 100644
--- a/tests/e2e/helpers.py
+++ b/tests/e2e/helpers.py
@@ -96,6 +96,13 @@ def route_logo(route: Route) -> None:
     route.fulfill(response=response)
 
 
+def static_text(request: APIRequestContext, path: str) -> str:
+    response = request.get(f"{_ATR_BASE_URL}/static/{path}")
+    if not response.ok:
+        raise RuntimeError(f"GET /static/{path} failed with status {response.status}")
+    return response.text()
+
+
 def visit(page: Page, path: str) -> None:
     page.goto(f"{_ATR_BASE_URL}{path}")
     page.wait_for_load_state()
diff --git a/tests/run-e2e-ci.sh b/tests/run-e2e-ci.sh
new file mode 100755
index 00000000..6a52f082
--- /dev/null
+++ b/tests/run-e2e-ci.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+
+cd "$(dirname "$0")"
+
+echo "Building and running ATR e2e tests against the built image..."
+if docker compose up atr e2e --build --abort-on-container-exit --exit-code-from e2e
+then
+  exit_code=0
+else
+  exit_code=$?
+  echo "ERROR: e2e tests failed with exit code ${exit_code}"
+  echo "Server logs:"
+  docker compose cp atr:/opt/atr/state/hypercorn/logs/hypercorn.log - 2>/dev/null \
+    | tar -xO 2>/dev/null \
+    | tail -n 200 || true
+fi
+
+# Clean up
+docker compose down -v
+
+exit "${exit_code}"
diff --git a/tests/unit/test_svn_export.py b/tests/unit/test_svn_export.py
index 439fa6a7..f66e4b75 100644
--- a/tests/unit/test_svn_export.py
+++ b/tests/unit/test_svn_export.py
@@ -136,6 +136,48 @@ async def test_list_files_authenticates(monkeypatch: pytest.MonkeyPatch) -> None
     )
 
 
+@requires_svn
+async def test_list_files_pinned_revision_of_deleted_path(
+    tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", "dummy", raising=False)
+    repository = tmp_path / "repository"
+    monkeypatch.setattr(config.get(), "SVN_PUBLISH_URL", f"file://{repository}", raising=False)
+    await svn.run_command("svnadmin", "create", str(repository))
+    url = f"file://{repository}/release"
+    source = tmp_path / "source"
+    source.mkdir()
+    (source / "artifact.tar.gz").write_bytes(b"artifact bytes")
+    await svn.run_command("svn", "import", str(source), url, "--non-interactive", "-m", "Import")
+    await svn.run_command("svn", "rm", url, "--non-interactive", "-m", "Remove")
+
+    assert await svn.list_files(url, 1) == ["artifact.tar.gz"]
+
+
+async def test_list_files_pins_revision(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(config.get(), "SVN_TOKEN", "dummy", raising=False)
+    monkeypatch.setattr(config.get(), "SVN_PUBLISH_URL", "file:///repo", raising=False)
+    run_command = mock.AsyncMock(return_value="a.tar.gz\n")
+    monkeypatch.setattr(svn, "run_command", run_command)
+
+    assert await svn.list_files("file:///repo/example", 42) == ["a.tar.gz"]
+
+    run_command.assert_awaited_once_with(
+        "svn",
+        "list",
+        "-r",
+        "42",
+        "--recursive",
+        "--username",
+        svn.ASF_TOOL,
+        "--password-from-stdin",
+        "--non-interactive",
+        "file:///repo/example@42",
+        timeout_seconds=svn.LIST_TIMEOUT_SECONDS,
+        stdin_bytes=b"dummy",
+    )
+
+
 async def test_list_files_requires_token(monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.setattr(config.get(), "SVN_TOKEN", None, raising=False)
     run_command = mock.AsyncMock(return_value="")


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org