From e50d8dc148767a36f3238b7a4599485fb6ab863d Mon Sep 17 00:00:00 2001 From: Kyle Reed Date: Sun, 28 May 2023 08:44:21 -0700 Subject: [PATCH] Move file_wrapper and make testable. (#1085) * WIP Move file_wrapper and make testable. * More tests, get text_editor compiling * Back to working * Run formatter --------- Co-authored-by: kallanreed --- firmware/application/apps/ui_text_editor.cpp | 231 +----------- firmware/application/apps/ui_text_editor.hpp | 73 +--- firmware/application/file.cpp | 6 +- firmware/application/file.hpp | 38 +- firmware/application/file_wrapper.hpp | 342 +++++++++++++++++ firmware/test/application/CMakeLists.txt | 3 + .../test/application/test_file_wrapper.cpp | 346 ++++++++++++++++++ 7 files changed, 731 insertions(+), 308 deletions(-) create mode 100644 firmware/application/file_wrapper.hpp create mode 100644 firmware/test/application/test_file_wrapper.cpp diff --git a/firmware/application/apps/ui_text_editor.cpp b/firmware/application/apps/ui_text_editor.cpp index 8dcdcb3f7..1329e0498 100644 --- a/firmware/application/apps/ui_text_editor.cpp +++ b/firmware/application/apps/ui_text_editor.cpp @@ -39,216 +39,6 @@ namespace { namespace ui { -/* FileWrapper ******************************************************/ - -FileWrapper::FileWrapper() { -} - -Optional FileWrapper::open(const fs::path& path) { - file_ = File(); - auto error = file_.open(path); - - if (!error) - initialize(); - - return error; -} - -std::string FileWrapper::get_text(Offset line, Offset col, Offset length) { - // TODO: better way to return errors. - auto range = line_range(line); - int32_t to_read = length; - - if (!range) - return "[UNCACHED LINE]"; - - // Don't read past end of line. - if (range->start + col + to_read >= range->end) - to_read = range->end - col - range->start; - - if (to_read <= 0) - return {}; - - return read(range->start + col, to_read); -} - -Optional FileWrapper::line_range(Line line) { - ensure_cached(line); - - auto offset = offset_for_line(line); - if (!offset) - return {}; - - auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1); - auto end = newlines_[*offset] + 1; - - return {Range{start, end}}; -} - -FileWrapper::Offset FileWrapper::line_length(Line line) { - auto range = line_range(line); - - if (range) - return range->end - range->start; - - return 0; -} - -void FileWrapper::initialize() { - start_offset_ = 0; - start_line_ = 0; - line_count_ = 0; - newlines_.clear(); - line_ending_ = LineEnding::LF; - - Offset offset = 0; - auto result = next_newline(offset); - - while (result) { - ++line_count_; - if (newlines_.size() < max_newlines) - newlines_.push_back(*result); - offset = *result + 1; - result = next_newline(offset); - } -} - -std::string FileWrapper::read(Offset offset, Offset length) { - // TODO: better way to return errors. - if (offset + length > file_.size()) - return {"[BAD OFFSET]"}; - - std::string buffer(length, '\0'); - file_.seek(offset); - - auto result = file_.read(&buffer[0], length); - if (result.is_ok()) - buffer.resize(*result); - else - return result.error().what(); - - return buffer; -} - -Optional FileWrapper::offset_for_line(Line line) const { - if (line >= line_count_) - return {}; - - Offset actual = line - start_line_; - if (actual < newlines_.size()) // NB: underflow wrap. - return {actual}; - - return {}; -} - -void FileWrapper::ensure_cached(Line line) { - if (line >= line_count_) - return; - - auto result = offset_for_line(line); - if (result) - return; - - if (line < start_line_) { - while (line < start_line_ && start_offset_ >= 2) { - // start_offset_ - 1 should be a newline. Need to - // find the new value for start_offset_. start_line_ - // has to be > 0 to get into this block so there should - // always be one newline before start_offset_. - auto offset = previous_newline(start_offset_ - 2); - newlines_.push_front(start_offset_ - 1); - - if (!offset) { - // Must be at beginning. - start_line_ = 0; - start_offset_ = 0; - } else { - // Found an previous newline, the new start_line_ - // starts at the newline offset + 1. - start_line_--; - start_offset_ = *offset + 1; - } - } - } else { - while (line >= start_line_ + newlines_.size()) { - auto offset = next_newline(newlines_.back() + 1); - if (offset) { - start_line_++; - start_offset_ = newlines_.front() + 1; - newlines_.push_back(*offset); - } /* else at the EOF. */ - } - } -} - -Optional FileWrapper::previous_newline(Offset start) { - char buffer[buffer_size]; - Offset offset = start; - auto to_read = buffer_size; - - do { - if (offset < to_read) { - // NB: Char at 'offset' was read in the previous iteration. - to_read = offset; - offset = 0; - } else - offset -= to_read; - - file_.seek(offset); - - auto result = file_.read(buffer, to_read); - if (result.is_error()) - break; - - // Find newlines in the buffer backwards. - for (int32_t i = *result - 1; i >= 0; --i) { - switch (buffer[i]) { - case '\n': - return {offset + i}; - } - } - - if (offset == 0) - break; - - } while (true); - - return {}; // Didn't find one. -} - -Optional FileWrapper::next_newline(Offset start) { - char buffer[buffer_size]; - Offset offset = start; - - // EOF, nothing to do. - if (start >= size()) - return {}; - - file_.seek(offset); - - while (true) { - auto result = file_.read(buffer, buffer_size); - if (result.is_error()) - return {}; - - // Find newlines in the buffer. - for (Offset i = 0; i < *result; ++i) { - switch (buffer[i]) { - case '\n': - return {offset + i}; - } - } - - offset += *result; - - if (*result < buffer_size) - break; - } - - // Fake a newline at the end for consistency. - return {offset}; -} - /* TextViewer *******************************************************/ TextViewer::TextViewer(Rect parent_rect) @@ -391,9 +181,6 @@ void TextViewer::redraw(bool redraw_text) { } void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) { - // CONSIDER: A line cache would use more memory but save a lot of IO. - // Only the new lines/characters would need to be refetched. - auto r = screen_rect(); // Draw the lines from the file @@ -403,14 +190,13 @@ void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) { auto str = file_->get_text(line + i, col, max_col); - // Draw text. - if (str.length() > 0) + if (str && str->length() > 0) painter.draw_string( {0, r.top() + (int)i * char_height}, - style_text, str); + style_text, *str); - // Clear empty line sections. - int32_t clear_width = max_col - str.length(); + // Clear empty line sections. This is less visually jarring than full clear. + int32_t clear_width = max_col - (str ? str->length() : 0); if (clear_width > 0) painter.fill_rectangle( {(max_col - clear_width) * char_width, @@ -567,15 +353,14 @@ void TextEditorView::on_show() { } void TextEditorView::open_file(const fs::path& path) { - auto file = std::make_unique(); - auto error = file->open(path); + auto result = FileWrapper::open(path); - if (error) { - nav_.display_modal("Read Error", "Cannot open file:\n" + error->what()); + if (!result) { + nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what()); file_.reset(); viewer.clear_file(); } else { - file_ = std::move(file); + file_ = result.take(); viewer.set_file(*file_); } diff --git a/firmware/application/apps/ui_text_editor.hpp b/firmware/application/apps/ui_text_editor.hpp index 5a0cad466..9f4ac65a4 100644 --- a/firmware/application/apps/ui_text_editor.hpp +++ b/firmware/application/apps/ui_text_editor.hpp @@ -28,90 +28,19 @@ #include "ui_painter.hpp" #include "ui_widget.hpp" -#include "circular_buffer.hpp" -#include "file.hpp" +#include "file_wrapper.hpp" #include "optional.hpp" #include #include -#include namespace ui { -/* TODO: - * - Copy on write into temp file so startup is fast. - */ - -enum class LineEnding : uint8_t { - LF, - CRLF -}; - enum class ScrollDirection : uint8_t { Vertical, Horizontal }; -/* Wraps a file and provides an API for accessing lines efficiently. */ -class FileWrapper { - public: - using Error = std::filesystem::filesystem_error; - using Offset = uint32_t; // TODO: make enums? - using Line = uint32_t; - using Column = uint32_t; - using Range = struct { - // Offset of the line start. - Offset start; - // Offset of one past the line end. - Offset end; - }; - - FileWrapper(); - - /* Prevent copies. */ - FileWrapper(const FileWrapper&) = delete; - FileWrapper& operator=(const FileWrapper&) = delete; - - Optional open(const std::filesystem::path& path); - std::string get_text(Line line, Column col, Offset length); - - File::Size size() const { return file_.size(); } - uint32_t line_count() const { return line_count_; } - - Optional line_range(Line line); - Offset line_length(Line line); - - private: - /* Number of newline offsets to cache. */ - static constexpr Offset max_newlines = 64; - static constexpr size_t buffer_size = 512; - - void initialize(); - std::string read(Offset offset, Offset length = 30); - - /* Returns the offset into the newline cache if valid. */ - Optional offset_for_line(Line line) const; - - /* Ensure specified line is in the newline cache. */ - void ensure_cached(Line line); - - /* Helpers for finding the prev/next newline. */ - Optional previous_newline(Offset start); - Optional next_newline(Offset start); - - File file_{}; - - /* Total number of lines in the file. */ - Offset line_count_{0}; - - /* The offset and line of the newlines cache. */ - Offset start_offset_{0}; - Offset start_line_{0}; - - LineEnding line_ending_{LineEnding::LF}; - CircularBuffer newlines_{}; -}; - /* Control that renders a text file. */ class TextViewer : public Widget { public: diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index b051d72fa..dbbeabd78 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -60,7 +60,7 @@ File::~File() { f_close(&f); } -File::Result File::read(void* const data, const Size bytes_to_read) { +File::Result File::read(void* data, Size bytes_to_read) { UINT bytes_read = 0; const auto result = f_read(&f, data, bytes_to_read, &bytes_read); if (result == FR_OK) { @@ -70,7 +70,7 @@ File::Result File::read(void* const data, const Size bytes_to_read) } } -File::Result File::write(const void* const data, const Size bytes_to_write) { +File::Result File::write(const void* data, Size bytes_to_write) { UINT bytes_written = 0; const auto result = f_write(&f, data, bytes_to_write, &bytes_written); if (result == FR_OK) { @@ -84,7 +84,7 @@ File::Result File::write(const void* const data, const Size bytes_to } } -File::Result File::seek(const Offset new_position) { +File::Result File::seek(Offset new_position) { /* NOTE: Returns *old* position, not new position */ const auto old_position = f_tell(&f); const auto result = f_lseek(&f, new_position); diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index b6023edc2..0245cd7ef 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -288,6 +288,7 @@ class File { using Timestamp = uint32_t; using Error = std::filesystem::filesystem_error; + // TODO: move to common. template struct Result { enum class Type { @@ -303,11 +304,15 @@ class File { return type == Type::Success; } + operator bool() const { + return is_ok(); + } + bool is_error() const { return type == Type::Error; } - const T& value() const { + const T& value() const& { return value_; } @@ -315,6 +320,15 @@ class File { return value_; } + /* Allows value to be moved out of the Result. */ + T take() { + if (is_error()) + return {}; + T temp; + std::swap(temp, value_); + return temp; + } + Error error() const { return error_; } @@ -322,9 +336,9 @@ class File { Result() = delete; constexpr Result( - T value) + T&& value) : type{Type::Success}, - value_{value} { + value_{std::forward(value)} { } constexpr Result( @@ -334,17 +348,21 @@ class File { } ~Result() { - if (type == Type::Success) { + if (is_ok()) value_.~T(); - } } }; File(){}; ~File(); - File(File&&) = default; - File& operator=(File&&) = default; + File(File&& other) { + std::swap(f, other.f); + } + File& operator=(File&& other) { + std::swap(f, other.f); + return *this; + } /* Prevent copies */ File(const File&) = delete; @@ -355,10 +373,10 @@ class File { Optional append(const std::filesystem::path& filename); Optional create(const std::filesystem::path& filename); - Result read(void* const data, const Size bytes_to_read); - Result write(const void* const data, const Size bytes_to_write); + Result read(void* data, const Size bytes_to_read); + Result write(const void* data, Size bytes_to_write); - Result seek(const uint64_t Offset); + Result seek(uint64_t Offset); // Timestamp created_date() const; Size size() const; diff --git a/firmware/application/file_wrapper.hpp b/firmware/application/file_wrapper.hpp new file mode 100644 index 000000000..13db3621c --- /dev/null +++ b/firmware/application/file_wrapper.hpp @@ -0,0 +1,342 @@ +/* + * 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_WRAPPER_HPP__ +#define __FILE_WRAPPER_HPP__ + +#include "circular_buffer.hpp" +#include "file.hpp" +#include "optional.hpp" + +#include +#include + +enum class LineEnding : uint8_t { + LF, + CRLF +}; + +/* TODO: + * - CRLF handling. + */ + +/* BufferType requires the following members + * Size size() + * Result read(void* data, Size bytes_to_read) + * Result seek(uint32_t offset) + */ + +/* Wraps a buffer and provides an API for accessing lines efficiently. */ +template +class BufferWrapper { + public: + using Offset = uint32_t; + using Line = uint32_t; + using Column = uint32_t; + using Range = struct { + // Offset of the line start. + Offset start; + // Offset of one past the line end. + Offset end; + }; + + BufferWrapper(BufferType* buffer) + : wrapped_{buffer} { + initialize(); + } + virtual ~BufferWrapper() {} + + /* Prevent copies */ + BufferWrapper(const BufferWrapper&) = delete; + BufferWrapper& operator=(const BufferWrapper&) = delete; + + Optional get_text(Line line, Column col, Offset length) { + auto range = line_range(line); + int32_t to_read = length; + + if (!range) + return {}; + + // Don't read past end of line. + if (range->start + col + to_read >= range->end) + to_read = range->end - col - range->start; + + if (to_read <= 0) + return {}; + + return read(range->start + col, to_read); + } + + /* Gets the size of the buffer in bytes. */ + File::Size size() const { return wrapped_->size(); } + + /* Get the count of the lines in the buffer. */ + uint32_t line_count() const { return line_count_; } + + /* Gets the range of the line if valid. */ + Optional line_range(Line line) { + ensure_cached(line); + + auto offset = offset_for_line(line); + if (!offset) + return {}; + + auto start = *offset == 0 ? start_offset_ : (newlines_[*offset - 1] + 1); + auto end = newlines_[*offset] + 1; + + return Range{start, end}; + } + + /* Gets the length of the line, or 0 if invalid. */ + Offset line_length(Line line) { + auto range = line_range(line); + + if (range) + return range->end - range->start; + + return 0; + } + + /* Gets the index of the first line in the cache. + * Only really useful for unit testing or diagnostics. */ + Offset start_line() { return start_line_; }; + + protected: + BufferWrapper() {} + + void set_buffer(BufferType* buffer) { + wrapped_ = buffer; + initialize(); + } + + private: + /* Number of newline offsets to cache. */ + static constexpr Offset max_newlines = CacheSize; + + /* Size of stack buffer used for reading/writing. */ + static constexpr size_t buffer_size = 512; + + void initialize() { + start_offset_ = 0; + start_line_ = 0; + line_count_ = 0; + newlines_.clear(); + + // Special case for empty files to keep them consistent. + if (size() == 0) { + line_count_ = 1; + newlines_.push_back(0); + return; + } + + Offset offset = 0; + auto result = next_newline(offset); + + while (result) { + ++line_count_; + if (newlines_.size() < max_newlines) + newlines_.push_back(*result); + offset = *result + 1; + result = next_newline(offset); + } + } + + Optional read(Offset offset, Offset length) { + if (offset + length > size()) + return {}; + + std::string buffer; + buffer.resize(length); + wrapped_->seek(offset); + + auto result = wrapped_->read(&buffer[0], length); + if (result.is_error()) + // TODO: better error handling. + return std::string{"[Bad Read]"}; + + buffer.resize(*result); + return buffer; + } + + /* Returns the offset of the line in the newline cache if valid. */ + Optional offset_for_line(Line line) const { + if (line >= line_count_) + return {}; + + Offset actual = line - start_line_; + if (actual >= newlines_.size()) // NB: underflow wrap. + return {}; + + return actual; + } + + /* Ensure specified line is in the newline cache. */ + void ensure_cached(Line line) { + if (line >= line_count_) + return; + + auto result = offset_for_line(line); + if (result) + return; + + if (line < start_line_) { + while (line < start_line_ && start_offset_ >= 2) { + // start_offset_ - 1 should be a newline. Need to + // find the new value for start_offset_. start_line_ + // has to be > 0 to get into this block so there should + // always be one newline before start_offset_. + auto offset = previous_newline(start_offset_ - 2); + newlines_.push_front(start_offset_ - 1); + + if (!offset) { + // Must be at beginning. + start_line_ = 0; + start_offset_ = 0; + } else { + // Found an previous newline, the new start_line_ + // starts at the newline offset + 1. + start_line_--; + start_offset_ = *offset + 1; + } + } + } else { + while (line >= start_line_ + newlines_.size()) { + auto offset = next_newline(newlines_.back() + 1); + if (offset) { + start_line_++; + start_offset_ = newlines_.front() + 1; + newlines_.push_back(*offset); + } /* else at the EOF. */ + } + } + } + + /* Helpers for finding the prev/next newline. */ + Optional previous_newline(Offset offset) { + char buffer[buffer_size]; + auto to_read = buffer_size; + + do { + if (offset < to_read) { + // NB: Char at 'offset' was read in the previous iteration. + to_read = offset; + offset = 0; + } else + offset -= to_read; + + wrapped_->seek(offset); + + auto result = wrapped_->read(buffer, to_read); + if (result.is_error()) + break; + + // Find newlines in the buffer backwards. + for (int32_t i = *result - 1; i >= 0; --i) { + switch (buffer[i]) { + case '\n': + return offset + i; + } + } + + if (offset == 0) + break; + + } while (true); + + return {}; // Didn't find one. + } + + Optional next_newline(Offset offset) { + // EOF, no more newlines to find. + if (offset >= size()) + return {}; + + char buffer[buffer_size]; + wrapped_->seek(offset); + + while (true) { + auto result = wrapped_->read(buffer, buffer_size); + if (result.is_error()) + return {}; + + // Find newlines in the buffer. + for (Offset i = 0; i < *result; ++i) { + switch (buffer[i]) { + case '\n': + return offset + i; + } + } + + offset += *result; + + if (*result < buffer_size) + break; + } + + // For consistency, treat the end of the file as a "newline". + return size() - 1; + } + + BufferType* wrapped_{}; + + /* Total number of lines in the buffer. */ + Offset line_count_{0}; + + /* The offset and line of the newlines cache. */ + Offset start_offset_{0}; + Offset start_line_{0}; + + LineEnding line_ending_{LineEnding::LF}; + CircularBuffer newlines_{}; +}; + +/* A BufferWrapper over a file. */ +class FileWrapper : public BufferWrapper { + public: + template + using Result = File::Result; + using Error = File::Error; + static Result> open(const std::filesystem::path& path) { + auto fw = std::unique_ptr(new FileWrapper()); + auto error = fw->file_.open(path); + + if (error) + return *error; + + fw->initialize(); + return fw; + } + + private: + FileWrapper() {} + void initialize() { + set_buffer(&file_); + } + + File file_{}; +}; + +template +BufferWrapper wrap_buffer(T& buffer) { + return {&buffer}; +} + +#endif // __FILE_WRAPPER_HPP__ \ No newline at end of file diff --git a/firmware/test/application/CMakeLists.txt b/firmware/test/application/CMakeLists.txt index 2968317b6..d11bd4735 100644 --- a/firmware/test/application/CMakeLists.txt +++ b/firmware/test/application/CMakeLists.txt @@ -36,11 +36,13 @@ 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_wrapper.cpp ${PROJECT_SOURCE_DIR}/test_optional.cpp ) target_include_directories(application_test PRIVATE ${DOCTESTINC} + ${PROJECT_SOURCE_DIR}/../../application ${COMMON} ${PORTINC} ${KERNINC} @@ -49,6 +51,7 @@ target_include_directories(application_test PRIVATE ${PLATFORMINC} ${BOARDINC} ${CHIBIOS}/os/various + ${FATFSINC} ${BASEBAND} ) diff --git a/firmware/test/application/test_file_wrapper.cpp b/firmware/test/application/test_file_wrapper.cpp new file mode 100644 index 000000000..dd0b22b69 --- /dev/null +++ b/firmware/test/application/test_file_wrapper.cpp @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2023 + * + * 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 "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 (offset >= size()) + return {static_cast(FR_BAD_SEEK)}; + + auto previous = offset_; + offset_ = offset; + return previous; + } + + 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; + } + + 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"}; + + 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("Result should be bad_seek") { + CHECK(r.is_error()); + CHECK_EQ(r.error().code(), FR_BAD_SEEK); + } + } + + WHEN("seek() offset > size()") { + auto r = f.seek(f.size() + 1); + + THEN("Result should be bad_seek") { + CHECK(r.is_error()); + CHECK_EQ(r.error().code(), FR_BAD_SEEK); + } + } + + 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_); + } + } + } + } +} + +TEST_SUITE_BEGIN("Test BufferWrapper"); + +TEST_CASE("It can wrap a MockFile.") { + MockFile f{""}; + auto w = wrap_buffer(f); +} + +SCENARIO("Empty file") { + GIVEN("An empty file") { + MockFile f{""}; + auto w = wrap_buffer(f); + + WHEN("Initializing") { + CHECK_EQ(w.size(), 0); + REQUIRE_EQ(w.line_count(), 1); + } + + WHEN("Getting line_range()") { + auto r = w.line_range(0); + CHECK(r); + CHECK_EQ(r->start, 0); + CHECK_EQ(r->end, 1); + } + + WHEN("Getting line_length()") { + CHECK_EQ(w.line_length(0), 1); + } + } +} + +SCENARIO("Basic file") { + GIVEN("A file") { + MockFile f{"abc\ndef"}; + auto w = wrap_buffer(f); + + WHEN("Initializing") { + CHECK_EQ(w.size(), 7); + REQUIRE_EQ(w.line_count(), 2); + } + + WHEN("Getting line_range()") { + auto r = w.line_range(0); + CHECK(r); + CHECK_EQ(r->start, 0); + CHECK_EQ(r->end, 4); + + r = w.line_range(1); + CHECK(r); + CHECK_EQ(r->start, 4); + CHECK_EQ(r->end, 7); + } + + WHEN("Getting line_length()") { + CHECK_EQ(w.line_length(0), 4); + CHECK_EQ(w.line_length(1), 3); + } + } +} + +SCENARIO("Reading file lines.") { + GIVEN("A valid file") { + MockFile f{"abc\ndef"}; + auto w = wrap_buffer(f); + + WHEN("Reading a line") { + auto str = w.get_text(0, 0, 10); + + THEN("It should read exactly one line.") { + REQUIRE(str); + CHECK_EQ(str->length(), 4); // Includes '\n' + CHECK_EQ(*str, "abc\n"); + } + } + + WHEN("Reading the last line") { + auto str = w.get_text(w.line_count() - 1, 0, 10); + + THEN("It should read exactly one line.") { + REQUIRE(str); + CHECK_EQ(str->length(), 3); + CHECK_EQ(*str, "def"); + } + } + + WHEN("Reading past the last line") { + auto str = w.get_text(w.line_count(), 0, 10); + + THEN("It should return empty value.") { + REQUIRE(!str); + } + } + } +} + +SCENARIO("Reading with cache miss.") { + GIVEN("A valid file") { + MockFile f{"abc\ndef\nghi\njkl\nmno"}; + constexpr uint32_t cache_size = 2; + auto w = wrap_buffer(f); + + CHECK_EQ(w.start_line(), 0); + + WHEN("Reading a cached line") { + auto str = w.get_text(0, 0, 10); + + THEN("It should read exactly one line.") { + REQUIRE(str); + CHECK_EQ(*str, "abc\n"); + } + } + + WHEN("Reading line after last cached line.") { + auto str = w.get_text(w.line_count() - 1, 0, 10); + + THEN("It should read exactly one line.") { + REQUIRE(str); + CHECK_EQ(*str, "mno"); + } + + THEN("It should push cache window forward to include line.") { + CHECK_EQ(w.start_line(), w.line_count() - cache_size); + } + } + + WHEN("Reading line before first cached line.") { + // First move cache forward to end. + w.get_text(w.line_count() - 1, 0, 10); + auto str = w.get_text(1, 0, 10); + + THEN("It should read exactly one line.") { + REQUIRE(str); + CHECK_EQ(*str, "def\n"); + } + + THEN("It should push cache window backward to include line.") { + CHECK_EQ(w.start_line(), 1); + } + } + + WHEN("Reading line 0 before first cached line.") { + // First move cache forward to end, then back to beginning. + w.get_text(w.line_count() - 1, 0, 10); + auto str = w.get_text(0, 0, 10); + + THEN("It should read exactly one line.") { + REQUIRE(str); + CHECK_EQ(*str, "abc\n"); + } + + THEN("It should push cache window backward to include line.") { + CHECK_EQ(w.start_line(), 0); + } + } + } +} + +TEST_SUITE_END(); \ No newline at end of file