From c7a568f6777a3e55e8b8ee0052d16e9ffd8f3129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Wed, 4 Dec 2024 14:56:45 +0100 Subject: [PATCH 1/8] Remove new line printing fix in non-interactive mode Since now every finished progressbar ends with a new line and in non interactive mode only finished progressbars are printed this additional print should never be needed. --- libdnf5-cli/progressbar/multi_progress_bar.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libdnf5-cli/progressbar/multi_progress_bar.cpp b/libdnf5-cli/progressbar/multi_progress_bar.cpp index 2aa67dfb6..cb9f026d9 100644 --- a/libdnf5-cli/progressbar/multi_progress_bar.cpp +++ b/libdnf5-cli/progressbar/multi_progress_bar.cpp @@ -112,8 +112,6 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) { text_buffer << "\033[" << (mbar.num_of_lines_to_clear - 1) << "A"; } text_buffer << "\r"; - } else if (mbar.line_printed) { - text_buffer << std::endl; } mbar.num_of_lines_to_clear = 0; From 72815336eac15c8facfd7a4511ed75200281d162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Wed, 4 Dec 2024 15:22:39 +0100 Subject: [PATCH 2/8] Fix new line printing for unfinished Total progress bar Previously the new line was always printed resulting in excessive empty line which was visible for example during the downloading packages phase of an update: ``` [ 1/109] kernel-modules-0:6.13.0-0.rc1.20241202gite70140ba0d2b.14.fc42.x86_64 7% [= ] | 781.3 KiB/s | 4.9 MiB | -01m18s [ 2/109] kernel-core-0:6.13.0-0.rc1.20241202gite70140ba0d2b.14.fc42.x86_64 8% [== ] | 381.1 KiB/s | 1.5 MiB | -00m45s [ 3/109] kernel-modules-core-0:6.13.0-0.rc1.20241202gite70140ba0d2b.14.fc42.x86_64 3% [= ] | 355.8 KiB/s | 1.2 MiB | -01m47s ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ [ 0/109] Total 3% [= ] | 1.5 MiB/s | 7.6 MiB | -02m05s ``` (last line is empty) This was introduced recently in PR https://github.com/rpm-software-management/dnf5/pull/1805 --- libdnf5-cli/progressbar/multi_progress_bar.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libdnf5-cli/progressbar/multi_progress_bar.cpp b/libdnf5-cli/progressbar/multi_progress_bar.cpp index cb9f026d9..1787d04bc 100644 --- a/libdnf5-cli/progressbar/multi_progress_bar.cpp +++ b/libdnf5-cli/progressbar/multi_progress_bar.cpp @@ -204,8 +204,12 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) { } text_buffer << mbar.total; - text_buffer << std::endl; - mbar.num_of_lines_to_clear += 3; + mbar.num_of_lines_to_clear += 2; + if (mbar.total.is_finished()) { + text_buffer << std::endl; + } else { + mbar.line_printed = true; + } } stream << text_buffer.str(); // Single syscall to output all commands From 1b34c4e8bb39740cbc875c72f35b9ab028317724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Tue, 3 Dec 2024 12:36:00 +0100 Subject: [PATCH 3/8] Add `cursor_down` TTY_COMMAND --- include/libdnf5-cli/tty.hpp | 1 + libdnf5-cli/tty.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/include/libdnf5-cli/tty.hpp b/include/libdnf5-cli/tty.hpp index 6db12b0aa..ed39ff271 100644 --- a/include/libdnf5-cli/tty.hpp +++ b/include/libdnf5-cli/tty.hpp @@ -46,6 +46,7 @@ LIBDNF_CLI_API std::ostream & white(std::ostream & stream); LIBDNF_CLI_API std::ostream & clear_line(std::ostream & stream); LIBDNF_CLI_API std::ostream & cursor_up(std::ostream & stream); +LIBDNF_CLI_API std::ostream & cursor_down(std::ostream & stream); LIBDNF_CLI_API std::ostream & cursor_hide(std::ostream & stream); LIBDNF_CLI_API std::ostream & cursor_show(std::ostream & stream); diff --git a/libdnf5-cli/tty.cpp b/libdnf5-cli/tty.cpp index 2eb7688bf..b72761b37 100644 --- a/libdnf5-cli/tty.cpp +++ b/libdnf5-cli/tty.cpp @@ -107,6 +107,9 @@ TTY_COMMAND(clear_line, "\033[2K") // tty::cursor_up TTY_COMMAND(cursor_up, "\x1b[A") +// tty::cursor_down +TTY_COMMAND(cursor_down, "\x1b[B") + // tty::cursor_hide TTY_COMMAND(cursor_hide, "\x1b[?25l") From 8077a29f2c2085f91b78468ce687bced8c90ebce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Wed, 4 Dec 2024 15:10:33 +0100 Subject: [PATCH 4/8] Fix overwriting of old output from `MultiProgressBar` Since PR https://github.com/rpm-software-management/dnf5/pull/1825 we are overwriting old output instead of specifically clearing it. This causes problems when messages are removed from progressbars like it happens when scriptlets finish successfully without any logs. Closes: https://github.com/rpm-software-management/dnf5/issues/1899 Also adds couple describing comments. --- .../progressbar/multi_progress_bar.hpp | 4 +++ .../progressbar/multi_progress_bar.cpp | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/include/libdnf5-cli/progressbar/multi_progress_bar.hpp b/include/libdnf5-cli/progressbar/multi_progress_bar.hpp index e5683e0db..557a2deb1 100644 --- a/include/libdnf5-cli/progressbar/multi_progress_bar.hpp +++ b/include/libdnf5-cli/progressbar/multi_progress_bar.hpp @@ -43,6 +43,9 @@ class LIBDNF_CLI_API MultiProgressBar { ~MultiProgressBar(); void add_bar(std::unique_ptr && bar); + + // In interactive mode MultiProgressBar doesn't print a newline after unfinished progressbars. + // Finished progressbars always end with a newline. void print() { std::cerr << *this; std::cerr << std::flush; @@ -69,6 +72,7 @@ class LIBDNF_CLI_API MultiProgressBar { std::vector bars_todo; std::vector bars_done; DownloadProgressBar total; + // Whether the last line was printed without a new line ending (such as an in progress bar) bool line_printed{false}; std::size_t num_of_lines_to_clear{0}; }; diff --git a/libdnf5-cli/progressbar/multi_progress_bar.cpp b/libdnf5-cli/progressbar/multi_progress_bar.cpp index 1787d04bc..d5016d7d3 100644 --- a/libdnf5-cli/progressbar/multi_progress_bar.cpp +++ b/libdnf5-cli/progressbar/multi_progress_bar.cpp @@ -106,6 +106,9 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) { text_buffer.str(""); text_buffer.clear(); + std::size_t last_num_of_lines_to_clear = mbar.num_of_lines_to_clear; + std::size_t num_of_lines_permanent = 0; + if (is_interactive && mbar.num_of_lines_to_clear > 0) { if (mbar.num_of_lines_to_clear > 1) { // Move the cursor up by the number of lines we want to write over @@ -133,6 +136,8 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) { numbers.pop_back(); text_buffer << *bar; text_buffer << std::endl; + num_of_lines_permanent++; + num_of_lines_permanent += bar->get_messages().size(); mbar.bars_done.push_back(bar); // TODO(dmach): use iterator mbar.bars_todo.erase(mbar.bars_todo.begin() + static_cast(i)); @@ -212,6 +217,26 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) { } } + // If we have written less lines than last time we need to clear the rest otherwise + // there would be garbage under the updated progressbar. This is because normally + // we don't actually clear the lines we just write over the old output to ensure smooth + // output updating. + // TODO(amatej): It would be sufficient to do this only once after all progressbars have + // finished but unfortunaly MultiProgressBar doesn't have a pImpl so we cannot + // store the highest line count it had. We could fix this when breaking ABI. + size_t all_written = num_of_lines_permanent + mbar.num_of_lines_to_clear; + if (last_num_of_lines_to_clear > all_written) { + auto delta = last_num_of_lines_to_clear - all_written; + if (!mbar.line_printed) { + text_buffer << tty::clear_line; + } + for (std::size_t i = 0; i < delta; i++) { + text_buffer << tty::cursor_down << tty::clear_line; + } + // Move cursor back up after clearing lines leftover from previous print + text_buffer << "\033[" << delta << "A"; + } + stream << text_buffer.str(); // Single syscall to output all commands return stream; From 190527aea56b5b324e917de56887d6c06a3b0f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Tue, 3 Dec 2024 12:38:37 +0100 Subject: [PATCH 5/8] When determining interactivity add `DNF5_FORCE_INTERACTIVE` override This is similar to the `FORCE_COLUMNS` override. --- libdnf5-cli/tty.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libdnf5-cli/tty.cpp b/libdnf5-cli/tty.cpp index b72761b37..51794ad3b 100644 --- a/libdnf5-cli/tty.cpp +++ b/libdnf5-cli/tty.cpp @@ -52,6 +52,18 @@ int get_width() { bool is_interactive() { + // Use a custom "DNF5_FORCE_INTERACTIVE" variable for testing purposes. + // "interactivity" depends on stdout configuration which is hard to control sometimes + char * force_interactive = std::getenv("DNF5_FORCE_INTERACTIVE"); + if (force_interactive != nullptr) { + try { + // Convert to an int which is then converted to bool, + // so when defined accept 0 as FALSE and non 0 as TRUE + return std::stoi(force_interactive); + } catch (std::invalid_argument & ex) { + } catch (std::out_of_range & ex) { + } + } return isatty(fileno(stdout)) == 1; } From 1735aee0e1d4cbe9b1c5801bb6309de768babf11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Wed, 4 Dec 2024 10:50:13 +0100 Subject: [PATCH 6/8] test: add `ASSERT_MATCHES` for convenient fnmatch pattern matching --- test/shared/utils.hpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/shared/utils.hpp b/test/shared/utils.hpp index d74432f14..46b2325ab 100644 --- a/test/shared/utils.hpp +++ b/test/shared/utils.hpp @@ -25,6 +25,7 @@ along with libdnf. If not, see . #include "utils/string.hpp" #include +#include #include #include #include @@ -347,4 +348,16 @@ std::vector to_vector(const libdnf5::Set to_vector(const libdnf5::rpm::ReldepList & reldep_list); std::vector to_vector(const libdnf5::rpm::PackageSet & package_set); +#define ASSERT_MATCHES(pattern, actual) \ + CPPUNIT_ASSERT_MESSAGE( \ + fmt::format("Expression:\n\"{}\"\ndoesn't match:\n\"{}\"", pattern.value, actual), pattern.matches(actual)) + +// Together with ASSERT_MATCHES implements convenient fnmatch pattern matching +class Pattern { +public: + Pattern(const char * str) : value(str){}; + bool matches(const std::string & actual) { return !fnmatch(value.c_str(), actual.c_str(), FNM_EXTMATCH); } + std::string value; +}; + #endif // TEST_LIBDNF5_UTILS_HPP From ffd3c83627513e75315c40dbf0781ce4cacfbda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Wed, 4 Dec 2024 11:03:22 +0100 Subject: [PATCH 7/8] test: enhance progressbar tests for non interactive mode Ensure the tests run in non interactive mode and force columns to 70. It also uses new `ASSERT_MATCH` macro. This simplifies the tests. In addition one test is extended and one new test is added. --- test/libdnf5-cli/test_progressbar.cpp | 104 +++++++++++++++++++------- test/libdnf5-cli/test_progressbar.hpp | 5 ++ 2 files changed, 84 insertions(+), 25 deletions(-) diff --git a/test/libdnf5-cli/test_progressbar.cpp b/test/libdnf5-cli/test_progressbar.cpp index 931181d4a..e1dcee9c5 100644 --- a/test/libdnf5-cli/test_progressbar.cpp +++ b/test/libdnf5-cli/test_progressbar.cpp @@ -21,9 +21,8 @@ along with libdnf. If not, see . #include "test_progressbar.hpp" #include "../shared/private_accessor.hpp" +#include "../shared/utils.hpp" -#include -#include #include #include @@ -38,23 +37,42 @@ create_getter(to_stream, &libdnf5::cli::progressbar::DownloadProgressBar::to_str } //namespace +void ProgressbarTest::setUp() { + // MultiProgressBar behaves differently depending on interactivity + setenv("DNF5_FORCE_INTERACTIVE", "0", 1); + // Force columns to 70 to make output independ of where it is run + setenv("FORCE_COLUMNS", "70", 1); +} + +void ProgressbarTest::tearDown() { + unsetenv("DNF5_FORCE_INTERACTIVE"); + unsetenv("FORCE_COLUMNS"); +} + void ProgressbarTest::test_download_progress_bar() { + // In non interactive mode download progress bar is printed only when finished. + auto download_progress_bar = std::make_unique(10, "test"); - download_progress_bar->set_ticks(10); - download_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + download_progress_bar->set_ticks(4); + download_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + auto download_progress_bar_raw = download_progress_bar.get(); std::ostringstream oss; (*download_progress_bar.*get(to_stream{}))(oss); - // When running the tests binary directly (run_tests_cli) it uses terminal size to determine white space count. - // To ensure the tests works match any number of white space. - std::string expected = "\\[0/0\\] test [ ]* 100% | 0.0 B\\/s | 10.0 B | ? "; - CPPUNIT_ASSERT_EQUAL_MESSAGE( - fmt::format("Expression: \"{}\" doesn't match output: \"{}\"", expected, oss.str()), - fnmatch(expected.c_str(), oss.str().c_str(), FNM_EXTMATCH), - 0); + Pattern expected = ""; + ASSERT_MATCHES(expected, oss.str()); + + download_progress_bar_raw->set_ticks(10); + download_progress_bar_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + (*download_progress_bar.*get(to_stream{}))(oss); + + expected = "\\[0/0\\] test 100% | ????? ??B\\/s | 10.0 B | ???????"; + ASSERT_MATCHES(expected, oss.str()); } void ProgressbarTest::test_multi_progress_bar() { + // In non interactive mode finished multiline progressbar ends with a new line. + auto download_progress_bar1 = std::make_unique(10, "test1"); download_progress_bar1->set_ticks(10); download_progress_bar1->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); @@ -68,18 +86,54 @@ void ProgressbarTest::test_multi_progress_bar() { multi_progress_bar.add_bar(std::move(download_progress_bar2)); std::ostringstream oss; oss << multi_progress_bar; - auto output = oss.str(); - - // When running the tests binary directly (run_tests_cli) it uses terminal size to determine white space and dash count. - // To ensure the tests works match any number of white space and dashes. - std::string expected = - "\\[1/2\\] test1 [ ]* 100% | 0.0 B\\/s | 10.0 B | ? \n" - "\\[2/2\\] test2 [ ]* 100% | 0.0 B\\/s | 10.0 B | ? \n" - "--------------------[-]*------------------------------------------------\n" - "\\[2/2\\] Total [ ]* 100% | ????? ??B\\/s | 20.0 B | ??m??s\n"; - - CPPUNIT_ASSERT_EQUAL_MESSAGE( - fmt::format("Expression: \"{}\" doesn't match output: \"{}\"", expected, oss.str()), - fnmatch(expected.c_str(), oss.str().c_str(), FNM_EXTMATCH), - 0); + + Pattern expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "----------------------------------------------------------------------\n" + "\\[2/2\\] Total 100% | ????? ??B\\/s | 20.0 B | ???????\n"; + ASSERT_MATCHES(expected, oss.str()); +} + +void ProgressbarTest::test_multi_progress_bar_unfinished() { + // In non-interacitve mode: + // - unfinished progressbars are not printed + // - total is not printed until all progressbars are finished + + auto download_progress_bar1 = std::make_unique(10, "test1"); + download_progress_bar1->set_ticks(10); + download_progress_bar1->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + + auto download_progress_bar2 = std::make_unique(10, "test2"); + download_progress_bar2->set_ticks(4); + download_progress_bar2->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + auto download_progress_bar2_raw = download_progress_bar2.get(); + + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + multi_progress_bar.add_bar(std::move(download_progress_bar1)); + multi_progress_bar.add_bar(std::move(download_progress_bar2)); + std::ostringstream oss; + + oss << multi_progress_bar; + Pattern expected = "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n"; + ASSERT_MATCHES(expected, oss.str()); + + // More iterations + download_progress_bar2_raw->set_ticks(5); + oss << multi_progress_bar; + download_progress_bar2_raw->set_ticks(6); + oss << multi_progress_bar; + expected = "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n"; + ASSERT_MATCHES(expected, oss.str()); + + // Next iteration + download_progress_bar2_raw->set_ticks(10); + download_progress_bar2_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + oss << multi_progress_bar; + expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "----------------------------------------------------------------------\n" + "\\[2/2\\] Total 100% | ????? ??B\\/s | 20.0 B | ???????\n"; + ASSERT_MATCHES(expected, oss.str()); } diff --git a/test/libdnf5-cli/test_progressbar.hpp b/test/libdnf5-cli/test_progressbar.hpp index e46e811af..2f66d3a81 100644 --- a/test/libdnf5-cli/test_progressbar.hpp +++ b/test/libdnf5-cli/test_progressbar.hpp @@ -29,12 +29,17 @@ class ProgressbarTest : public CppUnit::TestCase { CPPUNIT_TEST(test_download_progress_bar); CPPUNIT_TEST(test_multi_progress_bar); + CPPUNIT_TEST(test_multi_progress_bar_unfinished); CPPUNIT_TEST_SUITE_END(); public: + void setUp() override; + void tearDown() override; + void test_download_progress_bar(); void test_multi_progress_bar(); + void test_multi_progress_bar_unfinished(); }; From df0654547c0f91f9062d788f2a43cec91bfb8c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Wed, 4 Dec 2024 11:06:43 +0100 Subject: [PATCH 8/8] test: add progressbar tests for interactive mode In order to be able to compare the outputs it implements `perform_control_sequences` function which simulates a terminal emulator. Since this is a bit complicated there are also specific tests for this testing function. --- .../test_progressbar_interactive.cpp | 469 ++++++++++++++++++ .../test_progressbar_interactive.hpp | 56 +++ 2 files changed, 525 insertions(+) create mode 100644 test/libdnf5-cli/test_progressbar_interactive.cpp create mode 100644 test/libdnf5-cli/test_progressbar_interactive.hpp diff --git a/test/libdnf5-cli/test_progressbar_interactive.cpp b/test/libdnf5-cli/test_progressbar_interactive.cpp new file mode 100644 index 000000000..40b52af67 --- /dev/null +++ b/test/libdnf5-cli/test_progressbar_interactive.cpp @@ -0,0 +1,469 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libdnf. If not, see . +*/ + + +#include "test_progressbar_interactive.hpp" + +#include "../shared/private_accessor.hpp" +#include "../shared/utils.hpp" +#include "utils/string.hpp" + +#include +#include +#include + + +CPPUNIT_TEST_SUITE_REGISTRATION(ProgressbarInteractiveTest); + +namespace { + +// We look for control sequnces (such as move cursor up N times and carriage return) +// and perform them. It basically simulates a terminal emulator. +// +// It can look like: "\x1b[9A\r" = move cursor 9 times up followed by carriage return +std::string perform_control_sequences(std::string target) { + const char * raw_string = target.c_str(); + + size_t amount = 1; + enum ControlSequence { + EMPTY, + ESC, + CONTROL_SEQUENCE_INTRO, + CONTROL_SEQUENCE_AMOUNT, + }; + ControlSequence state = EMPTY; + std::vector output = {{}}; + size_t current_row = 0; + size_t current_column = 0; + for (size_t input_pos = 0; input_pos < strlen(raw_string); input_pos++) { + char current_value = raw_string[input_pos]; + if (current_value == '\n') { + current_row++; + current_column = 0; + } else if (current_value == '\r') { + current_column = 0; + } else if (current_value == '\x1b') { + state = ESC; + } else if (current_value == '[' && state == ESC) { + state = CONTROL_SEQUENCE_INTRO; + amount = 1; + } else if (isdigit(current_value) && state == CONTROL_SEQUENCE_INTRO) { + // current_value has to be one of: 0123456789 + amount = static_cast(current_value - '0'); + state = CONTROL_SEQUENCE_AMOUNT; + } else if ((state == CONTROL_SEQUENCE_INTRO || state == CONTROL_SEQUENCE_AMOUNT) && current_value == 'A') { + if (amount > current_row) { + CPPUNIT_FAIL(fmt::format("Cursor up control sequnce outside of output")); + } + current_row -= amount; + state = EMPTY; + } else if ((state == CONTROL_SEQUENCE_INTRO || state == CONTROL_SEQUENCE_AMOUNT) && current_value == 'B') { + current_row += amount; + state = EMPTY; + } else if (state == CONTROL_SEQUENCE_AMOUNT && amount == 2 && current_value == 'K') { + // erase the entire line + if (current_row < output.size()) { + output[current_row].clear(); + } + state = EMPTY; + } else { + while (current_row >= output.size()) { + output.push_back({}); + } + while (current_column >= output[current_row].length()) { + output[current_row].push_back(' '); + } + output[current_row][current_column] = current_value; + current_column++; + } + } + + while (current_row >= output.size()) { + output.push_back({}); + } + + return libdnf5::utils::string::join(output, "\n"); +} + +// Allows accessing private methods +create_private_getter_template; +create_getter(to_stream, &libdnf5::cli::progressbar::DownloadProgressBar::to_stream); + +} //namespace + +void ProgressbarInteractiveTest::setUp() { + // MultiProgressBar behaves differently depending on interactivity + setenv("DNF5_FORCE_INTERACTIVE", "1", 1); + // Force columns to 70 to make output independ of where it is run + setenv("FORCE_COLUMNS", "70", 1); +} + +void ProgressbarInteractiveTest::tearDown() { + unsetenv("DNF5_FORCE_INTERACTIVE"); + unsetenv("FORCE_COLUMNS"); +} + +void ProgressbarInteractiveTest::test_perform_control_sequences() { + // This tests the perform_control_sequences testing utility. + + std::string expected = "mmmmm\ndfdfdfdf"; + CPPUNIT_ASSERT_EQUAL(expected, perform_control_sequences("asd\ndfdfdfdf\x1b[1A\rmmmmm")); + + expected = "aaa\nmmmmbb\ncccccccc"; + CPPUNIT_ASSERT_EQUAL(expected, perform_control_sequences("aaa\nbbbbbb\ncccccccc\x1b[1A\rmmmm\n")); + + expected = "aaaa\nmmmmbb\ncccccccc\ndddddd"; + CPPUNIT_ASSERT_EQUAL(expected, perform_control_sequences("aaaa\nbbbbbb\ncccccccc\ndddddd\x1b[2A\rmmmm\n")); + + expected = "aaaa\nb\nmmmm\nxxx\nb\nbb\ncccccccc\ndddddd"; + CPPUNIT_ASSERT_EQUAL( + expected, perform_control_sequences("aaaa\nb\nb\nb\nb\nbb\ncccccccc\ndddddd\x1b[5A\rmmmm\nxxx\n")); + + expected = "aaa"; + CPPUNIT_ASSERT_EQUAL(expected, perform_control_sequences("xxx\x1b[2K\raaa")); + + expected = "xxx\naaa"; + CPPUNIT_ASSERT_EQUAL(expected, perform_control_sequences("xxx\n\x1b[2K\raaa")); + + expected = "xxx\naaa\nssss"; + CPPUNIT_ASSERT_EQUAL(expected, perform_control_sequences("xxx\nfffff\x1b[2K\raaa\nssss")); +} + +void ProgressbarInteractiveTest::test_download_progress_bar() { + // In interactive mode not finished download progress bar is printed + + auto download_progress_bar = std::make_unique(10, "test"); + download_progress_bar->set_ticks(4); + download_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + auto download_progress_bar_raw = download_progress_bar.get(); + + std::ostringstream oss; + (*download_progress_bar.*get(to_stream{}))(oss); + Pattern expected = "\\[0/0\\] test 40% | ????? ??B\\/s | 4.0 B | ???????"; + ASSERT_MATCHES(expected, oss.str()); + + download_progress_bar_raw->set_ticks(10); + download_progress_bar_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + oss.str(""); + (*download_progress_bar.*get(to_stream{}))(oss); + + expected = "\\[0/0\\] test 100% | ????? ??B\\/s | 10.0 B | ???????"; + ASSERT_MATCHES(expected, oss.str()); +} + +void ProgressbarInteractiveTest::test_download_progress_bar_with_messages() { + // In interactive mode not finished download progress bar with messages is printed + + auto download_progress_bar = std::make_unique(10, "test"); + download_progress_bar->set_ticks(4); + download_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + download_progress_bar->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + download_progress_bar->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message2"); + + std::ostringstream oss; + (*download_progress_bar.*get(to_stream{}))(oss); + Pattern expected = + "\\[0/0\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ">>> test message1\n" + ">>> test message2"; + ASSERT_MATCHES(expected, oss.str()); + + download_progress_bar->pop_message(); + download_progress_bar->pop_message(); + + oss.str(""); + (*download_progress_bar.*get(to_stream{}))(oss); + expected = "\\[0/0\\] test 40% | ????? ??B\\/s | 4.0 B | ???????"; + ASSERT_MATCHES(expected, oss.str()); +} + +void ProgressbarInteractiveTest::test_multi_progress_bar_with_total_finished() { + // In interactive mode finished multi progressbar ends with a new line. + + auto download_progress_bar1 = std::make_unique(10, "test1"); + download_progress_bar1->set_ticks(10); + download_progress_bar1->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + + auto download_progress_bar2 = std::make_unique(10, "test2"); + download_progress_bar2->set_ticks(10); + download_progress_bar2->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + multi_progress_bar.add_bar(std::move(download_progress_bar1)); + multi_progress_bar.add_bar(std::move(download_progress_bar2)); + std::ostringstream oss; + oss << multi_progress_bar; + + Pattern expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ??????\n" + "\\[2/2\\] test2 100% | ????? ??B\\/s | 10.0 B | ??????\n" + "----------------------------------------------------------------------\n" + "\\[2/2\\] Total 100% | ????? ??B\\/s | 20.0 B | ??????\n"; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); +} + +void ProgressbarInteractiveTest::test_multi_progress_bar_with_messages_with_total() { + // In interactive mode not finished progressbar with messages doesn't end with a new line. + // However finished progressbar does end with a new line. + // When messages are removed the multi progressbar shrinks, we cannot remove the already printed + // line but ensure it doesn't contain garbage. + + auto download_progress_bar = std::make_unique(10, "test"); + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + auto download_progress_bar_raw = download_progress_bar.get(); + multi_progress_bar.add_bar(std::move(download_progress_bar)); + + download_progress_bar_raw->set_ticks(4); + download_progress_bar_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + + std::ostringstream oss; + oss << multi_progress_bar; + Pattern expected = + "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ">>> test message1\n" + "----------------------------------------------------------------------\n" + "\\[0/1\\] Total 40% | ????? ??B\\/s | 4.0 B | ???????"; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + download_progress_bar_raw->pop_message(); + oss << multi_progress_bar; + + // Do several iterations, this simulates adding and removing scriplet messages. + // (They are removed when a scriptlet succeeds and doesn't print any output.) + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + oss << multi_progress_bar; + download_progress_bar_raw->pop_message(); + oss << multi_progress_bar; + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + oss << multi_progress_bar; + download_progress_bar_raw->pop_message(); + oss << multi_progress_bar; + + // Output ends with an empty line because the progressbar has previously already + // extended to 4 lines and there is not way to remove the line + expected = + "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n" + "----------------------------------------------------------------------\n" + "\\[0/1\\] Total 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ""; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message2"); + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message3"); + oss << multi_progress_bar; + download_progress_bar_raw->pop_message(); + download_progress_bar_raw->pop_message(); + download_progress_bar_raw->pop_message(); + download_progress_bar_raw->set_ticks(10); + download_progress_bar_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + oss << multi_progress_bar; + // Simulate appending after the finished MultiProgressBar to ensure cursor is at correct postion + oss << "Complete!"; + + expected = + "\\[1/1\\] test 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "----------------------------------------------------------------------\n" + "\\[1/1\\] Total 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "Complete!\n" + "\n" + ""; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); +} + +void ProgressbarInteractiveTest::test_multi_progress_bars_with_messages_with_total() { + // Same as above but with multiple bars in multi progressbar + + auto download_progress_bar1 = std::make_unique(10, "test1"); + download_progress_bar1->set_ticks(10); + download_progress_bar1->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + + auto download_progress_bar2 = std::make_unique(10, "test2"); + download_progress_bar2->set_auto_finish(false); + download_progress_bar2->start(); + + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + multi_progress_bar.add_bar(std::move(download_progress_bar1)); + auto download_progress_bar2_raw = download_progress_bar2.get(); + multi_progress_bar.add_bar(std::move(download_progress_bar2)); + + download_progress_bar2_raw->set_ticks(4); + download_progress_bar2_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message2"); + + std::ostringstream oss; + oss << multi_progress_bar; + Pattern expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ">>> test message1\n" + ">>> test message2\n" + "----------------------------------------------------------------------\n" + "\\[1/2\\] Total 70% | ????? ??B\\/s | 14.0 B | ???????"; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + download_progress_bar2_raw->pop_message(); + download_progress_bar2_raw->pop_message(); + oss << multi_progress_bar; + expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 40% | ????? ??B\\/s | 4.0 B | ???????\n" + "----------------------------------------------------------------------\n" + "\\[1/2\\] Total 70% | ????? ??B\\/s | 14.0 B | ???????\n" + "\n"; + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + // new iteration + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + oss << multi_progress_bar; + download_progress_bar2_raw->pop_message(); + oss << multi_progress_bar; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); +} + +void ProgressbarInteractiveTest::test_multi_progress_bar_with_messages() { + // With single bar and Total disabled + + auto download_progress_bar = std::make_unique(10, "test"); + + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + multi_progress_bar.set_total_bar_visible_limit(libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); + auto download_progress_bar_raw = download_progress_bar.get(); + multi_progress_bar.add_bar(std::move(download_progress_bar)); + + download_progress_bar_raw->set_ticks(4); + download_progress_bar_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + + std::ostringstream oss; + oss << multi_progress_bar; + Pattern expected = + "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ">>> test message1"; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + download_progress_bar_raw->pop_message(); + + oss << multi_progress_bar; + expected = + "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ""; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + // Do several iterations + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + oss << multi_progress_bar; + download_progress_bar_raw->pop_message(); + oss << multi_progress_bar; + download_progress_bar_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + oss << multi_progress_bar; + download_progress_bar_raw->pop_message(); + oss << multi_progress_bar; + + expected = + "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ""; + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); +} + +void ProgressbarInteractiveTest::test_multi_progress_bars_with_messages() { + // With multiple bars and Total disabled + + auto download_progress_bar1 = std::make_unique(10, "test1"); + download_progress_bar1->set_ticks(10); + download_progress_bar1->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + + auto download_progress_bar2 = std::make_unique(10, "test2"); + + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + multi_progress_bar.set_total_bar_visible_limit(libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); + multi_progress_bar.add_bar(std::move(download_progress_bar1)); + auto download_progress_bar2_raw = download_progress_bar2.get(); + multi_progress_bar.add_bar(std::move(download_progress_bar2)); + + download_progress_bar2_raw->set_ticks(4); + download_progress_bar2_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED); + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message2"); + + std::ostringstream oss; + oss << multi_progress_bar; + Pattern expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 40% | ????? ??B\\/s | 4.0 B | ???????\n" + ">>> test message1\n" + ">>> test message2"; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + download_progress_bar2_raw->pop_message(); + download_progress_bar2_raw->pop_message(); + oss << multi_progress_bar; + + expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 40% | ????? ??B\\/s | 4.0 B | ???????\n" + "\n"; + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + // new iteration + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + oss << multi_progress_bar; + download_progress_bar2_raw->pop_message(); + oss << multi_progress_bar; + + expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 40% | ????? ??B\\/s | 4.0 B | ???????\n" + "\n"; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); + + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + download_progress_bar2_raw->add_message(libdnf5::cli::progressbar::MessageType::INFO, "test message1"); + oss << multi_progress_bar; + download_progress_bar2_raw->pop_message(); + download_progress_bar2_raw->pop_message(); + download_progress_bar2_raw->pop_message(); + download_progress_bar2_raw->set_ticks(10); + download_progress_bar2_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + oss << multi_progress_bar; + // Simulate appending after the finished MultiProgressBar to ensure cursor is at correct postion + oss << "Complete!"; + + expected = + "\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "\\[2/2\\] test2 100% | ????? ??B\\/s | 10.0 B | ???????\n" + "Complete!\n" + "\n" + ""; + + ASSERT_MATCHES(expected, perform_control_sequences(oss.str())); +} diff --git a/test/libdnf5-cli/test_progressbar_interactive.hpp b/test/libdnf5-cli/test_progressbar_interactive.hpp new file mode 100644 index 000000000..2f55338f2 --- /dev/null +++ b/test/libdnf5-cli/test_progressbar_interactive.hpp @@ -0,0 +1,56 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libdnf. If not, see . +*/ + + +#ifndef TEST_LIBDNF5_CLI_PROGRESSBAR_INTERACTIVE_HPP +#define TEST_LIBDNF5_CLI_PROGRESSBAR_INTERACTIVE_HPP + +#include +#include + +class ProgressbarInteractiveTest : public CppUnit::TestCase { + CPPUNIT_TEST_SUITE(ProgressbarInteractiveTest); + + CPPUNIT_TEST(test_perform_control_sequences); + CPPUNIT_TEST(test_download_progress_bar); + CPPUNIT_TEST(test_download_progress_bar_with_messages); + CPPUNIT_TEST(test_multi_progress_bar_with_total_finished); + CPPUNIT_TEST(test_multi_progress_bar_with_messages_with_total); + CPPUNIT_TEST(test_multi_progress_bars_with_messages_with_total); + CPPUNIT_TEST(test_multi_progress_bar_with_messages); + CPPUNIT_TEST(test_multi_progress_bars_with_messages); + + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp() override; + void tearDown() override; + + void test_perform_control_sequences(); + void test_download_progress_bar(); + void test_download_progress_bar_with_messages(); + void test_multi_progress_bar_with_total_finished(); + void test_multi_progress_bar_with_messages_with_total(); + void test_multi_progress_bars_with_messages_with_total(); + void test_multi_progress_bar_with_messages(); + void test_multi_progress_bars_with_messages(); +}; + + +#endif // TEST_LIBDNF5_CLI_PROGRESSBAR_INTERACTIVE_HPP