diff --git a/firmware/application/apps/ui_looking_glass_app.cpp b/firmware/application/apps/ui_looking_glass_app.cpp index efc7a6ad..9d8ec416 100644 --- a/firmware/application/apps/ui_looking_glass_app.cpp +++ b/firmware/application/apps/ui_looking_glass_app.cpp @@ -22,6 +22,8 @@ */ #include "ui_looking_glass_app.hpp" +#include "file_reader.hpp" +#include "string_format.hpp" using namespace portapack; @@ -519,70 +521,42 @@ GlassView::GlassView( void GlassView::load_Presets() { File presets_file; - auto result = presets_file.open("LOOKINGGLASS/PRESETS.TXT"); - presets_db.clear(); // Start with fresh db - if (result.is_valid()) { - presets_Default(); // There is no txt, store a default range - } else { - std::string line; // There is a txt file - char one_char[1]; // Read it char by char - for (size_t pointer = 0; pointer < presets_file.size(); pointer++) { - presets_file.seek(pointer); - presets_file.read(one_char, 1); - if ((int)one_char[0] > 31) { // ascii space upwards - line += one_char[0]; // Add it to the textline - } else if (one_char[0] == '\n') { // New Line - txtline_process(line); // make sense of this textline - line.clear(); // Ready for next textline - } + auto error = presets_file.open("LOOKINGGLASS/PRESETS.TXT"); + presets_db.clear(); + + if (!error) { + auto reader = FileLineReader(presets_file); + for (const auto& line : reader) { + if (line.length() == 0 || line[0] == '#') + continue; + + auto cols = split_string(line, ','); + if (cols.size() != 3) + continue; + + // TODO: add some conversion helpers that take string_view. + presets_db.emplace_back(preset_entry{ + std::stoi(std::string{cols[0]}), + std::stoi(std::string{cols[1]}), + trimr(std::string{cols[2]})}); } - if (line.length() > 0) - txtline_process(line); // Last line had no newline at end ? - if (!presets_db.size()) - presets_Default(); // no antenna on txt, use default } + + // Couldn't load any from the file, load a default instead. + if (presets_db.empty()) + presets_Default(); + populate_Presets(); } -void GlassView::txtline_process(std::string& line) { - if (line.find("#") != std::string::npos) - return; // Line is just a comment - - size_t comma = line.find(","); // Get first comma position - if (comma == std::string::npos) - return; // No comma at all - - size_t previous = 0; - preset_entry new_preset; - - new_preset.min = std::stoi(line.substr(0, comma)); - if (!new_preset.min) - return; // No frequency! - - previous = comma + 1; - comma = line.find(",", previous); // Search for next delimiter - if (comma == std::string::npos) - return; // No comma at all - - new_preset.max = std::stoi(line.substr(previous, comma - previous)); - if (!new_preset.max) - return; // No frequency! - - new_preset.label = line.substr(comma + 1); - if (new_preset.label.size() == 0) - return; // No label ? - - presets_db.push_back(new_preset); // Add this preset. -} - void GlassView::populate_Presets() { using option_t = std::pair; using options_t = std::vector; options_t entries; - for (preset_entry preset : presets_db) { // go thru all available presets + for (const auto& preset : presets_db) entries.emplace_back(preset.label, entries.size()); - } + range_presets.set_options(entries); } diff --git a/firmware/application/apps/ui_sigfrx.hpp b/firmware/application/apps/ui_sigfrx.hpp index 441004c7..24de2088 100644 --- a/firmware/application/apps/ui_sigfrx.hpp +++ b/firmware/application/apps/ui_sigfrx.hpp @@ -51,10 +51,7 @@ class SIGFRXView : public View { uint8_t last_channel; uint8_t detect_counter = 0; - RxRadioState radio_state_{ - 1750000 /* bandwidth */, - 3072000 /* sampling rate */ - }; + RxRadioState radio_state_{}; const uint16_t sigfrx_marks[18] = { 10, 8, 0, diff --git a/firmware/application/apps/ui_text_editor.cpp b/firmware/application/apps/ui_text_editor.cpp index 6b2ca075..461e7e3d 100644 --- a/firmware/application/apps/ui_text_editor.cpp +++ b/firmware/application/apps/ui_text_editor.cpp @@ -115,8 +115,10 @@ bool TextViewer::on_encoder(EncoderEvent delta) { if (cursor_.dir == ScrollDirection::Horizontal) updated = apply_scrolling_constraints(0, delta); - else + else { + delta *= 16; updated = apply_scrolling_constraints(delta, 0); + } if (updated) redraw(); diff --git a/firmware/application/file_reader.hpp b/firmware/application/file_reader.hpp new file mode 100644 index 00000000..ca5f0720 --- /dev/null +++ b/firmware/application/file_reader.hpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program 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, or (at your option) + * any later version. + * + * This program 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 this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __FILE_READER_HPP__ +#define __FILE_READER_HPP__ + +#include "file.hpp" +#include +#include +#include +#include +#include + +/* BufferType requires the following members + * Size size() + * Result read(void* data, Size bytes_to_read) + * Result seek(uint32_t offset) + */ + +/* Iterates lines in buffer split on '\n'. + * NB: very basic iterator impl, don't try anything fancy with it. */ +template +class BufferLineReader { + public: + struct iterator { + bool operator!=(const iterator& other) { + return this->pos_ != other.pos_ || this->reader_ != other.reader_; + } + + const std::string& operator*() { + if (!cached_) { + bool ok = reader_->read_line(*this); + cached_ = true; + + if (!ok) *this = reader_->end(); + } + + return line_data_; + } + + iterator& operator++() { + const auto size = reader_->size(); + + if (pos_ < size) { + cached_ = false; + pos_ += line_data_.length(); + } + + if (pos_ >= size) + *this = reader_->end(); + + return *this; + } + + typename BufferType::Size pos_{}; + BufferLineReader* reader_{}; + bool cached_ = false; + std::string line_data_{}; + }; + + BufferLineReader(BufferType& buffer) + : buffer_{buffer} {} + + iterator begin() { return {0, this}; } + iterator end() { return {size(), this}; } + + typename BufferType::Size size() const { return buffer_.size(); } + + private: + BufferType& buffer_; + + bool read_line(iterator& it) { + constexpr size_t buf_size = 0x80; + char buf[buf_size]; + uint32_t offset = 0; + + it.line_data_.resize(buf_size); + buffer_.seek(it.pos_); + + while (true) { + auto read = buffer_.read(buf, buf_size); + if (!read) + return false; + + // Find newline. + auto len = 0u; + for (; len < *read; ++len) { + if (buf[len] == '\n') { + ++len; + break; + } + } + + // Reallocate if needed. + if (offset + len >= it.line_data_.length()) + it.line_data_.resize(offset + len); + + std::strncpy(&it.line_data_[offset], buf, len); + offset += len; + + if (len < buf_size) + break; + } + + it.line_data_.resize(offset); + return true; + } +}; + +using FileLineReader = BufferLineReader; + +/* Splits the string on the specified char and returns + * a vector of string_views. NB: the lifetime of the + * string to split must be maintained while the views + * are used or they will dangle. */ +std::vector split_string(std::string_view str, char c) { + std::vector cols; + size_t start = 0; + + while (start < str.length()) { + auto it = str.find(c, start); + + if (it == str.npos) + break; + + // TODO: allow empty? + cols.emplace_back(&str[start], it - start); + start = it + 1; + } + + if (start <= str.length() && !str.empty()) + cols.emplace_back(&str[start], str.length() - start); + + return cols; +} + +#endif diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index 35d4b32e..287be52d 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -281,14 +281,16 @@ double get_decimals(double num, int16_t mult, bool round) { return intnum; } +static const char* whitespace_str = " \t\r\n"; + std::string trim(const std::string& str) { - auto first = str.find_first_not_of(' '); - auto last = str.find_last_not_of(' '); + auto first = str.find_first_not_of(whitespace_str); + auto last = str.find_last_not_of(whitespace_str); return str.substr(first, last - first); } -std::string trimr(std::string str) { - size_t last = str.find_last_not_of(' '); +std::string trimr(const std::string& str) { + size_t last = str.find_last_not_of(whitespace_str); return (last != std::string::npos) ? str.substr(0, last + 1) : ""; // Remove the trailing spaces } diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index 7fa9254b..12a5ec30 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -68,8 +68,8 @@ std::string to_string_file_size(uint32_t file_size); std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precision); double get_decimals(double num, int16_t mult, bool round = false); // euquiq added -std::string trim(const std::string& str); // Remove whitespace at ends. -std::string trimr(std::string str); // Remove trailing spaces +std::string trim(const std::string& str); // Remove whitespace at ends. +std::string trimr(const std::string& str); // Remove trailing spaces std::string truncate(const std::string& str, size_t length); #endif /*__STRING_FORMAT_H__*/ diff --git a/firmware/test/application/CMakeLists.txt b/firmware/test/application/CMakeLists.txt index 08292222..59c5b0c4 100644 --- a/firmware/test/application/CMakeLists.txt +++ b/firmware/test/application/CMakeLists.txt @@ -36,7 +36,9 @@ add_executable(application_test EXCLUDE_FROM_ALL ${PROJECT_SOURCE_DIR}/main.cpp ${PROJECT_SOURCE_DIR}/test_basics.cpp ${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp + ${PROJECT_SOURCE_DIR}/test_file_reader.cpp ${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp + ${PROJECT_SOURCE_DIR}/test_mock_file.cpp ${PROJECT_SOURCE_DIR}/test_optional.cpp ${PROJECT_SOURCE_DIR}/test_utility.cpp ) diff --git a/firmware/test/application/mock_file.hpp b/firmware/test/application/mock_file.hpp new file mode 100644 index 00000000..1dea7743 --- /dev/null +++ b/firmware/test/application/mock_file.hpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program 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, or (at your option) + * any later version. + * + * This program 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 this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include "file.hpp" +#include +#include +#include + +/* Mocks the File interface with a backing string. */ +class MockFile { + public: + using Error = File::Error; + using Offset = File::Offset; + using Size = File::Size; + template + using Result = File::Result; + + MockFile(std::string data) + : data_{std::move(data)} {} + + Size size() { return data_.size(); } + + Result seek(uint32_t offset) { + if ((int32_t)offset < 0) + return {static_cast(FR_BAD_SEEK)}; + + auto previous = offset_; + + if (offset > size()) + data_.resize(offset); + + offset_ = offset; + return previous; + } + + Result truncate() { + data_.resize(offset_); + return offset_; + } + + Result read(void* data, Size bytes_to_read) { + if (offset_ + bytes_to_read > size()) + bytes_to_read = size() - offset_; + + if (bytes_to_read == 0 || bytes_to_read > size()) // NB: underflow wrap + return 0; + + memcpy(data, &data_[offset_], bytes_to_read); + offset_ += bytes_to_read; + return bytes_to_read; + } + + Result write(const void* data, Size bytes_to_write) { + auto new_offset = offset_ + bytes_to_write; + if (new_offset >= size()) + data_.resize(new_offset); + + memcpy(&data_[offset_], data, bytes_to_write); + offset_ = new_offset; + return bytes_to_write; + } + + Optional sync() { + return {}; + } + + std::string data_; + uint32_t offset_{0}; +}; diff --git a/firmware/test/application/test_file_reader.cpp b/firmware/test/application/test_file_reader.cpp new file mode 100644 index 00000000..a1833b80 --- /dev/null +++ b/firmware/test/application/test_file_reader.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program 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, or (at your option) + * any later version. + * + * This program 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 this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "doctest.h" +#include "mock_file.hpp" +#include "file_reader.hpp" +#include + +TEST_SUITE_BEGIN("Test BufferLineReader"); + +TEST_CASE("It can iterate file lines.") { + MockFile f{"abc\ndef\nhij"}; + BufferLineReader reader{f}; + int line_count = 0; + for (const auto& line : reader) { + printf("Line: %s", line.c_str()); + ++line_count; + } + + CHECK_EQ(line_count, 3); +} + +TEST_CASE("It can iterate file ending with newline.") { + MockFile f{"abc\ndef\nhij\n"}; + BufferLineReader reader{f}; + int line_count = 0; + for (const auto& line : reader) { + printf("Line: %s", line.c_str()); + ++line_count; + } + + CHECK_EQ(line_count, 3); +} + +TEST_CASE("It can iterate files with empty lines.") { + MockFile f{"abc\ndef\n\nhij\n\n"}; + BufferLineReader reader{f}; + int line_count = 0; + for (const auto& line : reader) { + printf("Line: %s", line.c_str()); + ++line_count; + } + + CHECK_EQ(line_count, 5); +} + +TEST_CASE("It can iterate large lines.") { + std::string long_line(0x90, 'b'); + long_line.back() = 'c'; + + MockFile f{long_line + "\n" + long_line + "\n"}; + BufferLineReader reader{f}; + int line_count = 0; + for (const auto& line : reader) { + CHECK_EQ(line.length(), 0x91); + printf("Line: %s", line.c_str()); + ++line_count; + } + + CHECK_EQ(line_count, 2); +} + +TEST_SUITE_END(); + +TEST_SUITE_BEGIN("Test split_string"); + +TEST_CASE("Empty string returns no results.") { + auto r = split_string("", ','); + CHECK(r.empty()); +} + +TEST_CASE("String without delimiter returns 1 result.") { + auto r = split_string("hello", ','); + REQUIRE_EQ(r.size(), 1); + CHECK(r[0] == "hello"); +} + +TEST_CASE("It will split on delimiter.") { + auto r = split_string("hello,world", ','); + REQUIRE_EQ(r.size(), 2); + CHECK(r[0] == "hello"); + CHECK(r[1] == "world"); +} + +TEST_CASE("It will return empty columns.") { + auto r = split_string("hello,,world", ','); + REQUIRE_EQ(r.size(), 3); + CHECK(r[0] == "hello"); + CHECK(r[1] == ""); + CHECK(r[2] == "world"); +} + +TEST_CASE("It will return empty first column.") { + auto r = split_string(",hello,world", ','); + REQUIRE_EQ(r.size(), 3); + CHECK(r[0] == ""); + CHECK(r[1] == "hello"); + CHECK(r[2] == "world"); +} + +TEST_CASE("It will return empty last column.") { + auto r = split_string("hello,world,", ','); + REQUIRE_EQ(r.size(), 3); + CHECK(r[0] == "hello"); + CHECK(r[1] == "world"); + CHECK(r[2] == ""); +} + +TEST_CASE("It will split only empty columns.") { + auto r = split_string(",,,,", ','); + REQUIRE_EQ(r.size(), 5); + CHECK(r[0] == ""); + CHECK(r[4] == ""); +} + +TEST_SUITE_END(); + +/* Simple example of how to use this to read settings by lines. */ +TEST_CASE("It can parse a settings file.") { + MockFile f{"100,File.txt,5\n200,File2.txt,7"}; + BufferLineReader reader{f}; + std::vector data; + + for (const auto& line : reader) { + auto cols = split_string(line, ','); + for (auto col : cols) + data.emplace_back(col); + } + + REQUIRE_EQ(data.size(), 6); + CHECK(data[0] == "100"); + CHECK(data[1] == "File.txt"); + CHECK(data[2] == "5\n"); // NB: Newlines need to be manually trimmed. + CHECK(data[3] == "200"); + CHECK(data[4] == "File2.txt"); + CHECK(data[5] == "7"); +} diff --git a/firmware/test/application/test_file_wrapper.cpp b/firmware/test/application/test_file_wrapper.cpp index 98376097..1b179df9 100644 --- a/firmware/test/application/test_file_wrapper.cpp +++ b/firmware/test/application/test_file_wrapper.cpp @@ -22,267 +22,7 @@ #include "doctest.h" #include "file.hpp" #include "file_wrapper.hpp" - -#include -#include -#include - -/* Mocks the File interface with a backing string. */ -class MockFile { - public: - using Error = File::Error; - using Offset = File::Offset; - using Size = File::Size; - template - using Result = File::Result; - - MockFile(std::string data) - : data_{std::move(data)} {} - - Size size() { return data_.size(); } - - Result seek(uint32_t offset) { - if ((int32_t)offset < 0) - return {static_cast(FR_BAD_SEEK)}; - - auto previous = offset_; - - if (offset > size()) - data_.resize(offset); - - offset_ = offset; - return previous; - } - - Result truncate() { - data_.resize(offset_); - return offset_; - } - - Result read(void* data, Size bytes_to_read) { - if (offset_ + bytes_to_read > size()) - bytes_to_read = size() - offset_; - - if (bytes_to_read == 0 || bytes_to_read > size()) // NB: underflow wrap - return 0; - - memcpy(data, &data_[offset_], bytes_to_read); - offset_ += bytes_to_read; - return bytes_to_read; - } - - Result write(const void* data, Size bytes_to_write) { - auto new_offset = offset_ + bytes_to_write; - if (new_offset >= size()) - data_.resize(new_offset); - - memcpy(&data_[offset_], data, bytes_to_write); - offset_ = new_offset; - return bytes_to_write; - } - - Optional sync() { - return {}; - } - - std::string data_; - uint32_t offset_{0}; -}; - -/* Verifies correctness of MockFile. */ -TEST_SUITE("Test MockFile") { - SCENARIO("File size") { - GIVEN("Empty string") { - MockFile f{""}; - - THEN("size() should be 0.") { - CHECK_EQ(f.size(), 0); - } - } - - GIVEN("Not empty string") { - MockFile f{"abc"}; - - THEN("size() should be string length.") { - CHECK_EQ(f.size(), 3); - } - } - } - - SCENARIO("File seek") { - GIVEN("Valid file") { - MockFile f{"abc\ndef"}; - auto init_size = f.size(); - - WHEN("seek()") { - f.seek(4); - THEN("offset_ should be updated.") { - CHECK_EQ(f.offset_, 4); - } - } - - WHEN("seek() negative offset") { - auto r = f.seek(-1); - - THEN("Result should be bad_seek.") { - CHECK(r.is_error()); - CHECK_EQ(r.error().code(), FR_BAD_SEEK); - } - } - - WHEN("seek() offset is size()") { - auto r = f.seek(f.size()); - - THEN("File should not grow.") { - CHECK(r.is_ok()); - CHECK_EQ(f.size(), init_size); - } - } - - WHEN("seek() offset > size()") { - auto r = f.seek(f.size() + 1); - - THEN("File should grow.") { - CHECK(r.is_ok()); - CHECK_EQ(f.size(), init_size + 1); - } - } - - WHEN("seek() offset < size()") { - auto r = f.seek(1); - - THEN("Result should be ok.") { - CHECK(r.is_ok()); - } - - r = f.seek(3); - - THEN("Result should be previous offset") { - CHECK(r); - CHECK_EQ(*r, 1); - } - } - } - } - - SCENARIO("File read") { - GIVEN("Valid file") { - MockFile f{"abc\ndef"}; - - const auto buf_len = 10; - std::string buf; - buf.resize(buf_len); - - WHEN("Reading") { - auto r = f.read(&buf[0], 3); - - THEN("Result should be number of bytes read") { - CHECK(r); - CHECK_EQ(*r, 3); - } - - buf.resize(*r); - THEN("Buffer should contain read data") { - CHECK_EQ(buf.length(), 3); - CHECK_EQ(buf, "abc"); - } - - r = f.read(&buf[0], 3); - THEN("Reading should continue where it left off") { - CHECK_EQ(buf.length(), 3); - CHECK_EQ(buf, "\nde"); - } - - r = f.read(&buf[0], 3); - THEN("Reading should stop at the end of the file") { - CHECK(r); - CHECK_EQ(*r, 1); - - buf.resize(*r); - CHECK_EQ(buf.length(), 1); - CHECK_EQ(buf, "f"); - } - } - - WHEN("Reading block larger than file size") { - auto r = f.read(&buf[0], buf_len); - buf.resize(*r); - - THEN("It should read to file end.") { - CHECK(r); - CHECK_EQ(*r, 7); - CHECK_EQ(buf, f.data_); - } - } - } - } - - SCENARIO("File write") { - GIVEN("Valid file") { - MockFile f{"abc\ndef"}; - - WHEN("Writing over existing region") { - f.write("xyz", 3); - - THEN("It should overwrite") { - CHECK_EQ(f.data_, "xyz\ndef"); - } - } - - WHEN("Writing over past end") { - f.seek(f.size()); - f.write("xyz", 3); - - THEN("It should extend file and write") { - CHECK_EQ(f.size(), 10); - CHECK_EQ(f.data_, "abc\ndefxyz"); - } - } - } - } - - // This scenario was tested on device. - SCENARIO("File truncate") { - GIVEN("Valid file") { - MockFile f{"hello world"}; - - WHEN("truncating at offset 5") { - f.seek(5); - f.truncate(); - - THEN("resulting file should be 'hello'.") { - CHECK_EQ(f.size(), 5); - CHECK_EQ(f.data_, "hello"); - } - } - } - } - - SCENARIO("File truncate") { - GIVEN("Valid file") { - MockFile f{"abc\ndef"}; - auto init_size = f.size(); - - WHEN("R/W pointer at end") { - f.seek(f.size()); - f.truncate(); - - THEN("It should not change size.") { - CHECK_EQ(f.size(), init_size); - } - } - - WHEN("R/W pointer in middle") { - f.seek(3); - f.truncate(); - - THEN("It should change size.") { - CHECK_EQ(f.size(), 3); - } - } - } - } -} +#include "mock_file.hpp" TEST_SUITE_BEGIN("Test BufferWrapper"); diff --git a/firmware/test/application/test_mock_file.cpp b/firmware/test/application/test_mock_file.cpp new file mode 100644 index 00000000..3e38de4e --- /dev/null +++ b/firmware/test/application/test_mock_file.cpp @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2023 Kyle Reed + * + * This file is part of PortaPack. + * + * This program 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, or (at your option) + * any later version. + * + * This program 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 this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "doctest.h" +#include "mock_file.hpp" + +/* Verifies correctness of MockFile. */ +TEST_SUITE("Test MockFile") { + SCENARIO("File size") { + GIVEN("Empty string") { + MockFile f{""}; + + THEN("size() should be 0.") { + CHECK_EQ(f.size(), 0); + } + } + + GIVEN("Not empty string") { + MockFile f{"abc"}; + + THEN("size() should be string length.") { + CHECK_EQ(f.size(), 3); + } + } + } + + SCENARIO("File seek") { + GIVEN("Valid file") { + MockFile f{"abc\ndef"}; + auto init_size = f.size(); + + WHEN("seek()") { + f.seek(4); + THEN("offset_ should be updated.") { + CHECK_EQ(f.offset_, 4); + } + } + + WHEN("seek() negative offset") { + auto r = f.seek(-1); + + THEN("Result should be bad_seek.") { + CHECK(r.is_error()); + CHECK_EQ(r.error().code(), FR_BAD_SEEK); + } + } + + WHEN("seek() offset is size()") { + auto r = f.seek(f.size()); + + THEN("File should not grow.") { + CHECK(r.is_ok()); + CHECK_EQ(f.size(), init_size); + } + } + + WHEN("seek() offset > size()") { + auto r = f.seek(f.size() + 1); + + THEN("File should grow.") { + CHECK(r.is_ok()); + CHECK_EQ(f.size(), init_size + 1); + } + } + + WHEN("seek() offset < size()") { + auto r = f.seek(1); + + THEN("Result should be ok.") { + CHECK(r.is_ok()); + } + + r = f.seek(3); + + THEN("Result should be previous offset") { + CHECK(r); + CHECK_EQ(*r, 1); + } + } + } + } + + SCENARIO("File read") { + GIVEN("Valid file") { + MockFile f{"abc\ndef"}; + + const auto buf_len = 10; + std::string buf; + buf.resize(buf_len); + + WHEN("Reading") { + auto r = f.read(&buf[0], 3); + + THEN("Result should be number of bytes read") { + CHECK(r); + CHECK_EQ(*r, 3); + } + + buf.resize(*r); + THEN("Buffer should contain read data") { + CHECK_EQ(buf.length(), 3); + CHECK_EQ(buf, "abc"); + } + + r = f.read(&buf[0], 3); + THEN("Reading should continue where it left off") { + CHECK_EQ(buf.length(), 3); + CHECK_EQ(buf, "\nde"); + } + + r = f.read(&buf[0], 3); + THEN("Reading should stop at the end of the file") { + CHECK(r); + CHECK_EQ(*r, 1); + + buf.resize(*r); + CHECK_EQ(buf.length(), 1); + CHECK_EQ(buf, "f"); + } + } + + WHEN("Reading block larger than file size") { + auto r = f.read(&buf[0], buf_len); + buf.resize(*r); + + THEN("It should read to file end.") { + CHECK(r); + CHECK_EQ(*r, 7); + CHECK_EQ(buf, f.data_); + } + } + } + } + + SCENARIO("File write") { + GIVEN("Valid file") { + MockFile f{"abc\ndef"}; + + WHEN("Writing over existing region") { + f.write("xyz", 3); + + THEN("It should overwrite") { + CHECK_EQ(f.data_, "xyz\ndef"); + } + } + + WHEN("Writing over past end") { + f.seek(f.size()); + f.write("xyz", 3); + + THEN("It should extend file and write") { + CHECK_EQ(f.size(), 10); + CHECK_EQ(f.data_, "abc\ndefxyz"); + } + } + } + } + + // This scenario was tested on device. + SCENARIO("File truncate") { + GIVEN("Valid file") { + MockFile f{"hello world"}; + + WHEN("truncating at offset 5") { + f.seek(5); + f.truncate(); + + THEN("resulting file should be 'hello'.") { + CHECK_EQ(f.size(), 5); + CHECK_EQ(f.data_, "hello"); + } + } + } + } + + SCENARIO("File truncate") { + GIVEN("Valid file") { + MockFile f{"abc\ndef"}; + auto init_size = f.size(); + + WHEN("R/W pointer at end") { + f.seek(f.size()); + f.truncate(); + + THEN("It should not change size.") { + CHECK_EQ(f.size(), init_size); + } + } + + WHEN("R/W pointer in middle") { + f.seek(3); + f.truncate(); + + THEN("It should change size.") { + CHECK_EQ(f.size(), 3); + } + } + } + } +}