Add file reader (#1155)

* Add file reader
* Add a simple test example of parsing settings.
* Use new FileLineReader to parse Glass presets
* Trim CRLF from Glass preset name
This commit is contained in:
Kyle Reed 2023-06-15 00:45:13 -07:00 committed by GitHub
parent a5c7eb2fbc
commit 34fefd1cad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 659 additions and 325 deletions

View File

@ -22,6 +22,8 @@
*/ */
#include "ui_looking_glass_app.hpp" #include "ui_looking_glass_app.hpp"
#include "file_reader.hpp"
#include "string_format.hpp"
using namespace portapack; using namespace portapack;
@ -519,70 +521,42 @@ GlassView::GlassView(
void GlassView::load_Presets() { void GlassView::load_Presets() {
File presets_file; File presets_file;
auto result = presets_file.open("LOOKINGGLASS/PRESETS.TXT"); auto error = presets_file.open("LOOKINGGLASS/PRESETS.TXT");
presets_db.clear(); // Start with fresh db presets_db.clear();
if (result.is_valid()) {
presets_Default(); // There is no txt, store a default range if (!error) {
} else { auto reader = FileLineReader(presets_file);
std::string line; // There is a txt file for (const auto& line : reader) {
char one_char[1]; // Read it char by char if (line.length() == 0 || line[0] == '#')
for (size_t pointer = 0; pointer < presets_file.size(); pointer++) { continue;
presets_file.seek(pointer);
presets_file.read(one_char, 1); auto cols = split_string(line, ',');
if ((int)one_char[0] > 31) { // ascii space upwards if (cols.size() != 3)
line += one_char[0]; // Add it to the textline continue;
} else if (one_char[0] == '\n') { // New Line
txtline_process(line); // make sense of this textline // TODO: add some conversion helpers that take string_view.
line.clear(); // Ready for next textline presets_db.emplace_back(preset_entry{
std::stoi(std::string{cols[0]}),
std::stoi(std::string{cols[1]}),
trimr(std::string{cols[2]})});
} }
} }
if (line.length() > 0)
txtline_process(line); // Last line had no newline at end ? // Couldn't load any from the file, load a default instead.
if (!presets_db.size()) if (presets_db.empty())
presets_Default(); // no antenna on txt, use default presets_Default();
}
populate_Presets(); populate_Presets();
} }
void GlassView::txtline_process(std::string& line) {
if (line.find("#") != std::string::npos)
return; // Line is just a comment
size_t comma = line.find(","); // Get first comma position
if (comma == std::string::npos)
return; // No comma at all
size_t previous = 0;
preset_entry new_preset;
new_preset.min = std::stoi(line.substr(0, comma));
if (!new_preset.min)
return; // No frequency!
previous = comma + 1;
comma = line.find(",", previous); // Search for next delimiter
if (comma == std::string::npos)
return; // No comma at all
new_preset.max = std::stoi(line.substr(previous, comma - previous));
if (!new_preset.max)
return; // No frequency!
new_preset.label = line.substr(comma + 1);
if (new_preset.label.size() == 0)
return; // No label ?
presets_db.push_back(new_preset); // Add this preset.
}
void GlassView::populate_Presets() { void GlassView::populate_Presets() {
using option_t = std::pair<std::string, int32_t>; using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>; using options_t = std::vector<option_t>;
options_t entries; options_t entries;
for (preset_entry preset : presets_db) { // go thru all available presets for (const auto& preset : presets_db)
entries.emplace_back(preset.label, entries.size()); entries.emplace_back(preset.label, entries.size());
}
range_presets.set_options(entries); range_presets.set_options(entries);
} }

View File

