Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ def print_plan_details(plan_entry: Dict[str, Any]) -> None:
print("")


def format_duration(seconds: float) -> str:
"""Formats duration in a human-readable way (e.g., 2m 05s)."""
if seconds < 60:
return f"{seconds:.1f}s"
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format_duration() can produce the inconsistent output "60.0s" for inputs just under 60 seconds (e.g., 59.95 rounds to 60.0 with :.1f while still taking the < 60 branch). Consider truncating (not rounding) sub-minute values, or adding a guard so any value that formats to 60.0s is emitted as "1m 00s" instead.

Suggested change
return f"{seconds:.1f}s"
# Use truncation instead of rounding for sub-minute values so that
# inputs like 59.95 do not produce the inconsistent "60.0s" output.
# We keep exactly one decimal place by truncating toward zero.
truncated = int(seconds * 10) / 10.0
return f"{truncated:.1f}s"

Copilot uses AI. Check for mistakes.

minutes = int(seconds // 60)
rem_seconds = int(seconds % 60)
return f"{minutes}m {rem_seconds:02d}s"
Comment on lines +259 to +266

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of format_duration has an inconsistency in precision. For durations less than 60 seconds, it preserves one decimal place (e.g., 59.9s), but for durations greater than or equal to 60 seconds, it truncates the remaining seconds by using int(). For example, a duration of 60.9s becomes 1m 00s, losing almost a full second of information. This can be misleading.

A more consistent and accurate approach would be to round the duration to the nearest second and then format it. This avoids information loss and also handles the transition around 60 seconds more gracefully (e.g., 59.9s would be rounded to 1m 00s).

Here is a suggested implementation that addresses this. Note that this change will require updating the corresponding test cases in tests/test_ux.py (e.g., (59.9, "59.9s") would become (59.9, "1m 00s") and (65.5, "1m 05s") would become (65.5, "1m 06s")).

Suggested change
def format_duration(seconds: float) -> str:
"""Formats duration in a human-readable way (e.g., 2m 05s)."""
if seconds < 60:
return f"{seconds:.1f}s"
minutes = int(seconds // 60)
rem_seconds = int(seconds % 60)
return f"{minutes}m {rem_seconds:02d}s"
def format_duration(seconds: float) -> str:
"""Formats duration in a human-readable way (e.g., 2m 05s)."""
if seconds < 59.5:
return f"{seconds:.1f}s"
total_seconds = int(round(seconds))
minutes = total_seconds // 60
rem_seconds = total_seconds % 60
return f"{minutes}m {rem_seconds:02d}s"



def countdown_timer(seconds: int, message: str = "Waiting") -> None:
"""Shows a countdown timer if strictly in a TTY, otherwise just sleeps."""
if not USE_COLORS:
Expand Down Expand Up @@ -836,7 +846,7 @@ def verify_access_and_get_folders(
MAX_RETRIES,
wait_time,
)
time.sleep(wait_time)
countdown_timer(int(wait_time), "Retrying")


def get_all_existing_rules(
Expand Down Expand Up @@ -1044,7 +1054,7 @@ def create_folder(
log.info(
f"Folder '{sanitize_for_log(name)}' not found yet. Retrying in {wait_time}s..."
)
time.sleep(wait_time)
countdown_timer(int(wait_time), "Waiting for folder")

log.error(
f"Folder {sanitize_for_log(name)} was not found after creation and retries."
Expand Down Expand Up @@ -1651,7 +1661,7 @@ def validate_profile_input(value: str) -> bool:
f"{res['profile']:<{profile_col_width}} | "
f"{res['folders']:>10} | "
f"{res['rules']:>10,} | "
f"{res['duration']:>9.1f}s | "
f"{format_duration(res['duration']):>10} | "
f"{status_color}{res['status_label']:<15}{Colors.ENDC}"
)
total_folders += res["folders"]
Expand Down Expand Up @@ -1682,7 +1692,7 @@ def validate_profile_input(value: str) -> bool:
f"{'TOTAL':<{profile_col_width}} | "
f"{total_folders:>10} | "
f"{total_rules:>10,} | "
f"{total_duration:>9.1f}s | "
f"{format_duration(total_duration):>10} | "
f"{total_status_color}{total_status_text:<15}{Colors.ENDC}"
)
print("=" * table_width + "\n")
Expand Down
14 changes: 14 additions & 0 deletions tests/test_ux.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
from unittest.mock import MagicMock
import main
import pytest

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

third party import "import pytest" should be placed before "import main" Warning test

third party import "import pytest" should be placed before "import main"

Check warning

Code scanning / Prospector (reported by Codacy)

Unable to import 'pytest' (import-error) Warning test

Unable to import 'pytest' (import-error)

def test_countdown_timer_visuals(monkeypatch):
"""Verify that countdown_timer writes a progress bar to stderr."""
Expand Down Expand Up @@ -44,3 +45,16 @@
mock_stderr.write.assert_not_called()
# Should call sleep exactly once with full seconds
mock_sleep.assert_called_once_with(3)

@pytest.mark.parametrize("seconds, expected", [
(5.2, "5.2s"),
(0.0, "0.0s"),
(59.9, "59.9s"),
(60.0, "1m 00s"),
(65.5, "1m 05s"),
(125.0, "2m 05s"),
(3600.0, "60m 00s"),
])
def test_format_duration(seconds, expected):
"""Verify format_duration output for various inputs."""
assert main.format_duration(seconds) == expected

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Loading