Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
435e701
* Remove "test_" prefix from hue::diff test functions
chrivers May 11, 2025
deb65e6
+ Add hue::diff::event_update_apply() for patching a json diff gener…
chrivers May 11, 2025
457724b
+ Add unit tests for hue::diff::event_update_apply
chrivers May 11, 2025
a14d351
+ Add HueError::Unmergable for use in event_update_apply()
chrivers May 11, 2025
4a2c6dd
* Add all compare macros to hue crate lib.rs under cfg(test)
chrivers May 21, 2025
c1c3ce0
* Use crate-level compare macros
chrivers May 21, 2025
46fd3cc
+ Add missing unit tests for invalid input, to gain 100% test covera…
chrivers May 21, 2025
366d648
* Use compare_xy() macro
chrivers May 21, 2025
17aa165
* Use crate-level compare macros in hue::xy::tests
chrivers May 21, 2025
2df4ddf
+ Add unit tests for many missing XY features
chrivers May 21, 2025
5885164
* Normalize formatting
chrivers May 21, 2025
734a2d2
* Make XY::from_rgb_unit() fall back to D50_WHITE_POINT, not D65_WHI…
chrivers May 21, 2025
4ee771c
* Make XY::from_rgb_unit() cap brightness at 255.0. Because of compo…
chrivers May 21, 2025
671560b
* Make Clamp::unit_to_u8_clamped() perform proper rounding. (found b…
chrivers May 21, 2025
564cfb8
* Simplify XY::to_quant()
chrivers May 21, 2025
53f5707
* Construct devicedb data as a LazyLock instance, to make it effecti…
chrivers May 22, 2025
7768786
+ Enable llvm-cov options globally, and set them for hue crate
chrivers May 22, 2025
2bebd06
+ Add unit tests for Gamma type
chrivers May 22, 2025
84488f3
+ Add unit test for update_url_for_bridge()
chrivers May 22, 2025
e0dd710
+ Add unit tests for SwVersion struct
chrivers May 22, 2025
887a22c
+ Add unit tests for TakeFlag trait
chrivers May 22, 2025
0087955
* Make hue::diff::tests::apply_invalid() test both cases, and disabl…
chrivers May 22, 2025
dd08c89
+ Add unit tests for hue::devicedb
chrivers May 22, 2025
7ac44e7
+ Cover hue::date_format entirely by unit tests
chrivers May 22, 2025
72ed65a
+ Add unit tests for hue::event
chrivers May 22, 2025
5c80e9a
+ Add missing .round() on Clamp impls (found by unit testing)
chrivers May 22, 2025
7231d30
+ Add remaining unit tests for Clamp impls
chrivers May 22, 2025
ebc0711
- Disable code coverage for trivial mapping functions
chrivers May 22, 2025
5a1b17e
+ Add unit tests for HueZigbeeUpdate serialization
chrivers May 22, 2025
06161c6
* Use .recip() for 1/x calculation
chrivers May 22, 2025
1f63c28
+ Add unit test for serialize_lower_case_mac
chrivers May 22, 2025
66b8bf6
* Make HueStreamPacket::parse() not panic on invalid input (found by…
chrivers May 22, 2025
c66d210
+ Add unit testing for hue::stream
chrivers May 22, 2025
cffaa04
+ Add unit test macros compare_xy_quant() and compare_matrix()
chrivers May 22, 2025
3592c7a
+ Improve debug output for compare_float()
chrivers May 22, 2025
1be375e
+ Add unit tests for functions in hue/lib.rs
chrivers May 22, 2025
4fbddc6
* Disable coverage for trivial mapping function
chrivers May 22, 2025
29b3c1d
- Disable coverage for test code
chrivers May 22, 2025
2a852fd
+ Add unit tests for hue::date_format::legacy_utc
chrivers May 22, 2025
7edae07
* Simplify ColorSpace unit tests using compare_matrix()
chrivers May 22, 2025
4fca7a6
+ Add more unit tests for matrix inversion
chrivers May 22, 2025
b148b8a
+ Derive {PartialEq, Eq} for GradientStyle, to make unit testing easier
chrivers May 22, 2025
6a2b736
* Fix HueEntSegmentLayout::pack(), which encoded (u16, u16) instead …
chrivers May 22, 2025
efc1f0d
+ Add helper function HueEntFrameLightRecord.mode()
chrivers May 22, 2025
b630138
+ Add extensive unit testing to hue::zigbee::entertainment
chrivers May 22, 2025
fc7d8dc
+ Add more unit tests to hue::zigbee::composite
chrivers May 22, 2025
723125f
+ Add more unit tests to hue::zigbee::stream
chrivers May 22, 2025
3d37ff4
* Fix id_v1 generation for delete events
chrivers May 11, 2025
808d0af
* Represent hue delete events as structured data
chrivers May 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ members = [
]

[workspace.lints.rust]
# NOTE: to use llvm-cov, comment out the "unstable_features" restriction:
unstable_features = "forbid"
unused_lifetimes = "warn"
unused_qualifications = "warn"

# Needed for llvm-cov
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }

[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
correctness = { level = "warn", priority = -1 }
Expand Down
1 change: 1 addition & 0 deletions crates/hue/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ thiserror = "2.0.11"
uuid = { version = "1.13.1", features = ["serde", "v5"] }

mac_address = { version = "1.1.8", features = ["serde"], optional = true }
maplit = "1.0.2"

[features]
default = ["event", "mac", "rng"]
Expand Down
68 changes: 64 additions & 4 deletions crates/hue/src/clamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ pub trait Clamp {
impl Clamp for f32 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn unit_to_u8_clamped(self) -> u8 {
(self * 255.0).clamp(0.0, 255.0) as u8
(self * 255.0).round().clamp(0.0, 255.0) as u8
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn unit_to_u8_clamped_light(self) -> u8 {
self.mul_add(253.0, 1.0).clamp(1.0, 254.0) as u8
self.mul_add(253.0, 1.0).round().clamp(1.0, 254.0) as u8
}

fn unit_from_u8(value: u8) -> Self {
Expand All @@ -23,15 +23,75 @@ impl Clamp for f32 {
impl Clamp for f64 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn unit_to_u8_clamped(self) -> u8 {
(self * 255.0).clamp(0.0, 255.0) as u8
(self * 255.0).round().clamp(0.0, 255.0) as u8
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn unit_to_u8_clamped_light(self) -> u8 {
self.mul_add(253.0, 1.0).clamp(1.0, 254.0) as u8
self.mul_add(253.0, 1.0).round().clamp(1.0, 254.0) as u8
}

fn unit_from_u8(value: u8) -> Self {
Self::from(value) / 255.0
}
}

#[cfg(test)]
mod tests {
use crate::clamp::Clamp;
use crate::{compare, compare_float};

#[test]
fn f32_unit_to_u8_clamped() {
assert_eq!((-1.0f32).unit_to_u8_clamped(), 0x00);
assert_eq!(0.0f32.unit_to_u8_clamped(), 0x00);
assert_eq!(0.5f32.unit_to_u8_clamped(), 0x80);
assert_eq!(1.0f32.unit_to_u8_clamped(), 0xFF);
assert_eq!(2.0f32.unit_to_u8_clamped(), 0xFF);
}

#[test]
fn f64_unit_to_u8_clamped() {
assert_eq!((-1.0f64).unit_to_u8_clamped(), 0x00);
assert_eq!(0.0f64.unit_to_u8_clamped(), 0x00);
assert_eq!(0.5f64.unit_to_u8_clamped(), 0x80);
assert_eq!(1.0f64.unit_to_u8_clamped(), 0xFF);
assert_eq!(2.0f64.unit_to_u8_clamped(), 0xFF);
}

#[test]
fn f32_unit_to_u8_clamped_light() {
assert_eq!((-1.0f32).unit_to_u8_clamped_light(), 0x01);
assert_eq!(0.0f32.unit_to_u8_clamped_light(), 0x01);
assert_eq!(0.5f32.unit_to_u8_clamped_light(), 0x80);
assert_eq!(1.0f32.unit_to_u8_clamped_light(), 0xFE);
assert_eq!(2.0f32.unit_to_u8_clamped_light(), 0xFE);
}

#[test]
fn f64_unit_to_u8_clamped_light() {
assert_eq!((-1.0f64).unit_to_u8_clamped_light(), 0x01);
assert_eq!(0.0f64.unit_to_u8_clamped_light(), 0x01);
assert_eq!(0.5f64.unit_to_u8_clamped_light(), 0x80);
assert_eq!(1.0f64.unit_to_u8_clamped_light(), 0xFE);
assert_eq!(2.0f64.unit_to_u8_clamped_light(), 0xFE);
}

#[test]
fn f32_unit_from_u8() {
compare!(f32::unit_from_u8(0x00), 0.0 / 255.0);
compare!(f32::unit_from_u8(0x01), 1.0 / 255.0);
compare!(f32::unit_from_u8(0x02), 2.0 / 255.0);
compare!(f32::unit_from_u8(0xFE), 254.0 / 255.0);
compare!(f32::unit_from_u8(0xFF), 255.0 / 255.0);
}

#[test]
fn f64_unit_from_u8() {
compare!(f64::unit_from_u8(0x00), 0.0 / 255.0);
compare!(f64::unit_from_u8(0x01), 1.0 / 255.0);
compare!(f64::unit_from_u8(0x02), 2.0 / 255.0);
compare!(f64::unit_from_u8(0xFE), 254.0 / 255.0);
compare!(f64::unit_from_u8(0xFF), 255.0 / 255.0);
}
}
36 changes: 18 additions & 18 deletions crates/hue/src/colorspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl Matrix3 {
}

// Divide the row by the diagonal term
let inv = 1.0 / d;
let inv = d.recip();
for c in 0..3 {
current[[i, c]] *= inv;
inverse[[i, c]] *= inv;
Expand Down Expand Up @@ -215,16 +215,8 @@ pub const ADOBE: ColorSpace = ColorSpace {
mod tests {
use std::iter::zip;

use crate::colorspace::{ADOBE, ColorSpace, SRGB, WIDE};

macro_rules! compare {
($expr:expr, $value:expr) => {
let a = $expr;
let b = $value;
eprintln!("{a} vs {b:.4}");
assert!((a - b).abs() < 1e-4);
};
}
use crate::colorspace::{ADOBE, ColorSpace, Matrix3, SRGB, WIDE};
use crate::{compare, compare_float, compare_matrix};

fn verify_matrix(cs: &ColorSpace) {
let xyz = &cs.xyz;
Expand All @@ -233,13 +225,8 @@ mod tests {
let xyzi = xyz.inverted().unwrap();
let rgbi = rgb.inverted().unwrap();

zip(xyz.0, rgbi.0).for_each(|(a, b)| {
compare!(a, b);
});

zip(rgb.0, xyzi.0).for_each(|(a, b)| {
compare!(a, b);
});
compare_matrix!(xyz.0, rgbi.0);
compare_matrix!(rgb.0, xyzi.0);
}

#[test]
Expand All @@ -256,4 +243,17 @@ mod tests {
fn iverse_adobe() {
verify_matrix(&ADOBE);
}

#[test]
fn invert_identity() {
let ident = Matrix3::identity();
let inv = ident.inverted().unwrap();
compare_matrix!(ident.0, inv.0);
}

#[test]
fn invert_zero() {
let zero = Matrix3([0.0; 9]);
assert!(zero.inverted().is_none());
}
}
19 changes: 1 addition & 18 deletions crates/hue/src/colortemp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,7 @@ pub fn cct_to_xy(cct: f64) -> XY {
mod tests {
use crate::colortemp::cct_to_xy;
use crate::xy::XY;

macro_rules! compare {
($expr:expr, $value:expr) => {
let a = $expr;
let b = $value;
eprintln!("{a} vs {b:.4}");
assert!((a - b).abs() < 1e-4);
};
}

macro_rules! compare_xy {
($expr:expr, $value:expr) => {
let a = $expr;
let b = $value;
compare!(a.x, b.x);
compare!(a.y, b.y);
};
}
use crate::{compare, compare_float, compare_xy};

// Regression tests, sanity checked against kelvin-to-blackbody raditation color
// data found here:
Expand Down
158 changes: 144 additions & 14 deletions crates/hue/src/date_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,37 +158,167 @@ pub mod legacy_utc_opt {
date_deserializer_utc_opt!(DateTime<Utc>, super::FORMAT_LOCAL);
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use chrono::{DateTime, TimeZone, Utc};
use std::fmt::Debug;

use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
use serde_json::de::StrRead;

use crate::error::HueResult;

fn date() -> (&'static str, DateTime<Utc>) {
fn de<T: Debug + Eq>(
ds: &'static str,
d1: &T,
desi: impl Fn(&mut serde_json::Deserializer<StrRead>) -> serde_json::Result<T>,
) -> HueResult<()> {
let mut deser = serde_json::Deserializer::from_str(ds);
let d2 = desi(&mut deser)?;

assert_eq!(*d1, d2);
Ok(())
}

fn se(
s1: &'static str,
seri: impl Fn(&mut serde_json::Serializer<&mut Vec<u8>>) -> serde_json::Result<()>,
) -> HueResult<()> {
let mut s2 = vec![];
let mut ser = serde_json::Serializer::new(&mut s2);
seri(&mut ser)?;

eprintln!("{} vs {}", s1, s2.escape_ascii());
assert_eq!(s1.as_bytes(), s2);
Ok(())
}

fn date_utc() -> (&'static str, DateTime<Utc>) {
let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap();
("\"2014-07-08T09:10:11Z\"", dt)
}

#[test]
fn utc_de() -> HueResult<()> {
let (ds, d1) = date();
let (ds, d1) = date_utc();
de(ds, &d1, |de| super::utc::deserialize(de))
}

let mut deser = serde_json::Deserializer::from_str(ds);
let d2 = super::utc::deserialize(&mut deser)?;
#[test]
fn utc_se() -> HueResult<()> {
let (s1, dt) = date_utc();
se(s1, |ser| super::utc::serialize(&dt, ser))
}

assert_eq!(d1, d2);
Ok(())
fn date_utc_ms() -> (&'static str, DateTime<Utc>) {
let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap();
let dt = Utc
.timestamp_millis_opt(dt.timestamp_millis() + 123)
.unwrap();
("\"2014-07-08T09:10:11.123Z\"", dt)
}

#[test]
fn utc_se() -> HueResult<()> {
let (s1, dt) = date();
fn utc_ms_de() -> HueResult<()> {
let (ds, d1) = date_utc_ms();
de(ds, &d1, |de| super::utc_ms::deserialize(de))
}

let mut s2 = vec![];
let mut ser = serde_json::Serializer::new(&mut s2);
super::utc::serialize(&dt, &mut ser)?;
#[test]
fn utc_ms_se() -> HueResult<()> {
let (s1, dt) = date_utc_ms();
se(s1, |ser| super::utc_ms::serialize(&dt, ser))
}

assert_eq!(s1.as_bytes(), s2);
Ok(())
#[test]
fn utc_ms_opt_de_some() -> HueResult<()> {
let (ds, d1) = date_utc_ms();
de(ds, &Some(d1), |de| super::utc_ms_opt::deserialize(de))
}

#[test]
fn utc_ms_opt_de_none() -> HueResult<()> {
de("null", &None, |de| super::utc_ms_opt::deserialize(de))
}

#[test]
fn utc_ms_opt_se_some() -> HueResult<()> {
let (s1, dt) = date_utc_ms();
se(s1, |ser| super::utc_ms_opt::serialize(&Some(dt), ser))
}

#[test]
fn utc_ms_opt_se_none() -> HueResult<()> {
se("null", |ser| super::utc_ms_opt::serialize(&None, ser))
}

fn date_legacy_naive() -> (&'static str, NaiveDateTime) {
let dt = NaiveDateTime::new(
NaiveDate::from_ymd_opt(2014, 7, 8).unwrap(),
NaiveTime::from_hms_opt(9, 10, 11).unwrap(),
);
("\"2014-07-08T09:10:11\"", dt)
}

#[test]
fn legacy_naive_de() -> HueResult<()> {
let (ds, d1) = date_legacy_naive();
de(ds, &d1, |de| super::legacy_naive::deserialize(de))
}

#[test]
fn legacy_naive_se() -> HueResult<()> {
let (s1, dt) = date_legacy_naive();
se(s1, |ser| super::legacy_naive::serialize(&dt, ser))
}

fn date_legacy_local_opt() -> (&'static str, DateTime<Local>) {
let dt = Local.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap();
("\"2014-07-08T09:10:11\"", dt)
}

#[test]
fn legacy_local_opt_de_some() -> HueResult<()> {
let (ds, d1) = date_legacy_local_opt();
de(ds, &Some(d1), |de| super::legacy_local_opt::deserialize(de))
}

#[test]
fn legacy_local_opt_se_some() -> HueResult<()> {
let (s1, dt) = date_legacy_local_opt();
se(s1, |ser| super::legacy_local_opt::serialize(&Some(dt), ser))
}

#[test]
fn legacy_local_opt_de_none() -> HueResult<()> {
de("null", &None, |de| super::legacy_local_opt::deserialize(de))
}

#[test]
fn legacy_local_opt_se_none() -> HueResult<()> {
se("null", |ser| super::legacy_local_opt::serialize(&None, ser))
}

#[test]
fn update_utc_de() -> HueResult<()> {
let (ds, d1) = date_utc();
de(ds, &d1, |de| super::update_utc::deserialize(de))
}

fn date_legacy_utc() -> (&'static str, DateTime<Utc>) {
let dt = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap();
("\"2014-07-08T09:10:11\"", dt)
}

#[test]
fn legacy_utc_de() -> HueResult<()> {
let (ds, d1) = date_legacy_utc();
de(ds, &d1, |de| super::legacy_utc::deserialize(de))
}

#[test]
fn legacy_utc_se() -> HueResult<()> {
let (s1, dt) = date_legacy_utc();
se(s1, |ser| super::legacy_utc::serialize(&dt, ser))
}
}
Loading