diff --git a/src/chat_app.cc b/src/chat_app.cc index 121861a..1fbf9b4 100644 --- a/src/chat_app.cc +++ b/src/chat_app.cc @@ -79,7 +79,7 @@ void Chat::on_realize() { std::cout << "Status label not found\n"; } -inline void Chat::session_connection() { +void Chat::session_connection() { // Disconnect if connected if ( session ) { session = nullptr; @@ -101,7 +101,8 @@ inline void Chat::session_connection() { // Try to connect as client try { session = std::make_unique(host, port); - status_label->set_label("Connected to server " + std::string(host) + ":" + std::to_string(port) + "!"); + if ( status_label ) + status_label->set_label("Connected to server " + std::string(host) + ":" + std::to_string(port) + "!"); } catch (const std::exception& e) { session = nullptr; // If not localhost, give up @@ -173,7 +174,7 @@ inline void Chat::session_connection() { session->set_poster([this]() { dispatcher->emit(); }); } -inline void Chat::message_buffer () { +void Chat::message_buffer () { if ( !message_entry->get_text_length() ) return; std::string message{message_entry->get_text()}; session->process_message(message); diff --git a/src/include/chat_app.hpp b/src/include/chat_app.hpp index 2e543c2..80598be 100644 --- a/src/include/chat_app.hpp +++ b/src/include/chat_app.hpp @@ -54,7 +54,7 @@ class Chat : public Gtk::Box { * * Does nothing if Entry is empty, else it adds the message and empties the Entry. */ - inline void message_buffer(); + void message_buffer(); /** * @brief Tries connecting as client, falls back to hosting if localhost and no server found. @@ -64,5 +64,5 @@ class Chat : public Gtk::Box { * If the host is localhost and the connection fails, starts a Server and * connects to it. */ - inline void session_connection(); + void session_connection(); }; diff --git a/src/include/session.hpp b/src/include/session.hpp index 89cd8c4..9459dba 100644 --- a/src/include/session.hpp +++ b/src/include/session.hpp @@ -26,7 +26,7 @@ struct Connection { tcp::socket socket; asio::steady_timer send_timer; - std::mutex send_mutex; + std::mutex send_mutex{}; std::string nickname{}; std::string fingerprint{}; std::string send_buffer{}; @@ -53,9 +53,13 @@ struct Connection { * Contains a sender and receiver asio functions and has a poster and disconnecter function so it can interact with GUI. */ class Session { + friend class ChatTest; + friend class ChatTest_ChatFunctionalTest_Test; + public: - std::vector receive_queue; - std::mutex receive_mutex; + std::vector receive_queue{}; + std::mutex receive_mutex{}; + std::string type{}; /* Default constructor for Session. */ Session() = default; @@ -92,13 +96,12 @@ class Session { virtual void process_message(std::string&, std::shared_ptr = nullptr) = 0; protected: - std::string host; - unsigned port; - asio::io_context ioc; std::thread ioc_thread; - std::function poster; - std::function disconnecter; + std::string host{}; + unsigned port{}; + std::function poster{}; + std::function disconnecter{}; /** * @brief Sender function to run so the session sends data. @@ -131,13 +134,13 @@ class Session { * and receives messages from the server. */ class Client : public Session { + friend class ChatTest; + friend class ChatTest_ChatFunctionalTest_Test; + public: /* Constructor for Client, initializes the connection and connects to the server. */ explicit Client(const std::string &host, unsigned port); - /* Destructor for Client. Closes socket if needed. */ - ~Client() noexcept; - /** * @brief Processes a message for Client. * @@ -149,7 +152,7 @@ class Client : public Session { void process_message(std::string&, std::shared_ptr = nullptr) override; private: - std::shared_ptr connection; + std::shared_ptr connection{}; /** * @brief Receiver function to run so the connection awaits for data. @@ -179,11 +182,14 @@ class Client : public Session { * clients, and echoes it back to the sender tagged with "(you)". */ class Server : public Session { + friend class ChatTest; + friend class ChatTest_ChatFunctionalTest_Test; + public: /* Constructor for Server, initializes the acceptor and starts the accept loop. */ explicit Server(unsigned port); - /* Destructor for Server. Closes the acceptor and all connections. */ + /* Destructor for Server. Closes the acceptor. */ ~Server() noexcept; /** @@ -203,7 +209,8 @@ class Server : public Session { /** * @brief Receiver function to run so the session awaits for data. - * + * Removes closed connections. + * * @param[in] socket: Socket connection to receive data from. */ awaitable receiver(std::shared_ptr) override; diff --git a/src/session.cc b/src/session.cc index 39b4e7c..588df6a 100644 --- a/src/session.cc +++ b/src/session.cc @@ -22,11 +22,9 @@ void Connection::add_to_send_buffer(std::string& message) { Session::~Session() noexcept { - poster = nullptr; - disconnecter = nullptr; if ( !ioc.stopped() ) ioc.stop(); if ( ioc_thread.joinable() ) ioc_thread.join(); - std::cout << "[Server] stopped\n"; + std::cout << "[" + type + "] stopped\n"; } awaitable Session::sender(std::shared_ptr connection) { @@ -44,7 +42,7 @@ awaitable Session::sender(std::shared_ptr connection) { co_await asio::async_write(connection->socket, asio::buffer(data), use_awaitable); } } catch (const std::exception& e) { - std::cout << "[Server] sender error: " << e.what() << "\n"; + std::cout << "[" + type + "] sender error: " << e.what() << "\n"; } } @@ -63,7 +61,7 @@ awaitable Session::receiver(std::shared_ptr connection) { process_message(line, connection); } } catch (const std::exception& e) { - std::cout << "[Server] " << connection->nickname << " disconnected: " << e.what() << "\n"; + std::cout << "[" + type + "] " << connection->nickname << " disconnected: " << e.what() << "\n"; } } @@ -76,6 +74,7 @@ void Session::set_disconnecter(std::function new_disconnecter) Client::Client(const std::string &host, unsigned port) : Session::Session(), connection (std::make_shared(ioc)) { + type = "Client"; connection->nickname = "Server"; this->host = host; this->port = port; @@ -87,19 +86,19 @@ Client::Client(const std::string &host, unsigned port) : connection->send_timer.expires_at(asio::steady_timer::time_point::min()); tcp::resolver resolver(ioc); - std::cout << "Connecting to " << host << ":" << port << "...\n"; + std::cout << "[Client] Connecting to " << host << ":" << port << "...\n"; asio::error_code ec; auto endpoints = resolver.resolve(host, std::to_string(port), ec); if (ec) { - std::cerr << "[error] resolve(): " << ec.message() + std::cerr << "[Client] resolve(): " << ec.message() << "\nIs the server running?\n"; throw std::runtime_error("Failed to resolve host"); } asio::connect(connection->socket, endpoints, ec); if (ec) { - std::cerr << "[error] connect(): " << ec.message() + std::cerr << "[Client] connect(): " << ec.message() << "\nIs the server running?\n"; throw std::runtime_error("Failed to connect to server"); } @@ -110,14 +109,10 @@ Client::Client(const std::string &host, unsigned port) : // Run the event loop in separate thread until ioc.stop() is called ioc_thread = std::thread([this](){ ioc.run(); }); + std::cout << "[Client] connected to " << host << ":" << port << "\n"; } -Client::~Client() noexcept { - if ( connection->socket.is_open() ) - connection->socket.close(); -} - awaitable Client::receiver(std::shared_ptr connection) { co_await Session::receiver(connection); if ( connection->socket.is_open() ) @@ -127,7 +122,7 @@ awaitable Client::receiver(std::shared_ptr connection) { void Client::broadcast(const std::string& message, std::shared_ptr origin) { (void) origin; - connection->send_buffer += message + "\n"; + connection->send_buffer.append(message + "\n"); connection->send_timer.cancel(); } @@ -148,10 +143,10 @@ void Client::process_message(std::string &message, std::shared_ptr o Server::Server(unsigned port) : Session::Session(), acceptor (tcp::acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port)))) { - host = "Server"; + type = "Server"; asio::co_spawn(ioc, accept_loop(), asio::detached); ioc_thread = std::thread([this](){ ioc.run(); }); - std::cout << "[Server] listening on port " << port << "\n"; + std::cout << "[" + type + "] listening on port " << port << "\n"; } Server::~Server() noexcept { @@ -163,9 +158,8 @@ awaitable Server::receiver(std::shared_ptr connection) { if ( connection->socket.is_open() ) connection->socket.close(); connections.remove(connection); - std::string farewell{connection->nickname + " has left the chat!!!"}; + std::string farewell{ connection->nickname + " has left the chat!!!" }; process_message(farewell); - if (disconnecter) disconnecter(); } awaitable Server::accept_loop() { @@ -174,7 +168,7 @@ awaitable Server::accept_loop() { asio::error_code ec; co_await acceptor.async_accept(connection->socket, asio::redirect_error(use_awaitable, ec)); if (ec) { - std::cout << "[server] accept loop ending: " << ec.message() << "\n"; + std::cout << "[" + type + "] accept loop ending: " << ec.message() << "\n"; co_return; } @@ -182,7 +176,7 @@ awaitable Server::accept_loop() { auto ep = connection->socket.remote_endpoint(); connection->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); } catch (...) { connection->fingerprint = "unknown"; } - std::cout << "[server] new client: " << connection->nickname << " (" << connection->fingerprint << ")\n"; + std::cout << "[" + type + "] new client: " << connection->nickname << " (" << connection->fingerprint << ")\n"; std::string message {connection->nickname + " says Hi!!!\n"}; process_message(message); @@ -192,6 +186,7 @@ awaitable Server::accept_loop() { connections.push_back(connection); } + if (disconnecter) disconnecter(); } void Server::broadcast(const std::string& message, std::shared_ptr origin) { diff --git a/tests/browser_app_tests.cc b/tests/browser_app_tests.cc index c3c7792..df4b543 100644 --- a/tests/browser_app_tests.cc +++ b/tests/browser_app_tests.cc @@ -1,7 +1,5 @@ #include "include/tests.hpp" -#include - #include #include diff --git a/tests/chat_app_tests.cc b/tests/chat_app_tests.cc index 22cfab3..7072ea1 100644 --- a/tests/chat_app_tests.cc +++ b/tests/chat_app_tests.cc @@ -1,27 +1,32 @@ #include "include/tests.hpp" -#include +#include +#include #include #include ChatTest::ChatTest() : - chat (Gtk::manage(new Chat())), - home_button (chat->home_button), - connect_button (chat->connect_button), - message_button (chat->message_button), - ip_entry (chat->ip_entry), - port_entry (chat->port_entry), - message_entry (chat->message_entry), - chat_box (chat->chat_box), - footer_box (chat->footer_box), - chat_scrolled (chat->chat_scrolled), - status_label (chat->status_label) {}; + server (Gtk::manage(new Chat())), + client_1 (Gtk::manage(new Chat())), + client_2 (Gtk::manage(new Chat())), + home_button (server->home_button), + connect_button (server->connect_button), + message_button (server->message_button), + ip_entry (server->ip_entry), + port_entry (server->port_entry), + message_entry (server->message_entry), + chat_box (server->chat_box), + footer_box (server->footer_box), + chat_scrolled (server->chat_scrolled), + status_label (server->status_label) {}; TEST_F(ChatTest, ChatStructuralTest) { - ASSERT_THAT(chat, ::testing::NotNull()); - auto header_t {dynamic_cast(chat->get_first_child())}; + ASSERT_THAT(server, ::testing::NotNull()); + ASSERT_THAT(client_1, ::testing::NotNull()); + ASSERT_THAT(client_2, ::testing::NotNull()); + auto header_t {dynamic_cast(server->get_first_child())}; ASSERT_THAT(header_t, ::testing::NotNull()); auto home_t {dynamic_cast(header_t->get_first_child())}; ASSERT_THAT(home_t, ::testing::NotNull()); @@ -55,5 +60,86 @@ TEST_F(ChatTest, ChatStructuralTest) { } TEST_F(ChatTest, ChatFunctionalTest) { - // Waiting for full function with server, due to the need of a server for validation + server->session_connection(); + ASSERT_THAT(server->session, ::testing::NotNull()); + ASSERT_THAT(server->session->type, ::testing::Eq("Server")); + + // Check new connections + client_1->session_connection(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_THAT(client_1->session, ::testing::NotNull()); + ASSERT_THAT(client_1->session->type, ::testing::Eq("Client")); + ASSERT_THAT(dynamic_cast(server->session.get())->connections.size(), ::testing::Eq(1)); + ASSERT_THAT(server->session->receive_queue.size(), ::testing::Eq(1)); + ASSERT_THAT(std::regex_search(server->session->receive_queue.front(), std::regex("User\\d says Hi!!!")), ::testing::Eq(true)); + + // Check poster and disconnecter setters + server->session->set_poster([this](){ std::cout << "Posting message from Server..." << std::endl; }); + server->session->set_disconnecter([this](){ std::cout << "Disconnecting from Server..." << std::endl; }); + ASSERT_THAT(server->session->poster, ::testing::NotNull()); + ASSERT_THAT(server->session->disconnecter, ::testing::NotNull()); + server->session->poster(); + server->session->disconnecter(); + client_1->session->set_poster([this](){ std::cout << "Posting message from Client..." << std::endl; }); + client_1->session->set_disconnecter([this](){ std::cout << "Disconnecting from Client..." << std::endl; }); + ASSERT_THAT(client_1->session->poster, ::testing::NotNull()); + ASSERT_THAT(client_1->session->disconnecter, ::testing::NotNull()); + client_1->session->poster(); + client_1->session->disconnecter(); + + // Check message processing and broadcasting on Server + std::string message {"Hello World!"}; + server->session->process_message(message); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_THAT(server->session->receive_queue.size(), ::testing::Eq(2)); + ASSERT_THAT(server->session->receive_queue.back(), ::testing::Eq(message)); + ASSERT_THAT(client_1->session->receive_queue.size(), ::testing::Eq(1)); + ASSERT_THAT(client_1->session->receive_queue.back(), ::testing::Eq(message)); + + // Check multiple clients and broadcasting + client_2->session_connection(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_THAT(client_2->session, ::testing::NotNull()); + ASSERT_THAT(client_2->session->type, ::testing::Eq("Client")); + ASSERT_THAT(dynamic_cast(server->session.get())->connections.size(), ::testing::Eq(2)); + ASSERT_THAT(server->session->receive_queue.size(), ::testing::Eq(3)); + ASSERT_THAT(std::regex_search(server->session->receive_queue.back(), std::regex("User\\d says Hi!!!")), ::testing::Eq(true)); + ASSERT_THAT(client_1->session->receive_queue.size(), ::testing::Eq(2)); + ASSERT_THAT(std::regex_search(client_1->session->receive_queue.back(), std::regex("User\\d says Hi!!!")), ::testing::Eq(true)); + + // Check message processing and broadcasting on Client + client_1->session->process_message(message); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_THAT(server->session->receive_queue.size(), ::testing::Eq(4)); + ASSERT_THAT(client_1->session->receive_queue.size(), ::testing::Eq(3)); + ASSERT_THAT(client_2->session->receive_queue.size(), ::testing::Eq(1)); + ASSERT_THAT(std::regex_search(server->session->receive_queue.back(), std::regex("\\(User\\d\\): " + message)), ::testing::Eq(true)); + ASSERT_THAT(client_1->session->receive_queue.back(), ::testing::Eq("(You): " + message)); + ASSERT_THAT(std::regex_search(client_2->session->receive_queue.back(), std::regex("\\(User\\d\\): " + message)), ::testing::Eq(true)); + + // Check message processing and broadcasting on Client with multiple clients + client_2->session->process_message(message); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_THAT(server->session->receive_queue.size(), ::testing::Eq(5)); + ASSERT_THAT(client_1->session->receive_queue.size(), ::testing::Eq(4)); + ASSERT_THAT(client_2->session->receive_queue.size(), ::testing::Eq(2)); + ASSERT_THAT(std::regex_search(server->session->receive_queue.back(), std::regex("\\(User\\d\\): " + message)), ::testing::Eq(true)); + ASSERT_THAT(std::regex_search(client_1->session->receive_queue.back(), std::regex("\\(User\\d\\): " + message)), ::testing::Eq(true)); + ASSERT_THAT(client_2->session->receive_queue.back(), ::testing::Eq("(You): " + message)); + + // Check Client disconnection and broadcasting from Server + client_1->session_connection(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_THAT(client_1->session, ::testing::IsNull()); + ASSERT_THAT(dynamic_cast(server->session.get())->connections.size(), ::testing::Eq(1)); + ASSERT_THAT(server->session->receive_queue.size(), ::testing::Eq(6)); + ASSERT_THAT(std::regex_search(server->session->receive_queue.back(), std::regex("User\\d has left the chat!!!")), ::testing::Eq(true)); + ASSERT_THAT(client_2->session->receive_queue.size(), ::testing::Eq(3)); + ASSERT_THAT(std::regex_search(client_2->session->receive_queue.back(), std::regex("User\\d has left the chat!!!")), ::testing::Eq(true)); + + // Check Server disconnection on Client + server->session_connection(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + ASSERT_THAT(server->session, ::testing::IsNull()); + ASSERT_THAT(dynamic_cast(client_2->session.get())->connection->socket.is_open(), ::testing::Eq(false)); } diff --git a/tests/fractal_app_tests.cc b/tests/fractal_app_tests.cc index a0b7822..0a0f673 100644 --- a/tests/fractal_app_tests.cc +++ b/tests/fractal_app_tests.cc @@ -1,7 +1,6 @@ #include "include/tests.hpp" #include -#include #include #include diff --git a/tests/include/tests.hpp b/tests/include/tests.hpp index dad9a6a..0b42445 100644 --- a/tests/include/tests.hpp +++ b/tests/include/tests.hpp @@ -1,13 +1,13 @@ #pragma once -#include -#include - #include "../../src/include/main_application.hpp" #include "../../src/include/browser_app.hpp" #include "../../src/include/fractal_app.hpp" #include "../../src/include/chat_app.hpp" +#include +#include + /** * @brief Contains MainApplication object and tests. @@ -103,13 +103,15 @@ class FractalTest : public testing::Test { class ChatTest : public testing::Test { protected: /** - * @brief Creates Chat object chat. + * @brief Creates Chat object server and client so it can check for functionality. * * Obtains chat object using default constructor for testing. * */ ChatTest(); - Chat *chat{}; + Chat *server{}; + Chat *client_1{}; + Chat *client_2{}; Gtk::Button *home_button{}; Gtk::Button *connect_button{}; diff --git a/tests/main.cc b/tests/main.cc index 73ea86e..d6bb779 100644 --- a/tests/main.cc +++ b/tests/main.cc @@ -1,4 +1,5 @@ #include "include/tests.hpp" + #include