@ -51,10 +51,7 @@ class SIGFRXView : public View {
uint8_t last_channel; uint8_t last_channel;
uint8_t detect_counter = 0; uint8_t detect_counter = 0;
RxRadioState radio_state_{ RxRadioState radio_state_{};
1750000 /* bandwidth */,
3072000 /* sampling rate */
};
const uint16_t sigfrx_marks[18] = { const uint16_t sigfrx_marks[18] = {
10, 8, 0, 10, 8, 0,

View File

@ -115,8 +115,10 @@ bool TextViewer::on_encoder(EncoderEvent delta) {
if (cursor_.dir == ScrollDirection::Horizontal) if (cursor_.dir == ScrollDirection::Horizontal)
updated = apply_scrolling_constraints(0, delta); updated = apply_scrolling_constraints(0, delta);
else else {
delta *= 16;
updated = apply_scrolling_constraints(delta, 0); updated = apply_scrolling_constraints(delta, 0);
}
if (updated) if (updated)
redraw(); redraw();

View File

@ -0,0 +1,155 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __FILE_READER_HPP__
#define __FILE_READER_HPP__
#include "file.hpp"
#include <cstring>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>
/* BufferType requires the following members
* Size size()
* Result<Size> read(void* data, Size bytes_to_read)
* Result<Offset> seek(uint32_t offset)
*/
/* Iterates lines in buffer split on '\n'.
* NB: very basic iterator impl, don't try anything fancy with it. */
template <typename BufferType>
class BufferLineReader {
public:
struct iterator {
bool operator!=(const iterator& other) {
return this->pos_ != other.pos_ || this->reader_ != other.reader_;
}
const std::string& operator*() {
if (!cached_) {
bool ok = reader_->read_line(*this);
cached_ = true;
if (!ok) *this = reader_->end();
}
return line_data_;
}
iterator& operator++() {
const auto size = reader_->size();
if (pos_ < size) {
cached_ = false;
pos_ += line_data_.length();
}
if (pos_ >= size)
*this = reader_->end();
return *this;
}
typename BufferType::Size pos_{};
BufferLineReader* reader_{};
bool cached_ = false;
std::string line_data_{};
};
BufferLineReader(BufferType& buffer)
: buffer_{buffer} {}
iterator begin() { return {0, this}; }
iterator end() { return {size(), this}; }
typename BufferType::Size size() const { return buffer_.size(); }
private:
BufferType& buffer_;
bool read_line(iterator& it) {
constexpr size_t buf_size = 0x80;
char buf[buf_size];
uint32_t offset = 0;
it.line_data_.resize(buf_size);
buffer_.seek(it.pos_);
while (true) {
auto read = buffer_.read(buf, buf_size);
if (!read)
return false;
// Find newline.
auto len = 0u;
for (; len < *read; ++len) {
if (buf[len] == '\n') {
++len;
break;
}
}
// Reallocate if needed.
if (offset + len >= it.line_data_.length())
it.line_data_.resize(offset + len);
std::strncpy(&it.line_data_[offset], buf, len);
offset += len;
if (len < buf_size)
break;
}
it.line_data_.resize(offset);
return true;
}
};
using FileLineReader = BufferLineReader<File>;
/* Splits the string on the specified char and returns
* a vector of string_views. NB: the lifetime of the
* string to split must be maintained while the views
* are used or they will dangle. */
std::vector<std::string_view> split_string(std::string_view str, char c) {
std::vector<std::string_view> cols;
size_t start = 0;
while (start < str.length()) {
auto it = str.find(c, start);
if (it == str.npos)
break;
// TODO: allow empty?
cols.emplace_back(&str[start], it - start);
start = it + 1;
}
if (start <= str.length() && !str.empty())
cols.emplace_back(&str[start], str.length() - start);
return cols;
}
#endif

View File

@ -281,14 +281,16 @@ double get_decimals(double num, int16_t mult, bool round) {
return intnum; return intnum;
} }
static const char* whitespace_str = " \t\r\n";
std::string trim(const std::string& str) { std::string trim(const std::string& str) {
auto first = str.find_first_not_of(' '); auto first = str.find_first_not_of(whitespace_str);
auto last = str.find_last_not_of(' '); auto last = str.find_last_not_of(whitespace_str);
return str.substr(first, last - first); return str.substr(first, last - first);
} }
std::string trimr(std::string str) { std::string trimr(const std::string& str) {
size_t last = str.find_last_not_of(' '); size_t last = str.find_last_not_of(whitespace_str);
return (last != std::string::npos) ? str.substr(0, last + 1) : ""; // Remove the trailing spaces return (last != std::string::npos) ? str.substr(0, last + 1) : ""; // Remove the trailing spaces
} }

View File

@ -69,7 +69,7 @@ std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precisi
double get_decimals(double num, int16_t mult, bool round = false); // euquiq added double get_decimals(double num, int16_t mult, bool round = false); // euquiq added
std::string trim(const std::string& str); // Remove whitespace at ends. std::string trim(const std::string& str); // Remove whitespace at ends.
std::string trimr(std::string str); // Remove trailing spaces std::string trimr(const std::string& str); // Remove trailing spaces
std::string truncate(const std::string& str, size_t length); std::string truncate(const std::string& str, size_t length);
#endif /*__STRING_FORMAT_H__*/ #endif /*__STRING_FORMAT_H__*/

View File

@ -36,7 +36,9 @@ 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_reader.cpp
${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp ${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp
${PROJECT_SOURCE_DIR}/test_mock_file.cpp
${PROJECT_SOURCE_DIR}/test_optional.cpp ${PROJECT_SOURCE_DIR}/test_optional.cpp
${PROJECT_SOURCE_DIR}/test_utility.cpp ${PROJECT_SOURCE_DIR}/test_utility.cpp
) )

View File

@ -0,0 +1,89 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#pragma once
#include "file.hpp"
#include <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 ((int32_t)offset < 0)
return {static_cast<Error>(FR_BAD_SEEK)};
auto previous = offset_;
if (offset > size())
data_.resize(offset);
offset_ = offset;
return previous;
}
Result<Offset> truncate() {
data_.resize(offset_);
return offset_;
}
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;
}
Result<Size> write(const void* data, Size bytes_to_write) {
auto new_offset = offset_ + bytes_to_write;
if (new_offset >= size())
data_.resize(new_offset);
memcpy(&data_[offset_], data, bytes_to_write);
offset_ = new_offset;
return bytes_to_write;
}
Optional<Error> sync() {
return {};
}
std::string data_;
uint32_t offset_{0};
};

