haz-domain 0.2.0

Shared domain types for the haz task runner (identifiers, paths, errors).
Documentation
//! Concurrency-cap workspace settings.
//!
//! Carries the workspace-wide default parallelism level and the
//! per-tag caps consulted by the executor (`EXEC-004`,
//! `EXEC-005`).

use std::collections::BTreeMap;
use std::num::NonZeroUsize;

use crate::name::TagName;

/// Workspace-wide default parallelism.
///
/// Either the literal "auto" mode (the executor maps to the host
/// logical-CPU count at runtime per `EXEC-004`) or an explicit
/// fixed cap of at least one concurrent task.
///
/// `Default::default()` returns [`ConcurrencyDefault::Auto`],
/// matching the `CFG-028` default.
///
/// # Examples
///
/// ```
/// use std::num::NonZeroUsize;
/// use haz_domain::settings::concurrency::ConcurrencyDefault;
///
/// assert_eq!(ConcurrencyDefault::default(), ConcurrencyDefault::Auto);
///
/// let fixed = ConcurrencyDefault::Fixed(NonZeroUsize::new(4).unwrap());
/// assert_ne!(fixed, ConcurrencyDefault::Auto);
/// ```
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum ConcurrencyDefault {
    /// Host-CPU-derived cap (resolved by the executor at runtime).
    #[default]
    Auto,
    /// Explicit cap, at least one concurrent task.
    Fixed(NonZeroUsize),
}

/// Workspace-wide concurrency settings.
///
/// Carries the workspace default (`EXEC-004`) plus an optional
/// per-tag cap table (`EXEC-005`).
///
/// `Default::default()` yields the `CFG-028` / `CFG-029` defaults:
/// [`ConcurrencyDefault::Auto`] with an empty per-tag table.
///
/// # Examples
///
/// ```
/// use std::collections::BTreeMap;
/// use std::num::NonZeroUsize;
/// use std::str::FromStr;
/// use haz_domain::name::TagName;
/// use haz_domain::settings::concurrency::{
///     ConcurrencyDefault, ConcurrencySettings,
/// };
///
/// let cs = ConcurrencySettings::default();
/// assert_eq!(cs.default, ConcurrencyDefault::Auto);
/// assert!(cs.tags.is_empty());
///
/// let mut tags = BTreeMap::new();
/// tags.insert(
///     TagName::from_str("native").unwrap(),
///     NonZeroUsize::new(2).unwrap(),
/// );
/// let custom = ConcurrencySettings {
///     default: ConcurrencyDefault::Fixed(NonZeroUsize::new(8).unwrap()),
///     tags,
/// };
/// assert_eq!(custom.tags.len(), 1);
/// ```
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConcurrencySettings {
    /// Workspace-wide default parallelism.
    pub default: ConcurrencyDefault,
    /// Per-tag caps. Tags absent from the map carry no extra cap
    /// beyond the workspace default.
    pub tags: BTreeMap<TagName, NonZeroUsize>,
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use std::num::NonZeroUsize;
    use std::str::FromStr;

    use crate::name::TagName;
    use crate::settings::concurrency::{ConcurrencyDefault, ConcurrencySettings};

    #[test]
    fn concurrency_default_is_auto() {
        assert_eq!(ConcurrencyDefault::default(), ConcurrencyDefault::Auto);
    }

    #[test]
    fn concurrency_default_fixed_distinct_from_auto() {
        let n = NonZeroUsize::new(1).unwrap();
        assert_ne!(ConcurrencyDefault::Fixed(n), ConcurrencyDefault::Auto);
    }

    #[test]
    fn concurrency_default_fixed_carries_chosen_value() {
        let four = NonZeroUsize::new(4).unwrap();
        match ConcurrencyDefault::Fixed(four) {
            ConcurrencyDefault::Fixed(n) => assert_eq!(n.get(), 4),
            ConcurrencyDefault::Auto => unreachable!(),
        }
    }

    #[test]
    fn concurrency_settings_default_has_auto_and_empty_tags() {
        let cs = ConcurrencySettings::default();
        assert_eq!(cs.default, ConcurrencyDefault::Auto);
        assert!(cs.tags.is_empty());
    }

    #[test]
    fn concurrency_settings_carry_tag_caps() {
        let mut tags = BTreeMap::new();
        tags.insert(
            TagName::from_str("native").unwrap(),
            NonZeroUsize::new(2).unwrap(),
        );
        tags.insert(
            TagName::from_str("io").unwrap(),
            NonZeroUsize::new(16).unwrap(),
        );
        let cs = ConcurrencySettings {
            default: ConcurrencyDefault::Fixed(NonZeroUsize::new(8).unwrap()),
            tags,
        };
        assert_eq!(cs.tags.len(), 2);
        assert_eq!(
            cs.tags
                .get(&TagName::from_str("native").unwrap())
                .map(|n| n.get()),
            Some(2)
        );
    }
}