Skip to content

Commit 04d74e1

Browse files
committed
refactor: redirect pytest output to log file without ANSI sequences
Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
1 parent a199365 commit 04d74e1

2 files changed

Lines changed: 49 additions & 22 deletions

File tree

compat_kit/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- At least one x86_64 server with XCP-ng installed (the release you want to test),
88
[updated with the latest updates](https://xcp-ng.org/docs/updates.html).
99
- A disk for the system, and a separate, empty disk for testing.
10-
- Optionally, a second identical host. If provided, will be automatically
10+
- Optionally, a second identical host. If provided, it will be automatically
1111
joined to the first to form a pool for storage migration and live migration
1212
tests. **Both hosts must have the same SSH root password.**
1313
- Internet access to download the test image.
@@ -18,6 +18,8 @@
1818

1919
The test VM is configured with a known access key and is **strictly reserved for testing purposes only**.
2020

21+
Test VM should not be exposed to untrusted networks.
22+
2123
## Quick Start
2224

2325
Interactive mode (recommended):

compat_kit/entrypoint.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import getpass
1919
import logging
2020
import os
21+
import re
2122
import shutil
2223
import subprocess
2324
import sys
@@ -231,9 +232,9 @@ def validate_pool_constraints(pool: Pool, second_host: str | None) -> None:
231232
if second_host is None:
232233
if num_hosts != 1:
233234
raise ValueError(
234-
f"Pool constraint violated: Master host's pool contains {num_hosts} host(s), "
235-
f"but only 1 host was provided. The pool must contain only the specified hosts. "
236-
f"Please eject the extra hosts before running the compatibility kit."
235+
f"Pool constraint violated: The master belongs to a pool with {num_hosts} host(s). "
236+
f"All hosts in that pool must be provided to the compatibility kit, "
237+
f"or the extra hosts must be ejected from the pool first."
237238
)
238239
else: # second_host is provided
239240
second_in_pool = any(h.hostname_or_ip == second_host for h in pool.hosts)
@@ -242,17 +243,18 @@ def validate_pool_constraints(pool: Pool, second_host: str | None) -> None:
242243
# Second host already in pool - verify it's a 2-host pool
243244
if num_hosts != 2:
244245
raise ValueError(
245-
f"Pool constraint violated: Second host is already in master's pool, "
246-
f"but the pool contains {num_hosts} host(s). Only 2 hosts are expected "
247-
f"(master and second). Please eject the extra hosts before running the compatibility kit."
246+
f"Pool constraint violated: The pool contains {num_hosts} host(s) "
247+
f"(including the second host you provided). "
248+
f"All hosts in that pool must be provided to the compatibility kit, "
249+
f"or the extra hosts must be ejected from the pool first."
248250
)
249251
else:
250252
# Second host not in pool - master must be alone
251253
if num_hosts != 1:
252254
raise ValueError(
253-
f"Pool constraint violated: Master host's pool contains {num_hosts} host(s), "
254-
f"but only the master host should be present before joining the second host. "
255-
f"Please eject the extra hosts before running the compatibility kit."
255+
f"Pool constraint violated: The master belongs to a pool with {num_hosts} host(s), "
256+
f"but you did not provide all of them to the compatibility kit. "
257+
f"Either provide all the pool hosts, or eject the extra hosts from the pool first."
256258
)
257259

258260

@@ -314,8 +316,6 @@ def run_pytest(phase: int, test_args: list[str], log_file: str) -> None:
314316
'--color=yes',
315317
'--no-header',
316318
'--maxfail=0',
317-
'--log-file-level=debug',
318-
f'--log-file={log_path}',
319319
f'--log-cli-level={logging.getLevelName(state.log_level)}',
320320
f'--vm={state.vm_image_url}',
321321
] + test_args
@@ -327,16 +327,43 @@ def run_pytest(phase: int, test_args: list[str], log_file: str) -> None:
327327

328328
logging.debug(f"Running: {' '.join(cmd)}")
329329
try:
330-
subprocess.run(cmd, check=True, timeout=3600)
331-
logging.info(f"Phase {phase}: Tests completed successfully")
332-
except subprocess.CalledProcessError as e:
333-
logging.warning(f"Phase {phase}: Tests failed with exit code {e.returncode}")
334-
state.tests_failed = True
335-
# Don't raise, allow other phases to run
330+
ansi_escape = re.compile(br'\x1b\[[0-9;]*[a-zA-Z]')
331+
332+
# Launch pytest. We force colors so the terminal output stays pretty.
333+
# 'bufsize=1' and 'universal_newlines=False' allow us to process line by line.
334+
process = subprocess.Popen(
335+
cmd,
336+
stdout=subprocess.PIPE,
337+
stderr=subprocess.STDOUT
338+
)
339+
340+
assert process.stdout is not None
341+
with open(log_path, "wb") as f:
342+
# Read from the pipe until the process finishes
343+
for line in iter(process.stdout.readline, b''):
344+
# 1. Write the original colorful line to the actual terminal
345+
sys.stdout.buffer.write(line)
346+
sys.stdout.buffer.flush()
347+
348+
# 2. Strip the codes and write the clean text to the file
349+
clean_line = ansi_escape.sub(b'', line)
350+
f.write(clean_line)
351+
f.flush()
352+
353+
process.wait()
354+
if process.returncode != 0:
355+
logging.warning(f"Phase {phase}: Tests failed with exit code {process.returncode}")
356+
state.tests_failed = True
357+
else:
358+
logging.info(f"Phase {phase}: Tests completed successfully")
336359
except subprocess.TimeoutExpired:
337360
logging.error(f"Phase {phase}: Tests timed out")
338361
state.tests_failed = True
339362
raise
363+
except Exception as e:
364+
logging.error(f"Phase {phase}: Tests failed: {e}")
365+
state.tests_failed = True
366+
raise
340367

341368

342369
def setup_config_files() -> None:
@@ -542,7 +569,7 @@ def run_workflow(
542569
print_summary()
543570
except KeyboardInterrupt:
544571
logging.error("Interrupted by user")
545-
sys.exit(1)
572+
sys.exit(130)
546573
except Exception as e:
547574
logging.error(f"Error: {e}", exc_info=True)
548575
sys.exit(1)
@@ -555,9 +582,7 @@ def main() -> None:
555582
parser.add_argument('--master-host', help="IP or hostname of the pool master")
556583
parser.add_argument("--second-host", help="IP or hostname of the second host (optional, for pool tests)")
557584
parser.add_argument("--password", help="SSH root password (if not provided, will be prompted)")
558-
parser.add_argument("--log-dir", default="/app/logs",
559-
help="Directory where log files will be written (default: /app/logs)",
560-
)
585+
parser.add_argument("--log-dir", default="./logs", help="Directory where log files will be written")
561586
parser.add_argument("--log-level", default="info",
562587
choices=["debug", "info", "warning", "error", "critical"],
563588
help="Logging level (default: INFO)")

0 commit comments

Comments
 (0)