View File

@ -0,0 +1,155 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "doctest.h"
#include "mock_file.hpp"
#include "file_reader.hpp"
#include <cstdio>
TEST_SUITE_BEGIN("Test BufferLineReader");
TEST_CASE("It can iterate file lines.") {
MockFile f{"abc\ndef\nhij"};
BufferLineReader<MockFile> reader{f};
int line_count = 0;
for (const auto& line : reader) {
printf("Line: %s", line.c_str());
++line_count;
}
CHECK_EQ(line_count, 3);
}
TEST_CASE("It can iterate file ending with newline.") {
MockFile f{"abc\ndef\nhij\n"};
BufferLineReader<MockFile> reader{f};
int line_count = 0;
for (const auto& line : reader) {
printf("Line: %s", line.c_str());
++line_count;
}
CHECK_EQ(line_count, 3);
}
TEST_CASE("It can iterate files with empty lines.") {
MockFile f{"abc\ndef\n\nhij\n\n"};
BufferLineReader<MockFile> reader{f};
int line_count = 0;
for (const auto& line : reader) {
printf("Line: %s", line.c_str());
++line_count;
}
CHECK_EQ(line_count, 5);
}
TEST_CASE("It can iterate large lines.") {
std::string long_line(0x90, 'b');
long_line.back() = 'c';
MockFile f{long_line + "\n" + long_line + "\n"};
BufferLineReader<MockFile> reader{f};
int line_count = 0;
for (const auto& line : reader) {
CHECK_EQ(line.length(), 0x91);
printf("Line: %s", line.c_str());
++line_count;
}
CHECK_EQ(line_count, 2);
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("Test split_string");
TEST_CASE("Empty string returns no results.") {
auto r = split_string("", ',');
CHECK(r.empty());
}
TEST_CASE("String without delimiter returns 1 result.") {
auto r = split_string("hello", ',');
REQUIRE_EQ(r.size(), 1);
CHECK(r[0] == "hello");
}
TEST_CASE("It will split on delimiter.") {
auto r = split_string("hello,world", ',');
REQUIRE_EQ(r.size(), 2);
CHECK(r[0] == "hello");
CHECK(r[1] == "world");
}
TEST_CASE("It will return empty columns.") {
auto r = split_string("hello,,world", ',');
REQUIRE_EQ(r.size(), 3);
CHECK(r[0] == "hello");
CHECK(r[1] == "");
CHECK(r[2] == "world");
}
TEST_CASE("It will return empty first column.") {
auto r = split_string(",hello,world", ',');
REQUIRE_EQ(r.size(), 3);
CHECK(r[0] == "");
CHECK(r[1] == "hello");
CHECK(r[2] == "world");
}
TEST_CASE("It will return empty last column.") {
auto r = split_string("hello,world,", ',');
REQUIRE_EQ(r.size(), 3);
CHECK(r[0] == "hello");
CHECK(r[1] == "world");
CHECK(r[2] == "");
}
TEST_CASE("It will split only empty columns.") {
auto r = split_string(",,,,", ',');
REQUIRE_EQ(r.size(), 5);
CHECK(r[0] == "");
CHECK(r[4] == "");
}
TEST_SUITE_END();
/* Simple example of how to use this to read settings by lines. */
TEST_CASE("It can parse a settings file.") {
MockFile f{"100,File.txt,5\n200,File2.txt,7"};
BufferLineReader<MockFile> reader{f};
std::vector<std::string> data;
for (const auto& line : reader) {
auto cols = split_string(line, ',');
for (auto col : cols)
data.emplace_back(col);
}
REQUIRE_EQ(data.size(), 6);
CHECK(data[0] == "100");
CHECK(data[1] == "File.txt");
CHECK(data[2] == "5\n"); // NB: Newlines need to be manually trimmed.
CHECK(data[3] == "200");
CHECK(data[4] == "File2.txt");
CHECK(data[5] == "7");
}

View File

@ -22,267 +22,7 @@
#include "doctest.h" #include "doctest.h"
#include "file.hpp" #include "file.hpp"
#include "file_wrapper.hpp" #include "file_wrapper.hpp"
#include "mock_file.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 ((int32_t)offset < 0)
return {static_cast<Error>(FR_BAD_SEEK)};
auto previous = offset_;
if (offset > size())
data_.resize(offset);
offset_ = offset;
return previous;
}
Result<Offset> truncate() {
data_.resize(offset_);
return offset_;
}
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;
}
Result<Size> write(const void* data, Size bytes_to_write) {
auto new_offset = offset_ + bytes_to_write;
if (new_offset >= size())
data_.resize(new_offset);
memcpy(&data_[offset_], data, bytes_to_write);
offset_ = new_offset;
return bytes_to_write;
}
Optional<Error> sync() {
return {};
}
std::string data_;
uint32_t offset_{0};
};
/* Verifies correctness of MockFile. */
TEST_SUITE("Test MockFile") {
SCENARIO("File size") {
GIVEN("Empty string") {
MockFile f{""};
THEN("size() should be 0.") {
CHECK_EQ(f.size(), 0);
}
}
GIVEN("Not empty string") {
MockFile f{"abc"};
THEN("size() should be string length.") {
CHECK_EQ(f.size(), 3);
}
}
}
SCENARIO("File seek") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
auto init_size = f.size();
WHEN("seek()") {
f.seek(4);
THEN("offset_ should be updated.") {
CHECK_EQ(f.offset_, 4);
}
}
WHEN("seek() negative offset") {
auto r = f.seek(-1);
THEN("Result should be bad_seek.") {
CHECK(r.is_error());
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
}
}
WHEN("seek() offset is size()") {
auto r = f.seek(f.size());
THEN("File should not grow.") {
CHECK(r.is_ok());
CHECK_EQ(f.size(), init_size);
}
}
WHEN("seek() offset > size()") {
auto r = f.seek(f.size() + 1);
THEN("File should grow.") {
CHECK(r.is_ok());
CHECK_EQ(f.size(), init_size + 1);
}
}
WHEN("seek() offset < size()") {
auto r = f.seek(1);
THEN("Result should be ok.") {
CHECK(r.is_ok());
}
r = f.seek(3);
THEN("Result should be previous offset") {
CHECK(r);
CHECK_EQ(*r, 1);
}
}
}
}
SCENARIO("File read") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
const auto buf_len = 10;
std::string buf;
buf.resize(buf_len);
WHEN("Reading") {
auto r = f.read(&buf[0], 3);
THEN("Result should be number of bytes read") {
CHECK(r);
CHECK_EQ(*r, 3);
}
buf.resize(*r);
THEN("Buffer should contain read data") {
CHECK_EQ(buf.length(), 3);
CHECK_EQ(buf, "abc");
}
r = f.read(&buf[0], 3);
THEN("Reading should continue where it left off") {
CHECK_EQ(buf.length(), 3);
CHECK_EQ(buf, "\nde");
}
r = f.read(&buf[0], 3);
THEN("Reading should stop at the end of the file") {
CHECK(r);
CHECK_EQ(*r, 1);
buf.resize(*r);
CHECK_EQ(buf.length(), 1);
CHECK_EQ(buf, "f");
}
}
WHEN("Reading block larger than file size") {
auto r = f.read(&buf[0], buf_len);
buf.resize(*r);
THEN("It should read to file end.") {
CHECK(r);
CHECK_EQ(*r, 7);
CHECK_EQ(buf, f.data_);
}
}
}
}
SCENARIO("File write") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
WHEN("Writing over existing region") {
f.write("xyz", 3);
THEN("It should overwrite") {
CHECK_EQ(f.data_, "xyz\ndef");
}
}
WHEN("Writing over past end") {
f.seek(f.size());
f.write("xyz", 3);
THEN("It should extend file and write") {
CHECK_EQ(f.size(), 10);
CHECK_EQ(f.data_, "abc\ndefxyz");
}
}
}
}
// This scenario was tested on device.
SCENARIO("File truncate") {
GIVEN("Valid file") {
MockFile f{"hello world"};
WHEN("truncating at offset 5") {
f.seek(5);
f.truncate();
THEN("resulting file should be 'hello'.") {
CHECK_EQ(f.size(), 5);
CHECK_EQ(f.data_, "hello");
}
}
}
}
SCENARIO("File truncate") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
auto init_size = f.size();
WHEN("R/W pointer at end") {
f.seek(f.size());
f.truncate();
THEN("It should not change size.") {
CHECK_EQ(f.size(), init_size);
}
}
WHEN("R/W pointer in middle") {
f.seek(3);
f.truncate();
THEN("It should change size.") {
CHECK_EQ(f.size(), 3);
}
}
}
}
}
TEST_SUITE_BEGIN("Test BufferWrapper"); TEST_SUITE_BEGIN("Test BufferWrapper");

