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