Updated README.md to include additional info about this project

Implemented reading settings from config file
ftsort now outputs total runtime in ms and peak virtual memory usage
Added some error handling
Added documenation to tape_config.h
Changed catch2 cmake dependency to be resolved using FetchContent
Added generate_input test binary to generate big input tape data sets
Header inclusion path changed to _include_ directory, so that tapelib has to be explicitly specified in include directrives
This commit is contained in:
Egor 2024-10-28 05:23:42 +03:00
parent d7bcbbfd08
commit f8bfc78cad
13 changed files with 207 additions and 13 deletions

View file

@ -4,7 +4,6 @@ project(yadro-task VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_BUILD_TYPE Debug)
# clang-tidy # clang-tidy
find_program(CLANG_TIDY_EXE NAMES clang-tidy REQUIRED) find_program(CLANG_TIDY_EXE NAMES clang-tidy REQUIRED)

View file

@ -11,3 +11,38 @@
- Написать класс, реализующий алгоритм сортировки данных с входной ленты на выходную. - Написать класс, реализующий алгоритм сортировки данных с входной ленты на выходную.
- Консольное приложение должно принимать на вход имя входного и выходного файлов и производить сортировку. - Консольное приложение должно принимать на вход имя входного и выходного файлов и производить сортировку.
- Желательно написать юнит-тесты. - Желательно написать юнит-тесты.
# Описание
Проект использует CMake для сборки. Некоторый функционал проекта несовместим с _Windows_ и _MacOS_ (получение используемой памяти, создание временных файлов и т. п.) Проект успешно собирался на _Linux_ _x86_64_ и _aarch64_. Документация реализована посредством этого __README__ файла, а также подробными комментариями в коде
Структура проекта:
- ```bin``` - Исходный код исполняемых файлов
- ```ftsort``` - Исходный код основной программы проекта
- ```include``` - Директория для заголовков
- ```tapelib``` - Директория для заголовков библиотеки ```tapelib```
- ```src``` - Исходный код библиотек
- ```tests``` - Исходный код unit-тестов
# Сборка
## Linux
1. Склонируйте данный репозиторий и перейдите в него
```
git clone https://git.obamna.ru/erius/yadro-task.git
cd yadro-task
```
2. Сгенерируйте файлы сборки CMake
```
cmake -S . -B build
```
3. Соберите проект с помощью CMake
```
cmake --build build
```
4. Запустите unit-тесты
```
ctest --test-dir build -V
```
Сборка и работоспособность проекта не проверялись на _Windows_ и _MacOS_

View file

@ -1,6 +1,26 @@
#include "tape_config.h" #include "tape_config.h"
#include "tape_util.h" #include "tapelib/tape_util.h"
#include <chrono>
#include <cstddef>
#include <fstream>
#include <iostream> #include <iostream>
#include <string>
size_t get_mem_peak() {
const static std::string mem_info_file = "/proc/self/status";
const static std::string mem_field = "VmPeak:";
std::ifstream status(mem_info_file);
size_t mem = 0;
std::string line;
while (std::getline(status, line)) {
if (line.find(mem_field) != std::string::npos) {
std::stringstream mem_line(line.substr(mem_field.size()));
mem_line >> mem;
return mem;
}
}
return mem;
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
AppSettings settings = parse_command_line(argc, argv); AppSettings settings = parse_command_line(argc, argv);
@ -12,15 +32,38 @@ int main(int argc, char *argv[]) {
std::cerr << HELP_MSG << std::endl; std::cerr << HELP_MSG << std::endl;
return 0; return 0;
} }
if (!settings.config_file_path.empty() &&
!read_settings_from_file(settings)) {
return -1;
}
tape::FileTape input(settings.input_file_name, settings.ft_settings); tape::FileTape input(settings.input_file_name, settings.ft_settings);
if (!input.is_open()) {
std::cerr << "Failed to create INPUT tape - file not found or "
"insufficient permissions"
<< std::endl;
return -1;
}
tape::FileTape output(input, settings.output_file_name, tape::FileTape output(input, settings.output_file_name,
settings.ft_settings); settings.ft_settings);
// tmp tape factory that captures cmd.settings from local scope if (!output.is_open()) {
std::cerr << "Failed to create OUTPUT tape - insufficient permissions"
<< std::endl;
return -1;
}
// tmp tape factory that captures settings.ft_settings from local scope
tape::TempTapeFactory factory = tape::TempTapeFactory factory =
[&](size_t cells) -> std::unique_ptr<tape::Tape> { [&](size_t cells) -> std::unique_ptr<tape::Tape> {
return std::make_unique<tape::FileTape>( return std::make_unique<tape::FileTape>(
tape::FileTape(cells, settings.ft_settings)); tape::FileTape(cells, settings.ft_settings));
}; };
auto start = std::chrono::high_resolution_clock::now();
tape::external_sort(input, output, factory, settings.memory_limit); tape::external_sort(input, output, factory, settings.memory_limit);
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<milliseconds>(end - start);
std::cout << "Successfully sorted " << settings.input_file_name << " into "
<< settings.output_file_name << std::endl;
std::cout << "The operation took " << duration.count()
<< " ms to complete and peaked at " << get_mem_peak()
<< " KiB of virtual memory usage" << std::endl;
return 0; return 0;
} }

