joularcore 0.2.0

Joular Core is a platform to measure power and energy across all systems, OSes and devices
Documentation
/*
 * Copyright (c) 2025-2026, Adel Noureddine.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the
 * GNU Lesser General Public License v3.0 only (LGPL-3.0-only)
 * which accompanies this distribution, and is available at
 * https://www.gnu.org/licenses/lgpl-3.0.en.html
 *
 * Author : Adel Noureddine
 */

//! The error type returned across the whole crate.

/// A convenience alias for results produced by Joular Core.
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// Everything that can go wrong while measuring power.
///
/// Sensor backends distinguish "the reading is genuinely zero" from "the sensor
/// could not be read": the latter surfaces as [`Error::SensorUnavailable`] or
/// [`Error::PermissionDenied`] rather than a silent `0.0`.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// A power or utilization sensor exists but could not be read right now.
    #[error("{sensor} is unavailable: {detail}")]
    SensorUnavailable {
        /// Which sensor failed, e.g. `"Intel RAPL"` or `"powermetrics"`.
        sensor: &'static str,
        /// Why it failed.
        detail: String,
    },
    /// The sensor requires privileges the current process does not have.
    #[error("{sensor} requires elevated privileges: {detail}")]
    PermissionDenied {
        /// Which sensor requires elevation.
        sensor: &'static str,
        /// What the caller has to do about it.
        detail: String,
    },
    /// The caller supplied something this platform or build cannot honour: a
    /// configuration it does not support, or a ring-buffer location it cannot
    /// use.
    #[error("invalid configuration: {0}")]
    Config(String),
    /// An underlying I/O operation failed.
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
}

/// Constructors, so code implementing [`crate::sensor::Platform`] outside this
/// crate can report failures the same way the built-in backends do.
impl Error {
    /// A sensor that exists but could not be read.
    pub fn sensor(sensor: &'static str, detail: impl Into<String>) -> Self {
        Error::SensorUnavailable {
            sensor,
            detail: detail.into(),
        }
    }

    /// A sensor that needs privileges this process does not have.
    pub fn permission(sensor: &'static str, detail: impl Into<String>) -> Self {
        Error::PermissionDenied {
            sensor,
            detail: detail.into(),
        }
    }

    /// A configuration this platform or build cannot honour.
    pub fn config(detail: impl Into<String>) -> Self {
        Error::Config(detail.into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error as _;

    // These messages reach end users through CLI and GUI front-ends, so the
    // wording is pinned rather than left to whatever the derive happens to emit.
    #[test]
    fn every_variant_renders_its_documented_message() {
        assert_eq!(
            Error::sensor("Intel RAPL", "no powercap domains").to_string(),
            "Intel RAPL is unavailable: no powercap domains"
        );
        assert_eq!(
            Error::permission("powermetrics", "must run as root").to_string(),
            "powermetrics requires elevated privileges: must run as root"
        );
        assert_eq!(
            Error::config("no power file given").to_string(),
            "invalid configuration: no power file given"
        );
        let io = Error::from(std::io::Error::other("disk on fire"));
        assert_eq!(io.to_string(), "I/O error: disk on fire");
    }

    #[test]
    fn only_the_io_variant_carries_a_source() {
        assert!(
            Error::from(std::io::Error::other("boom"))
                .source()
                .is_some()
        );
        assert!(Error::sensor("Intel RAPL", "boom").source().is_none());
    }

    #[test]
    fn an_io_failure_keeps_its_kind_through_the_crate_error() {
        // Sinks return `crate::Result`, so an `io::Error` reaches the caller
        // wrapped rather than converted. Its `ErrorKind` must survive that.
        let wrapped = Error::from(std::io::Error::new(
            std::io::ErrorKind::PermissionDenied,
            "nope",
        ));
        let Error::Io(inner) = &wrapped else {
            panic!("expected Error::Io, got {wrapped:?}");
        };
        assert_eq!(inner.kind(), std::io::ErrorKind::PermissionDenied);
    }
}