View File

@ -0,0 +1,218 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "doctest.h"
#include "mock_file.hpp"
/* Verifies correctness of MockFile. */
TEST_SUITE("Test MockFile") {
SCENARIO("File size") {
GIVEN("Empty string") {
MockFile f{""};
THEN("size() should be 0.") {
CHECK_EQ(f.size(), 0);
}
}
GIVEN("Not empty string") {
MockFile f{"abc"};
THEN("size() should be string length.") {
CHECK_EQ(f.size(), 3);
}
}
}
SCENARIO("File seek") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
auto init_size = f.size();
WHEN("seek()") {
f.seek(4);
THEN("offset_ should be updated.") {
CHECK_EQ(f.offset_, 4);
}
}
WHEN("seek() negative offset") {
auto r = f.seek(-1);
THEN("Result should be bad_seek.") {
CHECK(r.is_error());
CHECK_EQ(r.error().code(), FR_BAD_SEEK);
}
}
WHEN("seek() offset is size()") {
auto r = f.seek(f.size());
THEN("File should not grow.") {
CHECK(r.is_ok());
CHECK_EQ(f.size(), init_size);
}
}
WHEN("seek() offset > size()") {
auto r = f.seek(f.size() + 1);
THEN("File should grow.") {
CHECK(r.is_ok());
CHECK_EQ(f.size(), init_size + 1);
}
}
WHEN("seek() offset < size()") {
auto r = f.seek(1);
THEN("Result should be ok.") {
CHECK(r.is_ok());
}
r = f.seek(3);
THEN("Result should be previous offset") {
CHECK(r);
CHECK_EQ(*r, 1);
}
}
}
}
SCENARIO("File read") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
const auto buf_len = 10;
std::string buf;
buf.resize(buf_len);
WHEN("Reading") {
auto r = f.read(&buf[0], 3);
THEN("Result should be number of bytes read") {
CHECK(r);
CHECK_EQ(*r, 3);
}
buf.resize(*r);
THEN("Buffer should contain read data") {
CHECK_EQ(buf.length(), 3);
CHECK_EQ(buf, "abc");
}
r = f.read(&buf[0], 3);
THEN("Reading should continue where it left off") {
CHECK_EQ(buf.length(), 3);
CHECK_EQ(buf, "\nde");
}
r = f.read(&buf[0], 3);
THEN("Reading should stop at the end of the file") {
CHECK(r);
CHECK_EQ(*r, 1);
buf.resize(*r);
CHECK_EQ(buf.length(), 1);
CHECK_EQ(buf, "f");
}
}
WHEN("Reading block larger than file size") {
auto r = f.read(&buf[0], buf_len);
buf.resize(*r);
THEN("It should read to file end.") {
CHECK(r);
CHECK_EQ(*r, 7);
CHECK_EQ(buf, f.data_);
}
}
}
}
SCENARIO("File write") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
WHEN("Writing over existing region") {
f.write("xyz", 3);
THEN("It should overwrite") {
CHECK_EQ(f.data_, "xyz\ndef");
}
}
WHEN("Writing over past end") {
f.seek(f.size());
f.write("xyz", 3);
THEN("It should extend file and write") {
CHECK_EQ(f.size(), 10);
CHECK_EQ(f.data_, "abc\ndefxyz");
}
}
}
}
// This scenario was tested on device.
SCENARIO("File truncate") {
GIVEN("Valid file") {
MockFile f{"hello world"};
WHEN("truncating at offset 5") {
f.seek(5);
f.truncate();
THEN("resulting file should be 'hello'.") {
CHECK_EQ(f.size(), 5);
CHECK_EQ(f.data_, "hello");
}
}
}
}
SCENARIO("File truncate") {
GIVEN("Valid file") {
MockFile f{"abc\ndef"};
auto init_size = f.size();
WHEN("R/W pointer at end") {
f.seek(f.size());
f.truncate();
THEN("It should not change size.") {
CHECK_EQ(f.size(), init_size);
}
}
WHEN("R/W pointer in middle") {
f.seek(3);
f.truncate();
THEN("It should change size.") {
CHECK_EQ(f.size(), 3);
}
}
}
}
}