Add unit tests for all custom (de)serializers

This commit is contained in:
Dominik Nakamura 2020-12-30 18:09:03 +09:00
parent be2fca59b6
commit 5077adac0b
No known key found for this signature in database
GPG key ID: E4C6A749B2491910
6 changed files with 722 additions and 47 deletions

View file

@ -34,6 +34,7 @@ tungstenite = { version = "0.11.1", default-features = false }
anyhow = "1.0.37"
dotenv = "0.15.0"
pretty_env_logger = "0.4.0"
serde_test = "1.0.118"
tokio = { version = "0.3.6", features = ["fs", "macros", "rt-multi-thread", "time"] }
[features]

392
src/de.rs
View file

@ -28,16 +28,16 @@ enum Error {
ConversionFailed(String),
}
pub fn duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
pub fn duration_opt<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_option(OptDurationVisitor)
deserializer.deserialize_option(DurationOptVisitor)
}
struct OptDurationVisitor;
struct DurationOptVisitor;
impl<'de> Visitor<'de> for OptDurationVisitor {
impl<'de> Visitor<'de> for DurationOptVisitor {
type Value = Option<Duration>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -86,12 +86,12 @@ pub fn duration_millis_opt<'de, D>(deserializer: D) -> Result<Option<Duration>,
where
D: Deserializer<'de>,
{
deserializer.deserialize_i64(OptDurationMillisVisitor)
deserializer.deserialize_option(DurationMillisOptVisitor)
}
struct OptDurationMillisVisitor;
struct DurationMillisOptVisitor;
impl<'de> Visitor<'de> for OptDurationMillisVisitor {
impl<'de> Visitor<'de> for DurationMillisOptVisitor {
type Value = Option<Duration>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -254,28 +254,382 @@ where
#[cfg(test)]
mod tests {
use anyhow::Context;
use bitflags::bitflags;
use serde::Deserialize;
use serde_json::json;
use serde_test::{assert_de_tokens, assert_de_tokens_error, Token};
use super::*;
#[test]
fn deser_duration() {
fn deser_duration_opt() {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleDuration {
#[serde(deserialize_with = "duration")]
#[serde(deserialize_with = "duration_opt")]
value: Option<Duration>,
};
let input = json! {{ "value": "02:15:04.310" }};
let expect = SimpleDuration {
value: Some(
Duration::hours(2)
+ Duration::minutes(15)
+ Duration::seconds(4)
+ Duration::milliseconds(310),
),
assert_de_tokens(
&SimpleDuration {
value: Some(
Duration::hours(2)
+ Duration::minutes(15)
+ Duration::seconds(4)
+ Duration::milliseconds(310),
),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::Str("02:15:04.310"),
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleDuration { value: None },
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::None,
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleDuration {
value: Some(
Duration::hours(2)
+ Duration::minutes(15)
+ Duration::seconds(4)
+ Duration::milliseconds(310),
),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::Some,
Token::Str("02:15:04.310"),
Token::StructEnd,
],
);
}
#[test]
fn deser_duration_millis_opt() {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleDuration {
#[serde(deserialize_with = "duration_millis_opt")]
value: Option<Duration>,
};
assert_eq!(expect, serde_json::from_value(input).unwrap());
assert_de_tokens(
&SimpleDuration {
value: Some(Duration::milliseconds(150)),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::I64(150),
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleDuration { value: None },
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::I64(-1),
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleDuration {
value: Some(Duration::milliseconds(150)),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::U64(150),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleDuration>(
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::U64(u64::MAX),
Token::StructEnd,
],
"value 18446744073709551615 is too large for an i64: \
out of range integral type conversion attempted",
);
assert_de_tokens(
&SimpleDuration { value: None },
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::None,
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleDuration {
value: Some(Duration::milliseconds(150)),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::Some,
Token::I64(150),
Token::StructEnd,
],
);
}
#[test]
fn deser_duration_millis() {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleDuration {
#[serde(deserialize_with = "duration_millis")]
value: Duration,
};
assert_de_tokens(
&SimpleDuration {
value: Duration::milliseconds(150),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::I64(150),
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleDuration {
value: Duration::milliseconds(150),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::U64(150),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleDuration>(
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::U64(u64::MAX),
Token::StructEnd,
],
"value 18446744073709551615 is too large for an i64: \
out of range integral type conversion attempted",
);
}
#[test]
fn deser_duration_nanos() {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleDuration {
#[serde(deserialize_with = "duration_nanos")]
value: Duration,
};
assert_de_tokens(
&SimpleDuration {
value: Duration::nanoseconds(150),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::I64(150),
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleDuration {
value: Duration::nanoseconds(150),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::U64(150),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleDuration>(
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::U64(u64::MAX),
Token::StructEnd,
],
"value 18446744073709551615 is too large for an i64: \
out of range integral type conversion attempted",
);
}
#[test]
fn deser_bitflags_u8() {
bitflags! {
struct Flags: u8 {
const ONE = 1;
const TWO = 2;
}
}
impl TryFrom<u8> for Flags {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::from_bits(value).context("unknown flags found")
}
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleFlags {
#[serde(deserialize_with = "bitflags_u8")]
value: Flags,
};
assert_de_tokens(
&SimpleFlags {
value: Flags::ONE | Flags::TWO,
},
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::I64(3),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleFlags>(
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::I64(i64::MAX),
Token::StructEnd,
],
"value doesn't fit into an u8 integer: out of range integral type conversion attempted",
);
assert_de_tokens(
&SimpleFlags {
value: Flags::ONE | Flags::TWO,
},
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::U8(3),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleFlags>(
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::U8(100),
Token::StructEnd,
],
"conversion from u8 failed: unknown flags found",
);
assert_de_tokens(
&SimpleFlags {
value: Flags::ONE | Flags::TWO,
},
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::U64(3),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleFlags>(
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::U64(u64::MAX),
Token::StructEnd,
],
"value doesn't fit into an u8 integer: out of range integral type conversion attempted",
);
}
}

View file

@ -9,11 +9,11 @@ use crate::common::{SceneItem, SceneItemTransform};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Event {
#[serde(default, deserialize_with = "crate::de::duration")]
#[serde(default, deserialize_with = "crate::de::duration_opt")]
/// Time elapsed between now and stream start (only present if OBS Studio is streaming).
pub stream_timecode: Option<Duration>,
/// Time elapsed between now and recording start (only present if OBS Studio is recording).
#[serde(default, deserialize_with = "crate::de::duration")]
#[serde(default, deserialize_with = "crate::de::duration_opt")]
pub rec_timecode: Option<Duration>,
/// The type of event.
#[serde(flatten)]

View file

@ -1,6 +1,12 @@
use chrono::Duration;
use rgb::RGBA8;
use serde::ser::{Error, Serializer};
use serde::ser::{self, Serializer};
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("duration of {} days is too big to be serialized as nanoseconds", .0.num_days())]
DurationTooBig(Duration),
}
pub fn duration_millis_opt<S>(value: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
where
@ -25,9 +31,7 @@ where
{
match value.num_nanoseconds() {
Some(nanos) => serializer.serialize_i64(nanos),
None => Err(Error::custom(
"duration is too big to be serialized as nanoseconds",
)),
None => Err(ser::Error::custom(Error::DurationTooBig(*value))),
}
}
@ -57,3 +61,201 @@ where
None => serializer.serialize_none(),
}
}
#[cfg(test)]
mod tests {
use bitflags::bitflags;
use serde::Serialize;
use serde_test::{assert_ser_tokens, assert_ser_tokens_error, Token};
use super::*;
#[test]
fn ser_duration_millis_opt() {
#[derive(Serialize)]
struct SimpleDuration {
#[serde(serialize_with = "duration_millis_opt")]
value: Option<Duration>,
}
assert_ser_tokens(
&SimpleDuration {
value: Some(Duration::milliseconds(150)),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::Some,
Token::I64(150),
Token::StructEnd,
],
);
assert_ser_tokens(
&SimpleDuration { value: None },
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::None,
Token::StructEnd,
],
);
}
#[test]
fn ser_duration_millis() {
#[derive(Serialize)]
struct SimpleDuration {
#[serde(serialize_with = "duration_millis")]
value: Duration,
}
assert_ser_tokens(
&SimpleDuration {
value: Duration::milliseconds(150),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::I64(150),
Token::StructEnd,
],
);
}
#[test]
fn ser_duration_nanos() {
#[derive(Serialize)]
struct SimpleDuration {
#[serde(serialize_with = "duration_nanos")]
value: Duration,
}
assert_ser_tokens(
&SimpleDuration {
value: Duration::nanoseconds(150),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::I64(150),
Token::StructEnd,
],
);
assert_ser_tokens_error(
&SimpleDuration {
value: Duration::days(365_000_000),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
],
"duration of 365000000 days is too big to be serialized as nanoseconds",
);
}
#[test]
fn ser_bitflags_u8_opt() {
bitflags! {
struct Flags: u8 {
const ONE = 1;
const TWO = 2;
}
}
impl From<Flags> for u8 {
fn from(value: Flags) -> Self {
value.bits
}
}
#[derive(Serialize)]
struct SimpleFlags {
#[serde(serialize_with = "bitflags_u8_opt")]
value: Option<Flags>,
}
assert_ser_tokens(
&SimpleFlags {
value: Some(Flags::ONE | Flags::TWO),
},
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::Some,
Token::U8(3),
Token::StructEnd,
],
);
assert_ser_tokens(
&SimpleFlags { value: None },
&[
Token::Struct {
name: "SimpleFlags",
len: 1,
},
Token::Str("value"),
Token::None,
Token::StructEnd,
],
);
}
#[test]
fn ser_rgba8_inverse_opt() {
#[derive(Serialize)]
struct SimpleDuration {
#[serde(serialize_with = "rgba8_inverse_opt")]
value: Option<RGBA8>,
}
assert_ser_tokens(
&SimpleDuration {
value: Some(RGBA8::new(1, 2, 3, 4)),
},
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::Some,
Token::U32(0x04030201),
Token::StructEnd,
],
);
assert_ser_tokens(
&SimpleDuration { value: None },
&[
Token::Struct {
name: "SimpleDuration",
len: 1,
},
Token::Str("value"),
Token::None,
Token::StructEnd,
],
);
}
}

View file

@ -44,11 +44,11 @@ where
}
}
pub fn rgba8_inverse<'de, D>(deserializer: D) -> Result<Option<RGBA8>, D::Error>
pub fn rgba8_inverse_opt<'de, D>(deserializer: D) -> Result<Option<RGBA8>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_u32(Rgba8InverseOptVisitor)
deserializer.deserialize_option(Rgba8InverseOptVisitor)
}
struct Rgba8InverseOptVisitor;
@ -60,6 +60,16 @@ impl<'de> Visitor<'de> for Rgba8InverseOptVisitor {
formatter.write_str("a RGBA color value encoded as integer in inverse order (ABGR)")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
match u32::try_from(v) {
Ok(v) => self.visit_u32(v),
Err(e) => Err(Error::custom(e)),
}
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: Error,
@ -72,16 +82,6 @@ impl<'de> Visitor<'de> for Rgba8InverseOptVisitor {
)))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
match u32::try_from(v) {
Ok(v) => self.visit_u32(v),
Err(e) => Err(Error::custom(e)),
}
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
@ -110,7 +110,7 @@ impl<'de> Visitor<'de> for Rgba8InverseOptVisitor {
#[cfg(test)]
mod tests {
use serde::Deserialize;
use serde_json::json;
use serde_test::{assert_de_tokens, assert_de_tokens_error, Token};
use super::*;
@ -122,10 +122,128 @@ mod tests {
value: Vec<String>,
}
let input = json! {{ "value": "a,b,c" }};
let expect = SimpleList {
value: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()],
};
assert_eq!(expect, serde_json::from_value(input).unwrap());
assert_de_tokens(
&SimpleList {
value: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()],
},
&[
Token::Struct {
name: "SimpleList",
len: 1,
},
Token::Str("value"),
Token::Str("a,b,c"),
Token::StructEnd,
],
);
}
#[test]
fn deser_rgba8_inverse_opt() {
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct SimpleColor {
#[serde(deserialize_with = "rgba8_inverse_opt")]
value: Option<RGBA8>,
}
assert_de_tokens(
&SimpleColor {
value: Some(RGBA8::new(1, 2, 3, 4)),
},
&[
Token::Struct {
name: "SimpleColor",
len: 1,
},
Token::Str("value"),
Token::I64(0x04030201),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleColor>(
&[
Token::Struct {
name: "SimpleColor",
len: 1,
},
Token::Str("value"),
Token::I64(i64::MIN),
Token::StructEnd,
],
"out of range integral type conversion attempted",
);
assert_de_tokens(
&SimpleColor {
value: Some(RGBA8::new(1, 2, 3, 4)),
},
&[
Token::Struct {
name: "SimpleColor",
len: 1,
},
Token::Str("value"),
Token::U32(0x04030201),
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleColor {
value: Some(RGBA8::new(1, 2, 3, 4)),
},
&[
Token::Struct {
name: "SimpleColor",
len: 1,
},
Token::Str("value"),
Token::U64(0x04030201),
Token::StructEnd,
],
);
assert_de_tokens_error::<SimpleColor>(
&[
Token::Struct {
name: "SimpleColor",
len: 1,
},
Token::Str("value"),
Token::U64(u64::MAX),
Token::StructEnd,
],
"out of range integral type conversion attempted",
);
assert_de_tokens(
&SimpleColor { value: None },
&[
Token::Struct {
name: "SimpleColor",
len: 1,
},
Token::Str("value"),
Token::None,
Token::StructEnd,
],
);
assert_de_tokens(
&SimpleColor {
value: Some(RGBA8::new(1, 2, 3, 4)),
},
&[
Token::Struct {
name: "SimpleColor",
len: 1,
},
Token::Str("value"),
Token::Some,
Token::U32(0x04030201),
Token::StructEnd,
],
);
}
}

View file

@ -295,10 +295,10 @@ pub struct TextFreetype2Properties {
/// Source name.
pub source: String,
/// Gradient top color.
#[serde(default, deserialize_with = "de::rgba8_inverse")]
#[serde(default, deserialize_with = "de::rgba8_inverse_opt")]
pub color1: Option<RGBA8>,
/// Gradient bottom color.
#[serde(default, deserialize_with = "de::rgba8_inverse")]
#[serde(default, deserialize_with = "de::rgba8_inverse_opt")]
pub color2: Option<RGBA8>,
/// Custom width (0 to disable).
pub custom_width: Option<u32>,
@ -540,10 +540,10 @@ pub struct StreamingStatus {
/// Current recording status.
pub recording: bool,
/// Time elapsed since streaming started (only present if currently streaming).
#[serde(deserialize_with = "crate::de::duration")]
#[serde(deserialize_with = "crate::de::duration_opt")]
pub stream_timecode: Option<Duration>,
/// Time elapsed since recording started (only present if currently recording).
#[serde(deserialize_with = "crate::de::duration")]
#[serde(deserialize_with = "crate::de::duration_opt")]
pub rec_timecode: Option<Duration>,
/// Always false. Retrocompatibility with OBSRemote.
#[serde(default)]