View file

@ -1,6 +1,10 @@
#include "tape_config.h" #include "tape_config.h"
#include <bits/getopt_core.h> #include <bits/getopt_core.h>
#include <fstream>
#include <getopt.h> #include <getopt.h>
#include <iostream>
#include <stdexcept>
#include <string>
const static struct std::vector<option> CMD_OPTS{ const static struct std::vector<option> CMD_OPTS{
{.name = "write-delay", {.name = "write-delay",
@ -80,6 +84,9 @@ AppSettings parse_command_line(int argc, char **argv) {
// parse positional args INPUT_FILE OUTPUT_FILE // parse positional args INPUT_FILE OUTPUT_FILE
if (argc - optind < 2) { if (argc - optind < 2) {
settings.help = true; settings.help = true;
std::cerr << "Not enough positional arguments (Missing INPUT_FILE or "
"OUTPUT_FILE)"
<< std::endl;
return settings; return settings;
} }
// no linting to disable pointer arithmetic warning // no linting to disable pointer arithmetic warning
@ -87,3 +94,48 @@ AppSettings parse_command_line(int argc, char **argv) {
settings.output_file_name = argv[optind + 1]; // NOLINT settings.output_file_name = argv[optind + 1]; // NOLINT
return settings; return settings;
} }
const static char CFG_DELIMETER = '=';
bool read_settings_from_file(AppSettings &settings) {
std::ifstream config(settings.config_file_path);
if (!config.is_open()) {
std::cerr << "Failed to read config: file doesn't exist or "
"insufficient permissions"
<< std::endl;
return false;
}
std::string line;
size_t line_number = 1;
while (std::getline(config, line)) {
size_t delim_index = line.find(CFG_DELIMETER);
if (delim_index >= line.size()) {
continue;
}
std::string key = line.substr(0, delim_index);
std::string value = line.substr(delim_index + 1);
try {
if (key == "write-delay") {
settings.ft_settings.write_delay = parse_delay(value);
} else if (key == "read-delay") {
settings.ft_settings.read_delay = parse_delay(value);
} else if (key == "seek-forward-delay") {
settings.ft_settings.seek_forward_delay = parse_delay(value);
} else if (key == "seek-backwards-delay") {
settings.ft_settings.seek_backwards_delay = parse_delay(value);
} else if (key == "memory-limit") {
settings.memory_limit = std::stoi(value);
}
} catch (std::invalid_argument) {
std::cerr << "Failed to parse config at line " << line_number
<< ": value must be a positive integer" << std::endl;
return false;
} catch (std::out_of_range) {
std::cerr << "Failed to parse config at line " << line_number
<< ": value is out of allowed range" << std::endl;
return false;
}
line_number++;
}
return true;
}

View file

@ -1,7 +1,7 @@
#ifndef TAPE_CONFIG_H #ifndef TAPE_CONFIG_H
#define TAPE_CONFIG_H #define TAPE_CONFIG_H
#include "filetape.h" #include "tapelib/filetape.h"
// stringizing opearator to convert macro into string literal // stringizing opearator to convert macro into string literal
#define xstr(s) str(s) #define xstr(s) str(s)
@ -51,8 +51,26 @@ struct AppSettings {
tape::FileTapeSettings ft_settings; tape::FileTapeSettings ft_settings;
}; };
/**
* Parses command line arguments (from main) and creates an instance of
* AppSettings from them
*
* @param argc Args count from main
* @param argv Args array from main
* @return Instance of AppSettings filled with values from command line
*/
AppSettings parse_command_line(int argc, char **argv); AppSettings parse_command_line(int argc, char **argv);
void read_settings_from_file(AppSettings &settings); /**
* Accepts AppSettings prefilled with settings from command line, tries to open
* the file specified in the config_file_path field and reads values from the
* supplied config files into provided AppSettings, overwriting the settings.
*
* @param settings AppSettings, values of which will be overwritten by configs
* values
* @return true, if config file was opened and read successfully, otherwise
* false
*/
bool read_settings_from_file(AppSettings &settings);
#endif // !TAPE_CONFIG_H #endif // !TAPE_CONFIG_H

View file

@ -195,6 +195,8 @@ class FileTape : public Tape {
bool is_tmp() const { return this->tmp; } bool is_tmp() const { return this->tmp; }
bool is_open() const { return this->file.is_open(); }
const FileTapeSettings &get_settings() const { return this->settings; } const FileTapeSettings &get_settings() const { return this->settings; }
const std::string &get_file_name() const { return this->file_name; } const std::string &get_file_name() const { return this->file_name; }

View file

@ -2,4 +2,4 @@ add_library(tapelib)
target_sources(tapelib PRIVATE filetape.cpp target_sources(tapelib PRIVATE filetape.cpp
tape_util.cpp tape_util.cpp
) )
target_include_directories(tapelib PUBLIC ${PROJECT_SOURCE_DIR}/include/tapelib) target_include_directories(tapelib PUBLIC ${PROJECT_SOURCE_DIR}/include)

View file

@ -1,4 +1,4 @@
#include "filetape.h" #include "tapelib/filetape.h"
#include <cstddef> #include <cstddef>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@ -13,8 +13,6 @@ using tape::FileTape;
std::string generate_tmp_file_name() { std::string generate_tmp_file_name() {
std::array<char, L_tmpnam> tmp_file_name{}; std::array<char, L_tmpnam> tmp_file_name{};
// tmpnam is unsafe, but its cross-platform // tmpnam is unsafe, but its cross-platform
// TODO: make a safe, cross-platform tmp file handler
// (possibly utilizing std::filesystem::temp_directory_path())
std::tmpnam(tmp_file_name.data()); std::tmpnam(tmp_file_name.data());
return tmp_file_name.data(); return tmp_file_name.data();
} }

View file

@ -1,4 +1,4 @@
#include "tape_util.h" #include "tapelib/tape_util.h"
#include <algorithm> #include <algorithm>
using std::unique_ptr; using std::unique_ptr;

View file

@ -1,4 +1,12 @@
find_package(Catch2 3 REQUIRED) Include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.1
)
FetchContent_MakeAvailable(Catch2)
add_executable(filetape_tests filetape_tests.cpp) add_executable(filetape_tests filetape_tests.cpp)
target_link_libraries(filetape_tests PRIVATE tapelib) target_link_libraries(filetape_tests PRIVATE tapelib)
@ -11,3 +19,7 @@ target_link_libraries(filetape_sort_tests PRIVATE tapelib)
target_link_libraries(filetape_sort_tests PRIVATE Catch2::Catch2WithMain) target_link_libraries(filetape_sort_tests PRIVATE Catch2::Catch2WithMain)
add_test(filetape_sort_tests filetape_sort_tests) add_test(filetape_sort_tests filetape_sort_tests)
# Compile binary for generating big files for input tapes
add_executable(generate_input generate_input.cpp)
target_link_libraries(generate_input PRIVATE tapelib)

View file

@ -1,4 +1,4 @@
#include "tape_util.h" #include "tapelib/tape_util.h"
#include <algorithm> #include <algorithm>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>

View file

@ -1,4 +1,4 @@
#include "filetape.h" #include "tapelib/filetape.h"
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <vector> #include <vector>

35
tests/generate_input.cpp Normal file
View file

@ -0,0 +1,35 @@
#include "tapelib/filetape.h"
#include <cstddef>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <iostream>
#include <string>
/**
* Generate big input data sets for external sort testing.
* Takes 2 positional arguments - first one is the ouput file name,
* secon one is the amount of random cells that will be generated.
* Each cell value is in range [0; RAND_MAX]
*/
int main(int argc, char *argv[]) {
if (argc < 3) {
std::cerr << "Not enough arguments" << std::endl;
return -1;
}
std::string out_name = argv[1]; // NOLINT
size_t cells = std::stoi(argv[2]); // NOLINT
std::ofstream out(out_name);
if (!out.is_open()) {
std::cerr << "Failed to open file" << std::endl;
return -1;
}
srand(time(nullptr));
out << std::setfill('0') << std::setw(tape::FT_CELL_SIZE)
<< std::to_string(rand());
for (size_t i = 1; i < cells; i++) {
out << tape::FT_DELIMETER << std::setfill('0')
<< std::setw(tape::FT_CELL_SIZE) << std::to_string(rand());
}
return 0;
}