This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new 8a7a73e Add a test for uploading a new release candidate draft via
rsync
8a7a73e is described below
commit 8a7a73e36ede10cf5f5e64bdaefa2499fbcbab5a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Apr 14 19:15:15 2025 +0100
Add a test for uploading a new release candidate draft via rsync
---
Makefile | 5 +-
.../apache-tooling-test-example-0.2.tar.gz | Bin 0 -> 4416 bytes
.../apache-tooling-test-example-0.2.tar.gz.sha512 | 1 +
playwright/test.py | 166 +++++++++++++++------
4 files changed, 127 insertions(+), 45 deletions(-)
diff --git a/Makefile b/Makefile
index 061db2a..588d38e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
.PHONY: build build-alpine build-playwright build-ubuntu certs check \
docs generate-version obvfix report run run-dev run-playwright \
- run-staging stop serve sync sync-dev
+ run-playwright-slow run-staging stop serve sync sync-dev
BIND ?= 127.0.0.1:8080
IMAGE ?= tooling-trusted-release
@@ -57,6 +57,9 @@ run-dev:
run-playwright:
docker run --net=host -it atr-playwright python3 test.py --skip-slow
+run-playwright-slow:
+ docker run --net=host -it atr-playwright python3 test.py
+
run-staging:
BIND=127.0.0.1:8443 scripts/run
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
new file mode 100644
index 0000000..1c1ea36
Binary files /dev/null and
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
differ
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
new file mode 100644
index 0000000..0d246e2
--- /dev/null
+++
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
@@ -0,0 +1 @@
+7262935d3127abe6f3fda6457429e41fa68077537c7bcbb4c4c1f7a6642f8adb960bfce4de3236fde93ece1fd77ace49f42c5adc2a636820a4cd4934fe576f58
apache-tooling-test-example-0.2.tar.gz
diff --git a/playwright/test.py b/playwright/test.py
index da9917b..8d3a36c 100644
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -43,6 +43,13 @@ class Credentials:
password: str
+# If we did this then we'd have to call e.g. test.page, which is verbose
+# @dataclasses.dataclass
+# class TestArguments:
+# page: sync_api.Page
+# credentials: Credentials
+
+
def get_credentials() -> Credentials | None:
try:
username = input("Enter ASF Username: ")
@@ -112,6 +119,36 @@ def main() -> None:
run_tests(args.skip_slow)
+def release_remove(page: sync_api.Page, release_name: str) -> None:
+ logging.info(f"Checking whether the {release_name} release exists")
+ release_checkbox_locator =
page.locator(f'input[name="releases_to_delete"][value="{release_name}"]')
+
+ if release_checkbox_locator.is_visible():
+ logging.info(f"Found the {release_name} release, proceeding with
deletion")
+ logging.info(f"Selecting {release_name} for deletion")
+ release_checkbox_locator.check()
+
+ logging.info(f"Filling deletion confirmation for {release_name}")
+ page.locator("#confirm_delete").fill("DELETE")
+
+ logging.info(f"Submitting deletion form for {release_name}")
+ submit_button_locator =
page.locator('input[type="submit"][value="Delete selected releases
permanently"]')
+ sync_api.expect(submit_button_locator).to_be_enabled()
+ submit_button_locator.click()
+
+ logging.info(f"Waiting for page load after deletion submission for
{release_name}")
+ page.wait_for_load_state()
+ logging.info(f"Page loaded after deletion for {release_name}")
+
+ logging.info(f"Checking for success flash message for {release_name}")
+ flash_message_locator = page.locator("div.flash-success")
+ sync_api.expect(flash_message_locator).to_be_visible()
+ sync_api.expect(flash_message_locator).to_contain_text("Successfully
deleted 1 release(s)")
+ logging.info(f"Deletion successful for {release_name}")
+ else:
+ logging.info(f"Could not find the {release_name} release, no deletion
needed")
+
+
def run_tests(skip_slow: bool) -> None:
if (credentials := get_credentials()) is None:
logging.error("Cannot run tests: no credentials provided")
@@ -141,12 +178,15 @@ def run_tests_in_context(context:
sync_api.BrowserContext, credentials: Credenti
logging.info("Tests finished successfully")
-def run_tests_skipping_slow(tests: list[Callable[..., Any]], page:
sync_api.Page, skip_slow: bool) -> None:
+def run_tests_skipping_slow(
+ tests: list[Callable[..., Any]], page: sync_api.Page, credentials:
Credentials, skip_slow: bool
+) -> None:
for test in tests:
if skip_slow and ("slow" in test.__annotations__):
logging.info(f"Skipping slow test: {test.__name__}")
continue
- test(page)
+ # if "credentials" in test.__code__.co_varnames:
+ test(page, credentials)
def show_default_gateway_ip() -> None:
@@ -215,16 +255,17 @@ def test_all(page: sync_api.Page, credentials:
Credentials, skip_slow: bool) ->
]
tests["ssh"] = [
test_ssh_01_add_key,
+ test_ssh_02_rsync_upload,
]
# Order between our tests must be preserved
# Insertion order is reliable since Python 3.6
# Therefore iteration over tests matches the insertion order above
for key in tests:
- run_tests_skipping_slow(tests[key], page, skip_slow)
+ run_tests_skipping_slow(tests[key], page, credentials, skip_slow)
-def test_lifecycle_01_add_draft(page: sync_api.Page) -> None:
+def test_lifecycle_01_add_draft(page: sync_api.Page, credentials: Credentials)
-> None:
logging.info("Following link to add draft")
add_draft_link_locator = page.get_by_role("link", name="Add draft")
sync_api.expect(add_draft_link_locator).to_be_visible()
@@ -251,14 +292,14 @@ def test_lifecycle_01_add_draft(page: sync_api.Page) ->
None:
logging.info("Add draft actions completed successfully")
-def test_lifecycle_02_check_draft_added(page: sync_api.Page) -> None:
+def test_lifecycle_02_check_draft_added(page: sync_api.Page, credentials:
Credentials) -> None:
logging.info("Checking for draft 'tooling-0.1'")
draft_card_locator = page.locator(r"#tooling-0\.1")
sync_api.expect(draft_card_locator).to_be_visible()
logging.info("Draft 'tooling-0.1' found successfully")
-def test_lifecycle_03_add_file(page: sync_api.Page) -> None:
+def test_lifecycle_03_add_file(page: sync_api.Page, credentials: Credentials)
-> None:
logging.info("Navigating to the add file page for tooling-0.1")
go_to_path(page, "/draft/add/tooling/0.1")
logging.info("Add file page loaded")
@@ -284,7 +325,7 @@ def test_lifecycle_03_add_file(page: sync_api.Page) -> None:
logging.info("Navigation back to /drafts completed successfully")
-def test_lifecycle_04_promote_to_candidate(page: sync_api.Page) -> None:
+def test_lifecycle_04_promote_to_candidate(page: sync_api.Page, credentials:
Credentials) -> None:
logging.info("Locating draft promotion link for tooling-0.1")
draft_card_locator = page.locator(r"#tooling-0\.1")
promote_link_locator = draft_card_locator.locator('a[title="Promote draft
for Apache Tooling 0.1"]')
@@ -319,7 +360,7 @@ def test_lifecycle_04_promote_to_candidate(page:
sync_api.Page) -> None:
logging.info("Promote draft actions completed successfully")
-def test_lifecycle_05_vote_on_candidate(page: sync_api.Page) -> None:
+def test_lifecycle_05_vote_on_candidate(page: sync_api.Page, credentials:
Credentials) -> None:
logging.info("Locating the link to start a vote for tooling-0.1")
card_locator =
page.locator('div.card:has(input[name="candidate_name"][value="tooling-0.1"])')
start_vote_link_locator = card_locator.locator('a[title="Start vote for
Apache Tooling 0.1"]')
@@ -342,7 +383,7 @@ def test_lifecycle_05_vote_on_candidate(page:
sync_api.Page) -> None:
logging.info("Vote initiation actions completed successfully")
-def test_lifecycle_06_resolve_vote(page: sync_api.Page) -> None:
+def test_lifecycle_06_resolve_vote(page: sync_api.Page, credentials:
Credentials) -> None:
logging.info("Locating the form to resolve the vote for tooling-0.1")
form_locator =
page.locator('form:has(input[name="candidate_name"][value="tooling-0.1"])')
sync_api.expect(form_locator).to_be_visible()
@@ -362,7 +403,7 @@ def test_lifecycle_06_resolve_vote(page: sync_api.Page) ->
None:
logging.info("Vote resolution actions completed successfully")
-def test_lifecycle_07_promote_preview(page: sync_api.Page) -> None:
+def test_lifecycle_07_promote_preview(page: sync_api.Page, credentials:
Credentials) -> None:
logging.info("Locating the link to promote the preview for tooling-0.1")
promote_link_locator = page.locator('a[title="Promote Apache Tooling 0.1
to release"]')
sync_api.expect(promote_link_locator).to_be_visible()
@@ -395,7 +436,7 @@ def test_lifecycle_07_promote_preview(page: sync_api.Page)
-> None:
logging.info("Preview promotion actions completed successfully")
-def test_lifecycle_08_release_exists(page: sync_api.Page) -> None:
+def test_lifecycle_08_release_exists(page: sync_api.Page, credentials:
Credentials) -> None:
logging.info("Checking for release tooling-0.1 on the /releases page")
release_card_locator = page.locator('div.card:has(h3:has-text("Apache
Tooling 0.1"))')
@@ -459,7 +500,7 @@ def test_login(page: sync_api.Page, credentials:
Credentials) -> None:
@slow
-def test_projects_01_update(page: sync_api.Page) -> None:
+def test_projects_01_update(page: sync_api.Page, credentials: Credentials) ->
None:
logging.info("Navigating to the admin update projects page")
go_to_path(page, "/admin/projects/update")
logging.info("Admin update projects page loaded")
@@ -479,7 +520,7 @@ def test_projects_01_update(page: sync_api.Page) -> None:
logging.info("Project update completed successfully")
-def test_projects_02_check_directory(page: sync_api.Page) -> None:
+def test_projects_02_check_directory(page: sync_api.Page, credentials:
Credentials) -> None:
logging.info("Navigating to the project directory page")
go_to_path(page, "/projects")
logging.info("Project directory page loaded")
@@ -491,7 +532,7 @@ def test_projects_02_check_directory(page: sync_api.Page)
-> None:
logging.info("Apache Tooling project card found successfully")
-def test_projects_03_add_project(page: sync_api.Page) -> None:
+def test_projects_03_add_project(page: sync_api.Page, credentials:
Credentials) -> None:
project_name = "Apache Tooling Test Example"
project_label = "tooling-test-example"
base_project_option_label = "Apache Tooling"
@@ -522,7 +563,7 @@ def test_projects_03_add_project(page: sync_api.Page) ->
None:
logging.info("Project title confirmed on view page")
-def test_ssh_01_add_key(page: sync_api.Page) -> None:
+def test_ssh_01_add_key(page: sync_api.Page, credentials: Credentials) -> None:
logging.info("Starting SSH key addition test")
go_to_path(page, "/")
@@ -584,9 +625,71 @@ def test_ssh_01_add_key(page: sync_api.Page) -> None:
logging.info("SSH key fingerprint verified successfully on /keys page")
+def test_ssh_02_rsync_upload(page: sync_api.Page, credentials: Credentials) ->
None:
+ project_name = "tooling-test-example"
+ version_name = "0.2"
+ source_dir_rel = f"apache-{project_name}-{version_name}"
+ source_dir_abs = f"/run/tests/{source_dir_rel}"
+ file1 = f"apache-{project_name}-{version_name}.tar.gz"
+ file2 = f"{file1}.sha512"
+
+ logging.info(f"Starting rsync upload test for
{project_name}-{version_name}")
+
+ gateway_ip = get_default_gateway_ip()
+ if not gateway_ip:
+ raise RuntimeError("Cannot proceed without gateway IP")
+
+ username = credentials.username
+ ssh_command = "ssh -p 2222 -o StrictHostKeyChecking=no -o
UserKnownHostsFile=/dev/null"
+ source_path = f"{source_dir_abs}/"
+ destination = f"{username}@{gateway_ip}:/{project_name}/{version_name}/"
+
+ rsync_cmd = [
+ "rsync",
+ "-av",
+ "-e",
+ ssh_command,
+ source_path,
+ destination,
+ ]
+
+ logging.info(f"Executing rsync command: {' '.join(rsync_cmd)}")
+ try:
+ result = subprocess.run(rsync_cmd, check=True, capture_output=True,
text=True)
+ logging.info(f"rsync completed successfully. stdout:\n{result.stdout}")
+ if result.stderr:
+ logging.warning(f"rsync stderr:\n{result.stderr}")
+ except subprocess.CalledProcessError as e:
+ logging.error(f"rsync command failed with exit code {e.returncode}")
+ logging.error(f"rsync stdout:\n{e.stdout}")
+ logging.error(f"rsync stderr:\n{e.stderr}")
+ raise RuntimeError("rsync upload failed") from e
+ except FileNotFoundError:
+ logging.error("rsync command not found. Is rsync installed in the
container?")
+ raise RuntimeError("rsync command not found")
+
+ logging.info(f"Navigating to evaluate page for
{project_name}-{version_name}")
+ evaluate_path = f"/draft/evaluate/{project_name}/{version_name}"
+ go_to_path(page, evaluate_path)
+ logging.info(f"Checking for uploaded files on {evaluate_path}")
+
+ # Check for the existence of the files in the table using exact match
+ file1_locator = page.get_by_role("cell", name=file1, exact=True)
+ file2_locator = page.get_by_role("cell", name=file2, exact=True)
+
+ sync_api.expect(file1_locator).to_be_visible()
+ logging.info(f"Found file: {file1}")
+ sync_api.expect(file2_locator).to_be_visible()
+ logging.info(f"Found file: {file2}")
+ logging.info("rsync upload test completed successfully")
+
+
def test_tidy_up(page: sync_api.Page) -> None:
- test_tidy_up_release(page)
+ # Projects cannot be deleted if they have associated releases
+ # Therefore, we need to delete releases first
+ test_tidy_up_releases(page)
test_tidy_up_project(page)
+ # TODO: Tidy up SSH keys
def test_tidy_up_project(page: sync_api.Page) -> None:
@@ -626,38 +729,13 @@ def test_tidy_up_project(page: sync_api.Page) -> None:
logging.info(f"Project card for '{project_name}' not found, no
deletion needed")
-def test_tidy_up_release(page: sync_api.Page) -> None:
+def test_tidy_up_releases(page: sync_api.Page) -> None:
logging.info("Navigating to the admin delete release page")
go_to_path(page, "/admin/delete-release")
logging.info("Admin delete release page loaded")
- logging.info("Checking whether the tooling-0.1 release exists")
- release_checkbox_locator =
page.locator('input[name="releases_to_delete"][value="tooling-0.1"]')
-
- if release_checkbox_locator.is_visible():
- logging.info("Found the tooling-0.1 release, proceeding with deletion")
- logging.info("Selecting tooling-0.1 for deletion")
- release_checkbox_locator.check()
-
- logging.info("Filling deletion confirmation")
- page.locator("#confirm_delete").fill("DELETE")
-
- logging.info("Submitting deletion form")
- submit_button_locator =
page.locator('input[type="submit"][value="Delete selected releases
permanently"]')
- sync_api.expect(submit_button_locator).to_be_enabled()
- submit_button_locator.click()
-
- logging.info("Waiting for page load after deletion submission")
- page.wait_for_load_state()
- logging.info("Page loaded after deletion")
-
- logging.info("Checking for success flash message")
- flash_message_locator = page.locator("div.flash-success")
- sync_api.expect(flash_message_locator).to_be_visible()
- sync_api.expect(flash_message_locator).to_contain_text("Successfully
deleted 1 release(s)")
- logging.info("Deletion successful")
- else:
- logging.info("Could not find the tooling-0.1 release, no deletion
needed")
+ release_remove(page, "tooling-0.1")
+ release_remove(page, "tooling-test-example-0.2")
def wait_for_path(page: sync_api.Page, path: str) -> None:
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]