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
45 changes: 45 additions & 0 deletions .github/workflows/integration-functional.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

name: Integration functional

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

jobs:
integration-functional:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
# actions/checkout v6.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false

# actions/setup-python v6.2.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
with:
python-version: '3.12'

# astral-sh/setup-uv v8.1.0
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: 0.11.12

- name: Run integration functional tests
env:
# Use a repository *secret* (Settings → Secrets → Actions), not a variable.
GH_TEST_REPO_TOKEN: ${{ secrets.GH_TEST_REPO_TOKEN }}
run: bash scripts/integration-functional.sh

- name: Upload logs on failure
if: failure()
# actions/upload-artifact v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: integration-functional-logs
path: /tmp/compose-logs.txt
5 changes: 5 additions & 0 deletions .github/workflows/integration-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ jobs:
with:
python-version: '3.12'

# astral-sh/setup-uv v8.1.0
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: 0.11.12

- name: Run integration smoke tests
run: bash scripts/integration-smoke.sh

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ dev = [
"coverage[toml]>=7.6.0",
"pytest-cov>=7.1.0"
]
integration = [
"pytest>=8.3",
"pytest-timeout>=2.3.1"
]
lint = [
{include-group = "pre-commit"}
]
Expand Down
61 changes: 61 additions & 0 deletions scripts/integration-functional.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
# SPDX-License-Identifier: BSL-1.0

# Integration functional test entrypoint (P1).
# Builds the stack, waits for health, creates API token, extracts SSH pubkey,
# runs functional tests against a live Weblate instance.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=lib/weblate-stack.sh
source "${SCRIPT_DIR}/lib/weblate-stack.sh"

cleanup() {
local exit_code=$?
set +e
echo "--- Collecting logs ---"
stack_logs /tmp/compose-logs.txt
echo "--- Tearing down stack ---"
stack_down
exit "$exit_code"
}
trap cleanup EXIT

echo "=== Building stack ==="
stack_build

echo "=== Starting stack ==="
stack_up

echo "=== Waiting for Weblate ==="
stack_wait_healthy "${HEALTH_TIMEOUT:-180}"

echo "=== Creating API token ==="
WEBLATE_API_TOKEN="$(stack_create_token admin)"
export WEBLATE_API_TOKEN
export WEBLATE_LIVE_BASE_URL="${WEBLATE_LIVE_BASE_URL:-http://localhost:${WEBLATE_PORT:-8080}}"
export WEBLATE_COMPOSE_FILE="${COMPOSE_FILE}"
export WEBLATE_COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME}"

echo "=== Extracting Weblate SSH public key ==="
TMP_WEBLATE_SSH_PUBKEY="$(compose exec -T weblate cat /app/data/ssh/id_rsa.pub)"
if [[ -z "${TMP_WEBLATE_SSH_PUBKEY}" ]]; then
echo "ERROR: Failed to read Weblate SSH public key from container." >&2
exit 1
fi
export WEBLATE_SSH_PUBKEY="${TMP_WEBLATE_SSH_PUBKEY}"
unset TMP_WEBLATE_SSH_PUBKEY

if [[ -n "${GH_TEST_REPO_TOKEN:-}" ]]; then
export GH_TEST_REPO_TOKEN
echo "=== GH_TEST_REPO_TOKEN is set (${#GH_TEST_REPO_TOKEN} chars); GitHub E2E tests enabled ==="
else
echo "=== GH_TEST_REPO_TOKEN is not set; GitHub E2E/Celery tests will be skipped ==="
fi

echo "=== Running functional tests ==="
uv pip install --quiet --system --group integration
python -m pytest --confcutdir=tests/integration --override-ini addopts= \
tests/integration/test_functional.py -v --timeout=300
4 changes: 3 additions & 1 deletion scripts/integration-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export WEBLATE_COMPOSE_FILE="${COMPOSE_FILE}"
export WEBLATE_COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME}"

echo "=== Running smoke tests ==="
pip install --quiet pytest
# Same pytest floor as dev/pre-commit (pyproject.toml); not pinned to uv.lock.
# --system: setup-python on CI has no project venv (matches ci-dependencies.yml).
uv pip install --quiet --system --group integration
# Do not load tests/conftest.py (Django host setup); integration tests only need pytest + stdlib.
python -m pytest --confcutdir=tests/integration --override-ini addopts= tests/integration/test_smoke.py -v
6 changes: 3 additions & 3 deletions tests/endpoint/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_add_or_update_serializer_accepts_extensions() -> None:
data={
"organization": "o",
"version": "v",
"add_or_update": {"ja": ["unordered"]},
"add_or_update": {"zh_Hans": ["unordered"]},
"extensions": [".adoc", ".md"],
}
)
Expand Down Expand Up @@ -64,11 +64,11 @@ def test_add_or_update_serializer_rejects_non_list_submodules() -> None:
data={
"organization": "o",
"version": "v",
"add_or_update": {"ja": "json"},
"add_or_update": {"zh_Hans": "json"},
}
)
assert not ser.is_valid()
assert "ja" in ser.errors["add_or_update"]
assert "zh_Hans" in ser.errors["add_or_update"]


