use itertools::Itertools;
use rrd::{
ops::{
create, fetch, graph,
graph::{elements, props},
update,
},
ConsolidationFn,
};
use std::time;
#[test]
fn tutorial() -> anyhow::Result<()> {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
let tempdir = tempfile::tempdir()?;
let rrd_path = tempdir.path().join("data.rrd");
create::create(
&rrd_path,
time::UNIX_EPOCH + time::Duration::from_secs(920804400),
time::Duration::from_secs(300),
true,
None,
&[],
&[create::DataSource::counter(
&create::DataSourceName::new("speed")?,
600,
None,
None,
)],
&[
create::Archive::new(ConsolidationFn::Avg, 0.5, 1, 24)?,
create::Archive::new(ConsolidationFn::Avg, 0.5, 6, 10)?,
],
)?;
let update_data = [
(920804700_u64, 12345_u64),
(920805000, 12357),
(920805300, 12363),
(920805600, 12363),
(920805900, 12363),
(920806200, 12373),
(920806500, 12383),
(920806800, 12393),
(920807100, 12399),
(920807400, 12405),
(920807700, 12411),
(920808000, 12415),
(920808300, 12420),
(920808600, 12422),
(920808900, 12423),
]
.into_iter()
.map(|(ts, value)| {
(
update::BatchTime::from(time::UNIX_EPOCH + time::Duration::from_secs(ts)),
[update::Datum::from(value)],
)
})
.collect_vec();
for chunk in update_data.chunks(3) {
update::update_all(&rrd_path, update::Options::default(), chunk)?;
}
let fetched = fetch::fetch(
&rrd_path,
ConsolidationFn::Avg,
time::UNIX_EPOCH + time::Duration::from_secs(920804400),
time::UNIX_EPOCH + time::Duration::from_secs(920809200),
time::Duration::from_secs(300),
)?;
assert_eq!(
vec!["speed".to_string()],
fetched.ds_names().iter().cloned().collect_vec()
);
let fetched_expected = [
(920804700, f64::NAN),
(920805000, 4.0000000000e-02),
(920805300, 2.0000000000e-02),
(920805600, 0.0000000000e+00),
(920805900, 0.0000000000e+00),
(920806200, 3.3333333333e-02),
(920806500, 3.3333333333e-02),
(920806800, 3.3333333333e-02),
(920807100, 2.0000000000e-02),
(920807400, 2.0000000000e-02),
(920807700, 2.0000000000e-02),
(920808000, 1.3333333333e-02),
(920808300, 1.6666666667e-02),
(920808600, 6.6666666667e-03),
(920808900, 3.3333333333e-03),
(920809200, f64::NAN),
(920809500, f64::NAN),
]
.into_iter()
.map(|(ts, val)| (time::UNIX_EPOCH + time::Duration::from_secs(ts), val))
.collect_vec();
assert_eq!(
fetched_expected.iter().map(|(ts, _val)| *ts).collect_vec(),
fetched.rows().iter().map(|r| r.timestamp()).collect_vec()
);
fetched_expected
.iter()
.map(|(_ts, val)| val)
.zip_eq(fetched.rows().iter().map(|row| {
let values = row.as_slice();
assert_eq!(1, values.len());
values[0]
}))
.enumerate()
.for_each(|(index, (expected, actual))| {
assert!(
(expected.is_nan() && actual.is_nan())
|| (!expected.is_nan()
&& !actual.is_nan()
&& (expected - actual).abs() < 0.000000001),
"index {index}, expected {expected} actual {actual}"
);
});
let graph_start = time::UNIX_EPOCH + time::Duration::from_secs(920804400);
let graph_end = time::UNIX_EPOCH + time::Duration::from_secs(920808000);
let initial_expected_metadata = graph::GraphMetadata {
graph_left: 51,
graph_top: 15,
graph_width: 400,
graph_height: 100,
graph_start,
graph_end,
image_width: 481,
image_height: 141,
value_min: 0.0,
value_max: 0.04,
extra_info: Default::default(),
};
{
let var_name: elements::VarName = "myspeed".try_into()?;
let (png_data, metadata) = graph::graph(
props::ImageFormat::Png,
&props::GraphProps {
time_range: props::TimeRange {
start: Some(graph_start),
end: Some(graph_end),
..Default::default()
},
..Default::default()
},
&[
elements::Def {
var_name: var_name.clone(),
rrd: rrd_path.clone(),
ds_name: "speed".to_string(),
consolidation_fn: ConsolidationFn::Avg,
step: None,
start: None,
end: None,
reduce: None,
}
.into(),
elements::Line {
width: 2.0,
value: var_name,
color: Some(elements::ColorWithLegend {
color: graph::Color {
red: 0xFF,
green: 0x00,
blue: 0x00,
alpha: None,
},
legend: None,
}),
stack: false,
skip_scale: false,
dashes: None,
}
.into(),
],
)?;
assert_eq!(b"\x89PNG\r\n\x1a\n", &png_data[..8]);
assert_eq!(initial_expected_metadata, metadata);
}
{
let myspeed: elements::VarName = "myspeed".try_into()?;
let realspeed = "realspeed".try_into()?;
let (png_data, metadata) = graph::graph(
props::ImageFormat::Png,
&props::GraphProps {
time_range: props::TimeRange {
start: Some(graph_start),
end: Some(graph_end),
..Default::default()
},
..Default::default()
},
&[
elements::Def {
var_name: myspeed.clone(),
rrd: rrd_path.clone(),
ds_name: "speed".to_string(),
consolidation_fn: ConsolidationFn::Avg,
step: None,
start: None,
end: None,
reduce: None,
}
.into(),
elements::CDef {
var_name: realspeed,
rpn: "myspeed,1000,*".to_string(),
}
.into(),
elements::Line {
width: 2.0,
value: myspeed,
color: Some(elements::ColorWithLegend {
color: graph::Color {
red: 0xFF,
green: 0x00,
blue: 0x00,
alpha: None,
},
legend: None,
}),
stack: false,
skip_scale: false,
dashes: None,
}
.into(),
],
)?;
assert_eq!(b"\x89PNG\r\n\x1a\n", &png_data[..8]);
assert_eq!(initial_expected_metadata, metadata);
}
{
let myspeed: elements::VarName = "myspeed".try_into()?;
let good: elements::VarName = "good".try_into()?;
let fast: elements::VarName = "fast".try_into()?;
let (png_data, mut metadata) = graph::graph(
props::ImageFormat::Png,
&props::GraphProps {
time_range: props::TimeRange {
start: Some(graph_start),
end: Some(graph_end),
..Default::default()
},
labels: props::Labels {
vertical_label: Some("km/h".to_string()),
..Default::default()
},
..Default::default()
},
&[
elements::Def {
var_name: myspeed.clone(),
rrd: rrd_path.clone(),
ds_name: "speed".to_string(),
consolidation_fn: ConsolidationFn::Avg,
step: None,
start: None,
end: None,
reduce: None,
}
.into(),
elements::CDef {
var_name: "kmh".try_into()?,
rpn: "myspeed,3600,*".to_string(),
}
.into(),
elements::CDef {
var_name: fast.clone(),
rpn: "kmh,100,GT,kmh,0,IF".to_string(),
}
.into(),
elements::CDef {
var_name: good.clone(),
rpn: "kmh,100,GT,0,kmh,IF".to_string(),
}
.into(),
elements::HRule {
value: 100.0_f64.into(),
color: graph::Color {
red: 0,
green: 0,
blue: 0xFF,
alpha: None,
},
legend: Some("Maximum allowed".into()),
dashes: None,
}
.into(),
elements::Area {
value: good,
color: Some(elements::ColorWithLegend {
color: elements::AreaColor::Color(graph::Color {
red: 0,
green: 0xFF,
blue: 0,
alpha: None,
}),
legend: Some("Good speed".into()),
}),
stack: false,
skip_scale: false,
}
.into(),
elements::Area {
value: fast,
color: Some(elements::ColorWithLegend {
color: elements::AreaColor::Color(graph::Color {
red: 0xFF,
green: 0,
blue: 0,
alpha: None,
}),
legend: Some("Too fast".into()),
}),
stack: false,
skip_scale: false,
}
.into(),
],
)?;
assert_eq!(b"\x89PNG\r\n\x1a\n", &png_data[..8]);
let expected = graph::GraphMetadata {
graph_left: 67,
image_width: 497,
image_height: 155,
value_max: 200.0,
extra_info: [
("legend[0]", " Maximum allowed".into()),
("legend[1]", " Good speed".into()),
("legend[2]", " Too fast".into()),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
..initial_expected_metadata
};
assert!(Coords::from_str(
&metadata
.extra_info
.remove("coords[0]")
.expect("coords[0] is missing")
.into_string()
.expect("coords[0] is not a string")
)
.close_to(&Coords::from_str("16,134,135,148")));
assert!(Coords::from_str(
&metadata
.extra_info
.remove("coords[1]")
.expect("coords[1] is missing")
.into_string()
.expect("coords[1] is not a string")
)
.close_to(&Coords::from_str("231,134,315,148")));
assert!(Coords::from_str(
&metadata
.extra_info
.remove("coords[2]")
.expect("coords[2] is missing")
.into_string()
.expect("coords[2] is not a string")
)
.close_to(&Coords::from_str("411,134,481,148")));
assert_eq!(expected, metadata);
}
Ok(())
}
struct Coord {
x: i32,
y: i32,
}
struct Coords {
top_left: Coord,
bottom_right: Coord,
}
impl Coords {
fn from_str(s: &str) -> Self {
let parts = s.split(',').collect_vec();
assert!(
parts.len() == 4,
"Expected 4 parts in coords string, got {}",
parts.len()
);
Coords {
top_left: Coord {
x: parts[0]
.trim()
.parse()
.expect("Failed to parse x coordinate"),
y: parts[1]
.trim()
.parse()
.expect("Failed to parse y coordinate"),
},
bottom_right: Coord {
x: parts[2]
.trim()
.parse()
.expect("Failed to parse x coordinate"),
y: parts[3]
.trim()
.parse()
.expect("Failed to parse y coordinate"),
},
}
}
fn close_to(&self, other: &Coords) -> bool {
let tolerance = 1;
(self.top_left.x - other.top_left.x).abs() <= tolerance
&& (self.top_left.y - other.top_left.y).abs() <= tolerance
&& (self.bottom_right.x - other.bottom_right.x).abs() <= tolerance
&& (self.bottom_right.y - other.bottom_right.y).abs() <= tolerance
}
}