This is an automated email from the ASF dual-hosted git repository.
jasonliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new cb586ce385b Add e2e test for XComObjectStorageBackend (#62907)
cb586ce385b is described below
commit cb586ce385b4699e2c7376c55e7f825a0306dd75
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Mon Mar 9 10:44:34 2026 +0800
Add e2e test for XComObjectStorageBackend (#62907)
* Add e2e test for XComObjectStorageBackend
* Update GitHub Action and Breeze cmd
* Add aws s3fs deps
* Add debug log
* Add required AWS env to fix the e2e test failure
* Harden the client in e2e test
* Fix nits
* Fix copilot suggestion
Co-authored-by: Copilot <[email protected]>
---------
Co-authored-by: Copilot <[email protected]>
---
.github/workflows/additional-prod-image-tests.yml | 11 +++
.github/workflows/airflow-e2e-tests.yml | 4 +-
airflow-e2e-tests/scripts/init-aws.sh | 1 +
.../tests/airflow_e2e_tests/conftest.py | 32 +++++++-
.../tests/airflow_e2e_tests/constants.py | 3 +
.../airflow_e2e_tests/e2e_test_utils/clients.py | 51 +++++++++++--
.../remote_log_tests/test_remote_logging.py | 12 +--
.../xcom_object_storage_tests/__init__.py} | 4 -
.../test_xcom_object_storage_backend.py | 89 ++++++++++++++++++++++
.../images/output_testing_airflow-e2e-tests.svg | 24 +++---
.../images/output_testing_airflow-e2e-tests.txt | 2 +-
.../airflow_breeze/commands/testing_commands.py | 2 +-
12 files changed, 199 insertions(+), 36 deletions(-)
diff --git a/.github/workflows/additional-prod-image-tests.yml
b/.github/workflows/additional-prod-image-tests.yml
index 35c93a3344f..ad3f60042c9 100644
--- a/.github/workflows/additional-prod-image-tests.yml
+++ b/.github/workflows/additional-prod-image-tests.yml
@@ -226,6 +226,17 @@ jobs:
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "remote_log"
+ test-e2e-integration-tests-xcom-object-storage:
+ name: "XCom object storage backend tests with PROD image"
+ uses: ./.github/workflows/airflow-e2e-tests.yml
+ with:
+ workflow-name: "XCom object storage backend e2e test"
+ runners: ${{ inputs.runners }}
+ platform: ${{ inputs.platform }}
+ default-python-version: "${{ inputs.default-python-version }}"
+ use-uv: ${{ inputs.use-uv }}
+ e2e_test_mode: "xcom_object_storage"
+
test-ui-e2e-chromium:
name: "Chromium UI e2e tests with PROD image"
uses: ./.github/workflows/ui-e2e-tests.yml
diff --git a/.github/workflows/airflow-e2e-tests.yml
b/.github/workflows/airflow-e2e-tests.yml
index 5009987bc67..e9d9811ae29 100644
--- a/.github/workflows/airflow-e2e-tests.yml
+++ b/.github/workflows/airflow-e2e-tests.yml
@@ -49,7 +49,7 @@ on: # yamllint disable-line rule:truthy
type: string
required: true
e2e_test_mode:
- description: "Test mode - basic or remote_log"
+ description: "Test mode - basic, remote_log, or xcom_object_storage"
type: string
default: "basic"
@@ -80,7 +80,7 @@ on: # yamllint disable-line rule:truthy
type: string
default: ""
e2e_test_mode:
- description: "Test mode - quick or full"
+ description: "Test mode - basic, remote_log, or xcom_object_storage"
type: string
default: "basic"
diff --git a/airflow-e2e-tests/scripts/init-aws.sh
b/airflow-e2e-tests/scripts/init-aws.sh
index ca5a1cfe078..4c78d873570 100755
--- a/airflow-e2e-tests/scripts/init-aws.sh
+++ b/airflow-e2e-tests/scripts/init-aws.sh
@@ -17,4 +17,5 @@
# under the License.
aws --endpoint-url=http://localstack:4566 s3 mb s3://test-airflow-logs
+aws --endpoint-url=http://localstack:4566 s3 mb
s3://test-xcom-objectstorage-backend
aws --endpoint-url=http://localstack:4566 s3 ls
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
index 24caa32a213..ad071d4e02b 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
@@ -36,6 +36,7 @@ from airflow_e2e_tests.constants import (
LOCALSTACK_PATH,
LOGS_FOLDER,
TEST_REPORT_FILE,
+ XCOM_BUCKET,
)
from tests_common.test_utils.fernet import generate_fernet_key_string
@@ -48,13 +49,18 @@ class _E2ETestState:
airflow_logs_path: Path | None = None
-def _setup_s3_integration(dot_env_file, tmp_dir):
+def _copy_localstack_files(tmp_dir):
+ """Copy localstack compose file and init script into the temp directory."""
copyfile(LOCALSTACK_PATH, tmp_dir / "localstack.yml")
copyfile(AWS_INIT_PATH, tmp_dir / "init-aws.sh")
current_permissions = os.stat(tmp_dir / "init-aws.sh").st_mode
os.chmod(tmp_dir / "init-aws.sh", current_permissions | 0o111)
+
+def _setup_s3_integration(dot_env_file, tmp_dir):
+ _copy_localstack_files(tmp_dir)
+
dot_env_file.write_text(
f"AIRFLOW_UID={os.getuid()}\n"
"AWS_DEFAULT_REGION=us-east-1\n"
@@ -68,6 +74,27 @@ def _setup_s3_integration(dot_env_file, tmp_dir):
os.environ["ENV_FILE_PATH"] = str(dot_env_file)
+def _setup_xcom_object_storage_integration(dot_env_file, tmp_dir):
+ _copy_localstack_files(tmp_dir)
+
+ dot_env_file.write_text(
+ f"AIRFLOW_UID={os.getuid()}\n"
+ # XComObjectStorageBackend requires AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY as env vars
+ # because `universal-path` uses boto3's native S3 client, which relies
on environment variables
+ # for authentication rather than parsing credentials from the
connection URI
+ "AWS_ACCESS_KEY_ID=test\n"
+ "AWS_SECRET_ACCESS_KEY=test\n"
+ "AWS_DEFAULT_REGION=us-east-1\n"
+ "AWS_ENDPOINT_URL_S3=http://localstack:4566\n"
+ "AIRFLOW_CONN_AWS_DEFAULT=aws://test:test@\n"
+
"AIRFLOW__CORE__XCOM_BACKEND=airflow.providers.common.io.xcom.backend.XComObjectStorageBackend\n"
+
f"AIRFLOW__COMMON_IO__XCOM_OBJECTSTORAGE_PATH=s3://aws_default@{XCOM_BUCKET}/xcom\n"
+ "AIRFLOW__COMMON_IO__XCOM_OBJECTSTORAGE_THRESHOLD=0\n"
+ "_PIP_ADDITIONAL_REQUIREMENTS=apache-airflow-providers-amazon[s3fs]\n"
+ )
+ os.environ["ENV_FILE_PATH"] = str(dot_env_file)
+
+
def spin_up_airflow_environment(tmp_path_factory: pytest.TempPathFactory):
tmp_dir = tmp_path_factory.mktemp("airflow-e2e-tests")
@@ -97,6 +124,9 @@ def spin_up_airflow_environment(tmp_path_factory:
pytest.TempPathFactory):
if E2E_TEST_MODE == "remote_log":
compose_file_names.append("localstack.yml")
_setup_s3_integration(dot_env_file, tmp_dir)
+ elif E2E_TEST_MODE == "xcom_object_storage":
+ compose_file_names.append("localstack.yml")
+ _setup_xcom_object_storage_integration(dot_env_file, tmp_dir)
#
# Please Do not use this Fernet key in any deployments! Please generate
your own key.
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
index a208487da8b..764b4e56dc5 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
@@ -42,3 +42,6 @@ TEST_REPORT_FILE = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" /
"_e2e_test_report.j
LOCALSTACK_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" /
"localstack.yml"
E2E_TEST_MODE = os.environ.get("E2E_TEST_MODE", "basic")
AWS_INIT_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "scripts" /
"init-aws.sh"
+
+# s3 bucket name for XComObjectStorageBackend tests. This bucket will be
created in the `init-aws.sh` script that is run as part of the LocalStack
container initialization.
+XCOM_BUCKET = "test-xcom-objectstorage-backend"
diff --git
a/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py
index 32abeb5331d..3a11101d4ac 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/e2e_test_utils/clients.py
@@ -20,6 +20,7 @@ import time
from datetime import datetime, timezone
from functools import cached_property
+import boto3
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
@@ -31,19 +32,41 @@ from airflow_e2e_tests.constants import (
)
+def get_s3_client():
+ """Return a boto3 S3 client configured to use the local LocalStack
endpoint."""
+ return boto3.client(
+ "s3",
+ endpoint_url="http://localhost:4566",
+ aws_access_key_id="test",
+ aws_secret_access_key="test",
+ region_name="us-east-1",
+ )
+
+
+def create_request_session_with_retries(status_forcelist: list[int]):
+ """Create a requests Session with retry logic for handling transient
errors."""
+ Retry.DEFAULT_BACKOFF_MAX = 32
+ retry_strategy = Retry(
+ total=10,
+ backoff_factor=1,
+ status_forcelist=status_forcelist,
+ )
+ session = requests.Session()
+ adapter = HTTPAdapter(max_retries=retry_strategy)
+ session.mount("http://", adapter)
+ session.mount("https://", adapter)
+ return session
+
+
class AirflowClient:
"""Client for interacting with the Airflow REST API."""
def __init__(self):
- self.session = requests.Session()
+ self.session =
create_request_session_with_retries(status_forcelist=[429])
@cached_property
def token(self):
- Retry.DEFAULT_BACKOFF_MAX = 32
- retry = Retry(total=10, backoff_factor=1, status_forcelist=[429, 500,
502, 503, 504])
- session = requests.Session()
- session.mount("http://", HTTPAdapter(max_retries=retry))
- session.mount("https://", HTTPAdapter(max_retries=retry))
+ session = create_request_session_with_retries(status_forcelist=[429,
500, 502, 503, 504])
api_server_url = DOCKER_COMPOSE_HOST_PORT
if not api_server_url.startswith(("http://", "https://")):
@@ -121,11 +144,23 @@ class AirflowClient:
run_id=resp["dag_run_id"],
)
- def get_task_logs(self, dag_id: str, run_id: str, task_id: str,
try_number: int = 1):
+ def get_task_instances(self, dag_id: str, run_id: str):
+ """Get task instances for a given DAG run."""
+ return self._make_request(
+ method="GET",
+ endpoint=f"dags/{dag_id}/dagRuns/{run_id}/taskInstances",
+ )
+
+ def get_task_logs(
+ self, dag_id: str, run_id: str, task_id: str, try_number: int = 1,
map_index: int | None = None
+ ):
"""Get task logs via API."""
+ endpoint =
f"dags/{dag_id}/dagRuns/{run_id}/taskInstances/{task_id}/logs/{try_number}"
+ if map_index is not None:
+ endpoint += f"?map_index={map_index}"
return self._make_request(
method="GET",
-
endpoint=f"dags/{dag_id}/dagRuns/{run_id}/taskInstances/{task_id}/logs/{try_number}",
+ endpoint=endpoint,
)
diff --git
a/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py
index 52f19c5a1f5..9260f0abe03 100644
---
a/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py
+++
b/airflow-e2e-tests/tests/airflow_e2e_tests/remote_log_tests/test_remote_logging.py
@@ -19,10 +19,9 @@ from __future__ import annotations
import time
from datetime import datetime, timezone
-import boto3
import pytest
-from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient
+from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient,
get_s3_client
class TestRemoteLogging:
@@ -56,15 +55,10 @@ class TestRemoteLogging:
# This bucket will be created part of the docker-compose setup in
bucket_name = "test-airflow-logs"
- s3_client = boto3.client(
- "s3",
- endpoint_url="http://localhost:4566",
- aws_access_key_id="test",
- aws_secret_access_key="test",
- region_name="us-east-1",
- )
+ s3_client = get_s3_client()
# Wait for logs to be available in S3 before we call `get_task_logs`
+ contents = []
for _ in range(self.max_retries):
response = s3_client.list_objects_v2(Bucket=bucket_name)
contents = response.get("Contents", [])
diff --git a/airflow-e2e-tests/scripts/init-aws.sh
b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/__init__.py
old mode 100755
new mode 100644
similarity index 85%
copy from airflow-e2e-tests/scripts/init-aws.sh
copy to
airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/__init__.py
index ca5a1cfe078..13a83393a91
--- a/airflow-e2e-tests/scripts/init-aws.sh
+++
b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/__init__.py
@@ -1,4 +1,3 @@
-#!/bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
@@ -15,6 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-aws --endpoint-url=http://localstack:4566 s3 mb s3://test-airflow-logs
-aws --endpoint-url=http://localstack:4566 s3 ls
diff --git
a/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/test_xcom_object_storage_backend.py
b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/test_xcom_object_storage_backend.py
new file mode 100644
index 00000000000..8ddb9738b6b
--- /dev/null
+++
b/airflow-e2e-tests/tests/airflow_e2e_tests/xcom_object_storage_tests/test_xcom_object_storage_backend.py
@@ -0,0 +1,89 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import time
+from datetime import datetime, timezone
+from pprint import pprint
+from uuid import uuid4
+
+import pytest
+
+from airflow_e2e_tests.constants import XCOM_BUCKET
+from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient,
get_s3_client
+
+
+class TestXComObjectStorageBackend:
+ airflow_client = AirflowClient()
+ dag_id = "example_xcom_test"
+ retry_interval_in_seconds = 5
+ max_retries = 12
+
+ def test_dag_succeeds_and_xcom_values_stored_in_s3(self):
+ """Test that a DAG using XComObjectStorageBackend completes
successfully and persists XCom values to S3."""
+ self.airflow_client.un_pause_dag(self.dag_id)
+
+ trigger_resp = self.airflow_client.trigger_dag(
+ self.dag_id,
+ json={
+ "dag_run_id": f"test_xcom_object_storage_backend_{uuid4()}",
+ "logical_date": datetime.now(timezone.utc).isoformat(),
+ },
+ )
+ dag_run_id = trigger_resp["dag_run_id"]
+ state = self.airflow_client.wait_for_dag_run(
+ dag_id=self.dag_id,
+ run_id=dag_run_id,
+ )
+
+ # try to get all the logs to help debugging
+ if state != "success":
+ task_instances_resp =
self.airflow_client.get_task_instances(self.dag_id, dag_run_id)
+ for task_instance in task_instances_resp["task_instances"]:
+ task_id = task_instance["task_id"]
+ try_number = task_instance["try_number"]
+ try:
+ print(f"\nLogs for task {task_id} (try {try_number}):")
+ task_logs_resp = self.airflow_client.get_task_logs(
+ dag_id=self.dag_id, task_id=task_id,
run_id=dag_run_id, try_number=try_number
+ )
+ pprint(task_logs_resp)
+ except Exception as e:
+ print(f"Could not get logs for task {task_id} (try
{try_number}): {e}")
+
+ assert state == "success", f"DAG {self.dag_id} did not complete
successfully. Final state: {state}"
+
+ s3_client = get_s3_client()
+
+ contents = []
+ for _ in range(self.max_retries):
+ response = s3_client.list_objects_v2(Bucket=XCOM_BUCKET)
+ contents = response.get("Contents", [])
+ if contents:
+ break
+
+ print(f"No XCom objects found in S3 bucket {XCOM_BUCKET!r} yet.
Retrying...")
+ time.sleep(self.retry_interval_in_seconds)
+
+ if not contents:
+ pytest.fail(
+ f"Expected XCom objects in S3 bucket {XCOM_BUCKET!r}, but
bucket is empty.\n"
+ f"List Objects Response: {response}"
+ )
+
+ keys = [obj["Key"] for obj in contents]
+ print(f"Found {len(keys)} XCom object(s) in S3: {keys}")
diff --git a/dev/breeze/doc/images/output_testing_airflow-e2e-tests.svg
b/dev/breeze/doc/images/output_testing_airflow-e2e-tests.svg
index 3c444d9bfb5..9c48a6c8cb4 100644
--- a/dev/breeze/doc/images/output_testing_airflow-e2e-tests.svg
+++ b/dev/breeze/doc/images/output_testing_airflow-e2e-tests.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 538.0"
xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 562.4"
xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -43,7 +43,7 @@
<defs>
<clipPath id="breeze-testing-airflow-e2e-tests-clip-terminal">
- <rect x="0" y="0" width="1463.0" height="487.0" />
+ <rect x="0" y="0" width="1463.0" height="511.4" />
</clipPath>
<clipPath id="breeze-testing-airflow-e2e-tests-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -102,9 +102,12 @@
<clipPath id="breeze-testing-airflow-e2e-tests-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
+<clipPath id="breeze-testing-airflow-e2e-tests-line-19">
+ <rect x="0" y="465.1" width="1464" height="24.65"/>
+ </clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="536" rx="8"/><text
class="breeze-testing-airflow-e2e-tests-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: testing airflow-e2e-tests</text>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1"
x="1" y="1" width="1480" height="560.4" rx="8"/><text
class="breeze-testing-airflow-e2e-tests-title" fill="#c5c8c6"
text-anchor="middle" x="740"
y="27">Command: testing airflow-e2e-tests</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -127,13 +130,14 @@
</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="264"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-10)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="264" textLength="366"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-10)">--include-success-outputs     </text><text
class="breeze-testing-airflow-e2e-tests-r1" x="463.6" y="264"
textLength="841.8" clip-path="url(#breeze-testing-airflow-e2e-t [...]
</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="288.4"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-11)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="288.4" textLength="366"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-11)">--github-repository           </text><text
class="breeze-testing-airflow-e2e-tests-r6" x="414.8" y="288.4"
textLength="24.4" clip-path [...]
</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="312.8"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-12)">│</text><text
class="breeze-testing-airflow-e2e-tests-r7" x="463.6" y="312.8"
textLength="73.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-12)">(TEXT)</text><text
class="breeze-testing-airflow-e2e-tests-r5" x="1451.8" y="312.8"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-12)">│</text><text
class="breez [...]
-</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="337.2"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-13)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="337.2" textLength="366"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-13)">--e2e-test-mode               </text><text
class="breeze-testing-airflow-e2e-tests-r1" x="463.6" y="337.2" textLen [...]
-</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="361.6"
textLength="1464"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-14)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-airflow-e2e-tests-r1" x="1464" y="361.6"
textLength="12.2" clip-path="url(#breeze-testing-airflow-e2e-tests-line-14)">
-</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="386"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-15)">╭─</text><text
class="breeze-testing-airflow-e2e-tests-r5" x="24.4" y="386" textLength="195.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-15)"> Common options </text><text
class="breeze-testing-airflow-e2e-tests-r5" x="219.6" y="386"
textLength="1220"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-15)">─────── [...]
-</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="410.4"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-16)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="410.4"
textLength="109.8"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-16)">--verbose</text><text
class="breeze-testing-airflow-e2e-tests-r6" x="158.6" y="410.4"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-16)">-v</text><text
class="br [...]
-</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="434.8"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-17)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="434.8"
textLength="109.8"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-17)">--dry-run</text><text
class="breeze-testing-airflow-e2e-tests-r6" x="158.6" y="434.8"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-17)">-D</text><text
class="br [...]
-</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="459.2"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-18)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="459.2"
textLength="109.8"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-18)">--help   </text><text
class="breeze-testing-airflow-e2e-tests-r6" x="158.6" y="459.2"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-18)">-h</text> [...]
-</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="483.6"
textLength="1464"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-19)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-airflow-e2e-tests-r1" x="1464" y="483.6"
textLength="12.2" clip-path="url(#breeze-testing-airflow-e2e-tests-line-19)">
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="337.2"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-13)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="337.2" textLength="366"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-13)">--e2e-test-mode               </text><text
class="breeze-testing-airflow-e2e-tests-r1" x="463.6" y="337.2" textLen [...]
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="361.6"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-14)">│</text><text
class="breeze-testing-airflow-e2e-tests-r7" x="463.6" y="361.6"
textLength="463.6"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-14)">(basic|remote_log|xcom_object_storage)</text><text
class="breeze-testing-airflow-e2e-tests-r5" x="1451.8" y="361.6"
textLength="12.2" clip-path="url(#breeze-testing-airflow-e2e-tests-lin [...]
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="386"
textLength="1464"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-15)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-airflow-e2e-tests-r1" x="1464" y="386" textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-15)">
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="410.4"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-16)">╭─</text><text
class="breeze-testing-airflow-e2e-tests-r5" x="24.4" y="410.4"
textLength="195.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-16)"> Common options </text><text
class="breeze-testing-airflow-e2e-tests-r5" x="219.6" y="410.4"
textLength="1220" clip-path="url(#breeze-testing-airflow-e2e-tests-line-16)">─
[...]
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="434.8"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-17)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="434.8"
textLength="109.8"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-17)">--verbose</text><text
class="breeze-testing-airflow-e2e-tests-r6" x="158.6" y="434.8"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-17)">-v</text><text
class="br [...]
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="459.2"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-18)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="459.2"
textLength="109.8"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-18)">--dry-run</text><text
class="breeze-testing-airflow-e2e-tests-r6" x="158.6" y="459.2"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-18)">-D</text><text
class="br [...]
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="483.6"
textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-19)">│</text><text
class="breeze-testing-airflow-e2e-tests-r4" x="24.4" y="483.6"
textLength="109.8"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-19)">--help   </text><text
class="breeze-testing-airflow-e2e-tests-r6" x="158.6" y="483.6"
textLength="24.4"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-19)">-h</text> [...]
+</text><text class="breeze-testing-airflow-e2e-tests-r5" x="0" y="508"
textLength="1464"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-20)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
class="breeze-testing-airflow-e2e-tests-r1" x="1464" y="508" textLength="12.2"
clip-path="url(#breeze-testing-airflow-e2e-tests-line-20)">
</text>
</g>
</g>
diff --git a/dev/breeze/doc/images/output_testing_airflow-e2e-tests.txt
b/dev/breeze/doc/images/output_testing_airflow-e2e-tests.txt
index 050f043694d..1329a6ca615 100644
--- a/dev/breeze/doc/images/output_testing_airflow-e2e-tests.txt
+++ b/dev/breeze/doc/images/output_testing_airflow-e2e-tests.txt
@@ -1 +1 @@
-c0321134cc7d9545774114a6b78096ef
+7ae6755f2c4ee578b3ebf11d051e8809
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index 2386b700abf..30f3c7efcd3 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -1387,7 +1387,7 @@ option_e2e_test_mode = click.option(
default="basic",
show_default=True,
envvar="E2E_TEST_MODE",
- type=click.Choice(["basic", "remote_log"], case_sensitive=False),
+ type=click.Choice(["basic", "remote_log", "xcom_object_storage"],
case_sensitive=False),
)