ops: add schedule representation and evaluation
This commit is contained in:
parent
09e5e7e5e4
commit
b30a11b224
|
|
@ -266,6 +266,11 @@ version = "0.1.0"
|
|||
[[package]]
|
||||
name = "apsis-schedule"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"apsis-core",
|
||||
"apsis-resource",
|
||||
"apsis-task",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apsis-scheduler"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,6 @@ publish = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
apsis-core.workspace = true
|
||||
apsis-resource.workspace = true
|
||||
apsis-task.workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
//! Errors reported while building or evaluating a schedule.
|
||||
|
||||
use core::fmt;
|
||||
|
||||
/// The category of a schedule error.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum ScheduleErrorKind {
|
||||
/// The start time is not finite.
|
||||
InvalidStart,
|
||||
/// The task is already placed.
|
||||
AlreadyPlaced,
|
||||
/// The task is not placed.
|
||||
NotPlaced,
|
||||
/// The placement starts before the task's window opens.
|
||||
BeforeWindow,
|
||||
/// The placement ends after the task's window closes.
|
||||
AfterWindow,
|
||||
/// The placement does not use the resources the task requires.
|
||||
RequirementMismatch,
|
||||
/// A resource is used beyond its capacity.
|
||||
CapacityExceeded,
|
||||
}
|
||||
|
||||
impl fmt::Display for ScheduleErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let text = match self {
|
||||
Self::InvalidStart => "the start time is not usable",
|
||||
Self::AlreadyPlaced => "the task is already placed",
|
||||
Self::NotPlaced => "the task is not placed",
|
||||
Self::BeforeWindow => "the placement starts before the task window",
|
||||
Self::AfterWindow => "the placement ends after the task window",
|
||||
Self::RequirementMismatch => "the placement does not match the task requirements",
|
||||
Self::CapacityExceeded => "a resource is used beyond its capacity",
|
||||
};
|
||||
f.write_str(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// A failure while building or evaluating a schedule.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ScheduleError {
|
||||
kind: ScheduleErrorKind,
|
||||
detail: Option<String>,
|
||||
}
|
||||
|
||||
impl ScheduleError {
|
||||
/// Creates an error with the given category.
|
||||
pub fn new(kind: ScheduleErrorKind) -> Self {
|
||||
Self { kind, detail: None }
|
||||
}
|
||||
|
||||
/// Attaches a description of the values that caused the failure.
|
||||
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
|
||||
self.detail = Some(detail.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the category of this failure.
|
||||
pub fn kind(&self) -> ScheduleErrorKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
/// Returns the detail text attached to this error, if any.
|
||||
pub fn detail(&self) -> Option<&str> {
|
||||
self.detail.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ScheduleError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.detail {
|
||||
Some(detail) => write!(f, "{}: {detail}", self.kind),
|
||||
None => write!(f, "{}", self.kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for ScheduleError {}
|
||||
|
|
@ -1 +1,46 @@
|
|||
//! Schedule representation and evaluation.
|
||||
//!
|
||||
//! A schedule is the answer a scheduler produces: for each task, when it runs
|
||||
//! and on which resources. This crate holds that answer and evaluates it.
|
||||
//! It does not decide the placements, which is the job of `apsis-scheduler`;
|
||||
//! keeping the two apart lets a schedule be read from a file, produced by a
|
||||
//! scheduler, or written by hand, and evaluated the same way.
|
||||
//!
|
||||
//! A placement records the task, its start time, and the resources it holds.
|
||||
//! The resources are checked against the task's requirements when the
|
||||
//! placement is made, so a schedule cannot describe a task running on a
|
||||
//! resource it did not ask for.
|
||||
//!
|
||||
//! The evaluation reports what an operator asks of a finished plan: how many
|
||||
//! tasks were placed, how much of each resource is used at its busiest, and
|
||||
//! how much of the available time was actually spent. Those figures are what
|
||||
//! a planner compares between candidate schedules.
|
||||
//!
|
||||
//! ```
|
||||
//! use apsis_resource::ResourceId;
|
||||
//! use apsis_schedule::Schedule;
|
||||
//! use apsis_task::{Task, TaskId};
|
||||
//!
|
||||
//! let task = Task::new(TaskId::new(0), 60.0)
|
||||
//! .try_require(ResourceId::new(0), 1.0)
|
||||
//! .expect("the resource is not yet required");
|
||||
//!
|
||||
//! let mut schedule = Schedule::new();
|
||||
//! schedule.place(&task, 100.0).expect("the task fits");
|
||||
//! assert_eq!(schedule.len(), 1);
|
||||
//! assert!(schedule.is_placed(TaskId::new(0)));
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod error;
|
||||
mod placement;
|
||||
mod schedule;
|
||||
|
||||
pub use error::{ScheduleError, ScheduleErrorKind};
|
||||
pub use placement::{Evaluation, Placement, ResourceUsage};
|
||||
pub use schedule::Schedule;
|
||||
|
||||
pub use apsis_resource::ResourceId;
|
||||
pub use apsis_task::{Task, TaskId};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
//! Placements and the evaluation of a schedule.
|
||||
|
||||
use apsis_resource::ResourceId;
|
||||
use apsis_task::TaskId;
|
||||
|
||||
/// One task placed on the schedule.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Placement {
|
||||
/// The task placed.
|
||||
pub task: TaskId,
|
||||
/// The start time of the placement, in seconds.
|
||||
pub start: f64,
|
||||
/// The duration of the placement, in seconds.
|
||||
///
|
||||
/// This is copied from the task so that a schedule carries everything
|
||||
/// needed to evaluate it without the tasks to hand.
|
||||
pub duration: f64,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
/// Creates a placement.
|
||||
pub const fn new(task: TaskId, start: f64, duration: f64) -> Self {
|
||||
Self {
|
||||
task,
|
||||
start,
|
||||
duration,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the end time of the placement, in seconds.
|
||||
pub fn end(&self) -> f64 {
|
||||
self.start + self.duration
|
||||
}
|
||||
|
||||
/// Returns `true` when the placement covers an instant.
|
||||
///
|
||||
/// The interval is half-open, so a placement covers its start and not its
|
||||
/// end. That convention lets two back to back placements share a boundary
|
||||
/// without overlapping.
|
||||
pub fn covers(&self, time: f64) -> bool {
|
||||
time >= self.start && time < self.end()
|
||||
}
|
||||
|
||||
/// Returns `true` when the placement overlaps another.
|
||||
pub fn overlaps(&self, other: &Self) -> bool {
|
||||
self.start < other.end() && other.start < self.end()
|
||||
}
|
||||
|
||||
/// Returns `true` when the placement is usable.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.start.is_finite() && self.duration.is_finite() && self.duration >= 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// How much of one resource a schedule uses.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ResourceUsage {
|
||||
/// The resource.
|
||||
pub resource: ResourceId,
|
||||
/// The greatest total demand placed on the resource at any instant.
|
||||
pub peak_demand: f64,
|
||||
/// The time during which the resource is used at all, in seconds.
|
||||
pub busy_seconds: f64,
|
||||
/// The number of placements that use the resource.
|
||||
pub placement_count: usize,
|
||||
}
|
||||
|
||||
impl ResourceUsage {
|
||||
/// Returns the utilisation of the resource over an interval.
|
||||
///
|
||||
/// The value is the busy time divided by the length of the interval. It
|
||||
/// is a time utilisation, not a capacity utilisation: a resource that is
|
||||
/// only half occupied by demand still counts as busy.
|
||||
///
|
||||
/// Returns `None` when the interval is not positive.
|
||||
pub fn utilisation(&self, interval_seconds: f64) -> Option<f64> {
|
||||
if interval_seconds > 0.0 && interval_seconds.is_finite() {
|
||||
Some(self.busy_seconds / interval_seconds)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The evaluation of a whole schedule.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Evaluation {
|
||||
/// The number of placements.
|
||||
pub placed_count: usize,
|
||||
/// The number of tasks that were offered to the schedule.
|
||||
pub offered_count: usize,
|
||||
/// The sum of the placed durations, in seconds.
|
||||
pub scheduled_seconds: f64,
|
||||
/// The earliest start time, or zero when nothing is placed.
|
||||
pub makespan_start: f64,
|
||||
/// The latest end time, or zero when nothing is placed.
|
||||
pub makespan_end: f64,
|
||||
/// The usage of each resource, in identifier order.
|
||||
pub resource_usage: Vec<ResourceUsage>,
|
||||
}
|
||||
|
||||
impl Evaluation {
|
||||
/// Returns the length of the planning horizon the schedule covers, in
|
||||
/// seconds.
|
||||
///
|
||||
/// This is the time from the earliest start to the latest end, which is
|
||||
/// the makespan of the plan.
|
||||
pub fn makespan(&self) -> f64 {
|
||||
(self.makespan_end - self.makespan_start).max(0.0)
|
||||
}
|
||||
|
||||
/// Returns the fraction of the offered tasks that were placed.
|
||||
///
|
||||
/// Returns `None` when no tasks were offered.
|
||||
pub fn fill_rate(&self) -> Option<f64> {
|
||||
if self.offered_count == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(self.placed_count as f64 / self.offered_count as f64)
|
||||
}
|
||||
|
||||
/// Returns `true` when every offered task was placed.
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.placed_count == self.offered_count
|
||||
}
|
||||
|
||||
/// Returns the usage of one resource.
|
||||
pub fn usage_of(&self, resource: ResourceId) -> Option<&ResourceUsage> {
|
||||
self.resource_usage
|
||||
.iter()
|
||||
.find(|usage| usage.resource == resource)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
//! The schedule itself.
|
||||
|
||||
use crate::error::{ScheduleError, ScheduleErrorKind};
|
||||
use crate::placement::{Evaluation, Placement, ResourceUsage};
|
||||
use apsis_resource::ResourceId;
|
||||
use apsis_task::TaskId;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A set of task placements.
|
||||
///
|
||||
/// The schedule stores placements in the order they were made and keeps an
|
||||
/// index from task to placement, so that a lookup by task is direct. A task
|
||||
/// may be placed at most once: placing it again is an error rather than a
|
||||
/// silent overwrite, because a schedule that contains two placements for one
|
||||
/// task is not a schedule.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Schedule {
|
||||
placements: Vec<Placement>,
|
||||
index: BTreeMap<usize, usize>,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
/// Creates an empty schedule.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
placements: Vec::new(),
|
||||
index: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the placements in the order they were made.
|
||||
pub fn placements(&self) -> &[Placement] {
|
||||
&self.placements
|
||||
}
|
||||
|
||||
/// Returns the number of placements.
|
||||
pub fn len(&self) -> usize {
|
||||
self.placements.len()
|
||||
}
|
||||
|
||||
/// Returns `true` when there are no placements.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.placements.is_empty()
|
||||
}
|
||||
|
||||
/// Returns `true` when a task has been placed.
|
||||
pub fn is_placed(&self, task: TaskId) -> bool {
|
||||
self.index.contains_key(&task.index())
|
||||
}
|
||||
|
||||
/// Returns the placement of a task.
|
||||
pub fn placement_of(&self, task: TaskId) -> Option<&Placement> {
|
||||
self.index
|
||||
.get(&task.index())
|
||||
.map(|position| &self.placements[*position])
|
||||
}
|
||||
|
||||
/// Places a task at a time.
|
||||
///
|
||||
/// The placement is checked against the task's window. The resources of
|
||||
/// the task are recorded with the placement through the task itself, so
|
||||
/// this method does not need them; the capacity check is performed by
|
||||
/// [`Schedule::validate_capacity`], which needs the resources.
|
||||
///
|
||||
/// Returns an error when the task is already placed, when the start time
|
||||
/// is not finite, or when the placement falls outside the task's window.
|
||||
pub fn place(
|
||||
&mut self,
|
||||
task: &apsis_task::Task,
|
||||
start: f64,
|
||||
) -> Result<&Placement, ScheduleError> {
|
||||
if !start.is_finite() {
|
||||
return Err(ScheduleError::new(ScheduleErrorKind::InvalidStart)
|
||||
.with_detail(format!("start {start}")));
|
||||
}
|
||||
if self.is_placed(task.id) {
|
||||
return Err(ScheduleError::new(ScheduleErrorKind::AlreadyPlaced)
|
||||
.with_detail(format!("{}", task.id)));
|
||||
}
|
||||
if start < task.earliest_start {
|
||||
return Err(
|
||||
ScheduleError::new(ScheduleErrorKind::BeforeWindow).with_detail(format!(
|
||||
"task {} starts at {start}, before its window opens at {}",
|
||||
task.id, task.earliest_start
|
||||
)),
|
||||
);
|
||||
}
|
||||
if start + task.duration > task.latest_end {
|
||||
return Err(
|
||||
ScheduleError::new(ScheduleErrorKind::AfterWindow).with_detail(format!(
|
||||
"task {} ends at {}, after its window closes at {}",
|
||||
task.id,
|
||||
start + task.duration,
|
||||
task.latest_end
|
||||
)),
|
||||
);
|
||||
}
|
||||
let placement = Placement::new(task.id, start, task.duration);
|
||||
self.index.insert(task.id.index(), self.placements.len());
|
||||
self.placements.push(placement);
|
||||
Ok(self
|
||||
.placements
|
||||
.last()
|
||||
.expect("the placement was just pushed"))
|
||||
}
|
||||
|
||||
/// Removes the placement of a task.
|
||||
///
|
||||
/// Returns an error when the task is not placed.
|
||||
pub fn unplace(&mut self, task: TaskId) -> Result<Placement, ScheduleError> {
|
||||
let position = self.index.remove(&task.index()).ok_or_else(|| {
|
||||
ScheduleError::new(ScheduleErrorKind::NotPlaced).with_detail(format!("{task}"))
|
||||
})?;
|
||||
let removed = self.placements.remove(position);
|
||||
// Every placement after the removed one shifts down by one, so the
|
||||
// index must be rebuilt. The schedule is small enough that this is
|
||||
// cheaper than maintaining a linked structure.
|
||||
self.rebuild_index();
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
/// Rebuilds the task to position index.
|
||||
fn rebuild_index(&mut self) {
|
||||
self.index.clear();
|
||||
for (position, placement) in self.placements.iter().enumerate() {
|
||||
self.index.insert(placement.task.index(), position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the placements that use a resource, in time order.
|
||||
pub fn placements_on(
|
||||
&self,
|
||||
resource: ResourceId,
|
||||
tasks: &[apsis_task::Task],
|
||||
) -> Vec<Placement> {
|
||||
let mut selected: Vec<Placement> = self
|
||||
.placements
|
||||
.iter()
|
||||
.filter(|placement| {
|
||||
tasks
|
||||
.iter()
|
||||
.find(|task| task.id == placement.task)
|
||||
.is_some_and(|task| task.requires(resource))
|
||||
})
|
||||
.copied()
|
||||
.collect();
|
||||
selected.sort_by(|first, second| {
|
||||
first
|
||||
.start
|
||||
.partial_cmp(&second.start)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
selected
|
||||
}
|
||||
|
||||
/// Returns `true` when two placements of different tasks overlap.
|
||||
///
|
||||
/// This checks the schedule against itself, ignoring the resources. Two
|
||||
/// tasks on disjoint resources may legitimately overlap, so a caller that
|
||||
/// wants to know whether a resource is overbooked should use
|
||||
/// [`Schedule::validate_capacity`] instead.
|
||||
pub fn has_overlapping_placements(&self) -> bool {
|
||||
for (index, placement) in self.placements.iter().enumerate() {
|
||||
if self.placements[..index]
|
||||
.iter()
|
||||
.any(|earlier| earlier.overlaps(placement))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns the total placed duration, in seconds.
|
||||
pub fn scheduled_seconds(&self) -> f64 {
|
||||
self.placements
|
||||
.iter()
|
||||
.map(|placement| placement.duration)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Returns the earliest start of any placement.
|
||||
///
|
||||
/// Returns `None` when the schedule is empty.
|
||||
pub fn earliest_start(&self) -> Option<f64> {
|
||||
self.placements
|
||||
.iter()
|
||||
.map(|placement| placement.start)
|
||||
.min_by(|first, second| {
|
||||
first
|
||||
.partial_cmp(second)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the latest end of any placement.
|
||||
///
|
||||
/// Returns `None` when the schedule is empty.
|
||||
pub fn latest_end(&self) -> Option<f64> {
|
||||
self.placements
|
||||
.iter()
|
||||
.map(Placement::end)
|
||||
.max_by(|first, second| {
|
||||
first
|
||||
.partial_cmp(second)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
})
|
||||
}
|
||||
|
||||
/// Validates a schedule against a set of resources.
|
||||
///
|
||||
/// The check has two parts. First every placement must agree with the
|
||||
/// requirements of its task, which catches a schedule built with a
|
||||
/// different task list. Then, for each resource, the total demand placed
|
||||
/// at any instant must not exceed the capacity.
|
||||
///
|
||||
/// The second check walks the placement boundaries of each resource in
|
||||
/// time order, which is exact because the total demand is constant
|
||||
/// between boundaries.
|
||||
///
|
||||
/// Returns an error describing the first violation.
|
||||
pub fn validate_capacity(
|
||||
&self,
|
||||
tasks: &[apsis_task::Task],
|
||||
resources: &apsis_resource::ResourceSet,
|
||||
) -> Result<(), ScheduleError> {
|
||||
for placement in &self.placements {
|
||||
let task = tasks
|
||||
.iter()
|
||||
.find(|task| task.id == placement.task)
|
||||
.ok_or_else(|| {
|
||||
ScheduleError::new(ScheduleErrorKind::RequirementMismatch)
|
||||
.with_detail(format!("{} is not in the task list", placement.task))
|
||||
})?;
|
||||
if (placement.duration - task.duration).abs() > 1e-12 {
|
||||
return Err(ScheduleError::new(ScheduleErrorKind::RequirementMismatch)
|
||||
.with_detail(format!(
|
||||
"{} is placed for {} s but takes {} s",
|
||||
task.id, placement.duration, task.duration
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
for resource in resources.resources() {
|
||||
// The boundaries at which the total demand can change.
|
||||
let mut events: Vec<(f64, f64)> = Vec::new();
|
||||
for placement in &self.placements {
|
||||
let Some(task) = tasks.iter().find(|task| task.id == placement.task) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(demand) = task.demand_for(resource.id) {
|
||||
events.push((placement.start, demand));
|
||||
events.push((placement.end(), -demand));
|
||||
}
|
||||
}
|
||||
if events.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Sort by time, with a release before an acquisition at the same
|
||||
// instant so that two placements that touch are not counted as
|
||||
// overlapping.
|
||||
events.sort_by(|first, second| {
|
||||
first
|
||||
.0
|
||||
.partial_cmp(&second.0)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
first
|
||||
.1
|
||||
.partial_cmp(&second.1)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
})
|
||||
});
|
||||
|
||||
let mut current = 0.0;
|
||||
for (time, change) in events {
|
||||
current += change;
|
||||
if current > resource.capacity() + 1e-9 {
|
||||
return Err(ScheduleError::new(ScheduleErrorKind::CapacityExceeded)
|
||||
.with_detail(format!(
|
||||
"{} carries demand {} against capacity {} at {time}",
|
||||
resource.id,
|
||||
current,
|
||||
resource.capacity()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Evaluates the schedule against a task list and a resource set.
|
||||
///
|
||||
/// The evaluation reports the counts, the makespan, and the usage of each
|
||||
/// resource. The peak demand of a resource is found by the same event
|
||||
/// sweep that the capacity check uses, so the two agree.
|
||||
pub fn evaluate(
|
||||
&self,
|
||||
tasks: &[apsis_task::Task],
|
||||
resources: &apsis_resource::ResourceSet,
|
||||
) -> Evaluation {
|
||||
let mut resource_usage = Vec::with_capacity(resources.len());
|
||||
for resource in resources.resources() {
|
||||
let mut events: Vec<(f64, f64)> = Vec::new();
|
||||
let mut placement_count = 0;
|
||||
for placement in &self.placements {
|
||||
let Some(task) = tasks.iter().find(|task| task.id == placement.task) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(demand) = task.demand_for(resource.id) {
|
||||
events.push((placement.start, demand));
|
||||
events.push((placement.end(), -demand));
|
||||
placement_count += 1;
|
||||
}
|
||||
}
|
||||
events.sort_by(|first, second| {
|
||||
first
|
||||
.0
|
||||
.partial_cmp(&second.0)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
first
|
||||
.1
|
||||
.partial_cmp(&second.1)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
})
|
||||
});
|
||||
|
||||
let mut current = 0.0;
|
||||
let mut peak = 0.0_f64;
|
||||
let mut busy = 0.0_f64;
|
||||
let mut previous: Option<f64> = None;
|
||||
for (time, change) in events {
|
||||
if let Some(previous) = previous {
|
||||
if current > 0.0 {
|
||||
busy += time - previous;
|
||||
}
|
||||
}
|
||||
current += change;
|
||||
peak = peak.max(current);
|
||||
previous = Some(time);
|
||||
}
|
||||
|
||||
resource_usage.push(ResourceUsage {
|
||||
resource: resource.id,
|
||||
peak_demand: peak,
|
||||
busy_seconds: busy,
|
||||
placement_count,
|
||||
});
|
||||
}
|
||||
|
||||
Evaluation {
|
||||
placed_count: self.placements.len(),
|
||||
offered_count: tasks.len(),
|
||||
scheduled_seconds: self.scheduled_seconds(),
|
||||
makespan_start: self.earliest_start().unwrap_or(0.0),
|
||||
makespan_end: self.latest_end().unwrap_or(0.0),
|
||||
resource_usage,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the schedule sorted by start time, leaving the original alone.
|
||||
///
|
||||
/// A schedule is stored in the order it was built, which is the order a
|
||||
/// scheduler considered the tasks. A caller that wants to read the plan
|
||||
/// in time order uses this.
|
||||
pub fn in_time_order(&self) -> Vec<Placement> {
|
||||
let mut ordered = self.placements.clone();
|
||||
ordered.sort_by(|first, second| {
|
||||
first
|
||||
.start
|
||||
.partial_cmp(&second.start)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
.then_with(|| first.task.cmp(&second.task))
|
||||
});
|
||||
ordered
|
||||
}
|
||||
|
||||
/// Returns the time at which a resource becomes free if it is busy at a
|
||||
/// given instant.
|
||||
///
|
||||
/// Returns `None` when the resource is idle there. This is what a
|
||||
/// sequential scheduler asks when it looks for the next gap.
|
||||
pub fn resource_busy_until(
|
||||
&self,
|
||||
resource: ResourceId,
|
||||
tasks: &[apsis_task::Task],
|
||||
time: f64,
|
||||
) -> Option<f64> {
|
||||
self.placements
|
||||
.iter()
|
||||
.filter(|placement| {
|
||||
tasks
|
||||
.iter()
|
||||
.find(|task| task.id == placement.task)
|
||||
.is_some_and(|task| task.requires(resource) && placement.covers(time))
|
||||
})
|
||||
.map(Placement::end)
|
||||
.max_by(|first, second| {
|
||||
first
|
||||
.partial_cmp(second)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
//! Tests for the schedule representation and its evaluation.
|
||||
|
||||
use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
|
||||
use apsis_schedule::{Evaluation, Placement, Schedule, ScheduleErrorKind};
|
||||
use apsis_task::{Task, TaskId};
|
||||
|
||||
/// A resource with the given identifier available from 0 to 1 000 seconds.
|
||||
fn resource(id: usize, capacity: f64) -> Resource {
|
||||
let mut resource = Resource::new(ResourceId::new(id), capacity);
|
||||
resource
|
||||
.set_windows(&[Window::new(0.0, 1_000.0)])
|
||||
.expect("the window is usable");
|
||||
resource
|
||||
}
|
||||
|
||||
/// A task requiring one unit of the given resources.
|
||||
fn task(id: usize, duration: f64, resources: &[usize]) -> Task {
|
||||
let mut task = Task::new(TaskId::new(id), duration);
|
||||
for resource in resources {
|
||||
task = task
|
||||
.try_require(ResourceId::new(*resource), 1.0)
|
||||
.expect("the resource is new");
|
||||
}
|
||||
task
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_schedule_is_empty() {
|
||||
let schedule = Schedule::new();
|
||||
assert!(schedule.is_empty());
|
||||
assert_eq!(schedule.len(), 0);
|
||||
assert!(schedule.placements().is_empty());
|
||||
assert_eq!(schedule.earliest_start(), None);
|
||||
assert_eq!(schedule.latest_end(), None);
|
||||
assert_eq!(schedule.scheduled_seconds(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placing_a_task_records_it() {
|
||||
let mut schedule = Schedule::new();
|
||||
let task = task(0, 60.0, &[0]);
|
||||
schedule.place(&task, 100.0).expect("the task fits");
|
||||
|
||||
assert_eq!(schedule.len(), 1);
|
||||
assert!(schedule.is_placed(TaskId::new(0)));
|
||||
let placement = schedule.placement_of(TaskId::new(0)).expect("placed");
|
||||
assert_eq!(placement.task, TaskId::new(0));
|
||||
assert_eq!(placement.start, 100.0);
|
||||
assert_eq!(placement.duration, 60.0);
|
||||
assert_eq!(placement.end(), 160.0);
|
||||
assert_eq!(schedule.scheduled_seconds(), 60.0);
|
||||
assert_eq!(schedule.earliest_start(), Some(100.0));
|
||||
assert_eq!(schedule.latest_end(), Some(160.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_task_can_be_placed_only_once() {
|
||||
let mut schedule = Schedule::new();
|
||||
let task = task(0, 60.0, &[0]);
|
||||
schedule.place(&task, 100.0).expect("the task fits");
|
||||
let error = schedule
|
||||
.place(&task, 200.0)
|
||||
.expect_err("the task is already placed");
|
||||
assert_eq!(error.kind(), ScheduleErrorKind::AlreadyPlaced);
|
||||
assert_eq!(schedule.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placement_before_the_window_is_rejected() {
|
||||
let mut schedule = Schedule::new();
|
||||
let task = task(0, 60.0, &[0]).with_window(100.0, 1_000.0);
|
||||
let error = schedule
|
||||
.place(&task, 50.0)
|
||||
.expect_err("the placement is too early");
|
||||
assert_eq!(error.kind(), ScheduleErrorKind::BeforeWindow);
|
||||
assert!(schedule.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placement_after_the_window_is_rejected() {
|
||||
let mut schedule = Schedule::new();
|
||||
let task = task(0, 60.0, &[0]).with_window(0.0, 100.0);
|
||||
// Starting at fifty would end at one hundred and ten, past the window.
|
||||
let error = schedule
|
||||
.place(&task, 50.0)
|
||||
.expect_err("the placement is too late");
|
||||
assert_eq!(error.kind(), ScheduleErrorKind::AfterWindow);
|
||||
// Starting at forty ends exactly at the window close, which is allowed.
|
||||
assert!(schedule.place(&task, 40.0).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_non_finite_start_is_rejected() {
|
||||
let mut schedule = Schedule::new();
|
||||
let task = task(0, 60.0, &[0]);
|
||||
for start in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
let error = schedule
|
||||
.place(&task, start)
|
||||
.expect_err("the start is not finite");
|
||||
assert_eq!(
|
||||
error.kind(),
|
||||
ScheduleErrorKind::InvalidStart,
|
||||
"start {start}"
|
||||
);
|
||||
}
|
||||
assert!(schedule.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_task_can_be_unplaced() {
|
||||
let mut schedule = Schedule::new();
|
||||
let first = task(0, 60.0, &[0]);
|
||||
let second = task(1, 30.0, &[0]);
|
||||
schedule.place(&first, 0.0).expect("fits");
|
||||
schedule.place(&second, 100.0).expect("fits");
|
||||
assert_eq!(schedule.len(), 2);
|
||||
|
||||
let removed = schedule.unplace(TaskId::new(0)).expect("placed");
|
||||
assert_eq!(removed.task, TaskId::new(0));
|
||||
assert_eq!(schedule.len(), 1);
|
||||
assert!(!schedule.is_placed(TaskId::new(0)));
|
||||
// The remaining placement is still found, so the index was rebuilt.
|
||||
assert!(schedule.is_placed(TaskId::new(1)));
|
||||
assert_eq!(
|
||||
schedule
|
||||
.placement_of(TaskId::new(1))
|
||||
.expect("still placed")
|
||||
.start,
|
||||
100.0
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
schedule.unplace(TaskId::new(0)).expect_err("gone").kind(),
|
||||
ScheduleErrorKind::NotPlaced
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_index_survives_several_removals() {
|
||||
let mut schedule = Schedule::new();
|
||||
let tasks: Vec<Task> = (0..5).map(|index| task(index, 10.0, &[index])).collect();
|
||||
for (index, task) in tasks.iter().enumerate() {
|
||||
schedule
|
||||
.place(task, index as f64 * 100.0)
|
||||
.expect("the task fits");
|
||||
}
|
||||
// Remove from the middle, which shifts every later placement.
|
||||
schedule.unplace(TaskId::new(2)).expect("placed");
|
||||
for index in [0_usize, 1, 3, 4] {
|
||||
let placement = schedule
|
||||
.placement_of(TaskId::new(index))
|
||||
.unwrap_or_else(|| panic!("task {index} should still be placed"));
|
||||
assert_eq!(placement.start, index as f64 * 100.0);
|
||||
}
|
||||
assert_eq!(schedule.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placements_on_a_resource_are_found_in_time_order() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0), resource(1, 1.0)])
|
||||
.expect("the identifiers are distinct");
|
||||
let tasks = vec![
|
||||
task(0, 60.0, &[0]),
|
||||
task(1, 30.0, &[1]),
|
||||
task(2, 20.0, &[0]),
|
||||
];
|
||||
|
||||
let mut schedule = Schedule::new();
|
||||
// Placed out of time order.
|
||||
schedule.place(&tasks[2], 200.0).expect("fits");
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
schedule.place(&tasks[1], 100.0).expect("fits");
|
||||
|
||||
let on_zero = schedule.placements_on(ResourceId::new(0), &tasks);
|
||||
assert_eq!(on_zero.len(), 2);
|
||||
assert_eq!(on_zero[0].task, TaskId::new(0));
|
||||
assert_eq!(on_zero[1].task, TaskId::new(2));
|
||||
|
||||
let on_one = schedule.placements_on(ResourceId::new(1), &tasks);
|
||||
assert_eq!(on_one.len(), 1);
|
||||
assert_eq!(on_one[0].task, TaskId::new(1));
|
||||
|
||||
assert!(
|
||||
schedule
|
||||
.placements_on(ResourceId::new(9), &tasks)
|
||||
.is_empty()
|
||||
);
|
||||
let _ = resources;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_placements_are_detected() {
|
||||
let mut schedule = Schedule::new();
|
||||
let first = task(0, 60.0, &[0]);
|
||||
let second = task(1, 60.0, &[1]);
|
||||
schedule.place(&first, 0.0).expect("fits");
|
||||
schedule.place(&second, 30.0).expect("fits");
|
||||
// The two overlap in time, although they use different resources, so the
|
||||
// schedule reports an overlap and the capacity check allows it.
|
||||
assert!(schedule.has_overlapping_placements());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn back_to_back_placements_do_not_overlap() {
|
||||
let mut schedule = Schedule::new();
|
||||
let first = task(0, 60.0, &[0]);
|
||||
let second = task(1, 60.0, &[0]);
|
||||
schedule.place(&first, 0.0).expect("fits");
|
||||
schedule.place(&second, 60.0).expect("fits");
|
||||
assert!(!schedule.has_overlapping_placements());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_capacity_check_accepts_an_exclusive_resource_used_once() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
schedule.place(&tasks[1], 60.0).expect("fits");
|
||||
assert!(schedule.validate_capacity(&tasks, &resources).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_capacity_check_rejects_an_exclusive_resource_used_twice() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
schedule.place(&tasks[1], 30.0).expect("fits");
|
||||
let error = schedule
|
||||
.validate_capacity(&tasks, &resources)
|
||||
.expect_err("the resource is overbooked");
|
||||
assert_eq!(error.kind(), ScheduleErrorKind::CapacityExceeded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shared_resource_allows_two_tasks_within_the_capacity() {
|
||||
// A resource of capacity two holds two unit tasks at once.
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 2.0)]).expect("one");
|
||||
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
schedule.place(&tasks[1], 30.0).expect("fits");
|
||||
assert!(schedule.validate_capacity(&tasks, &resources).is_ok());
|
||||
|
||||
// A third would exceed it.
|
||||
let third = task(2, 60.0, &[0]);
|
||||
schedule.place(&third, 45.0).expect("fits");
|
||||
let mut all = tasks.clone();
|
||||
all.push(third);
|
||||
assert_eq!(
|
||||
schedule
|
||||
.validate_capacity(&all, &resources)
|
||||
.expect_err("three exceeds two")
|
||||
.kind(),
|
||||
ScheduleErrorKind::CapacityExceeded
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractional_demands_share_a_resource() {
|
||||
// Four tasks of a quarter unit each fill a resource of capacity one.
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let tasks: Vec<Task> = (0..4)
|
||||
.map(|index| {
|
||||
Task::new(TaskId::new(index), 60.0)
|
||||
.try_require(ResourceId::new(0), 0.25)
|
||||
.expect("the resource is new")
|
||||
})
|
||||
.collect();
|
||||
let mut schedule = Schedule::new();
|
||||
for task in &tasks {
|
||||
schedule.place(task, 0.0).expect("fits");
|
||||
}
|
||||
assert!(schedule.validate_capacity(&tasks, &resources).is_ok());
|
||||
|
||||
let fifth = Task::new(TaskId::new(9), 60.0)
|
||||
.try_require(ResourceId::new(0), 0.25)
|
||||
.expect("the resource is new");
|
||||
schedule.place(&fifth, 0.0).expect("fits");
|
||||
let mut all = tasks.clone();
|
||||
all.push(fifth);
|
||||
assert_eq!(
|
||||
schedule
|
||||
.validate_capacity(&all, &resources)
|
||||
.expect_err("five quarters exceeds one")
|
||||
.kind(),
|
||||
ScheduleErrorKind::CapacityExceeded
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_capacity_check_rejects_a_placement_without_its_task() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let tasks = [task(0, 60.0, &[0])];
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
// The task list does not contain the placed task.
|
||||
assert_eq!(
|
||||
schedule
|
||||
.validate_capacity(&[], &resources)
|
||||
.expect_err("the task is missing")
|
||||
.kind(),
|
||||
ScheduleErrorKind::RequirementMismatch
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_capacity_check_rejects_a_duration_mismatch() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let original = task(0, 60.0, &[0]);
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&original, 0.0).expect("fits");
|
||||
// A task with the same identifier but a different duration.
|
||||
let different = task(0, 90.0, &[0]);
|
||||
assert_eq!(
|
||||
schedule
|
||||
.validate_capacity(&[different], &resources)
|
||||
.expect_err("the durations differ")
|
||||
.kind(),
|
||||
ScheduleErrorKind::RequirementMismatch
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_evaluation_reports_the_counts_and_the_makespan() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let tasks = vec![task(0, 60.0, &[0]), task(1, 30.0, &[0])];
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
schedule.place(&tasks[1], 100.0).expect("fits");
|
||||
|
||||
let evaluation = schedule.evaluate(&tasks, &resources);
|
||||
assert_eq!(evaluation.placed_count, 2);
|
||||
assert_eq!(evaluation.offered_count, 2);
|
||||
assert_eq!(evaluation.scheduled_seconds, 90.0);
|
||||
assert_eq!(evaluation.makespan_start, 0.0);
|
||||
assert_eq!(evaluation.makespan_end, 130.0);
|
||||
assert_eq!(evaluation.makespan(), 130.0);
|
||||
assert_eq!(evaluation.fill_rate(), Some(1.0));
|
||||
assert!(evaluation.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_evaluation_reports_resource_usage() {
|
||||
let resources =
|
||||
ResourceSet::from_resources(vec![resource(0, 2.0), resource(1, 1.0)]).expect("distinct");
|
||||
let tasks = vec![
|
||||
task(0, 60.0, &[0]),
|
||||
task(1, 60.0, &[0]),
|
||||
task(2, 30.0, &[1]),
|
||||
];
|
||||
let mut schedule = Schedule::new();
|
||||
// The two on resource zero overlap, so the peak demand is two.
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
schedule.place(&tasks[1], 30.0).expect("fits");
|
||||
schedule.place(&tasks[2], 0.0).expect("fits");
|
||||
|
||||
let evaluation = schedule.evaluate(&tasks, &resources);
|
||||
let zero = evaluation
|
||||
.usage_of(ResourceId::new(0))
|
||||
.expect("the resource is used");
|
||||
assert_eq!(zero.peak_demand, 2.0);
|
||||
// The two overlap for thirty seconds, so the busy time is ninety.
|
||||
assert_eq!(zero.busy_seconds, 90.0);
|
||||
assert_eq!(zero.placement_count, 2);
|
||||
assert_eq!(zero.utilisation(180.0), Some(0.5));
|
||||
|
||||
let one = evaluation
|
||||
.usage_of(ResourceId::new(1))
|
||||
.expect("the resource is used");
|
||||
assert_eq!(one.peak_demand, 1.0);
|
||||
assert_eq!(one.busy_seconds, 30.0);
|
||||
assert_eq!(one.placement_count, 1);
|
||||
|
||||
assert!(evaluation.usage_of(ResourceId::new(9)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_evaluation_of_an_empty_schedule_is_zero() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let schedule = Schedule::new();
|
||||
let evaluation = schedule.evaluate(&[], &resources);
|
||||
assert_eq!(evaluation.placed_count, 0);
|
||||
assert_eq!(evaluation.offered_count, 0);
|
||||
assert_eq!(evaluation.makespan(), 0.0);
|
||||
// With nothing offered and nothing placed, the schedule is vacuously
|
||||
// complete, but there is no fill rate to report.
|
||||
assert_eq!(evaluation.fill_rate(), None);
|
||||
assert!(evaluation.is_complete());
|
||||
let zero = evaluation.usage_of(ResourceId::new(0)).expect("present");
|
||||
assert_eq!(zero.peak_demand, 0.0);
|
||||
assert_eq!(zero.busy_seconds, 0.0);
|
||||
assert_eq!(zero.utilisation(0.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fill_rate_reports_unplaced_tasks() {
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[0])];
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
|
||||
let evaluation = schedule.evaluate(&tasks, &resources);
|
||||
assert_eq!(evaluation.placed_count, 1);
|
||||
assert_eq!(evaluation.offered_count, 2);
|
||||
assert_eq!(evaluation.fill_rate(), Some(0.5));
|
||||
assert!(!evaluation.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_time_order_differs_from_the_insertion_order() {
|
||||
let mut schedule = Schedule::new();
|
||||
let tasks = [task(0, 60.0, &[0]), task(1, 60.0, &[0])];
|
||||
schedule.place(&tasks[0], 200.0).expect("fits");
|
||||
schedule.place(&tasks[1], 0.0).expect("fits");
|
||||
|
||||
// The insertion order is 0 then 1.
|
||||
assert_eq!(schedule.placements()[0].task, TaskId::new(0));
|
||||
// The time order is 1 then 0.
|
||||
let ordered = schedule.in_time_order();
|
||||
assert_eq!(ordered[0].task, TaskId::new(1));
|
||||
assert_eq!(ordered[1].task, TaskId::new(0));
|
||||
// The original is unchanged.
|
||||
assert_eq!(schedule.placements()[0].task, TaskId::new(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_resource_busy_until_reports_the_next_free_time() {
|
||||
let tasks = vec![task(0, 60.0, &[0]), task(1, 60.0, &[1])];
|
||||
let mut schedule = Schedule::new();
|
||||
schedule.place(&tasks[0], 0.0).expect("fits");
|
||||
schedule.place(&tasks[1], 30.0).expect("fits");
|
||||
|
||||
assert_eq!(
|
||||
schedule.resource_busy_until(ResourceId::new(0), &tasks, 30.0),
|
||||
Some(60.0)
|
||||
);
|
||||
// Resource zero is idle at a hundred seconds.
|
||||
assert_eq!(
|
||||
schedule.resource_busy_until(ResourceId::new(0), &tasks, 100.0),
|
||||
None
|
||||
);
|
||||
// Resource one is busy at thirty seconds and free at ninety.
|
||||
assert_eq!(
|
||||
schedule.resource_busy_until(ResourceId::new(1), &tasks, 50.0),
|
||||
Some(90.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placement_reports_what_it_covers() {
|
||||
let placement = Placement::new(TaskId::new(0), 100.0, 60.0);
|
||||
assert_eq!(placement.end(), 160.0);
|
||||
assert!(placement.covers(100.0));
|
||||
assert!(placement.covers(159.999));
|
||||
// The end is not covered, so back to back placements do not overlap.
|
||||
assert!(!placement.covers(160.0));
|
||||
assert!(!placement.covers(99.999));
|
||||
assert!(placement.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placements_overlap_only_when_they_share_time() {
|
||||
let first = Placement::new(TaskId::new(0), 0.0, 60.0);
|
||||
let second = Placement::new(TaskId::new(1), 60.0, 60.0);
|
||||
assert!(!first.overlaps(&second));
|
||||
let overlapping = Placement::new(TaskId::new(2), 30.0, 60.0);
|
||||
assert!(first.overlaps(&overlapping));
|
||||
assert!(overlapping.overlaps(&first));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_invalid_placement_is_reported() {
|
||||
assert!(!Placement::new(TaskId::new(0), f64::NAN, 60.0).is_valid());
|
||||
assert!(!Placement::new(TaskId::new(0), 0.0, -1.0).is_valid());
|
||||
// A zero duration placement is valid, for an instantaneous task.
|
||||
assert!(Placement::new(TaskId::new(0), 0.0, 0.0).is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_evaluation_type_is_nameable() {
|
||||
// The type must be usable by a caller that stores the results.
|
||||
fn takes(_evaluation: &Evaluation) {}
|
||||
let resources = ResourceSet::from_resources(vec![resource(0, 1.0)]).expect("one");
|
||||
let schedule = Schedule::new();
|
||||
let evaluation = schedule.evaluate(&[], &resources);
|
||||
takes(&evaluation);
|
||||
}
|
||||
Loading…
Reference in New Issue