mirror of
https://github.com/eried/portapack-mayhem.git
synced 2024-10-01 01:26:06 -04:00
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:
parent
a5c7eb2fbc
commit
34fefd1cad
@ -22,6 +22,8 @@
|
||||
*/
|
||||
|
||||
#include "ui_looking_glass_app.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@ -519,70 +521,42 @@ GlassView::GlassView(
|
||||
|
||||
void GlassView::load_Presets() {
|
||||
File presets_file;
|
||||
auto result = presets_file.open("LOOKINGGLASS/PRESETS.TXT");
|
||||
presets_db.clear(); // Start with fresh db
|
||||
if (result.is_valid()) {
|
||||
presets_Default(); // There is no txt, store a default range
|
||||
} else {
|
||||
std::string line; // There is a txt file
|
||||
char one_char[1]; // Read it char by char
|
||||
for (size_t pointer = 0; pointer < presets_file.size(); pointer++) {
|
||||
presets_file.seek(pointer);
|
||||
presets_file.read(one_char, 1);
|
||||
if ((int)one_char[0] > 31) { // ascii space upwards
|
||||
line += one_char[0]; // Add it to the textline
|
||||
} else if (one_char[0] == '\n') { // New Line
|
||||
txtline_process(line); // make sense of this textline
|
||||
line.clear(); // Ready for next textline
|
||||
auto error = presets_file.open("LOOKINGGLASS/PRESETS.TXT");
|
||||
presets_db.clear();
|
||||
|
||||
if (!error) {
|
||||
auto reader = FileLineReader(presets_file);
|
||||
for (const auto& line : reader) {
|
||||
if (line.length() == 0 || line[0] == '#')
|
||||
continue;
|
||||
|
||||
auto cols = split_string(line, ',');
|
||||
if (cols.size() != 3)
|
||||
continue;
|
||||
|
||||
// TODO: add some conversion helpers that take string_view.
|
||||
presets_db.emplace_back(preset_entry{
|
||||
std::stoi(std::string{cols[0]}),
|
||||
std::stoi(std::string{cols[1]}),
|
||||
trimr(std::string{cols[2]})});
|
||||
}
|
||||
}
|
||||
if (line.length() > 0)
|
||||
txtline_process(line); // Last line had no newline at end ?
|
||||
if (!presets_db.size())
|
||||
presets_Default(); // no antenna on txt, use default
|
||||
}
|
||||
|
||||
// Couldn't load any from the file, load a default instead.
|
||||
if (presets_db.empty())
|
||||
presets_Default();
|
||||
|
||||
populate_Presets();
|
||||
}
|
||||
|
||||
void GlassView::txtline_process(std::string& line) {
|
||||
if (line.find("#") != std::string::npos)
|
||||
return; // Line is just a comment
|
||||
|
||||
size_t comma = line.find(","); // Get first comma position
|
||||
if (comma == std::string::npos)
|
||||
return; // No comma at all
|
||||
|
||||
size_t previous = 0;
|
||||
preset_entry new_preset;
|
||||
|
||||
new_preset.min = std::stoi(line.substr(0, comma));
|
||||
if (!new_preset.min)
|
||||
return; // No frequency!
|
||||
|
||||
previous = comma + 1;
|
||||
comma = line.find(",", previous); // Search for next delimiter
|
||||
if (comma == std::string::npos)
|
||||
return; // No comma at all
|
||||
|
||||
new_preset.max = std::stoi(line.substr(previous, comma - previous));
|
||||
if (!new_preset.max)
|
||||
return; // No frequency!
|
||||
|
||||
new_preset.label = line.substr(comma + 1);
|
||||
if (new_preset.label.size() == 0)
|
||||
return; // No label ?
|
||||
|
||||
presets_db.push_back(new_preset); // Add this preset.
|
||||
}
|
||||
|
||||
void GlassView::populate_Presets() {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t entries;
|
||||
|
||||
for (preset_entry preset : presets_db) { // go thru all available presets
|
||||
for (const auto& preset : presets_db)
|
||||
entries.emplace_back(preset.label, entries.size());
|
||||
}
|
||||
|
||||
range_presets.set_options(entries);
|
||||
}
|
||||
|
||||
|
@ -51,10 +51,7 @@ class SIGFRXView : public View {
|
||||
uint8_t last_channel;
|
||||
uint8_t detect_counter = 0;
|
||||
|
||||
RxRadioState radio_state_{
|
||||
1750000 /* bandwidth */,
|
||||
3072000 /* sampling rate */
|
||||
};
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
const uint16_t sigfrx_marks[18] = {
|
||||
10, 8, 0,
|
||||
|
@ -115,8 +115,10 @@ bool TextViewer::on_encoder(EncoderEvent delta) {
|
||||
|
||||
if (cursor_.dir == ScrollDirection::Horizontal)
|
||||
updated = apply_scrolling_constraints(0, delta);
|
||||
else
|
||||
else {
|
||||
delta *= 16;
|
||||
updated = apply_scrolling_constraints(delta, 0);
|
||||
}
|
||||
|
||||
if (updated)
|
||||
redraw();
|
||||
|
155
firmware/application/file_reader.hpp
Normal file
155
firmware/application/file_reader.hpp
Normal 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
|
@ -281,14 +281,16 @@ double get_decimals(double num, int16_t mult, bool round) {
|
||||
return intnum;
|
||||
}
|
||||
|
||||
static const char* whitespace_str = " \t\r\n";
|
||||
|
||||
std::string trim(const std::string& str) {
|
||||
auto first = str.find_first_not_of(' ');
|
||||
auto last = str.find_last_not_of(' ');
|
||||
auto first = str.find_first_not_of(whitespace_str);
|
||||
auto last = str.find_last_not_of(whitespace_str);
|
||||
return str.substr(first, last - first);
|
||||
}
|
||||
|
||||
std::string trimr(std::string str) {
|
||||
size_t last = str.find_last_not_of(' ');
|
||||
std::string trimr(const std::string& str) {
|
||||
size_t last = str.find_last_not_of(whitespace_str);
|
||||
return (last != std::string::npos) ? str.substr(0, last + 1) : ""; // Remove the trailing spaces
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
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);
|
||||
|
||||
#endif /*__STRING_FORMAT_H__*/
|
||||
|
@ -36,7 +36,9 @@ add_executable(application_test EXCLUDE_FROM_ALL
|
||||
${PROJECT_SOURCE_DIR}/main.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_basics.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_circular_buffer.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_file_reader.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_mock_file.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_optional.cpp
|
||||
${PROJECT_SOURCE_DIR}/test_utility.cpp
|
||||
)
|
||||
|
89
firmware/test/application/mock_file.hpp
Normal file
89
firmware/test/application/mock_file.hpp
Normal 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};
|
||||
};
|
155
firmware/test/application/test_file_reader.cpp
Normal file
155
firmware/test/application/test_file_reader.cpp
Normal 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");
|
||||
}
|
@ -22,267 +22,7 @@
|
||||
#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 ((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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#include "mock_file.hpp"
|
||||
|
||||
TEST_SUITE_BEGIN("Test BufferWrapper");
|
||||
|
||||
|
218
firmware/test/application/test_mock_file.cpp
Normal file
218
firmware/test/application/test_mock_file.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user