1818import getpass
1919import logging
2020import os
21+ import re
2122import shutil
2223import subprocess
2324import 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
342369def 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