diff --git a/engine/cli/utils/download_progress.cc b/engine/cli/utils/download_progress.cc index 898a0667b..017d71d0e 100644 --- a/engine/cli/utils/download_progress.cc +++ b/engine/cli/utils/download_progress.cc @@ -55,8 +55,9 @@ bool DownloadProgress::Handle(const DownloadType& event_type) { std::vector> items; indicators::show_console_cursor(false); - auto handle_message = [this, &bars, &items, - event_type](const std::string& message) { + auto start = std::chrono::steady_clock::now(); + auto handle_message = [this, &bars, &items, event_type, + start](const std::string& message) { CTL_INF(message); auto pad_string = [](const std::string& str, @@ -80,7 +81,7 @@ bool DownloadProgress::Handle(const DownloadType& event_type) { if (ev.download_task_.type != event_type) { return; } - + auto now = std::chrono::steady_clock::now(); if (!bars) { bars = std::make_unique< indicators::DynamicProgress>(); @@ -91,7 +92,7 @@ bool DownloadProgress::Handle(const DownloadType& event_type) { indicators::option::End{"]"}, indicators::option::PrefixText{pad_string(Repo2Engine(i.id))}, indicators::option::ForegroundColor{indicators::Color::white}, - indicators::option::ShowRemainingTime{true})); + indicators::option::ShowRemainingTime{false})); bars->push_back(*(items.back())); } } @@ -101,6 +102,17 @@ bool DownloadProgress::Handle(const DownloadType& event_type) { uint64_t downloaded = it.downloadedBytes.value_or(0u); uint64_t total = it.bytes.value_or(std::numeric_limits::max()); + auto d = std::chrono::duration_cast(now - start) + .count(); + uint64_t bytes_per_sec = downloaded / (d + 1); + std::string time_remaining; + if (downloaded == total || bytes_per_sec == 0) { + time_remaining = "00m:00s"; + } else { + time_remaining = format_utils::TimeDownloadFormat( + (total - downloaded) / bytes_per_sec); + } + (*bars)[i].set_option(indicators::option::PrefixText{ pad_string(Repo2Engine(it.id)) + std::to_string(int(static_cast(downloaded) / total * 100)) + @@ -108,6 +120,7 @@ bool DownloadProgress::Handle(const DownloadType& event_type) { (*bars)[i].set_progress( int(static_cast(downloaded) / total * 100)); (*bars)[i].set_option(indicators::option::PostfixText{ + time_remaining + " " + format_utils::BytesToHumanReadable(downloaded) + "/" + format_utils::BytesToHumanReadable(total)}); } else if (ev.type_ == DownloadStatus::DownloadSuccess) { @@ -115,8 +128,8 @@ bool DownloadProgress::Handle(const DownloadType& event_type) { it.bytes.value_or(std::numeric_limits::max()); (*bars)[i].set_progress(100); auto total_str = format_utils::BytesToHumanReadable(total); - (*bars)[i].set_option( - indicators::option::PostfixText{total_str + "/" + total_str}); + (*bars)[i].set_option(indicators::option::PostfixText{ + "00m:00s " + total_str + "/" + total_str}); (*bars)[i].set_option(indicators::option::PrefixText{ pad_string(Repo2Engine(it.id)) + "100%"}); (*bars)[i].set_progress(100); diff --git a/engine/test/components/test_format_utils.cc b/engine/test/components/test_format_utils.cc index 4042b4e65..cd777d5fa 100644 --- a/engine/test/components/test_format_utils.cc +++ b/engine/test/components/test_format_utils.cc @@ -8,7 +8,8 @@ class FormatUtilsTest : public ::testing::Test {}; TEST_F(FormatUtilsTest, WriteKeyValue) { { YAML::Node node; - std::string result = format_utils::writeKeyValue("key", node["does_not_exist"]); + std::string result = + format_utils::writeKeyValue("key", node["does_not_exist"]); EXPECT_EQ(result, ""); } @@ -109,4 +110,45 @@ TEST_F(FormatUtilsTest, PrintFloat) { result = format_utils::print_float("key", std::numeric_limits::quiet_NaN()); EXPECT_EQ(result, ""); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_ZeroSeconds) { + EXPECT_EQ(format_utils::TimeDownloadFormat(0), "00m:00s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_LessThanOneMinute) { + EXPECT_EQ(format_utils::TimeDownloadFormat(30), "00m:30s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_ExactlyOneMinute) { + EXPECT_EQ(format_utils::TimeDownloadFormat(60), "01m:00s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_LessThanOneHour) { + EXPECT_EQ(format_utils::TimeDownloadFormat(125), "02m:05s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_ExactlyOneHour) { + EXPECT_EQ(format_utils::TimeDownloadFormat(3600), "01h:00m:00s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_MoreThanOneHour) { + EXPECT_EQ(format_utils::TimeDownloadFormat(3661), "01h:01m:01s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_LessThanOneDay) { + EXPECT_EQ(format_utils::TimeDownloadFormat(86399), + "23h:59m:59s"); // 1 second less than a day +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_ExactlyOneDay) { + EXPECT_EQ(format_utils::TimeDownloadFormat(86400), "01d:00h:00m:00s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_MoreThanOneDay) { + EXPECT_EQ(format_utils::TimeDownloadFormat(90061), "01d:01h:01m:01s"); +} + +TEST_F(FormatUtilsTest, TimeDownloadFormat_LargeNumberOfSeconds) { + EXPECT_EQ(format_utils::TimeDownloadFormat(1000000), "11d:13h:46m:40s"); } \ No newline at end of file diff --git a/engine/test/components/test_string_utils.cc b/engine/test/components/test_string_utils.cc index c412c5ec4..e396f0ed1 100644 --- a/engine/test/components/test_string_utils.cc +++ b/engine/test/components/test_string_utils.cc @@ -287,3 +287,5 @@ TEST_F(StringUtilsTestSuite, LargeInputPerformance) { // and doesn't crash with large inputs EXPECT_EQ(RemoveSubstring(large_input, to_remove), ""); } + + diff --git a/engine/utils/format_utils.h b/engine/utils/format_utils.h index 38ab1b3fb..141866378 100644 --- a/engine/utils/format_utils.h +++ b/engine/utils/format_utils.h @@ -82,10 +82,10 @@ inline std::string writeKeyValue(const std::string& key, }; inline std::string BytesToHumanReadable(uint64_t bytes) { - const uint64_t KB = 1024; - const uint64_t MB = KB * 1024; - const uint64_t GB = MB * 1024; - const uint64_t TB = GB * 1024; + constexpr const uint64_t KB = 1024; + constexpr const uint64_t MB = KB * 1024; + constexpr const uint64_t GB = MB * 1024; + constexpr const uint64_t TB = GB * 1024; double result; std::string unit; @@ -112,4 +112,40 @@ inline std::string BytesToHumanReadable(uint64_t bytes) { out << std::fixed << std::setprecision(2) << result << " " << unit; return out.str(); } + +inline std::string TimeDownloadFormat(int seconds) { + // Constants for time units + constexpr const uint64_t kSecondsInMinute = 60; + constexpr const uint64_t kSecondsInHour = kSecondsInMinute * 60; + constexpr const uint64_t kSecondsInDay = kSecondsInHour * 24; + + uint64_t days = seconds / kSecondsInDay; + seconds %= kSecondsInDay; + + uint64_t hours = seconds / kSecondsInHour; + seconds %= kSecondsInHour; + + uint64_t minutes = seconds / kSecondsInMinute; + seconds %= kSecondsInMinute; + + std::ostringstream oss; + + auto pad = [](const std::string& v) -> std::string { + if (v.size() == 1) + return "0" + v; + return v; + }; + + if (days > 0) { + oss << pad(std::to_string(days)) << "d:"; + } + if (hours > 0 || days > 0) { + oss << pad(std::to_string(hours)) << "h:"; + } + oss << pad(std::to_string(minutes)) << "m:"; + + oss << pad(std::to_string(seconds)) << "s"; + + return oss.str(); +}; } // namespace format_utils