From 27d9a805d9f884d8e30ca22c191dd70e0add82e1 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 7 Oct 2025 09:16:22 +0200 Subject: [PATCH] WIP: Fix carriage return --- pkgs/clan-cli/clan_lib/cmd/__init__.py | 28 +++++++-- .../clan_lib/cmd/test_carriage_return.py | 59 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 pkgs/clan-cli/clan_lib/cmd/test_carriage_return.py diff --git a/pkgs/clan-cli/clan_lib/cmd/__init__.py b/pkgs/clan-cli/clan_lib/cmd/__init__.py index b730a34b4..c83e5ecad 100644 --- a/pkgs/clan-cli/clan_lib/cmd/__init__.py +++ b/pkgs/clan-cli/clan_lib/cmd/__init__.py @@ -126,9 +126,19 @@ def handle_io( # If Log.STDOUT is set, log the stdout output if ret and log in [Log.STDOUT, Log.BOTH]: - lines = ret.decode("utf-8", "replace").rstrip("\n").rstrip().split("\n") + decoded = ret.decode("utf-8", "replace").rstrip("\n").rstrip() + # Handle carriage returns: split by \r and only keep the last segment of each line + # This prevents progress indicators that use \r from creating multiple log lines + lines = [] + for line in decoded.split("\n"): + if "\r" in line: + # Only keep the last segment after the final \r (what would be visible) + lines.append(line.split("\r")[-1]) + else: + lines.append(line) for line in lines: - cmdlog.info(line, extra=stdout_extra) + if line: # Only log non-empty lines + cmdlog.info(line, extra=stdout_extra) # If stdout file is set, stream the stdout output if ret and stdout: @@ -143,9 +153,19 @@ def handle_io( # If Log.STDERR is set, log the stderr output if ret and log in [Log.STDERR, Log.BOTH]: - lines = ret.decode("utf-8", "replace").rstrip("\n").rstrip().split("\n") + decoded = ret.decode("utf-8", "replace").rstrip("\n").rstrip() + # Handle carriage returns: split by \r and only keep the last segment of each line + # This prevents progress indicators that use \r from creating multiple log lines + lines = [] + for line in decoded.split("\n"): + if "\r" in line: + # Only keep the last segment after the final \r (what would be visible) + lines.append(line.split("\r")[-1]) + else: + lines.append(line) for line in lines: - cmdlog.info(line, extra=stderr_extra) + if line: # Only log non-empty lines + cmdlog.info(line, extra=stderr_extra) # If stderr file is set, stream the stderr output if ret and stderr: diff --git a/pkgs/clan-cli/clan_lib/cmd/test_carriage_return.py b/pkgs/clan-cli/clan_lib/cmd/test_carriage_return.py new file mode 100644 index 000000000..0f91f5cce --- /dev/null +++ b/pkgs/clan-cli/clan_lib/cmd/test_carriage_return.py @@ -0,0 +1,59 @@ +import logging +from pathlib import Path + +from clan_lib.cmd import Log, RunOpts, run + + +def test_carriage_return_handling(caplog: logging.LogRecord) -> None: + """Test that carriage returns are handled properly to avoid duplicate progress lines.""" + # Set logging to capture INFO level + caplog.set_level(logging.INFO) + + # Run a command that simulates mkfs.ext4 progress output with carriage returns + result = run( + [ + "bash", + "-c", + 'printf "Progress: 1/5\\rProgress: 2/5\\rProgress: 3/5\\rProgress: 4/5\\rProgress: 5/5\\n"', + ], + RunOpts(log=Log.STDOUT, cwd=Path.cwd()), + ) + + # Check that the command succeeded + assert result.returncode == 0 + + # Check that only the final progress line was logged, not all intermediate ones + log_messages = [record.message for record in caplog.records] + + # Should only see the final "Progress: 5/5" message, not all the intermediate ones + assert "Progress: 5/5" in log_messages + # Count how many "Progress:" messages there are - should be only 1 + progress_messages = [msg for msg in log_messages if "Progress:" in msg] + assert len(progress_messages) == 1, f"Expected 1 progress message, got {len(progress_messages)}: {progress_messages}" + + +def test_carriage_return_multiple_lines(caplog: logging.LogRecord) -> None: + """Test carriage returns on multiple separate lines.""" + caplog.set_level(logging.INFO) + + # Simulate multiple lines with progress indicators + result = run( + [ + "bash", + "-c", + 'printf "Line 1\\nProgress: 10%%\\rProgress: 50%%\\rProgress: 100%%\\nLine 3\\n"', + ], + RunOpts(log=Log.STDOUT, cwd=Path.cwd()), + ) + + assert result.returncode == 0 + log_messages = [record.message for record in caplog.records] + + # Should see Line 1, Progress: 100%, and Line 3 + assert "Line 1" in log_messages + assert "Progress: 100%" in log_messages + assert "Line 3" in log_messages + + # Should NOT see the intermediate progress messages + assert "Progress: 10%" not in log_messages + assert "Progress: 50%" not in log_messages