mod bin_array;
mod item;
use crate::Result;
use std::collections::HashMap;
use item::Item;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
pub fn define(py: Python, m: &PyModule) -> PyResult<()> {
let subm = PyModule::new(py, "test_ids")?;
subm.add_class::<TestIDs>()?;
subm.add_class::<AllocationOptions>()?;
subm.add_class::<Pool>()?;
m.add_submodule(subm)?;
Ok(())
}
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestIDs {
pub bins: Item,
pub softbins: Item,
pub numbers: Item,
allocations: HashMap<String, Allocation>,
}
#[cfg(not(feature = "python"))]
impl TestIDs {
pub fn new() -> Self {
TestIDs {
bins: Item::new(),
softbins: Item::new(),
numbers: Item::new(),
allocations: HashMap::new(),
}
}
pub fn from_file(file: &str) -> Result<TestIDs> {
let json = std::fs::read_to_string(file)?;
let tids: TestIDs = serde_json::from_str(&json)?;
Ok(tids)
}
}
#[cfg_attr(feature = "python", pymethods)]
impl TestIDs {
#[cfg(feature = "python")]
#[new]
pub fn new() -> PyResult<Self> {
Ok(TestIDs {
bins: Item::new(),
softbins: Item::new(),
numbers: Item::new(),
allocations: HashMap::new(),
})
}
#[cfg(feature = "python")]
#[staticmethod]
pub fn from_file(file: &str) -> Result<TestIDs> {
let json = std::fs::read_to_string(file)?;
let tids: TestIDs = serde_json::from_str(&json)?;
Ok(tids)
}
#[cfg(feature = "python")]
pub fn push(&mut self, pool: Pool, value: u32) {
match pool {
Pool::BinInclude => self.bins.include.push(value),
Pool::BinExclude => self.bins.exclude.push(value),
Pool::SoftBinInclude => self.softbins.include.push(value),
Pool::SoftBinExclude => self.softbins.exclude.push(value),
Pool::NumberInclude => self.numbers.include.push(value),
Pool::NumberExclude => self.numbers.exclude.push(value),
}
}
#[cfg(feature = "python")]
pub fn push_range(&mut self, pool: Pool, start: u32, end: u32) {
match pool {
Pool::BinInclude => self.bins.include.push_range(start, end),
Pool::BinExclude => self.bins.exclude.push_range(start, end),
Pool::SoftBinInclude => self.softbins.include.push_range(start, end),
Pool::SoftBinExclude => self.softbins.exclude.push_range(start, end),
Pool::NumberInclude => self.numbers.include.push_range(start, end),
Pool::NumberExclude => self.numbers.exclude.push_range(start, end),
}
}
#[cfg(feature = "python")]
pub fn set_increment(&mut self, pool: Pool, size: u32) {
match pool {
Pool::BinInclude | Pool::BinExclude => self.bins.increment = size,
Pool::SoftBinInclude | Pool::SoftBinExclude => self.softbins.increment = size,
Pool::NumberInclude | Pool::NumberExclude => self.numbers.increment = size,
}
}
pub fn allocate(&mut self, test_name: &str) -> Result<Allocation> {
let opts = AllocationOptions::default();
self.allocate_with_options(test_name, opts)
}
pub fn allocate_with_options(&mut self, test_name: &str, options: AllocationOptions) -> Result<Allocation> {
let key = test_name.to_lowercase();
let key = key.trim();
if !self.allocations.contains_key(key) || !options.is_default() {
let a = {
if let Some(a) = self.allocations.get_mut(key) {
a
} else {
self.allocations.insert(key.to_string(), Allocation::default());
self.allocations.get_mut(key).unwrap()
}
};
if options.no_bin {
a.bin = None;
} else {
if let Some(b) = options.bin {
if a.bin != Some(b) {
self.bins.reserved.push(b);
a.bin = Some(b);
}
} else {
if a.bin.is_none() {
a.bin = self.bins.next(options.size);
}
}
}
if options.no_softbin {
a.softbin = None;
} else {
if let Some(b) = options.softbin {
if a.softbin != Some(b) {
self.softbins.reserved.push(b);
a.softbin = Some(b);
}
} else {
if a.softbin.is_none() {
a.softbin = self.softbins.next(options.size);
}
}
}
if options.no_number {
a.number = None;
} else {
if let Some(b) = options.number {
if a.number != Some(b) {
self.numbers.reserved.push(b);
a.number = Some(b);
}
} else {
if a.number.is_none() {
a.number = self.numbers.next(options.size);
}
}
}
}
let allocation = self.allocations[key].clone();
if let Some(b) = allocation.bin {
let size = options.size.unwrap_or(self.bins.increment);
for i in 0..size {
self.bins.record_reference(b + i);
}
}
if let Some(b) = allocation.softbin {
let size = options.size.unwrap_or(self.softbins.increment);
for i in 0..size {
self.softbins.record_reference(b + i);
}
}
if let Some(n) = allocation.number {
let size = options.size.unwrap_or(self.numbers.increment);
for i in 0..size {
self.numbers.record_reference(n + i);
}
}
Ok(allocation)
}
pub fn save(&self, file: &str) -> Result<()> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(file, json)?;
Ok(())
}
}
#[cfg_attr(feature = "python", pyclass)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Pool {
BinInclude,
BinExclude,
SoftBinInclude,
SoftBinExclude,
NumberInclude,
NumberExclude,
}
#[cfg_attr(feature = "python", pyclass(get_all))]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Allocation {
pub bin: Option<u32>,
pub softbin: Option<u32>,
pub number: Option<u32>,
}
#[cfg_attr(feature = "python", pymethods)]
impl Allocation {
pub fn to_hashmap(&self) -> HashMap<String, u32> {
let mut h = HashMap::new();
if let Some(b) = self.bin {
h.insert("bin".to_string(), b);
}
if let Some(b) = self.softbin {
h.insert("softbin".to_string(), b);
}
if let Some(b) = self.number {
h.insert("number".to_string(), b);
}
h
}
}
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct AllocationOptions {
pub bin: Option<u32>,
pub softbin: Option<u32>,
pub number: Option<u32>,
pub no_bin: bool,
pub no_softbin: bool,
pub no_number: bool,
pub size: Option<u32>,
}
#[cfg_attr(feature = "python", pymethods)]
impl AllocationOptions {
#[cfg(feature = "python")]
#[new]
pub fn new() -> PyResult<Self> {
Ok(AllocationOptions::default())
}
}
impl AllocationOptions {
fn is_default(&self) -> bool {
self == &AllocationOptions::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_alive() {
let mut tids = TestIDs::new();
tids.bins.include.push(3);
tids.bins.include.push(4);
assert_eq!(tids.allocate("t1").unwrap().bin, Some(3));
}
#[test]
fn bin_numbers_increment() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(1, 3);
assert_eq!(tids.allocate("t1").unwrap().bin, Some(1));
assert_eq!(tids.allocate("t2").unwrap().bin, Some(2));
assert_eq!(tids.allocate("t3").unwrap().bin, Some(3));
}
#[test]
fn duplicate_tests_pick_up_the_same_number() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(1, 3);
assert_eq!(tids.allocate("t1").unwrap().bin, Some(1));
assert_eq!(tids.allocate("t2").unwrap().bin, Some(2));
assert_eq!(tids.allocate("t1").unwrap().bin, Some(1));
assert_eq!(tids.allocate("t3").unwrap().bin, Some(3));
}
#[test]
fn caller_can_override_bin_number() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(1, 4);
let opts = AllocationOptions {
bin: Some(3),
..Default::default()
};
assert_eq!(tids.allocate("t1").unwrap().bin, Some(1));
assert_eq!(tids.allocate_with_options("t2", opts).unwrap().bin, Some(3));
assert_eq!(tids.allocate("t2").unwrap().bin, Some(3));
assert_eq!(tids.allocate("t3").unwrap().bin, Some(2));
}
#[test]
fn manually_assigned_bins_are_reserved() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(1, 4);
let opts = AllocationOptions {
bin: Some(3),
..Default::default()
};
assert_eq!(tids.allocate("t1").unwrap().bin, Some(1));
assert_eq!(tids.allocate_with_options("t2", opts).unwrap().bin, Some(3));
assert_eq!(tids.allocate("t3").unwrap().bin, Some(2));
assert_eq!(tids.allocate("t4").unwrap().bin, Some(4));
}
#[test]
fn assignments_can_be_inhibited() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(1, 4);
let opts = AllocationOptions {
no_bin: true,
..Default::default()
};
assert_eq!(tids.allocate_with_options("t1", opts).unwrap().bin, None);
}
#[test]
fn excluded_bins_are_not_used() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(1, 10);
tids.bins.exclude.push(3);
tids.bins.exclude.push_range(5, 9);
assert_eq!(tids.allocate("t1").unwrap().bin, Some(1));
assert_eq!(tids.allocate("t2").unwrap().bin, Some(2));
assert_eq!(tids.allocate("t3").unwrap().bin, Some(4));
assert_eq!(tids.allocate("t4").unwrap().bin, Some(10));
}
#[test]
fn the_system_can_be_saved_to_a_file_and_resumed() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(1, 10);
tids.bins.exclude.push(3);
tids.bins.exclude.push_range(5, 9);
assert_eq!(tids.allocate("t1").unwrap().bin, Some(1));
assert_eq!(tids.allocate("t2").unwrap().bin, Some(2));
assert_eq!(tids.allocate("t3").unwrap().bin, Some(4));
tids.save("tids.json").unwrap();
let mut tids = TestIDs::from_file("tids.json").unwrap();
assert_eq!(tids.allocate("t2").unwrap().bin, Some(2));
assert_eq!(tids.allocate("t4").unwrap().bin, Some(10));
std::fs::remove_file("tids.json").unwrap();
}
#[test]
fn tests_can_reserve_multiple_bins() {
let mut tids = TestIDs::new();
tids.bins.include.push_range(10, 30);
tids.bins.increment = 5;
let opts = AllocationOptions {
size: Some(2),
..Default::default()
};
assert_eq!(tids.allocate_with_options("t1", opts).unwrap().bin, Some(10));
assert_eq!(tids.allocate("t2").unwrap().bin, Some(12)); assert_eq!(tids.allocate("t3").unwrap().bin, Some(17)); assert_eq!(tids.allocate("t4").unwrap().bin, Some(22));
assert_eq!(tids.allocate("t5").unwrap().bin, Some(10)); assert_eq!(tids.allocate("t6").unwrap().bin, Some(15));
}
}