Files
roro9stack/test/test_file_receiver/test_file_receiver.cpp
T
twislaandClaude Opus 5.5 5b199c2436 sd put: copy a file to the SD card over USB serial
scripts/sd_put.sh <file> [card path] sends a file (by default into
/updates, for Update from SD) without taking the card out. The serial
driver drops bytes once its receive buffer is full, so the transfer is
stop-and-wait: 1 KB chunks, each acknowledged once the Storage Service
has written it, into a 2 KB receive buffer. The device checks the
SHA-256 before renaming <path>.part into place, and gives up after 5 s
of silence or a card job that never returns. FileReceiver holds the
logic, with 12 host tests. About 55 KB/s: 1.6 MB in under 30 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-04 02:31:19 +02:00

203 lines
7.4 KiB
C++

#include <unity.h>
#include <string>
#include <vector>
#include "file_receiver.h"
#include "sha256.h"
using namespace roro;
void setUp() {}
void tearDown() {}
static std::string hexSha(const std::vector<uint8_t>& data) {
uint8_t d[32];
Sha256::hash(data.data(), data.size(), d);
static const char* hex = "0123456789abcdef";
std::string s;
for (uint8_t b : d) {
s += hex[b >> 4];
s += hex[b & 15];
}
return s;
}
static std::vector<uint8_t> bytes(size_t n) {
std::vector<uint8_t> v(n);
for (size_t i = 0; i < n; i++) v[i] = static_cast<uint8_t>(i * 7 + 3);
return v;
}
static std::string args(const std::string& path, const std::vector<uint8_t>& data) {
return path + " " + std::to_string(data.size()) + " " + hexSha(data);
}
void test_begin_accepts_path_size_and_checksum() {
FileReceiver r;
auto data = bytes(10);
TEST_ASSERT_EQUAL_STRING("", r.begin(args("/updates/a.ota", data), 0).c_str());
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Receiving);
TEST_ASSERT_EQUAL_STRING("/updates/a.ota", r.path().c_str());
TEST_ASSERT_EQUAL_STRING("/updates/a.ota.part", r.partPath().c_str());
TEST_ASSERT_EQUAL_UINT32(10, r.size());
}
void test_begin_refuses_bad_arguments() {
FileReceiver r;
std::string sha(64, 'a');
TEST_ASSERT_NOT_EQUAL(0, r.begin("updates/a.ota 10 " + sha, 0).size()); // not absolute
TEST_ASSERT_NOT_EQUAL(0, r.begin("/a/../b.ota 10 " + sha, 0).size()); // climbs out
TEST_ASSERT_NOT_EQUAL(0, r.begin("/a.ota 0 " + sha, 0).size()); // empty
TEST_ASSERT_NOT_EQUAL(0, r.begin("/a.ota 99999999 " + sha, 0).size()); // too big
TEST_ASSERT_NOT_EQUAL(0, r.begin("/a.ota 1x " + sha, 0).size()); // not a number
TEST_ASSERT_NOT_EQUAL(0, r.begin("/a.ota 10 abc", 0).size()); // short checksum
TEST_ASSERT_NOT_EQUAL(0, r.begin("/a.ota 10 " + std::string(64, 'g'), 0).size());
TEST_ASSERT_NOT_EQUAL(0, r.begin("/a.ota 10", 0).size());
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Idle);
}
void test_bytes_are_handed_out_one_chunk_at_a_time() {
FileReceiver r;
auto data = bytes(FileReceiver::kChunk * 2 + 100);
r.begin(args("/f.bin", data), 0);
// Everything arrives at once: only the first chunk is taken.
TEST_ASSERT_EQUAL(FileReceiver::kChunk, r.feed(data.data(), data.size(), 1));
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Writing);
TEST_ASSERT_EQUAL(FileReceiver::kChunk, r.chunk().size());
TEST_ASSERT_EQUAL(0, r.feed(data.data() + FileReceiver::kChunk, 10, 2)); // nothing more while writing
r.chunkWritten(true, 3);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Receiving);
TEST_ASSERT_EQUAL_UINT32(FileReceiver::kChunk, r.received());
r.feed(data.data() + FileReceiver::kChunk, FileReceiver::kChunk, 4);
r.chunkWritten(true, 5);
// The last, short chunk is complete as soon as the announced size is reached.
TEST_ASSERT_EQUAL(100, r.feed(data.data() + 2 * FileReceiver::kChunk, 100, 6));
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Writing);
TEST_ASSERT_EQUAL(100, r.chunk().size());
TEST_ASSERT_EQUAL_UINT8(data[2 * FileReceiver::kChunk], r.chunk()[0]);
r.chunkWritten(true, 7);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Finishing);
r.finished(true);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Done);
}
void test_chunk_split_across_reads() {
FileReceiver r;
auto data = bytes(30);
r.begin(args("/f.bin", data), 0);
TEST_ASSERT_EQUAL(10, r.feed(data.data(), 10, 1));
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Receiving);
TEST_ASSERT_EQUAL(20, r.feed(data.data() + 10, 20, 2));
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Writing);
TEST_ASSERT_EQUAL(30, r.chunk().size());
}
void test_wrong_checksum_fails_before_the_last_write() {
FileReceiver r;
auto data = bytes(50);
r.begin(args("/f.bin", data), 0);
data[20] ^= 1; // corrupted on the way
r.feed(data.data(), data.size(), 1);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Failed);
TEST_ASSERT_EQUAL_STRING("checksum mismatch", r.error().c_str());
}
void test_failed_write_fails_the_transfer() {
FileReceiver r;
auto data = bytes(50);
r.begin(args("/f.bin", data), 0);
r.feed(data.data(), data.size(), 1);
r.chunkWritten(false, 2);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Failed);
TEST_ASSERT_EQUAL_STRING("write failed", r.error().c_str());
}
void test_failed_rename_fails_the_transfer() {
FileReceiver r;
auto data = bytes(5);
r.begin(args("/f.bin", data), 0);
r.feed(data.data(), data.size(), 1);
r.chunkWritten(true, 2);
r.finished(false);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Failed);
}
void test_silence_times_out() {
FileReceiver r;
auto data = bytes(FileReceiver::kChunk * 2);
r.begin(args("/f.bin", data), 1000);
r.tick(1000 + FileReceiver::kTimeoutMs - 1);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Receiving);
r.feed(data.data(), 10, 5000); // bytes reset the clock
r.tick(5000 + FileReceiver::kTimeoutMs - 1);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Receiving);
r.tick(5000 + FileReceiver::kTimeoutMs);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Failed);
TEST_ASSERT_EQUAL_STRING("timed out", r.error().c_str());
}
void test_a_write_that_never_completes_times_out() {
// With no card mounted, the storage task drops the job and never answers.
FileReceiver r;
auto data = bytes(10);
r.begin(args("/f.bin", data), 0);
r.feed(data.data(), data.size(), 100);
r.tick(100 + FileReceiver::kTimeoutMs);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Failed);
}
void test_wanted_counts_down_within_a_chunk() {
FileReceiver r;
auto data = bytes(FileReceiver::kChunk + 10);
TEST_ASSERT_EQUAL(0, r.wanted());
r.begin(args("/f.bin", data), 0);
TEST_ASSERT_EQUAL(FileReceiver::kChunk, r.wanted());
r.feed(data.data(), 24, 1);
TEST_ASSERT_EQUAL(FileReceiver::kChunk - 24, r.wanted());
r.feed(data.data() + 24, FileReceiver::kChunk - 24, 2);
TEST_ASSERT_EQUAL(0, r.wanted()); // writing
r.chunkWritten(true, 3);
TEST_ASSERT_EQUAL(10, r.wanted()); // the short last chunk
}
void test_a_rename_that_never_completes_times_out() {
FileReceiver r;
auto data = bytes(10);
r.begin(args("/f.bin", data), 0);
r.feed(data.data(), data.size(), 1);
r.chunkWritten(true, 2);
r.tick(2 + FileReceiver::kTimeoutMs);
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Failed);
}
void test_reset_returns_to_idle() {
FileReceiver r;
auto data = bytes(10);
r.begin(args("/f.bin", data), 0);
r.reset();
TEST_ASSERT_TRUE(r.state() == FileReceiver::State::Idle);
TEST_ASSERT_FALSE(r.active());
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_begin_accepts_path_size_and_checksum);
RUN_TEST(test_begin_refuses_bad_arguments);
RUN_TEST(test_bytes_are_handed_out_one_chunk_at_a_time);
RUN_TEST(test_chunk_split_across_reads);
RUN_TEST(test_wrong_checksum_fails_before_the_last_write);
RUN_TEST(test_failed_write_fails_the_transfer);
RUN_TEST(test_failed_rename_fails_the_transfer);
RUN_TEST(test_silence_times_out);
RUN_TEST(test_a_write_that_never_completes_times_out);
RUN_TEST(test_wanted_counts_down_within_a_chunk);
RUN_TEST(test_a_rename_that_never_completes_times_out);
RUN_TEST(test_reset_returns_to_idle);
return UNITY_END();
}