mirror of
https://github.com/eried/portapack-mayhem.git
synced 2025-06-24 06:44:25 -04:00
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:
parent
23c24355ab
commit
e50d8dc148
7 changed files with 731 additions and 308 deletions
|
@ -39,216 +39,6 @@ namespace {
|
||||||
|
|
||||||
namespace ui {
|
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::TextViewer(Rect parent_rect)
|
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) {
|
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();
|
auto r = screen_rect();
|
||||||
|
|
||||||
// Draw the lines from the file
|
// 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);
|
auto str = file_->get_text(line + i, col, max_col);
|
||||||
|
|
||||||
// Draw text.
|
if (str && str->length() > 0)
|
||||||
if (str.length() > 0)
|
|
||||||
painter.draw_string(
|
painter.draw_string(
|
||||||
{0, r.top() + (int)i * char_height},
|
{0, r.top() + (int)i * char_height},
|
||||||
style_text, str);
|
style_text, *str);
|
||||||
|
|
||||||
// Clear empty line sections.
|
// Clear empty line sections. This is less visually jarring than full clear.
|
||||||
int32_t clear_width = max_col - str.length();
|
int32_t clear_width = max_col - (str ? str->length() : 0);
|
||||||
if (clear_width > 0)
|
if (clear_width > 0)
|
||||||
painter.fill_rectangle(
|
painter.fill_rectangle(
|
||||||
{(max_col - clear_width) * char_width,
|
{(max_col - clear_width) * char_width,
|
||||||
|
@ -567,15 +353,14 @@ void TextEditorView::on_show() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextEditorView::open_file(const fs::path& path) {
|
void TextEditorView::open_file(const fs::path& path) {
|
||||||
auto file = std::make_unique<FileWrapper>();
|
auto result = FileWrapper::open(path);
|
||||||
auto error = file->open(path);
|
|
||||||
|
|
||||||
if (error) {
|
if (!result) {
|
||||||
nav_.display_modal("Read Error", "Cannot open file:\n" + error->what());
|
nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());
|
||||||
file_.reset();
|
file_.reset();
|
||||||
viewer.clear_file();
|
viewer.clear_file();
|
||||||
} else {
|
} else {
|
||||||
file_ = std::move(file);
|
file_ = result.take();
|
||||||
viewer.set_file(*file_);
|
viewer.set_file(*file_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,90 +28,19 @@
|
||||||
#include "ui_painter.hpp"
|
#include "ui_painter.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
|
|
||||||
#include "circular_buffer.hpp"
|
#include "file_wrapper.hpp"
|
||||||
#include "file.hpp"
|
|
||||||
#include "optional.hpp"
|
#include "optional.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace ui {
|
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 {
|
enum class ScrollDirection : uint8_t {
|
||||||
Vertical,
|
Vertical,
|
||||||
Horizontal
|
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. */
|
/* Control that renders a text file. */
|
||||||
class TextViewer : public Widget {
|
class TextViewer : public Widget {
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -60,7 +60,7 @@ File::~File() {
|
||||||
f_close(&f);
|
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;
|
UINT bytes_read = 0;
|
||||||
const auto result = f_read(&f, data, bytes_to_read, &bytes_read);
|
const auto result = f_read(&f, data, bytes_to_read, &bytes_read);
|
||||||
if (result == FR_OK) {
|
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;
|
UINT bytes_written = 0;
|
||||||
const auto result = f_write(&f, data, bytes_to_write, &bytes_written);
|
const auto result = f_write(&f, data, bytes_to_write, &bytes_written);
|
||||||
if (result == FR_OK) {
|
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 */
|
/* NOTE: Returns *old* position, not new position */
|
||||||
const auto old_position = f_tell(&f);
|
const auto old_position = f_tell(&f);
|
||||||
const auto result = f_lseek(&f, new_position);
|
const auto result = f_lseek(&f, new_position);
|
||||||
|
|
|
@ -288,6 +288,7 @@ class File {
|
||||||
using Timestamp = uint32_t;
|
using Timestamp = uint32_t;
|
||||||
using Error = std::filesystem::filesystem_error;
|
using Error = std::filesystem::filesystem_error;
|
||||||
|
|
||||||
|
// TODO: move to common.
|
||||||
template <typename T>
|
template <typename T>
|
||||||
struct Result {
|
struct Result {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
|
@ -303,11 +304,15 @@ class File {
|
||||||
return type == Type::Success;
|
return type == Type::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operator bool() const {
|
||||||
|
return is_ok();
|
||||||
|
}
|
||||||
|
|
||||||
bool is_error() const {
|
bool is_error() const {
|
||||||
return type == Type::Error;
|
return type == Type::Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const T& value() const {
|
const T& value() const& {
|
||||||
return value_;
|
return value_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,6 +320,15 @@ class File {
|
||||||
return value_;
|
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 {
|
Error error() const {
|
||||||
return error_;
|
return error_;
|
||||||
}
|
}
|
||||||
|
@ -322,9 +336,9 @@ class File {
|
||||||
Result() = delete;
|
Result() = delete;
|
||||||
|
|
||||||
constexpr Result(
|
constexpr Result(
|
||||||
T value)
|
T&& value)
|
||||||
: type{Type::Success},
|
: type{Type::Success},
|
||||||
value_{value} {
|
value_{std::forward<T>(value)} {
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr Result(
|
constexpr Result(
|
||||||
|
@ -334,17 +348,21 @@ class File {
|
||||||
}
|
}
|
||||||
|
|
||||||
~Result() {
|
~Result() {
|
||||||
if (type == Type::Success) {
|
if (is_ok())
|
||||||
value_.~T();
|
value_.~T();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
File(){};
|
File(){};
|
||||||
~File();
|
~File();
|
||||||
|
|
||||||
File(File&&) = default;
|
File(File&& other) {
|
||||||
File& operator=(File&&) = default;
|
std::swap(f, other.f);
|
||||||
|
}
|
||||||
|
File& operator=(File&& other) {
|
||||||
|
std::swap(f, other.f);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
/* Prevent copies */
|
/* Prevent copies */
|
||||||
File(const File&) = delete;
|
File(const File&) = delete;
|
||||||
|
@ -355,10 +373,10 @@ class File {
|
||||||
Optional<Error> append(const std::filesystem::path& filename);
|
Optional<Error> append(const std::filesystem::path& filename);
|
||||||
Optional<Error> create(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> read(void* data, const Size bytes_to_read);
|
||||||
Result<Size> write(const void* const data, const Size bytes_to_write);
|
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;
|
// Timestamp created_date() const;
|
||||||
Size size() const;
|
Size size() const;
|
||||||
|
|
||||||
|
|
342
firmware/application/file_wrapper.hpp
Normal file
342
firmware/application/file_wrapper.hpp
Normal 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__
|
|
@ -36,11 +36,13 @@ add_executable(application_test EXCLUDE_FROM_ALL
|
||||||
${PROJECT_SOURCE_DIR}/main.cpp
|
${PROJECT_SOURCE_DIR}/main.cpp
|
||||||
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
||||||
${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp
|
${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp
|
||||||
|
${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp
|
||||||
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(application_test PRIVATE
|
target_include_directories(application_test PRIVATE
|
||||||
${DOCTESTINC}
|
${DOCTESTINC}
|
||||||
|
${PROJECT_SOURCE_DIR}/../../application
|
||||||
${COMMON}
|
${COMMON}
|
||||||
${PORTINC}
|
${PORTINC}
|
||||||
${KERNINC}
|
${KERNINC}
|
||||||
|
@ -49,6 +51,7 @@ target_include_directories(application_test PRIVATE
|
||||||
${PLATFORMINC}
|
${PLATFORMINC}
|
||||||
${BOARDINC}
|
${BOARDINC}
|
||||||
${CHIBIOS}/os/various
|
${CHIBIOS}/os/various
|
||||||
|
${FATFSINC}
|
||||||
${BASEBAND}
|
${BASEBAND}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
346
firmware/test/application/test_file_wrapper.cpp
Normal file
346
firmware/test/application/test_file_wrapper.cpp
Normal 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();
|
Loading…
Add table
Add a link
Reference in a new issue