Skip to content

Commit 724350d

Browse files
committed
ci: upload integration logs on failure
Preserve CCM log and config files before integration cleanup clears cluster directories, then upload them as failure-only artifacts for integration test jobs.
1 parent 8f772c8 commit 724350d

3 files changed

Lines changed: 102 additions & 1 deletion

File tree

.github/workflows/integration-tests.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,33 @@ jobs:
9393
- name: Test with pytest
9494
env:
9595
EVENT_LOOP_MANAGER: ${{ matrix.event_loop_manager }}
96+
INTEGRATION_LOG_ARTIFACT_DIR: integration-test-logs
9697
PROTOCOL_VERSION: 4
9798
run: |
9899
if [[ "${{ matrix.python-version }}" =~ t$ ]]; then
99100
export PYTHON_GIL=0
100101
fi
101102
uv run pytest -v tests/integration/standard/ tests/integration/cqlengine/
103+
104+
- name: Upload ccm logs on failure
105+
if: failure()
106+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
107+
with:
108+
name: integration-logs-${{ matrix.event_loop_manager }}-${{ matrix.python-version }}
109+
path: |
110+
integration-test-logs/**
111+
tests/integration/ccm/**/*.conf
112+
tests/integration/ccm/**/*.log
113+
tests/integration/ccm/**/*.yaml
114+
tests/integration/ccm/**/*.yml
115+
tests/integration/ccm/**/conf/**
116+
tests/integration/ccm/**/logs/**
117+
/home/runner/.ccm/**/*.conf
118+
/home/runner/.ccm/**/*.log
119+
/home/runner/.ccm/**/*.yaml
120+
/home/runner/.ccm/**/*.yml
121+
/home/runner/.ccm/**/conf/**
122+
/home/runner/.ccm/**/logs/**
123+
if-no-files-found: warn
124+
include-hidden-files: true
125+
retention-days: 14

tests/integration/__init__.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import time
3232
import traceback
3333
import platform
34+
from pathlib import Path
3435
from threading import Event
3536
from subprocess import call
3637
from itertools import groupby
@@ -72,10 +73,71 @@
7273
if not os.path.exists(path):
7374
os.mkdir(path)
7475

76+
_failed_reports = []
77+
_preserved_ccm_log_scopes = set()
78+
_log_artifact_dir = os.environ.get('INTEGRATION_LOG_ARTIFACT_DIR', 'integration-test-logs')
79+
_preserved_file_extensions = ('.conf', '.log', '.yaml', '.yml')
80+
7581
cass_version = None
7682
cql_version = None
7783

7884

85+
def record_failed_test_report(report):
86+
if report.failed:
87+
_failed_reports.append(report)
88+
89+
90+
def _forget_preserved_ccm_log_scope(cluster_name=None):
91+
_preserved_ccm_log_scopes.discard(cluster_name)
92+
_preserved_ccm_log_scopes.discard(None)
93+
94+
95+
def preserve_ccm_logs_on_failure(cluster_name=None):
96+
if not _failed_reports:
97+
return
98+
99+
ccm_root = Path(path)
100+
if cluster_name:
101+
ccm_root = ccm_root / cluster_name
102+
if not ccm_root.is_dir():
103+
return
104+
105+
destination = Path(_log_artifact_dir).resolve()
106+
ccm_destination = destination / 'ccm'
107+
if cluster_name:
108+
ccm_destination = ccm_destination / cluster_name
109+
ccm_destination.mkdir(parents=True, exist_ok=True)
110+
111+
with (destination / 'failed-tests.txt').open('w') as failed_tests:
112+
for report in _failed_reports:
113+
failed_tests.write('%s %s\n' % (report.when, report.nodeid))
114+
115+
if (cluster_name in _preserved_ccm_log_scopes or
116+
(cluster_name and None in _preserved_ccm_log_scopes)):
117+
log.debug("CCM logs already preserved for %s", cluster_name or ccm_root)
118+
return
119+
120+
# Preserve CCM logs/configs before cleanup removes the cluster directories.
121+
for source_file in ccm_root.rglob('*'):
122+
if not source_file.is_file():
123+
continue
124+
125+
rel_file = source_file.relative_to(ccm_root)
126+
preserve_all = bool({'conf', 'logs'}.intersection(rel_file.parts))
127+
if not preserve_all and not source_file.name.endswith(_preserved_file_extensions):
128+
continue
129+
130+
destination_file = ccm_destination / rel_file
131+
try:
132+
destination_file.parent.mkdir(parents=True, exist_ok=True)
133+
shutil.copy2(source_file, destination_file)
134+
except OSError as exc:
135+
log.warning("Failed to preserve file %s: %s", source_file, exc)
136+
137+
_preserved_ccm_log_scopes.add(cluster_name)
138+
log.info("Preserved ccm logs for failed tests under %s", destination)
139+
140+
79141
def get_server_versions():
80142
"""
81143
Probe system.local table to determine Cassandra and CQL version.
@@ -383,7 +445,10 @@ def remove_cluster():
383445
tries = 0
384446
while tries < 100:
385447
try:
448+
cluster_name = CCM_CLUSTER.name
449+
preserve_ccm_logs_on_failure(CCM_CLUSTER.name)
386450
CCM_CLUSTER.remove()
451+
_forget_preserved_ccm_log_scope(cluster_name)
387452
CCM_CLUSTER = None
388453
return
389454
except OSError:
@@ -455,7 +520,9 @@ def use_cluster(cluster_name, nodes, ipformat=None, start=True, workloads=None,
455520
try:
456521
CCM_CLUSTER = CCMClusterFactory.load(path, cluster_name)
457522
log.debug("Found existing CCM cluster, {0}; clearing.".format(cluster_name))
523+
preserve_ccm_logs_on_failure(cluster_name)
458524
CCM_CLUSTER.clear()
525+
_forget_preserved_ccm_log_scope(cluster_name)
459526
CCM_CLUSTER.set_install_dir(**ccm_options)
460527
CCM_CLUSTER.set_configuration_options(configuration_options)
461528
CCM_CLUSTER.set_dse_configuration_options(dse_options)
@@ -471,7 +538,9 @@ def use_cluster(cluster_name, nodes, ipformat=None, start=True, workloads=None,
471538
# Make sure we cleanup old cluster dir if it exists
472539
cluster_path = os.path.join(path, cluster_name)
473540
if os.path.exists(cluster_path):
541+
preserve_ccm_logs_on_failure(cluster_name)
474542
shutil.rmtree(cluster_path)
543+
_forget_preserved_ccm_log_scope(cluster_name)
475544

476545
if SCYLLA_VERSION:
477546
# `experimental: True` enable all experimental features.
@@ -553,6 +622,7 @@ def teardown_package():
553622
return
554623
# when multiple modules are run explicitly, this runs between them
555624
# need to make sure CCM_CLUSTER is properly cleared for that case
625+
preserve_ccm_logs_on_failure()
556626
remove_cluster()
557627
for cluster_name in [CLUSTER_NAME, MULTIDC_CLUSTER_NAME]:
558628
try:

tests/integration/conftest.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44
import pytest
55
from ccmlib.cluster_factory import ClusterFactory as CCMClusterFactory
66

7-
from tests.integration import teardown_package
7+
from tests.integration import record_failed_test_report, preserve_ccm_logs_on_failure, teardown_package
88

99
from . import CLUSTER_NAME, SINGLE_NODE_CLUSTER_NAME, MULTIDC_CLUSTER_NAME
1010
from . import path as ccm_path
1111

1212

13+
def pytest_runtest_logreport(report):
14+
record_failed_test_report(report)
15+
16+
1317
@pytest.fixture(scope="session", autouse=True)
1418
def cleanup_clusters():
1519

1620
yield
1721

22+
preserve_ccm_logs_on_failure()
23+
1824
if not os.environ.get('DISABLE_CLUSTER_CLEANUP'):
1925
for cluster_name in [CLUSTER_NAME, SINGLE_NODE_CLUSTER_NAME, MULTIDC_CLUSTER_NAME,
2026
'cluster_tests', 'shared_aware', 'sni_proxy', 'test_ip_change', 'test_client_routes_replacement']:
@@ -29,4 +35,5 @@ def cleanup_clusters():
2935
def setup_and_teardown_packages():
3036
print('setup')
3137
yield
38+
preserve_ccm_logs_on_failure()
3239
teardown_package()

0 commit comments

Comments
 (0)