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 <kallanreed@outlook.com>
This commit is contained in:
Kyle Reed 2023-05-28 08:44:21 -07:00 committed by GitHub
parent 23c24355ab
commit e50d8dc148
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 731 additions and 308 deletions

View File

@ -39,216 +39,6 @@ namespace {
namespace ui {
/* FileWrapper ******************************************************/
FileWrapper::FileWrapper() {
}
Optional<FileWrapper::Error> 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::Range> 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> 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::Offset> 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::Offset> 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<FileWrapper>();
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_);
}

View File

@ -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 <memory>
#include <string>
#include <vector>
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<Error> 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<Range> 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> 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<Offset> previous_newline(Offset start);
Optional<Offset> 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<Offset, max_newlines + 1> newlines_{};
};
/* Control that renders a text file. */
class TextViewer : public Widget {
public:

View File

@ -60,7 +60,7 @@ File::~File() {
f_close(&f);
}
File::Result<File::Size> File::read(void* const data, const Size bytes_to_read) {
File::Result<File::Size> 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::Size> File::read(void* const data, const Size bytes_to_read)
}
}
File::Result<File::Size> File::write(const void* const data, const Size bytes_to_write) {
File::Result<File::Size> 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::Size> File::write(const void* const data, const Size bytes_to
}
}
File::Result<File::Offset> File::seek(const Offset new_position) {
File::Result<File::Offset> 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);

View File

@ -288,6 +288,7 @@ class File {
using Timestamp = uint32_t;
using Error = std::filesystem::filesystem_error;
// TODO: move to common.
template <typename T>
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<T>(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<Error> append(const std::filesystem::path& filename);
Optional<Error> create(const std::filesystem::path& filename);
Result<Size> read(void* const data, const Size bytes_to_read);
Result<Size> write(const void* const data, const Size bytes_to_write);
Result<Size> read(void* data, const Size bytes_to_read);
Result<Size> write(const void* data, Size bytes_to_write);
Result<Offset> seek(const uint64_t Offset);
Result<Offset> seek(uint64_t Offset);
// Timestamp created_date() const;
Size size() const;

View File

@ -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 <memory>
#include <string>
enum class LineEnding : uint8_t {
LF,
CRLF
};
/* TODO:
* - CRLF handling.
*/
/* BufferType requires the following members
* Size size()
* Result<Size> read(void* data, Size bytes_to_read)
* Result<Offset> seek(uint32_t offset)
*/
/* Wraps a buffer and provides an API for accessing lines efficiently. */
template <typename BufferType, uint32_t CacheSize>
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<std::string> 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<Range> 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<std::string> 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> 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<Offset> 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<Offset> 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<Offset, max_newlines + 1> newlines_{};
};
/* A BufferWrapper over a file. */
class FileWrapper : public BufferWrapper<File, 64> {
public:
template <typename T>
using Result = File::Result<T>;
using Error = File::Error;
static Result<std::unique_ptr<FileWrapper>> open(const std::filesystem::path& path) {
auto fw = std::unique_ptr<FileWrapper>(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 <uint32_t CacheSize = 64, typename T>
BufferWrapper<T, CacheSize> wrap_buffer(T& buffer) {
return {&buffer};
}
#endif // __FILE_WRAPPER_HPP__

View File

@ -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}
)

View File

@ -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 <cstring>
#include <cstdio>
#include <string>
/* Mocks the File interface with a backing string. */
class MockFile {
public:
using Error = File::Error;
using Offset = File::Offset;
using Size = File::Size;
template <typename T>
using Result = File::Result<T>;
MockFile(std::string data)
: data_{std::move(data)} {}
Size size() { return data_.size(); }
Result<Offset> seek(uint32_t offset) {
if (offset >= size())
return {static_cast<Error>(FR_BAD_SEEK)};
auto previous = offset_;
offset_ = offset;
return previous;
}
Result<Size> 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<cache_size>(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();