aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorEgor Tensin <Egor.Tensin@gmail.com>2019-11-30 01:38:08 +0300
committerEgor Tensin <Egor.Tensin@gmail.com>2019-12-01 05:23:11 +0300
commit90bd600c5025ede4db99122f13dfb07b27de46ae (patch)
tree0b581b43e5fcb54114c6c373352ed6ab5fcd61dc
downloadmath-server-90bd600c5025ede4db99122f13dfb07b27de46ae.tar.gz
math-server-90bd600c5025ede4db99122f13dfb07b27de46ae.zip
initial commit
-rw-r--r--.gitattributes1
-rw-r--r--CMakeLists.txt11
-rw-r--r--client/CMakeLists.txt5
-rw-r--r--client/client.hpp52
-rw-r--r--client/error.hpp15
-rw-r--r--client/input.hpp139
-rw-r--r--client/main.cpp34
-rw-r--r--client/settings.hpp111
-rw-r--r--client/transport.hpp104
-rw-r--r--common.cmake107
-rw-r--r--server/CMakeLists.txt11
-rw-r--r--server/error.hpp15
-rw-r--r--server/lexer.hpp261
-rw-r--r--server/log.hpp49
-rw-r--r--server/main.cpp34
-rw-r--r--server/parser.hpp168
-rw-r--r--server/server.cpp111
-rw-r--r--server/server.hpp34
-rw-r--r--server/session.cpp113
-rw-r--r--server/session.hpp42
-rw-r--r--server/session_manager.cpp37
-rw-r--r--server/session_manager.hpp30
-rw-r--r--server/settings.hpp87
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/unit_tests/CMakeLists.txt6
-rw-r--r--test/unit_tests/lexer.cpp109
-rw-r--r--test/unit_tests/main.cpp2
-rw-r--r--test/unit_tests/parser.cpp48
28 files changed, 1737 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..176a458
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..0d02300
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,11 @@
+project(math_server CXX)
+
+option(ENABLE_TESTS "build the tests")
+
+include(common.cmake)
+
+add_subdirectory(client)
+add_subdirectory(server)
+if(ENABLE_TESTS)
+ add_subdirectory(test)
+endif()
diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
new file mode 100644
index 0000000..cd23de8
--- /dev/null
+++ b/client/CMakeLists.txt
@@ -0,0 +1,5 @@
+find_package(Boost REQUIRED COMPONENTS filesystem program_options)
+
+add_executable(client main.cpp)
+target_include_directories(client SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
+target_link_libraries(client PRIVATE ${Boost_LIBRARIES})
diff --git a/client/client.hpp b/client/client.hpp
new file mode 100644
index 0000000..139ebb2
--- /dev/null
+++ b/client/client.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "input.hpp"
+#include "settings.hpp"
+#include "transport.hpp"
+
+#include <iostream>
+#include <string>
+#include <utility>
+
+namespace math::client {
+
+class Client {
+public:
+ explicit Client(const Settings& settings)
+ : Client{make_input_reader(settings), make_transport(settings)}
+ { }
+
+ Client(input::ReaderPtr&& input_reader, TransportPtr&& transport)
+ : m_input_reader{std::move(input_reader)}
+ , m_transport{std::move(transport)}
+ { }
+
+ void run() {
+ m_input_reader->for_each_input([this] (const std::string& input) {
+ m_transport->send_query(input, [] (const std::string& reply) {
+ std::cout << reply << '\n';
+ });
+ return true;
+ });
+ }
+
+private:
+ static input::ReaderPtr make_input_reader(const Settings& settings) {
+ if (settings.input_from_string()) {
+ return input::make_string_reader(settings.m_input);
+ }
+ if (settings.input_from_files()) {
+ return input::make_file_reader(settings.m_files);
+ }
+ return input::make_console_reader();
+ }
+
+ static TransportPtr make_transport(const Settings& settings) {
+ return make_blocking_network_transport(settings.m_host, settings.m_port);
+ }
+
+ const input::ReaderPtr m_input_reader;
+ TransportPtr m_transport;
+};
+
+}
diff --git a/client/error.hpp b/client/error.hpp
new file mode 100644
index 0000000..98e1305
--- /dev/null
+++ b/client/error.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <stdexcept>
+#include <string>
+
+namespace math::client {
+
+class Error : public std::runtime_error {
+public:
+ explicit Error(const std::string& what)
+ : std::runtime_error{"client error: " + what}
+ { }
+};
+
+}
diff --git a/client/input.hpp b/client/input.hpp
new file mode 100644
index 0000000..077d5c7
--- /dev/null
+++ b/client/input.hpp
@@ -0,0 +1,139 @@
+#pragma once
+
+#include "error.hpp"
+
+#include <exception>
+#include <fstream>
+#include <functional>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace math::client::input {
+
+class Error : public client::Error {
+public:
+ explicit Error(const std::string& what)
+ : client::Error{"input error: " + what} {
+ }
+};
+
+class Reader {
+public:
+ using InputHandler = std::function<bool (const std::string&)>;
+
+ virtual ~Reader() = default;
+
+ virtual bool for_each_input(const InputHandler& process) const = 0;
+};
+
+using ReaderPtr = std::unique_ptr<input::Reader>;
+
+class FileReader : public Reader {
+public:
+ explicit FileReader(const std::string& path)
+ : m_path{path}
+ { }
+
+ bool for_each_input(const InputHandler& process) const override {
+ return enum_lines(process);
+ }
+
+private:
+ bool enum_lines(const InputHandler& process) const {
+ std::ifstream ifs;
+ ifs.exceptions(std::ifstream::badbit);
+
+ try {
+ ifs.open(m_path);
+ if (!ifs.is_open()) {
+ throw Error{"couldn't open file: " + m_path};
+ }
+
+ for (std::string line; std::getline(ifs, line);) {
+ if (!process(line)) {
+ return false;
+ }
+ }
+ } catch (const std::exception& e) {
+ throw Error{e.what()};
+ }
+
+ return true;
+ }
+
+ const std::string m_path;
+};
+
+class MultiFileReader : public Reader {
+public:
+ explicit MultiFileReader(const std::vector<std::string>& paths)
+ : m_paths{paths}
+ { }
+
+ bool for_each_input(const InputHandler& process) const override {
+ for (const auto& path : m_paths) {
+ const FileReader reader{path};
+ if (!reader.for_each_input(process)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+private:
+ const std::vector<std::string> m_paths;
+};
+
+inline input::ReaderPtr make_file_reader(const std::string& path) {
+ return std::make_unique<input::FileReader>(path);
+}
+
+inline input::ReaderPtr make_file_reader(const std::vector<std::string>& paths) {
+ return std::make_unique<input::MultiFileReader>(paths);
+}
+
+class StringReader : public Reader {
+public:
+ explicit StringReader(const std::string& input)
+ : m_input{input}
+ { }
+
+ bool for_each_input(const InputHandler& process) const override {
+ return process(m_input);
+ }
+
+private:
+ const std::string m_input;
+};
+
+inline input::ReaderPtr make_string_reader(const std::string& input) {
+ return std::make_unique<input::StringReader>(input);
+}
+
+class ConsoleReader : public Reader {
+public:
+ ConsoleReader() = default;
+
+ bool for_each_input(const InputHandler& process) const override {
+ std::string line;
+ while (read_line(line)) {
+ if (!process(line)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+private:
+ static bool read_line(std::string& dest) {
+ return static_cast<bool>(std::getline(std::cin, dest));
+ }
+};
+
+inline input::ReaderPtr make_console_reader() {
+ return std::make_unique<input::ConsoleReader>();
+}
+
+}
diff --git a/client/main.cpp b/client/main.cpp
new file mode 100644
index 0000000..cfe5026
--- /dev/null
+++ b/client/main.cpp
@@ -0,0 +1,34 @@
+#include "client.hpp"
+#include "settings.hpp"
+
+#include <boost/program_options.hpp>
+
+#include <exception>
+#include <iostream>
+
+int main(int argc, char* argv[]) {
+ try {
+ math::client::SettingsParser parser{argv[0]};
+
+ try {
+ const auto settings = parser.parse(argc, argv);
+ if (settings.exit_with_usage()) {
+ parser.usage();
+ return 0;
+ }
+
+ math::client::Client client{settings};
+ client.run();
+ } catch (const boost::program_options::error& e) {
+ parser.usage_error(e);
+ return 1;
+ }
+ } catch (const std::exception& e) {
+ std::cerr << "An error occured: " << e.what() << "\n";
+ return 1;
+ } catch (...) {
+ std::cerr << "An unknown error occured\n";
+ return 1;
+ }
+ return 0;
+}
diff --git a/client/settings.hpp b/client/settings.hpp
new file mode 100644
index 0000000..5c2522e
--- /dev/null
+++ b/client/settings.hpp
@@ -0,0 +1,111 @@
+#pragma once
+
+#include "transport.hpp"
+
+#include <boost/filesystem.hpp>
+#include <boost/program_options.hpp>
+
+#include <exception>
+#include <iostream>
+#include <string>
+#include <vector>
+
+namespace math::client {
+
+struct Settings {
+ std::string m_input;
+ std::string m_host;
+ std::string m_port;
+ std::vector<std::string> m_files;
+
+ bool exit_with_usage() const { return m_vm.count("help"); }
+
+ bool input_from_string() const {
+ return m_vm.count("command");
+ }
+
+ bool input_from_files() const {
+ return !input_from_string() && !m_files.empty();
+ }
+
+ bool input_from_console() const {
+ return !input_from_string() && !input_from_files();
+ }
+
+ boost::program_options::variables_map m_vm;
+};
+
+class SettingsParser {
+public:
+ explicit SettingsParser(const std::string& argv0)
+ : m_prog_name{extract_filename(argv0)}
+ {
+ m_visible.add_options()
+ ("help,h",
+ "show this message and exit")
+ ("command,c",
+ boost::program_options::value(&m_settings.m_input),
+ "evaluate the argument expression and exit")
+ ("host,H",
+ boost::program_options::value(&m_settings.m_host)->default_value("localhost"),
+ "server host address")
+ ("port,p",
+ boost::program_options::value(&m_settings.m_port)->default_value(NetworkTransport::DEFAULT_PORT),
+ "server port number");
+ m_hidden.add_options()
+ ("files",
+ boost::program_options::value<std::vector<std::string>>(&m_settings.m_files),
+ "shouldn't be visible");
+ m_positional.add("files", -1);
+ }
+
+ static const char* get_short_description() {
+ return "[-h|--help] [-c|--command arg] [-H|--host] [-p|--port] [file...]";
+ }
+
+ Settings parse(int argc, char* argv[]) {
+ boost::program_options::options_description all;
+ all.add(m_hidden).add(m_visible);
+ boost::program_options::store(
+ boost::program_options::command_line_parser{argc, argv}
+ .options(all)
+ .positional(m_positional)
+ .run(),
+ m_settings.m_vm);
+ if (m_settings.exit_with_usage()) {
+ return m_settings;
+ }
+ boost::program_options::notify(m_settings.m_vm);
+ return m_settings;
+ }
+
+ void usage() const {
+ std::cout << *this;
+ }
+
+ void usage_error(const std::exception& e) const {
+ std::cerr << "usage error: " << e.what() << '\n';
+ std::cerr << *this;
+ }
+
+private:
+ static std::string extract_filename(const std::string& path) {
+ return boost::filesystem::path{path}.filename().string();
+ }
+
+ const std::string m_prog_name;
+
+ boost::program_options::options_description m_hidden;
+ boost::program_options::options_description m_visible;
+ boost::program_options::positional_options_description m_positional;
+
+ Settings m_settings;
+
+ friend std::ostream& operator<<(std::ostream& os, const SettingsParser& parser) {
+ os << "usage: " << parser.m_prog_name << ' ' << get_short_description() << '\n';
+ os << parser.m_visible;
+ return os;
+ }
+};
+
+}
diff --git a/client/transport.hpp b/client/transport.hpp
new file mode 100644
index 0000000..ae56f73
--- /dev/null
+++ b/client/transport.hpp
@@ -0,0 +1,104 @@
+#pragma once
+
+#include "error.hpp"
+
+#include <boost/asio.hpp>
+#include <boost/system/system_error.hpp>
+
+#include <functional>
+#include <memory>
+#include <string>
+
+namespace math::client {
+namespace transport {
+
+class Error : public client::Error {
+public:
+ explicit Error(const std::string& msg)
+ : client::Error{"transport error: " + msg}
+ { }
+};
+
+}
+
+class Transport {
+public:
+ virtual ~Transport() = default;
+
+ using ProcessResult = std::function<void (const std::string&)>;
+
+ virtual void send_query(const std::string&, const ProcessResult&) = 0;
+};
+
+using TransportPtr = std::unique_ptr<Transport>;
+
+class NetworkTransport : public Transport {
+public:
+ static constexpr auto DEFAULT_PORT = "18000";
+
+ NetworkTransport(const std::string& host, const std::string& port)
+ : m_host{host}, m_port{port}
+ { }
+
+protected:
+ const std::string m_host;
+ const std::string m_port;
+};
+
+class BlockingNetworkTransport : public NetworkTransport {
+public:
+ BlockingNetworkTransport(const std::string &host, const std::string& port)
+ : NetworkTransport{host, port}, m_socket{m_io_context} {
+ try {
+ connect();
+ } catch (const boost::system::system_error& e) {
+ throw transport::Error{e.what()};
+ }
+ }
+
+ void send_query(const std::string& query, const ProcessResult& on_reply) override {
+ std::string reply;
+ try {
+ reply = send_query(query);
+ } catch (const boost::system::system_error& e) {
+ throw transport::Error{e.what()};
+ }
+ on_reply(reply);
+ }
+
+private:
+ void connect() {
+ boost::asio::ip::tcp::resolver resolver{m_io_context};
+ boost::asio::connect(m_socket, resolver.resolve(m_host, m_port));
+ }
+
+ std::string send_query(const std::string& query) {
+ write(query);
+ return read_line();
+ }
+
+ void write(std::string input) {
+ input += '\n';
+ boost::asio::write(m_socket, boost::asio::const_buffer{input.c_str(), input.size()});
+ }
+
+ std::string read_line() {
+ const auto bytes = boost::asio::read_until(m_socket, m_buffer, '\n');
+ const auto data = boost::asio::buffer_cast<const char*>(m_buffer.data());
+ const std::string result{data, bytes - 1};
+ m_buffer.consume(bytes);
+ return result;
+ }
+
+ boost::asio::io_context m_io_context;
+ boost::asio::ip::tcp::socket m_socket;
+ boost::asio::streambuf m_buffer;
+};
+
+inline TransportPtr make_blocking_network_transport(
+ const std::string& host, const std::string& port) {
+
+ return std::make_unique<BlockingNetworkTransport>(host, port);
+}
+
+}
diff --git a/common.cmake b/common.cmake
new file mode 100644
index 0000000..2d5e9f9
--- /dev/null
+++ b/common.cmake
@@ -0,0 +1,107 @@
+# Copyright (c) 2017 Egor Tensin <Egor.Tensin@gmail.com>
+# It's a CMake code snippet I use in all of my CMake projects.
+# It makes targets link the runtime statically by default + strips debug
+# symbols in release builds.
+# The latest version can be found at
+# https://github.com/egor-tensin/cmake-common.
+# Distributed under the MIT License.
+
+# Version: 2019-11-29T22:30:28Z
+
+get_directory_property(parent_directory PARENT_DIRECTORY)
+set(is_root_project $<NOT:parent_directory>)
+
+if(CMAKE_C_COMPILER)
+ set(toolset "${CMAKE_C_COMPILER_ID}")
+elseif(CMAKE_CXX_COMPILER)
+ set(toolset "${CMAKE_CXX_COMPILER_ID}")
+else()
+ set(toolset "unknown")
+endif()
+
+set(USE_STATIC_RUNTIME "${is_root_project}" CACHE BOOL "Link the runtime statically")
+set(STRIP_SYMBOL_TABLE "${is_root_project}" CACHE BOOL "Strip symbol tables")
+set(CXX_STANDARD "17" CACHE BOOL "Set C++ standard version")
+
+if(is_root_project)
+ if(MSVC)
+ add_compile_options(/MP /W4)
+ elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
+ add_compile_options(-Wall -Wextra)
+ else()
+ message(WARNING "Unrecognized toolset: ${toolset}")
+ endif()
+endif()
+
+set(CMAKE_CXX_STANDARD "${CXX_STANDARD}")
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+function(use_static_runtime_msvc target)
+ get_target_property(target_type "${target}" TYPE)
+ if(target_type STREQUAL INTERFACE_LIBRARY)
+ else()
+ target_compile_options("${target}" PRIVATE
+ $<$<CONFIG:Debug>:/MTd>
+ $<$<NOT:$<CONFIG:Debug>>:/MT>)
+ endif()
+endfunction()
+
+function(use_static_runtime_gcc target)
+ get_target_property(target_type "${target}" TYPE)
+ if(target_type STREQUAL EXECUTABLE)
+ target_link_libraries("${target}" PRIVATE -static)
+ endif()
+endfunction()
+
+function(use_static_runtime target)
+ if(MSVC)
+ use_static_runtime_msvc("${target}")
+ elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
+ use_static_runtime_gcc("${target}")
+ else()
+ message(WARNING "Unrecognized toolset: ${toolset}")
+ endif()
+endfunction()
+
+function(strip_symbol_table_gcc target)
+ get_target_property(target_type "${target}" TYPE)
+ set(release_build $<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>>)
+ if(target_type STREQUAL INTERFACE_LIBRARY)
+ else()
+ target_link_libraries("${target}" PRIVATE $<${release_build}:-s>)
+ endif()
+endfunction()
+
+function(strip_symbol_table target)
+ if(MSVC)
+ elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
+ strip_symbol_table_gcc("${target}")
+ else()
+ message(WARNING "Unrecognized toolset: ${toolset}")
+ endif()
+endfunction()
+
+function(apply_common_settings target)
+ if(TARGET "${target}")
+ get_target_property(target_imported "${target}" IMPORTED)
+ if(target_imported STREQUAL NOTFOUND OR NOT target_imported)
+ if(STRIP_SYMBOL_TABLE)
+ strip_symbol_table("${target}")
+ endif()
+ if(USE_STATIC_RUNTIME)
+ use_static_runtime("${target}")
+ endif()
+ endif()
+ endif()
+endfunction()
+
+macro(add_executable target)
+ _add_executable(${ARGV})
+ apply_common_settings("${target}")
+endmacro()
+
+macro(add_library target)
+ _add_library(${ARGV})
+ apply_common_settings("${target}")
+endmacro()
diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt
new file mode 100644
index 0000000..7bf356a
--- /dev/null
+++ b/server/CMakeLists.txt
@@ -0,0 +1,11 @@
+find_package(Boost REQUIRED COMPONENTS filesystem program_options)
+
+option(DEBUG_ASIO "enable debug output for Boost.Asio" OFF)
+
+add_executable(server main.cpp server.cpp session.cpp session_manager.cpp)
+target_include_directories(server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
+target_link_libraries(server PRIVATE ${Boost_LIBRARIES})
+
+if(DEBUG_ASIO)
+ target_compile_definitions(server PRIVATE BOOST_ASIO_ENABLE_HANDLER_TRACKING)
+endif()
diff --git a/server/error.hpp b/server/error.hpp
new file mode 100644
index 0000000..cbfbb1e
--- /dev/null
+++ b/server/error.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <stdexcept>
+#include <string>
+
+namespace math::server {
+
+class Error : public std::runtime_error {
+public:
+ explicit Error(const std::string& what)
+ : std::runtime_error{"server error: " + what}
+ { }
+};
+
+}
diff --git a/server/lexer.hpp b/server/lexer.hpp
new file mode 100644
index 0000000..8afe15c
--- /dev/null
+++ b/server/lexer.hpp
@@ -0,0 +1,261 @@
+#pragma once
+
+#include "error.hpp"
+
+#include <cmath>
+
+#include <exception>
+#include <functional>
+#include <limits>
+#include <optional>
+#include <regex>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace math::server {
+namespace lexer {
+
+class Error : public server::Error {
+public:
+ explicit Error(const std::string &what)
+ : server::Error{"lexer error: " + what}
+ { }
+};
+
+class Token {
+public:
+ enum class Type {
+ LEFT_PAREN,
+ RIGHT_PAREN,
+ PLUS,
+ MINUS,
+ ASTERISK,
+ SLASH,
+ NUMBER,
+ };
+
+ explicit Token(Type type)
+ : m_type{type}, m_number_value{nan()}
+ { }
+
+ explicit Token(double number_value)
+ : m_type{Type::NUMBER}, m_number_value{number_value}
+ { }
+
+ bool operator==(const Token& other) const {
+ return m_type == other.m_type
+ && ((is_nan(m_number_value) && is_nan(other.m_number_value))
+ || m_number_value == other.m_number_value);
+ }
+
+ bool operator!=(const Token& other) const { return !(*this == other); }
+
+ Type get_type() const { return m_type; }
+
+ double get_number_value() const {
+ if (get_type() != Type::NUMBER) {
+ throw Error{"token must be a number to query its value"};
+ }
+ return m_number_value;
+ }
+
+private:
+ static constexpr double nan() { return std::numeric_limits<double>::quiet_NaN(); }
+
+ static bool is_nan(double x) { return std::isnan(x); }
+
+ Type m_type;
+ double m_number_value;
+};
+
+namespace details {
+
+inline std::string_view match_number(const std::string_view& input) {
+ static constexpr std::regex::flag_type flags =
+ std::regex_constants::ECMAScript |
+ std::regex_constants::icase;
+ // This is a hacky attempt to describe a C-like grammar for floating-point
+ // numbers using a regex (the tests seem to pass though).
+ // A proper NFA would be better, I guess.
+ static const std::regex number_regex{R"REGEX(^(?:\d+(?:\.\d*)?|\.\d+)(e[+-]?(\d*))?)REGEX", flags};
+
+ std::cmatch match;
+ if (!std::regex_search(input.cbegin(), input.cend(), match, number_regex)) {
+ return {};
+ }
+ // If we have the numeric part of a number followed by 'e' and no digits,
+ // 1) that 'e' definitely belongs to this number token,
+ // 2) the user forgot to type in the required digits.
+ const auto& exponent = match[1];
+ const auto& abs_power = match[2];
+ if (exponent.matched && abs_power.matched && abs_power.length() == 0) {
+ throw lexer::Error{"exponent has no digits: " + match[0].str()};
+ }
+ return {match[0].first, match[0].length()};
+}
+
+inline std::optional<double> parse_number(const std::string_view& input, std::string_view& token) {
+ const auto view = match_number(input);
+ if (!view.data()) {
+ return {};
+ }
+ try {
+ const auto result = std::stod(std::string{view});
+ token = view;
+ return result;
+ } catch (const std::exception& e) {
+ throw lexer::Error{"couldn't parse number from: " + std::string{view}};
+ }
+ return {};
+}
+
+inline std::optional<double> parse_number(const std::string_view& input) {
+ std::string_view token;
+ return parse_number(input, token);
+}
+
+inline bool starts_with(const std::string_view& a, const std::string_view& b) noexcept {
+ return a.length() >= b.length()
+ && a.compare(0, b.length(), b) == 0;
+}
+
+inline std::optional<Token::Type> parse_const_token(const std::string_view& input, std::string_view& token) {
+ // FIXME: Potentially error-prone if there's const token A which is a
+ // prefix of token B (if the map is not sorted, we'd parse token A, when it
+ // could've been token B).
+ // Can be solved by sorting the keys accordingly.
+
+ static const std::unordered_map<std::string_view, Token::Type> const_tokens{
+ {"(", Token::Type::LEFT_PAREN},
+ {")", Token::Type::RIGHT_PAREN},
+ {"+", Token::Type::PLUS},
+ {"-", Token::Type::MINUS},
+ {"*", Token::Type::ASTERISK},
+ {"/", Token::Type::SLASH},
+ };
+
+ for (const auto& it : const_tokens) {
+ const auto& str = it.first;
+ const auto& type = it.second;
+
+ if (starts_with(input, str)) {
+ token = input.substr(0, str.length());
+ return type;
+ }
+ }
+
+ return {};
+}
+
+inline std::optional<Token::Type> parse_const_token(const std::string_view& input) {
+ std::string_view token;
+ return parse_const_token(input, token);
+}
+
+inline std::string_view parse_whitespace(const std::string_view& input) {
+ static const std::regex ws_regex{R"(\s*)"};
+
+ std::cmatch match;
+ if (std::regex_search(input.cbegin(), input.cend(), match, ws_regex)) {
+ return {match[0].first, match[0].length()};
+ }
+ return {};
+}
+
+}
+
+}
+
+class Lexer {
+public:
+ explicit Lexer(const std::string_view& input)
+ : m_input{input} {
+ }
+
+ using TokenProcessor = std::function<bool (const lexer::Token&)>;
+
+ bool for_each_token(const TokenProcessor& process) {
+ parse_token();
+ for (auto token = peek_token(); token.has_value(); drop_token(), token = peek_token()) {
+ if (!process(*token)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ std::vector<lexer::Token> get_tokens() {
+ std::vector<lexer::Token> tokens;
+ for_each_token([&tokens] (const lexer::Token& token) {
+ tokens.emplace_back(token);
+ return true;
+ });
+ return tokens;
+ }
+
+ void parse_token() {
+ if (m_input.length() == 0) {
+ return;
+ }
+ std::string_view token_view;
+ m_token_buffer = parse_token(token_view);
+ if (m_token_buffer.has_value()) {
+ m_input.remove_prefix(token_view.length());
+ }
+ }
+
+ bool has_token() const {
+ return peek_token().has_value();
+ }
+
+ std::optional<lexer::Token> peek_token() const {
+ return m_token_buffer;
+ }
+
+ void drop_token() {
+ if (!has_token()) {
+ throw lexer::Error{"internal: no tokens to drop"};
+ }
+ m_token_buffer = {};
+ parse_token();
+ }
+
+ std::optional<lexer::Token> drop_token_if(lexer::Token::Type type) {
+ if (!has_token()) {
+ throw lexer::Error{"internal: no tokens to drop"};
+ }
+ if (m_token_buffer.value().get_type() != type) {
+ return {};
+ }
+ const auto result = m_token_buffer;
+ drop_token();
+ return result;
+ }
+
+private:
+ void consume_whitespace() {
+ const auto ws = lexer::details::parse_whitespace(m_input);
+ m_input.remove_prefix(ws.length());
+ }
+
+ std::optional<lexer::Token> parse_token(std::string_view& token_view) {
+ consume_whitespace();
+ if (m_input.length() == 0) {
+ return {};
+ }
+ if (const auto const_token = lexer::details::parse_const_token(m_input, token_view); const_token.has_value()) {
+ return lexer::Token{*const_token};
+ }
+ if (const auto number = lexer::details::parse_number(m_input, token_view); number.has_value()) {
+ return lexer::Token{*number};
+ }
+ throw lexer::Error{"invalid input at: " + std::string{m_input}};
+ }
+
+ std::string_view m_input;
+ std::optional<lexer::Token> m_token_buffer;
+};
+
+}
diff --git a/server/log.hpp b/server/log.hpp
new file mode 100644
index 0000000..ca0fafd
--- /dev/null
+++ b/server/log.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <boost/format.hpp>
+#include <boost/system/error_code.hpp>
+
+#include <ctime>
+
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <utility>
+
+namespace math::server::log {
+
+namespace details {
+
+inline std::thread::id get_tid() { return std::this_thread::get_id(); }
+
+inline std::string get_timestamp() {
+ const auto tt = std::time(nullptr);
+ std::ostringstream oss;
+ oss << std::put_time(std::gmtime(&tt), "%Y-%m-%d %H:%M:%S");
+ return oss.str();
+}
+
+inline void log(const std::string& msg) {
+ std::clog << get_timestamp() << " | " << get_tid() << " | " << msg << '\n';
+}
+
+}
+
+template <typename... Args>
+inline void log(const std::string_view& fmt, Args&&... args) {
+ details::log(boost::str((boost::format(fmt.data()) % ... % args)));
+}
+
+template <typename... Args>
+inline void error(const std::string_view& fmt, Args&&... args) {
+ details::log(boost::str((boost::format(fmt.data()) % ... % args)));
+}
+
+inline void error(const boost::system::error_code& ec) {
+ details::log(ec.message());
+}
+
+}
diff --git a/server/main.cpp b/server/main.cpp
new file mode 100644
index 0000000..2cf6d35
--- /dev/null
+++ b/server/main.cpp
@@ -0,0 +1,34 @@
+#include "server.hpp"
+#include "settings.hpp"
+
+#include <boost/program_options.hpp>
+
+#include <exception>
+#include <iostream>
+
+int main(int argc, char* argv[]) {
+ try {
+ math::server::SettingsParser parser{argv[0]};
+
+ try {
+ const auto settings = parser.parse(argc, argv);
+ if (settings.exit_with_usage()) {
+ parser.usage();
+ return 0;
+ }
+
+ math::server::Server server{settings};
+ server.run();
+ } catch (const boost::program_options::error& e) {
+ parser.usage_error(e);
+ return 1;
+ }
+ } catch (const std::exception& e) {
+ std::cerr << "An error occured: " << e.what() << "\n";
+ return 1;
+ } catch (...) {
+ std::cerr << "An unknown error occured\n";
+ return 1;
+ }
+ return 0;
+}
diff --git a/server/parser.hpp b/server/parser.hpp
new file mode 100644
index 0000000..a9e5f54
--- /dev/null
+++ b/server/parser.hpp
@@ -0,0 +1,168 @@
+#pragma once
+
+#include "error.hpp"
+#include "lexer.hpp"
+
+#include <optional>
+#include <string>
+#include <string_view>
+
+namespace math::server {
+namespace parser {
+
+class Error : public server::Error {
+public:
+ explicit Error(const std::string& what)
+ : server::Error{"parser error: " + what}
+ { }
+};
+
+class BinaryOp {
+public:
+ static bool is(const lexer::Token& token) {
+ using Type = lexer::Token::Type;
+ switch (token.get_type()) {
+ case Type::PLUS:
+ case Type::MINUS:
+ case Type::ASTERISK:
+ case Type::SLASH:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ static BinaryOp from_token(const lexer::Token& token) {
+ if (!is(token)) {
+ throw Error{"internal: token is not a binary operator"};
+ }
+ return BinaryOp{token};
+ }
+
+ static constexpr unsigned min_precedence() { return 0; }
+
+ unsigned get_precedence() const {
+ using Type = lexer::Token::Type;
+ switch (m_type) {
+ case Type::PLUS:
+ case Type::MINUS:
+ return min_precedence();
+
+ case Type::ASTERISK:
+ case Type::SLASH:
+ return min_precedence() + 1;
+
+ default:
+ throw Error{"internal: undefined operator precedence"};
+ }
+ }
+
+ double exec(double lhs, double rhs) const {
+ using Type = lexer::Token::Type;
+ switch (m_type) {
+ case Type::PLUS:
+ return lhs + rhs;
+ case Type::MINUS:
+ return lhs - rhs;
+ case Type::ASTERISK:
+ return lhs * rhs;
+ case Type::SLASH:
+ // Trapping the CPU would be better?
+ if (rhs == 0.) {
+ throw Error{"division by zero"};
+ }
+ return lhs / rhs;
+ default:
+ throw Error{"internal: unsupported operator"};
+ }
+ }
+
+private:
+ explicit BinaryOp(const lexer::Token& token)
+ : m_type{token.get_type()}
+ { }
+
+ lexer::Token::Type m_type;
+};
+
+}
+
+class Parser {
+public:
+ // I did simple recursive descent parsing a long time ago (see
+ // https://github.com/egor-tensin/simple-interpreter), this appears to be
+ // a finer algorithm for parsing arithmetic expressions.
+ // Reference: https://en.wikipedia.org/wiki/Operator-precedence_parser
+
+ explicit Parser(const std::string_view& input)
+ : m_lexer{input}
+ { }
+
+ double exec() {
+ m_lexer.parse_token();
+ const auto result = exec_expr();
+ if (m_lexer.has_token()) {
+ throw parser::Error{"expected a binary operator"};
+ }
+ return result;
+ }
+
+private:
+ double exec_expr() {
+ return exec_expr(exec_primary(), parser::BinaryOp::min_precedence());
+ }
+
+ double exec_expr(double lhs, unsigned min_prec) {
+ for (auto op = peek_operator(); op.has_value() && op->get_precedence() >= min_prec;) {
+ const auto lhs_op = *op;
+ m_lexer.drop_token();
+ auto rhs = exec_primary();
+
+ for (op = peek_operator(); op.has_value() && op->get_precedence() > lhs_op.get_precedence(); op = peek_operator()) {
+ rhs = exec_expr(rhs, op->get_precedence());
+ }
+
+ lhs = lhs_op.exec(lhs, rhs);
+ }
+ return lhs;
+ }
+
+ std::optional<parser::BinaryOp> peek_operator() {
+ const auto token = m_lexer.peek_token();
+ if (!token.has_value() || !parser::BinaryOp::is(*token)) {
+ return {};
+ }
+ return parser::BinaryOp::from_token(*token);
+ }
+
+ double exec_primary() {
+ if (!m_lexer.has_token()) {
+ throw parser::Error{"expected '-', '(' or a number"};
+ }
+
+ using Type = lexer::Token::Type;
+
+ if (m_lexer.drop_token_if(Type::MINUS).has_value()) {
+ return -exec_primary();
+ }
+
+ if (m_lexer.drop_token_if(Type::LEFT_PAREN).has_value()) {
+ const auto inner = exec_expr();
+ if (!m_lexer.has_token() || !m_lexer.drop_token_if(Type::RIGHT_PAREN).has_value()) {
+ throw parser::Error{"missing closing ')'"};
+ }
+ return inner;
+ }
+
+ if (const auto token = m_lexer.drop_token_if(Type::NUMBER); token.has_value()) {
+ return token.value().get_number_value();
+ }
+
+ throw parser::Error{"expected '-', '(' or a number"};
+ }
+
+ Lexer m_lexer;
+};
+
+}
diff --git a/server/server.cpp b/server/server.cpp
new file mode 100644
index 0000000..4dc672c
--- /dev/null
+++ b/server/server.cpp
@@ -0,0 +1,111 @@
+#include "error.hpp"
+#include "log.hpp"
+#include "server.hpp"
+#include "session.hpp"
+#include "session_manager.hpp"
+#include "settings.hpp"
+
+#include <boost/asio.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/system/system_error.hpp>
+
+#include <cstddef>
+
+#include <exception>
+#include <thread>
+#include <vector>
+
+namespace math::server {
+namespace {
+
+boost::asio::ip::tcp::endpoint make_endpoint(unsigned port) {
+ return {boost::asio::ip::tcp::v4(), port};
+}
+
+void configure_acceptor(boost::asio::ip::tcp::acceptor& acceptor, unsigned port) {
+ try {
+ const auto endpoint = make_endpoint(port);
+ acceptor.open(endpoint.protocol());
+ acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
+ acceptor.bind(endpoint);
+ acceptor.listen();
+ } catch (const boost::system::system_error& e) {
+ throw Error{e.what()};
+ }
+}
+
+}
+
+Server::Server(const Settings& settings)
+ : Server{settings.m_port, settings.m_threads}
+{ }
+
+Server::Server(unsigned port, unsigned threads)
+ : m_numof_threads{threads}
+ , m_signals{m_io_context}
+ , m_acceptor{m_io_context} {
+
+ wait_for_signal();
+ configure_acceptor(m_acceptor, port);
+
+ accept();
+}
+
+void Server::run() {
+ std::vector<std::thread> threads{m_numof_threads};
+ for (std::size_t i = 0; i < m_numof_threads; ++i) {
+ threads[i] = std::thread{[this] () { m_io_context.run(); }};
+ }
+
+ for (std::size_t i = 0; i < m_numof_threads; ++i) {
+ threads[i].join();
+ }
+}
+
+void Server::wait_for_signal() {
+ try {
+ m_signals.add(SIGINT);
+ m_signals.add(SIGTERM);
+
+ m_signals.async_wait([this] (const boost::system::error_code& ec, int signo) {
+ handle_signal(ec, signo);
+ });
+ } catch (const boost::system::system_error& e) {
+ throw Error{e.what()};
+ }
+}
+
+void Server::handle_signal(const boost::system::error_code& ec, int signo) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ }
+
+ log::log("Caught signal %1%", signo);
+
+ try {
+ m_acceptor.close();
+ m_session_mgr.stop_all();
+ } catch (const std::exception& e) {
+ log::error(e.what());
+ }
+}
+
+void Server::accept() {
+ const auto session = m_session_mgr.make_session(m_io_context);
+ m_acceptor.async_accept(session->socket(),
+ [session, this] (const boost::system::error_code& ec) {
+ handle_accept(session, ec);
+ });
+}
+
+void Server::handle_accept(SessionPtr session, const boost::system::error_code& ec) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ return;
+ }
+
+ m_session_mgr.start(session);
+ accept();
+}
+
+}
diff --git a/server/server.hpp b/server/server.hpp
new file mode 100644
index 0000000..5524f88
--- /dev/null
+++ b/server/server.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "session_manager.hpp"
+#include "settings.hpp"
+
+#include <boost/asio.hpp>
+#include <boost/system/error_code.hpp>
+
+namespace math::server {
+
+class Server {
+public:
+ Server(const Settings& settings);
+ Server(unsigned port, unsigned threads);
+
+ void run();
+
+private:
+ void wait_for_signal();
+ void handle_signal(const boost::system::error_code&, int);
+
+ void accept();
+ void handle_accept(SessionPtr session, const boost::system::error_code& ec);
+
+ const unsigned m_numof_threads;
+
+ boost::asio::io_context m_io_context;
+ boost::asio::signal_set m_signals;
+ boost::asio::ip::tcp::acceptor m_acceptor;
+
+ SessionManager m_session_mgr;
+};
+
+}
diff --git a/server/session.cpp b/server/session.cpp
new file mode 100644
index 0000000..409ca5a
--- /dev/null
+++ b/server/session.cpp
@@ -0,0 +1,113 @@
+#include "error.hpp"
+#include "log.hpp"
+#include "parser.hpp"
+#include "session.hpp"
+#include "session_manager.hpp"
+
+#include <boost/asio.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/system/system_error.hpp>
+
+#include <cstddef>
+
+#include <exception>
+#include <string>
+#include <utility>
+
+namespace math::server {
+namespace {
+
+std::string reply_to_string(double result) {
+ return boost::lexical_cast<std::string>(result);
+}
+
+std::string calc_reply(const std::string& input) {
+ std::string reply;
+ try {
+ reply = reply_to_string(Parser{input}.exec());
+ } catch (const std::exception& e) {
+ reply = e.what();
+ }
+ return reply;
+}
+
+}
+
+Session::Session(SessionManager& mgr, boost::asio::io_context& io_context)
+ : m_session_mgr{mgr}, m_strand{io_context}, m_socket{io_context}
+{ }
+
+boost::asio::ip::tcp::socket& Session::socket() {
+ return m_socket;
+}
+
+void Session::start() {
+ read();
+}
+
+void Session::stop() {
+ close();
+}
+
+void Session::close() {
+ try {
+ m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
+ m_socket.close();
+ } catch (const boost::system::system_error& e) {
+ throw Error{e.what()};
+ }
+}
+
+void Session::read() {
+ const auto self = shared_from_this();
+
+ // Stop at LF
+ boost::asio::async_read_until(m_socket, m_buffer, '\n', boost::asio::bind_executor(m_strand,
+ [this, self] (const boost::system::error_code& ec, std::size_t bytes) {
+ handle_read(ec, bytes);
+ }));
+}
+
+void Session::handle_read(const boost::system::error_code& ec, std::size_t bytes) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ m_session_mgr.stop(shared_from_this());
+ return;
+ }
+
+ write(calc_reply(consume_input(bytes)));
+}
+
+std::string Session::consume_input(std::size_t bytes) {
+ const auto data = boost::asio::buffer_cast<const char*>(m_buffer.data());
+ const std::string input{data, bytes - 1};
+ m_buffer.consume(bytes);
+ return input;
+}
+
+void Session::write(std::string output) {
+ const auto self = shared_from_this();
+
+ // Include CR (so that Windows' telnet client works)
+ output += "\r\n";
+
+ boost::asio::const_buffer buffer{output.c_str(), output.length()};
+
+ boost::asio::async_write(m_socket, std::move(buffer), boost::asio::bind_executor(m_strand,
+ [this, self] (const boost::system::error_code& ec, std::size_t bytes) {
+ handle_write(ec, bytes);
+ }));
+}
+
+void Session::handle_write(const boost::system::error_code& ec, std::size_t bytes) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ m_session_mgr.stop(shared_from_this());
+ return;
+ }
+
+ read();
+}
+
+}
diff --git a/server/session.hpp b/server/session.hpp
new file mode 100644
index 0000000..ace3755
--- /dev/null
+++ b/server/session.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <boost/asio.hpp>
+#include <boost/system/error_code.hpp>
+
+#include <cstddef>
+
+#include <memory>
+#include <string>
+
+namespace math::server {
+
+class SessionManager;
+
+class Session : public std::enable_shared_from_this<Session> {
+public:
+ Session(SessionManager& mgr, boost::asio::io_context& io_context);
+
+ boost::asio::ip::tcp::socket& socket();
+
+ void start();
+ void stop();
+
+private:
+ void close();
+
+ void read();
+ void write(std::string);
+
+ void handle_read(const boost::system::error_code&, std::size_t);
+ void handle_write(const boost::system::error_code&, std::size_t);
+
+ std::string consume_input(std::size_t);
+
+ SessionManager& m_session_mgr;
+
+ boost::asio::io_context::strand m_strand;
+ boost::asio::ip::tcp::socket m_socket;
+ boost::asio::streambuf m_buffer;
+};
+
+}
diff --git a/server/session_manager.cpp b/server/session_manager.cpp
new file mode 100644
index 0000000..a42fca4
--- /dev/null
+++ b/server/session_manager.cpp
@@ -0,0 +1,37 @@
+#include "log.hpp"
+#include "session.hpp"
+#include "session_manager.hpp"
+
+#include <memory>
+#include <mutex>
+
+namespace math::server {
+
+SessionPtr SessionManager::make_session(boost::asio::io_context& io_context) {
+ return std::make_shared<Session>(*this, io_context);
+}
+
+void SessionManager::start(const SessionPtr& session) {
+ std::lock_guard<std::mutex> lck{m_mtx};
+ m_sessions.emplace(session);
+ session->start();
+}
+
+void SessionManager::stop(const SessionPtr& session) {
+ std::lock_guard<std::mutex> lck{m_mtx};
+ const auto removed = m_sessions.erase(session) > 0;
+ if (removed) {
+ session->stop();
+ }
+}
+
+void SessionManager::stop_all() {
+ std::lock_guard<std::mutex> lck{m_mtx};
+ log::log("Closing the remaining %1% session(s)...", m_sessions.size());
+ for (const auto& session : m_sessions) {
+ session->stop();
+ }
+ m_sessions.clear();
+}
+
+}
diff --git a/server/session_manager.hpp b/server/session_manager.hpp
new file mode 100644
index 0000000..f0bec0b
--- /dev/null
+++ b/server/session_manager.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <boost/asio.hpp>
+
+#include <mutex>
+#include <memory>
+#include <unordered_set>
+
+namespace math::server {
+
+class Session;
+using SessionPtr = std::shared_ptr<Session>;
+
+class SessionManager {
+public:
+ SessionManager() = default;
+
+ SessionPtr make_session(boost::asio::io_context&);
+
+ void start(const SessionPtr&);
+ void stop(const SessionPtr&);
+
+ void stop_all();
+
+private:
+ std::mutex m_mtx;
+ std::unordered_set<SessionPtr> m_sessions;
+};
+
+}
diff --git a/server/settings.hpp b/server/settings.hpp
new file mode 100644
index 0000000..310163f
--- /dev/null
+++ b/server/settings.hpp
@@ -0,0 +1,87 @@
+#pragma once
+
+#include <boost/filesystem.hpp>
+#include <boost/program_options.hpp>
+
+#include <exception>
+#include <iostream>
+#include <string>
+#include <thread>
+#include <vector>
+
+namespace math::server {
+
+struct Settings {
+ static constexpr unsigned DEFAULT_PORT = 18000;
+
+ static unsigned default_threads() { return std::thread::hardware_concurrency(); }
+
+ unsigned m_port;
+ unsigned m_threads;
+
+ bool exit_with_usage() const { return m_vm.count("help"); }
+
+ boost::program_options::variables_map m_vm;
+};
+
+class SettingsParser {
+public:
+ explicit SettingsParser(const std::string& argv0)
+ : m_prog_name{extract_filename(argv0)}
+ {
+ m_visible.add_options()
+ ("help,h",
+ "show this message and exit")
+ ("port,p",
+ boost::program_options::value(&m_settings.m_port)->default_value(Settings::DEFAULT_PORT),
+ "server port number")
+ ("threads,n",
+ boost::program_options::value(&m_settings.m_threads)->default_value(Settings::default_threads()),
+ "number of threads");
+ }
+
+ static const char* get_short_description() {
+ return "[-h|--help] [-p|--port] [-n|--threads]";
+ }
+
+ Settings parse(int argc, char* argv[]) {
+ boost::program_options::store(
+ boost::program_options::command_line_parser{argc, argv}
+ .options(m_visible)
+ .run(),
+ m_settings.m_vm);
+ if (m_settings.exit_with_usage()) {
+ return m_settings;
+ }
+ boost::program_options::notify(m_settings.m_vm);
+ return m_settings;
+ }
+
+ void usage() const {
+ std::cout << *this;
+ }
+
+ void usage_error(const std::exception& e) const {
+ std::cerr << "usage error: " << e.what() << '\n';
+ std::cerr << *this;
+ }
+
+private:
+ static std::string extract_filename(const std::string& path) {
+ return boost::filesystem::path{path}.filename().string();
+ }
+
+ const std::string m_prog_name;
+
+ boost::program_options::options_description m_visible;
+
+ Settings m_settings;
+
+ friend std::ostream& operator<<(std::ostream& os, const SettingsParser& parser) {
+ os << "usage: " << parser.m_prog_name << ' ' << get_short_description() << '\n';
+ os << parser.m_visible;
+ return os;
+ }
+};
+
+}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..bed23ce
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(unit_tests)
diff --git a/test/unit_tests/CMakeLists.txt b/test/unit_tests/CMakeLists.txt
new file mode 100644
index 0000000..d320974
--- /dev/null
+++ b/test/unit_tests/CMakeLists.txt
@@ -0,0 +1,6 @@
+find_package(Boost REQUIRED)
+
+add_executable(unit_tests main.cpp lexer.cpp parser.cpp)
+target_include_directories(unit_tests SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
+
+target_include_directories(unit_tests PRIVATE ../..)
diff --git a/test/unit_tests/lexer.cpp b/test/unit_tests/lexer.cpp
new file mode 100644
index 0000000..7b513e8
--- /dev/null
+++ b/test/unit_tests/lexer.cpp
@@ -0,0 +1,109 @@
+#include <server/lexer.hpp>
+
+#include <boost/test/unit_test.hpp>
+
+#include <vector>
+
+BOOST_AUTO_TEST_CASE(test_lexer_parse_number) {
+ using namespace math::server::lexer;
+
+ // These are valid numbers:
+ BOOST_TEST(details::parse_number("0").value() == 0);
+ BOOST_TEST(details::parse_number("1.").value() == 1.);
+ BOOST_TEST(details::parse_number(".1").value() == .1);
+ // parse_* functions only consume a single token:
+ BOOST_TEST(details::parse_number(".1+1").value() == .1);
+ BOOST_TEST(details::parse_number("1e3").value() == 1e3);
+ BOOST_TEST(details::parse_number(".123e1").value() == .123e1);
+ BOOST_TEST(details::parse_number("123e-1").value() == 123e-1);
+ BOOST_TEST(details::parse_number("123e+1").value() == 123e+1);
+ BOOST_TEST(details::parse_number("1.e6").value() == 1.e6);
+ BOOST_TEST(details::parse_number("2.3e-4").value() == 2.3e-4);
+ // These are not numbers, but perhaps something else?
+ BOOST_TEST(!details::parse_number(".").has_value());
+ BOOST_TEST(!details::parse_number(".e3").has_value());
+ BOOST_TEST(!details::parse_number("e12").has_value());
+ // This is definitely a number, but a malformed one (an exponent must be
+ // followed by some digits).
+ BOOST_CHECK_THROW(details::parse_number("12e"), Error);
+}
+
+BOOST_AUTO_TEST_CASE(test_lexer_parse_const_token) {
+ using namespace math::server::lexer;
+
+ // TODO: No time to implement the required string conversions, hence the
+ // extra parentheses.
+ BOOST_TEST((details::parse_const_token("+").value() == Token::Type::PLUS));
+ // parse_* functions only consume a single token:
+ BOOST_TEST((details::parse_const_token("+++").value() == Token::Type::PLUS));
+ BOOST_TEST((details::parse_const_token("-").value() == Token::Type::MINUS));
+ BOOST_TEST(!details::parse_const_token("&+").has_value());
+}
+
+BOOST_AUTO_TEST_CASE(test_lexer_get_tokens) {
+ using namespace math::server;
+ using namespace math::server::lexer;
+
+ // TODO: No time to implement the required string conversions, hence the
+ // extra parentheses.
+ {
+ Lexer lexer{""};
+ BOOST_TEST((lexer.get_tokens() == std::vector<Token>{}));
+ }
+ {
+ Lexer lexer{" + - "};
+ BOOST_TEST((lexer.get_tokens() == std::vector<Token>{
+ Token{Token::Type::PLUS},
+ Token{Token::Type::MINUS},
+ }));
+ }
+ {
+ Lexer lexer{"&"};
+ BOOST_CHECK_THROW((lexer.get_tokens()), lexer::Error);
+ }
+ {
+ Lexer lexer{" 1 + 123 & 456"};
+ BOOST_CHECK_THROW((lexer.get_tokens()), lexer::Error);
+ }
+ {
+ Lexer lexer{"1+2"};
+ BOOST_TEST((lexer.get_tokens() == std::vector<Token>{
+ Token{1},
+ Token{Token::Type::PLUS},
+ Token{2},
+ }));
+ }
+ {
+ Lexer lexer{"1+2 * (3- 4e-2)"};
+ BOOST_TEST((lexer.get_tokens() == std::vector<Token>{
+ Token{1},
+ Token{Token::Type::PLUS},
+ Token{2},
+ Token{Token::Type::ASTERISK},
+ Token{Token::Type::LEFT_PAREN},
+ Token{3},
+ Token{Token::Type::MINUS},
+ Token{4e-2},
+ Token{Token::Type::RIGHT_PAREN},
+ }));
+ }
+ {
+ Lexer lexer{" 2 * (1 + 3 * (1 - -3)) "};
+ BOOST_TEST((lexer.get_tokens() == std::vector<Token>{
+ Token{2},
+ Token{Token::Type::ASTERISK},
+ Token{Token::Type::LEFT_PAREN},
+ Token{1},
+ Token{Token::Type::PLUS},
+ Token{3},
+ Token{Token::Type::ASTERISK},
+ Token{Token::Type::LEFT_PAREN},
+ Token{1},
+ Token{Token::Type::MINUS},
+ Token{Token::Type::MINUS},
+ Token{3},
+ Token{Token::Type::RIGHT_PAREN},
+ Token{Token::Type::RIGHT_PAREN},
+ }));
+ }
+}
diff --git a/test/unit_tests/main.cpp b/test/unit_tests/main.cpp
new file mode 100644
index 0000000..5d8b492
--- /dev/null
+++ b/test/unit_tests/main.cpp
@@ -0,0 +1,2 @@
+#define BOOST_TEST_MODULE math_server tests
+#include <boost/test/included/unit_test.hpp>
diff --git a/test/unit_tests/parser.cpp b/test/unit_tests/parser.cpp
new file mode 100644
index 0000000..11f48d3
--- /dev/null
+++ b/test/unit_tests/parser.cpp
@@ -0,0 +1,48 @@
+#include <server/parser.hpp>
+
+#include <boost/test/unit_test.hpp>
+
+BOOST_AUTO_TEST_CASE(test_parser_exec) {
+ using namespace math::server;
+
+ {
+ Parser parser{""};
+ BOOST_CHECK_THROW(parser.exec(), parser::Error);
+ }
+ {
+ Parser parser{"1"};
+ BOOST_TEST(parser.exec() == 1);
+ }
+ {
+ Parser parser{" 1 + "};
+ BOOST_CHECK_THROW(parser.exec(), parser::Error);
+ }
+ {
+ Parser parser{" 1 + 2 "};
+ BOOST_TEST(parser.exec() == 3);
+ }
+ {
+ Parser parser{" 2 * 1 + 3 "};
+ BOOST_TEST(parser.exec() == 5);
+ }
+ {
+ Parser parser{" 2 * (1 + 3) "};
+ BOOST_TEST(parser.exec() == 8);
+ }
+ {
+ Parser parser{" 2 * (1 + 3 "};
+ BOOST_CHECK_THROW(parser.exec(), parser::Error);
+ }
+ {
+ Parser parser{" 2 * (1 + 3) )"};
+ BOOST_CHECK_THROW(parser.exec(), parser::Error);
+ }
+ {
+ Parser parser{" 2 * (1 + 3 * (1 - -3)) "};
+ BOOST_TEST(parser.exec() == 26);
+ }
+ {
+ Parser parser{" -2 * ---- (3 + -100e-1) "}; // Looks weird, but also works in e.g. Python
+ BOOST_TEST(parser.exec() == 14);
+ }
+}