Expanded the e2e test suite and fake device data
Check, format and test / build (push) Successful in 46s
Check, format and test / build (push) Successful in 46s
- fake_records now generates a full year (2026) of classic workday punches (09:00-13:00, 14:00-18:00) for the employees in fake_users; this spans many 1024-byte blocks, exercising the multi-block streaming path - fake_users/fake_records are now public so tests can assert against them instead of hardcoded expectations - scoped #[allow(clippy::unwrap_used)] to only the functions that actually use unwrap, instead of a blanket file-level allow - added gets_total_record_count, streams_records, and gets_none_for_unknown_employee tests Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+50
-2
@@ -1,14 +1,62 @@
|
|||||||
//! End-to-end tests exercising R701's public API against the emulated
|
//! End-to-end tests exercising R701's public API against the emulated
|
||||||
//! device in `mock`.
|
//! device in `mock`.
|
||||||
|
|
||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
mod mock;
|
mod mock;
|
||||||
|
|
||||||
use r701::R701;
|
use r701::R701;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn connects_and_pings_successfully() {
|
async fn connects_and_pings_successfully() {
|
||||||
let addr = mock::spawn().await;
|
let addr = mock::spawn().await;
|
||||||
assert!(R701::connect(addr).await.is_ok());
|
assert!(R701::connect(addr).await.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
async fn gets_employee_name() {
|
||||||
|
let addr = mock::spawn().await;
|
||||||
|
let r701 = R701::connect(addr).await.unwrap();
|
||||||
|
let expected_name = mock::fake_users()[&1].clone();
|
||||||
|
|
||||||
|
match r701.get_name(1).await.unwrap() {
|
||||||
|
Some(Ok(name)) => assert_eq!(name, expected_name),
|
||||||
|
other => panic!("expected Some(Ok({expected_name:?})), got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
async fn gets_none_for_unknown_employee() {
|
||||||
|
let addr = mock::spawn().await;
|
||||||
|
let r701 = R701::connect(addr).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(r701.get_name(999).await.unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[allow(clippy::unwrap_used, clippy::cast_possible_truncation)]
|
||||||
|
async fn gets_total_record_count() {
|
||||||
|
let addr = mock::spawn().await;
|
||||||
|
let r701 = R701::connect(addr).await.unwrap();
|
||||||
|
let expected_count = mock::fake_records().len() as u16;
|
||||||
|
|
||||||
|
assert_eq!(r701.get_total_record_count().await.unwrap(), expected_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
async fn streams_records() {
|
||||||
|
let addr = mock::spawn().await;
|
||||||
|
let r701 = R701::connect(addr).await.unwrap();
|
||||||
|
// The device streams newest first, the reverse of fake_records' order
|
||||||
|
let expected_records: Vec<_> = mock::fake_records().into_iter().rev().collect();
|
||||||
|
|
||||||
|
let mut records = r701.into_record_iter().await.unwrap();
|
||||||
|
for expected in expected_records {
|
||||||
|
let record = records.next().await.unwrap().unwrap();
|
||||||
|
assert_eq!(record, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(records.next().await.is_none());
|
||||||
|
}
|
||||||
|
|||||||
+43
-13
@@ -2,31 +2,59 @@
|
|||||||
//! client against it, grounded in the protocol documented at
|
//! client against it, grounded in the protocol documented at
|
||||||
//! <https://nicolabelluti.me/posts/2024/05/studying-a-communication-protocol/>.
|
//! <https://nicolabelluti.me/posts/2024/05/studying-a-communication-protocol/>.
|
||||||
|
|
||||||
use chrono::{Datelike, Local, TimeZone, Timelike};
|
use chrono::{Datelike, Duration, Local, TimeZone, Timelike};
|
||||||
use r701::{Clock, Record};
|
use r701::{Clock, Record};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
/// The employee names the fake device reports
|
/// The employee names the fake device reports
|
||||||
fn fake_users() -> HashMap<u32, String> {
|
pub fn fake_users() -> HashMap<u32, String> {
|
||||||
HashMap::from([(1, "mario".to_string())])
|
HashMap::from([(1, "alice".to_string()), (2, "bob".to_string())])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The attendance records the fake device reports
|
/// The attendance records the fake device reports: a full year of classic
|
||||||
fn fake_records() -> Vec<Record> {
|
/// workday punches (09:00 - 13:00, 14:00 - 18:00) for the employees in
|
||||||
vec![Record {
|
/// `fake_users()`, staggered by punch time rather than grouped by employee.
|
||||||
employee_id: 1,
|
#[allow(clippy::unwrap_used)]
|
||||||
clock: Clock::FirstIn,
|
pub fn fake_records() -> Vec<Record> {
|
||||||
datetime: Local
|
let first_day = Local
|
||||||
.with_ymd_and_hms(1970, 1, 1, 0, 0, 0)
|
.with_ymd_and_hms(2026, 1, 1, 9, 0, 0)
|
||||||
.single()
|
.single()
|
||||||
.unwrap(),
|
.unwrap();
|
||||||
}]
|
|
||||||
|
let mut employee_ids: Vec<u32> = fake_users().into_keys().collect();
|
||||||
|
employee_ids.sort_unstable();
|
||||||
|
|
||||||
|
let mut records = Vec::new();
|
||||||
|
for day in 0..365 {
|
||||||
|
let morning_in = first_day + Duration::days(day);
|
||||||
|
let morning_out = morning_in + Duration::hours(4);
|
||||||
|
let afternoon_in = morning_out + Duration::hours(1);
|
||||||
|
let afternoon_out = afternoon_in + Duration::hours(4);
|
||||||
|
|
||||||
|
for (clock, datetime) in [
|
||||||
|
(Clock::FirstIn, morning_in),
|
||||||
|
(Clock::FirstOut, morning_out),
|
||||||
|
(Clock::SecondIn, afternoon_in),
|
||||||
|
(Clock::SecondOut, afternoon_out),
|
||||||
|
] {
|
||||||
|
for &employee_id in &employee_ids {
|
||||||
|
records.push(Record {
|
||||||
|
employee_id,
|
||||||
|
clock,
|
||||||
|
datetime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a fake R701 device in the background and returns the address to
|
/// Starts a fake R701 device in the background and returns the address to
|
||||||
/// connect to.
|
/// connect to.
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
pub async fn spawn() -> String {
|
pub async fn spawn() -> String {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
let addr = listener.local_addr().unwrap().to_string();
|
let addr = listener.local_addr().unwrap().to_string();
|
||||||
@@ -70,6 +98,7 @@ pub async fn spawn() -> String {
|
|||||||
/// Builds the 34-byte get-name response: a 12-byte header (whether the
|
/// Builds the 34-byte get-name response: a 12-byte header (whether the
|
||||||
/// employee was found), followed by their name (if found) null-padded to
|
/// employee was found), followed by their name (if found) null-padded to
|
||||||
/// 10 bytes, followed by 12 don't-care bytes.
|
/// 10 bytes, followed by 12 don't-care bytes.
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fn get_name_response(payload: &[u8]) -> Vec<u8> {
|
fn get_name_response(payload: &[u8]) -> Vec<u8> {
|
||||||
let employee_id = u32::from_le_bytes(payload[2..6].try_into().unwrap());
|
let employee_id = u32::from_le_bytes(payload[2..6].try_into().unwrap());
|
||||||
let mut response = [0u8; 34];
|
let mut response = [0u8; 34];
|
||||||
@@ -99,6 +128,7 @@ fn get_total_record_count_response() -> Vec<u8> {
|
|||||||
/// requested 1024-byte block of records (`ff`-padded past the last real
|
/// requested 1024-byte block of records (`ff`-padded past the last real
|
||||||
/// one, since records don't align to 1024-byte block boundaries), and 2
|
/// one, since records don't align to 1024-byte block boundaries), and 2
|
||||||
/// trailing bytes.
|
/// trailing bytes.
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fn get_record_bytes_response(payload: &[u8]) -> Vec<u8> {
|
fn get_record_bytes_response(payload: &[u8]) -> Vec<u8> {
|
||||||
let sequence_number = u16::from_le_bytes(payload[8..10].try_into().unwrap());
|
let sequence_number = u16::from_le_bytes(payload[8..10].try_into().unwrap());
|
||||||
let all_bytes: Vec<u8> = fake_records().iter().flat_map(encode_record).collect();
|
let all_bytes: Vec<u8> = fake_records().iter().flat_map(encode_record).collect();
|
||||||
|
|||||||
Reference in New Issue
Block a user