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] 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