use powerio_core::{Diagnostic, DiagnosticInfo, DiagnosticSeverity, render_diagnostics};
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Diagnostics(Vec<Diagnostic>);
#[allow(
dead_code,
reason = "the collector copies stay identical across crates"
)]
impl Diagnostics {
#[must_use]
pub(crate) fn new() -> Self {
Self(Vec::new())
}
pub(crate) fn push(&mut self, info: &'static DiagnosticInfo, message: impl Into<String>) {
self.0.push(Diagnostic::of(info, message));
}
pub(crate) fn push_at(
&mut self,
info: &'static DiagnosticInfo,
severity: DiagnosticSeverity,
message: impl Into<String>,
) {
self.0
.push(Diagnostic::of(info, message).with_severity(severity));
}
pub(crate) fn record(&mut self, diagnostic: Diagnostic) {
self.0.push(diagnostic);
}
pub(crate) fn absorb(&mut self, other: impl IntoIterator<Item = Diagnostic>) {
self.0.extend(other);
}
pub(crate) fn prepend(&mut self, other: impl IntoIterator<Item = Diagnostic>) {
let mut front: Vec<_> = other.into_iter().collect();
front.append(&mut self.0);
self.0 = front;
}
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub(crate) fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub(crate) fn records(&self) -> &[Diagnostic] {
&self.0
}
#[must_use]
pub(crate) fn into_records(self) -> Vec<Diagnostic> {
self.0
}
#[must_use]
pub(crate) fn lines(&self) -> Vec<String> {
render_diagnostics(&self.0)
}
#[must_use]
pub(crate) fn worst_severity(&self) -> Option<DiagnosticSeverity> {
self.0.iter().map(Diagnostic::severity).max()
}
}
impl From<Vec<Diagnostic>> for Diagnostics {
fn from(records: Vec<Diagnostic>) -> Self {
Self(records)
}
}
impl From<Diagnostics> for Vec<Diagnostic> {
fn from(diagnostics: Diagnostics) -> Self {
diagnostics.0
}
}
impl IntoIterator for Diagnostics {
type Item = Diagnostic;
type IntoIter = std::vec::IntoIter<Diagnostic>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
const DROPPED: DiagnosticInfo = DiagnosticInfo::new(
"EMIT.PSSE.FIELD_DROPPED",
DiagnosticSeverity::Warning,
"a field with no PSS/E record was dropped",
);
const REFUSED: DiagnosticInfo = DiagnosticInfo::new(
"READ.DSS.INCLUDE_REFUSED",
DiagnosticSeverity::Error,
"an include escaping the case directory was refused",
);
#[test]
fn a_site_takes_the_registered_severity_unless_it_names_one() {
let mut d = Diagnostics::new();
d.push(&DROPPED, "gencost dropped");
d.push_at(&DROPPED, DiagnosticSeverity::Remark, "areas dropped");
assert_eq!(d.records()[0].severity(), DiagnosticSeverity::Warning);
assert_eq!(d.records()[1].severity(), DiagnosticSeverity::Remark);
assert_eq!(d.worst_severity(), Some(DiagnosticSeverity::Warning));
}
#[test]
fn the_text_channel_is_rendered_from_the_records() {
let mut d = Diagnostics::new();
d.push(&DROPPED, "gencost dropped");
d.push(&REFUSED, "../shared.dss escapes the case directory");
assert_eq!(
d.lines(),
[
"EMIT.PSSE.FIELD_DROPPED: gencost dropped",
"READ.DSS.INCLUDE_REFUSED: ../shared.dss escapes the case directory",
]
);
}
#[test]
fn the_read_side_goes_ahead_of_the_write_side() {
let mut write = Diagnostics::new();
write.push(&DROPPED, "write");
let mut read = Diagnostics::new();
read.push(&REFUSED, "read");
write.prepend(read);
assert_eq!(
write.lines(),
[
"READ.DSS.INCLUDE_REFUSED: read",
"EMIT.PSSE.FIELD_DROPPED: write",
]
);
assert_eq!(write.len(), 2);
assert!(!write.is_empty());
}
}