From dbe97f0df7cff12a507f44c2c2c92e962c11a736 Mon Sep 17 00:00:00 2001 From: vandomej Date: Fri, 26 Nov 2021 18:58:17 -0800 Subject: [PATCH] Finished unit testing --- gemla/Cargo.toml | 1 + gemla/src/core/mod.rs | 190 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 158 insertions(+), 33 deletions(-) diff --git a/gemla/Cargo.toml b/gemla/Cargo.toml index 2ea4026..77c42e1 100644 --- a/gemla/Cargo.toml +++ b/gemla/Cargo.toml @@ -27,5 +27,6 @@ log = "0.4.14" env_logger = "0.9.0" futures = "0.3.17" smol = "1.2.5" +smol-potat = "1.1.2" num_cpus = "1.13.0" easy-parallel = "3.1.0" \ No newline at end of file diff --git a/gemla/src/core/mod.rs b/gemla/src/core/mod.rs index 1ca3121..7564423 100644 --- a/gemla/src/core/mod.rs +++ b/gemla/src/core/mod.rs @@ -17,7 +17,43 @@ use uuid::Uuid; type SimulationTree = Box>>; -#[derive(Serialize, Deserialize)] +/// Provides configuration options for managing a [`Gemla`] object as it executes. +/// +/// # Examples +/// ``` +/// #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +/// struct TestState { +/// pub score: f64, +/// } +/// +/// impl genetic_node::GeneticNode for TestState { +/// fn simulate(&mut self) -> Result<(), Error> { +/// self.score += 1.0; +/// Ok(()) +/// } +/// +/// fn mutate(&mut self) -> Result<(), Error> { +/// Ok(()) +/// } +/// +/// fn initialize() -> Result, Error> { +/// Ok(Box::new(TestState { score: 0.0 })) +/// } +/// +/// fn merge(left: &TestState, right: &TestState) -> Result, Error> { +/// Ok(Box::new(if left.score > right.score { +/// left.clone() +/// } else { +/// right.clone() +/// })) +/// } +/// } +/// +/// fn main() { +/// +/// } +/// ``` +#[derive(Serialize, Deserialize, Copy, Clone)] pub struct GemlaConfig { pub generations_per_node: u64, pub overwrite: bool, @@ -43,6 +79,8 @@ where { pub fn new(path: &Path, config: GemlaConfig) -> Result { match File::open(path) { + // If the file exists we either want to overwrite the file or read from the file + // based on the configuration provided Ok(_) => Ok(Gemla { data: if config.overwrite { FileLinked::new((None, config), path)? @@ -51,6 +89,7 @@ where }, threads: HashMap::new(), }), + // If the file doesn't exist we must create it Err(error) if error.kind() == ErrorKind::NotFound => Ok(Gemla { data: FileLinked::new((None, config), path)?, threads: HashMap::new(), @@ -59,6 +98,10 @@ where } } + pub fn tree_ref(&self) -> Option<&SimulationTree> { + self.data.readonly().0.as_ref() + } + pub async fn simulate(&mut self, steps: u64) -> Result<(), Error> { // Before we can process nodes we must create blank nodes in their place to keep track of which nodes have been processed // in the tree and which nodes have not. @@ -69,20 +112,15 @@ where info!( "Height of simulation tree increased to {}", - self.data - .readonly() - .0 - .as_ref() + self.tree_ref() .map(|t| format!("{}", t.height())) .unwrap_or_else(|| "Tree is not defined".to_string()) ); loop { + // We need to keep simulating until the tree has been completely processed. if self - .data - .readonly() - .0 - .as_ref() + .tree_ref() .map(|t| Gemla::is_completed(t)) .unwrap_or(false) { @@ -93,10 +131,7 @@ where } if let Some(node) = self - .data - .readonly() - .0 - .as_ref() + .tree_ref() .and_then(|t| self.get_unprocessed_node(t)) { trace!("Adding node to process list {}", node.id()); @@ -118,15 +153,17 @@ where trace!("Joining threads for nodes {:?}", self.threads.keys()); let results = future::join_all(self.threads.values_mut()).await; + // Converting a list of results into a result wrapping the list let reduced_results: Result>, Error> = results.into_iter().collect(); - self.threads.clear(); + // We need to retrieve the processed nodes from the resulting list and replace them in the original list reduced_results.and_then(|r| { self.data.mutate(|(d, _)| { if let Some(t) = d { let failed_nodes = Gemla::replace_nodes(t, r); + // We receive a list of nodes that were unable to be found in the original tree if !failed_nodes.is_empty() { warn!( "Unable to find {:?} to replace in tree", @@ -134,6 +171,7 @@ where ) } + // Once the nodes are replaced we need to find nodes that can be merged from the completed children nodes Gemla::merge_completed_nodes(t) } else { warn!("Unable to replce nodes {:?} in empty tree", r); @@ -149,6 +187,8 @@ where fn merge_completed_nodes(tree: &mut SimulationTree) -> Result<(), Error> { if tree.val.state() == GeneticState::Initialize { match (&mut tree.left, &mut tree.right) { + // If the current node has been initialized, and has children nodes that are completed, then we need + // to merge the children nodes together into the parent node (Some(l), Some(r)) if l.val.state() == GeneticState::Finish && r.val.state() == GeneticState::Finish => @@ -167,6 +207,7 @@ where Gemla::merge_completed_nodes(l)?; Gemla::merge_completed_nodes(r)?; } + // If there is only one child node that's completed then we want to copy it to the parent node (Some(l), None) if l.val.state() == GeneticState::Finish => { trace!("Copying node {}", l.val.id()); @@ -199,14 +240,15 @@ where } fn get_unprocessed_node(&self, tree: &SimulationTree) -> Option> { + // If the current node has been processed or exists in the thread list then we want to stop recursing. Checking if it exists in the thread list + // should be fine because we process the tree from bottom to top. if tree.val.state() != GeneticState::Finish && !self.threads.contains_key(&tree.val.id()) { match (&tree.left, &tree.right) { + // If the children are finished we can start processing the currrent node. The current node should be merged from the children already + // during join_threads. (Some(l), Some(r)) if l.val.state() == GeneticState::Finish - && r.val.state() == GeneticState::Finish => - { - Some(tree.val.clone()) - } + && r.val.state() == GeneticState::Finish => Some(tree.val.clone()), (Some(l), Some(r)) => self .get_unprocessed_node(l) .or_else(|| self.get_unprocessed_node(r)), @@ -223,6 +265,7 @@ where tree: &mut SimulationTree, mut nodes: Vec>, ) -> Vec> { + // Replacing nodes as we recurse through the tree if let Some(i) = nodes.iter().position(|n| n.id() == tree.val.id()) { tree.val = nodes.remove(i); } @@ -243,15 +286,16 @@ where if amount == 0 { tree } else { - let right_branch_height = + let left_branch_right = tree.as_ref().map(|t| t.height() as u64).unwrap_or(0) + amount - 1; - + Some(Box::new(Tree::new( GeneticNodeWrapper::new(config.generations_per_node), Gemla::increase_height(tree, config, amount - 1), - if right_branch_height > 0 { + // The right branch height has to equal the left branches total height + if left_branch_right > 0 { Some(Box::new(btree!(GeneticNodeWrapper::new( - right_branch_height * config.generations_per_node + left_branch_right * config.generations_per_node )))) } else { None @@ -261,16 +305,8 @@ where } fn is_completed(tree: &SimulationTree) -> bool { - if tree.val.state() == GeneticState::Finish { - match (&tree.left, &tree.right) { - (Some(l), Some(r)) => Gemla::is_completed(l) && Gemla::is_completed(r), - (Some(l), None) => Gemla::is_completed(l), - (None, Some(r)) => Gemla::is_completed(r), - (None, None) => true, - } - } else { - false - } + // If the current node is finished, then by convention the children should all be finished as well + tree.val.state() == GeneticState::Finish } async fn process_node(mut node: GeneticNodeWrapper) -> Result, Error> { @@ -297,8 +333,33 @@ where #[cfg(test)] mod tests { use crate::core::*; - use serde::{Deserialize, Serialize}; + use std::path::PathBuf; + use std::fs; + + struct CleanUp { + path: PathBuf, + } + + impl CleanUp { + fn new(path: &Path) -> CleanUp { + CleanUp { + path: path.to_path_buf(), + } + } + + pub fn run Result<(), Error>>(&self, op: F) -> Result<(), Error> { + op(&self.path) + } + } + + impl Drop for CleanUp { + fn drop(&mut self) { + if self.path.exists() { + fs::remove_file(&self.path).expect("Unable to remove file"); + } + } + } #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] struct TestState { @@ -327,4 +388,67 @@ mod tests { })) } } + + #[test] + fn test_new() -> Result<(), Error> { + let path = PathBuf::from("test_new_non_existing"); + CleanUp::new(&path).run(|p| { + assert!(!path.exists()); + + // Testing initial creation + let mut config = GemlaConfig { + generations_per_node: 1, + overwrite: true + }; + let mut gemla = Gemla::::new(&p, config)?; + + smol::block_on(gemla.simulate(2))?; + assert_eq!(gemla.data.readonly().0.as_ref().unwrap().height(), 2); + + drop(gemla); + assert!(path.exists()); + + // Testing overwriting data + let mut gemla = Gemla::::new(&p, config)?; + + smol::block_on(gemla.simulate(2))?; + assert_eq!(gemla.data.readonly().0.as_ref().unwrap().height(), 2); + + drop(gemla); + assert!(path.exists()); + + // Testing not-overwriting data + config.overwrite = false; + let mut gemla = Gemla::::new(&p, config)?; + + smol::block_on(gemla.simulate(2))?; + assert_eq!(gemla.tree_ref().unwrap().height(), 4); + + drop(gemla); + assert!(path.exists()); + + Ok(()) + }) + } + + #[test] + fn test_simulate() -> Result<(), Error> { + let path = PathBuf::from("test_simulate"); + CleanUp::new(&path).run(|p| { + // Testing initial creation + let config = GemlaConfig { + generations_per_node: 10, + overwrite: true + }; + let mut gemla = Gemla::::new(&p, config)?; + + smol::block_on(gemla.simulate(5))?; + let tree = gemla.tree_ref().unwrap(); + assert_eq!(tree.height(), 5); + assert_eq!(tree.val.as_ref().unwrap().score, 50.0); + + Ok(()) + }) + } + }