def test_add_or_update_serializer_missing_required_fields() -> None:
Expand Down
12 changes: 6 additions & 6 deletions tests/endpoint/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_add_or_update_requires_authentication(
{
"organization": "o",
"version": "v",
"add_or_update": {"ja": ["json"]},
"add_or_update": {"zh_Hans": ["json"]},
},
format="json",
)
Expand All @@ -98,7 +98,7 @@ def test_add_or_update_accepts_and_enqueues_like_boost_weblate(
{
"organization": "o",
"version": "v",
"add_or_update": {"ja": ["json"]},
"add_or_update": {"zh_Hans": ["json"]},
},
format="json",
)
Expand All @@ -122,7 +122,7 @@ def test_add_or_update_accepts_and_enqueues_like_boost_weblate(

delay_mock.assert_called_once_with(
organization="o",
add_or_update={"ja": ["json"]},
add_or_update={"zh_Hans": ["json"]},
version="v",
extensions=None,
user_id=42,
Expand Down Expand Up @@ -168,16 +168,16 @@ def process_all(self, submodules, *, user, request=None): # noqa: ANN001

result = tasks_mod.boost_add_or_update_task.run(
organization="org",
add_or_update={"ja": ["json"], "zh": ["a"]},
add_or_update={"zh_Hans": ["a"], "ja": ["json"]},
version="boost-1.0",
extensions=[".md"],
user_id=7,
)

get_mock.assert_called_once_with(pk=7)
assert calls == [("ja", ["json"]), ("zh", ["a"])]
assert calls == [("zh_Hans", ["a"]), ("ja", ["json"])]
assert result["zh_Hans"]["organization"] == "org"
assert result["ja"]["submodules"] == ["json"]
assert result["zh"]["organization"] == "org"


def test_boost_add_or_update_task_propagates_service_errors(
Expand Down
88 changes: 88 additions & 0 deletions tests/fixtures/asciidoc_fixture.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Synthetic fixture for AsciiDoc parser tests.
// Patterns adapted from Beast documentation (sections,
// headings, tables with source blocks, description lists, admonitions, links).

= Complex AsciiDoc test fixture

[#complex_fixture]
== Complex AsciiDoc test fixture

This opening paragraph soft-wraps onto a second line while keeping an
https://example.com/path[RFC-style link] and a named
xref:message[`message`] reference in the same paragraph block.

== Section headings and lists

* First bullet names xref:request[`request`].
* Second bullet continues the list with plain prose.

[[anchor_skipped]]

[#custom]
=== Custom heading with id

[quote]
____
This is a single-line blockquote for translation.
____

.NOTE
====
This is a multi-line admonition body. It contains a paragraph that should
still be extracted when the note block is parsed recursively.

A second paragraph inside the same note uses
https://tools.ietf.org/html/rfc6455[WebSocket] markup.
====

[#nested_inner]
== Nested section title here

Inner section prose explains that `template` parameters accept any
xref:fields[`fields`] type meeting requirements.
The paragraph joins wrapped source lines into one translatable unit.

[source]
----
// This indented line starts a code block (non-translatable).
// It must not become its own translation unit.
----

After the code block, prose resumes with an image that is skipped:

image::beast/images/message.png[width=100px,height=50px]

.Message patterns
|===
|Name |Description

|__message__
a|```
/// Class template overview
template<class Body, class Fields>
class message;
```

|xref:request[`request`]
a|```
/// HTTP request alias
template<class Body, class Fields = fields>
using request = message<true, Body, Fields>;
```

|Plain prose cell
|This cell has human-readable text only, without a source block.
|===

[qanda]
Does this fixture include a description list?::
Yes. This pair mimics patterns from the FAQ chapter: quoted question
strings and multi-sentence answers with blank-line-separated paragraphs.

Second paragraph in the same answer cell.

What about table titles?::
The table above includes an explicit title line before the opening delimiter.
+

.WARNING: This is a one-line warning about edge cases in handshakes.
Loading
Loading