blob: c0052264f6c691e060350f0ba4909a70677cf910 [file] [log] [blame]
// Copyright 2024 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include "modules/morse_code/encoder.h"
#include <chrono>
#include <cstdint>
#include "modules/led/monochrome_led_fake.h"
#include "modules/worker/test_worker.h"
#include "pw_containers/vector.h"
#include "pw_status/status.h"
#include "pw_sync/timed_thread_notification.h"
#include "pw_thread/sleep.h"
#include "pw_thread/thread.h"
#include "pw_unit_test/framework.h"
namespace sense {
// Test fixtures.
class MorseCodeEncoderTest : public ::testing::Test {
protected:
using Event = ::sense::MonochromeLedFake::Event;
using State = ::sense::MonochromeLedFake::State;
static constexpr uint32_t kIntervalMs = 10;
// TODO(b/352327457): Ideally this would use simulated time, but no
// simulated system timer exists yet. For now, relax the constraint by
// checking that the LED was in the right state for _at least_ the expected
// number of intervals. On some platforms, the fake LED is implemented using
// threads, and may sleep a bit longer.
MorseCodeEncoderTest()
: clock_(pw::chrono::VirtualSystemClock::RealClock()) {}
void Expect(std::string_view msg) {
EXPECT_EQ(end_of_pattern_count_, expected_messages_);
auto& events = led_.events();
auto event = events.begin();
// Skip until the first "turn on" event is seen, and use it as the starting
// point.
while (true) {
if (event == events.end()) {
EXPECT_TRUE(msg.empty());
return;
}
if (event->state == State::kActive) {
break;
}
++event;
}
auto start = event->timestamp;
++event;
while (!msg.empty()) {
size_t off_intervals;
size_t offset;
if (msg.substr(1, 2) == " ") {
// Word break
off_intervals = 7;
offset = 3;
} else if (msg.substr(1, 1) == " ") {
// Letter break
off_intervals = 3;
offset = 2;
} else {
// Symbol break
off_intervals = 1;
offset = 1;
}
// Check that the LED turns off after the roght amount of time, implying
/// it was on.
ASSERT_NE(event, events.end());
EXPECT_EQ(event->state, State::kInactive);
if (msg[0] == '.') {
EXPECT_GE(ToMs(event->timestamp - start), interval_ms_);
} else if (msg[0] == '-') {
EXPECT_GE(ToMs(event->timestamp - start), interval_ms_ * 3);
} else {
FAIL();
}
start = event->timestamp;
++event;
msg = msg.substr(offset);
if (msg.empty()) {
break;
}
// Check that the LED turns on after the roght amount of time, implying
/// it was off.
ASSERT_NE(event, events.end());
EXPECT_EQ(event->state, State::kActive);
EXPECT_GE(ToMs(event->timestamp - start), interval_ms_ * off_intervals);
start = event->timestamp;
++event;
}
events.clear();
}
uint32_t ToMs(pw::chrono::SystemClock::duration interval) {
return std::chrono::duration_cast<std::chrono::milliseconds>(interval)
.count();
}
/// Waits until the given encoder is idle.
void SleepUntilDone() {
auto interval = pw::chrono::SystemClock::for_at_least(
std::chrono::milliseconds(interval_ms_));
while (!encoder_.IsIdle()) {
pw::this_thread::sleep_for(interval);
}
}
auto LedOutput() {
return [this](bool turn_on, const Encoder::State& state) {
FakeLedOutput(turn_on, state.message_finished());
};
}
Encoder encoder_;
uint32_t interval_ms_ = kIntervalMs;
// Number of messages encoded before the next Expect() call.
int expected_messages_ = 1;
private:
void FakeLedOutput(bool turn_on, bool pattern_finished) {
if (turn_on) {
led_.TurnOn();
} else {
led_.TurnOff();
}
// Track how many times the last bit in the pattern is seen.
if (pattern_finished) {
end_of_pattern_count_ += 1;
EXPECT_LE(end_of_pattern_count_, expected_messages_);
} else if (expected_messages_ == 0u) {
EXPECT_EQ(end_of_pattern_count_, expected_messages_);
} else {
EXPECT_LT(end_of_pattern_count_, expected_messages_);
}
}
pw::chrono::VirtualSystemClock& clock_;
MonochromeLedFake led_;
int end_of_pattern_count_ = 0;
};
// Unit tests.
TEST_F(MorseCodeEncoderTest, EncodeEmpty) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
expected_messages_ = 0;
EXPECT_EQ(encoder_.Encode("", 1, interval_ms_), pw::OkStatus());
SleepUntilDone();
worker.Stop();
Expect("");
}
TEST_F(MorseCodeEncoderTest, EncodeOneLetter) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
EXPECT_EQ(encoder_.Encode("E", 1, interval_ms_), pw::OkStatus());
SleepUntilDone();
worker.Stop();
Expect(".");
}
TEST_F(MorseCodeEncoderTest, EncodeOneWord) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
EXPECT_EQ(encoder_.Encode("PARIS", 1, interval_ms_), pw::OkStatus());
SleepUntilDone();
worker.Stop();
Expect(".--. .- .-. .. ...");
}
TEST_F(MorseCodeEncoderTest, EncodeHelloWorld) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
EXPECT_EQ(encoder_.Encode("hello world", 1, interval_ms_), pw::OkStatus());
SleepUntilDone();
worker.Stop();
Expect(".... . .-.. .-.. --- .-- --- .-. .-.. -..");
}
// TODO(b/352327457): Without simulated time, this test is too slow to run every
// case on device.
#if defined(AM_MORSE_CODE_ENCODER_TEST_FULL) && AM_MORSE_CODE_ENCODER_TEST_FULL
TEST_F(MorseCodeEncoderTest, EncodeRepeated) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
expected_messages_ = 2;
EXPECT_EQ(encoder_.Encode("hello", 2, interval_ms_), pw::OkStatus());
SleepUntilDone();
worker.Stop();
Expect(".... . .-.. .-.. --- .... . .-.. .-.. ---");
}
TEST_F(MorseCodeEncoderTest, EncodeSlow) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
interval_ms_ = 25;
EXPECT_EQ(encoder_.Encode("hello", 1, interval_ms_), pw::OkStatus());
SleepUntilDone();
worker.Stop();
Expect(".... . .-.. .-.. ---");
}
TEST_F(MorseCodeEncoderTest, EncodeConsecutiveWhitespace) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
EXPECT_EQ(encoder_.Encode("hello world", 1, interval_ms_), pw::OkStatus());
SleepUntilDone();
worker.Stop();
Expect(".... . .-.. .-.. --- .-- --- .-. .-.. -..");
}
TEST_F(MorseCodeEncoderTest, EncodeInvalidChars) {
TestWorker<> worker;
encoder_.Init(worker, LedOutput());
char s[2];
s[1] = 0;
expected_messages_ = 0;
for (char c = 127; c != 0; --c) {
if (isspace(c) || isalnum(c) || c == '?' || c == '@') {
continue;
}
s[0] = c;
expected_messages_ += 1;
EXPECT_EQ(encoder_.Encode(s, 1, interval_ms_), pw::OkStatus());
SleepUntilDone();
Expect("..--..");
}
worker.Stop();
}
#endif // AM_MORSE_CODE_ENCODER_TEST_FULL
} // namespace sense