Finished unit testing
This commit is contained in:
parent
8d9b39865a
commit
dbe97f0df7
2 changed files with 158 additions and 33 deletions
|
@ -27,5 +27,6 @@ log = "0.4.14"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
futures = "0.3.17"
|
futures = "0.3.17"
|
||||||
smol = "1.2.5"
|
smol = "1.2.5"
|
||||||
|
smol-potat = "1.1.2"
|
||||||
num_cpus = "1.13.0"
|
num_cpus = "1.13.0"
|
||||||
easy-parallel = "3.1.0"
|
easy-parallel = "3.1.0"
|
|
@ -17,7 +17,43 @@ use uuid::Uuid;
|
||||||
|
|
||||||
type SimulationTree<T> = Box<Tree<GeneticNodeWrapper<T>>>;
|
type SimulationTree<T> = Box<Tree<GeneticNodeWrapper<T>>>;
|
||||||
|
|
||||||
#[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<Box<TestState>, Error> {
|
||||||
|
/// Ok(Box::new(TestState { score: 0.0 }))
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn merge(left: &TestState, right: &TestState) -> Result<Box<TestState>, 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 struct GemlaConfig {
|
||||||
pub generations_per_node: u64,
|
pub generations_per_node: u64,
|
||||||
pub overwrite: bool,
|
pub overwrite: bool,
|
||||||
|
@ -43,6 +79,8 @@ where
|
||||||
{
|
{
|
||||||
pub fn new(path: &Path, config: GemlaConfig) -> Result<Self, Error> {
|
pub fn new(path: &Path, config: GemlaConfig) -> Result<Self, Error> {
|
||||||
match File::open(path) {
|
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 {
|
Ok(_) => Ok(Gemla {
|
||||||
data: if config.overwrite {
|
data: if config.overwrite {
|
||||||
FileLinked::new((None, config), path)?
|
FileLinked::new((None, config), path)?
|
||||||
|
@ -51,6 +89,7 @@ where
|
||||||
},
|
},
|
||||||
threads: HashMap::new(),
|
threads: HashMap::new(),
|
||||||
}),
|
}),
|
||||||
|
// If the file doesn't exist we must create it
|
||||||
Err(error) if error.kind() == ErrorKind::NotFound => Ok(Gemla {
|
Err(error) if error.kind() == ErrorKind::NotFound => Ok(Gemla {
|
||||||
data: FileLinked::new((None, config), path)?,
|
data: FileLinked::new((None, config), path)?,
|
||||||
threads: HashMap::new(),
|
threads: HashMap::new(),
|
||||||
|
@ -59,6 +98,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tree_ref(&self) -> Option<&SimulationTree<T>> {
|
||||||
|
self.data.readonly().0.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn simulate(&mut self, steps: u64) -> Result<(), Error> {
|
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
|
// 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.
|
// in the tree and which nodes have not.
|
||||||
|
@ -69,20 +112,15 @@ where
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Height of simulation tree increased to {}",
|
"Height of simulation tree increased to {}",
|
||||||
self.data
|
self.tree_ref()
|
||||||
.readonly()
|
|
||||||
.0
|
|
||||||
.as_ref()
|
|
||||||
.map(|t| format!("{}", t.height()))
|
.map(|t| format!("{}", t.height()))
|
||||||
.unwrap_or_else(|| "Tree is not defined".to_string())
|
.unwrap_or_else(|| "Tree is not defined".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// We need to keep simulating until the tree has been completely processed.
|
||||||
if self
|
if self
|
||||||
.data
|
.tree_ref()
|
||||||
.readonly()
|
|
||||||
.0
|
|
||||||
.as_ref()
|
|
||||||
.map(|t| Gemla::is_completed(t))
|
.map(|t| Gemla::is_completed(t))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
|
@ -93,10 +131,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(node) = self
|
if let Some(node) = self
|
||||||
.data
|
.tree_ref()
|
||||||
.readonly()
|
|
||||||
.0
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|t| self.get_unprocessed_node(t))
|
.and_then(|t| self.get_unprocessed_node(t))
|
||||||
{
|
{
|
||||||
trace!("Adding node to process list {}", node.id());
|
trace!("Adding node to process list {}", node.id());
|
||||||
|
@ -118,15 +153,17 @@ where
|
||||||
trace!("Joining threads for nodes {:?}", self.threads.keys());
|
trace!("Joining threads for nodes {:?}", self.threads.keys());
|
||||||
|
|
||||||
let results = future::join_all(self.threads.values_mut()).await;
|
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<Vec<GeneticNodeWrapper<T>>, Error> =
|
let reduced_results: Result<Vec<GeneticNodeWrapper<T>>, Error> =
|
||||||
results.into_iter().collect();
|
results.into_iter().collect();
|
||||||
|
|
||||||
self.threads.clear();
|
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| {
|
reduced_results.and_then(|r| {
|
||||||
self.data.mutate(|(d, _)| {
|
self.data.mutate(|(d, _)| {
|
||||||
if let Some(t) = d {
|
if let Some(t) = d {
|
||||||
let failed_nodes = Gemla::replace_nodes(t, r);
|
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() {
|
if !failed_nodes.is_empty() {
|
||||||
warn!(
|
warn!(
|
||||||
"Unable to find {:?} to replace in tree",
|
"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)
|
Gemla::merge_completed_nodes(t)
|
||||||
} else {
|
} else {
|
||||||
warn!("Unable to replce nodes {:?} in empty tree", r);
|
warn!("Unable to replce nodes {:?} in empty tree", r);
|
||||||
|
@ -149,6 +187,8 @@ where
|
||||||
fn merge_completed_nodes(tree: &mut SimulationTree<T>) -> Result<(), Error> {
|
fn merge_completed_nodes(tree: &mut SimulationTree<T>) -> Result<(), Error> {
|
||||||
if tree.val.state() == GeneticState::Initialize {
|
if tree.val.state() == GeneticState::Initialize {
|
||||||
match (&mut tree.left, &mut tree.right) {
|
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))
|
(Some(l), Some(r))
|
||||||
if l.val.state() == GeneticState::Finish
|
if l.val.state() == GeneticState::Finish
|
||||||
&& r.val.state() == GeneticState::Finish =>
|
&& r.val.state() == GeneticState::Finish =>
|
||||||
|
@ -167,6 +207,7 @@ where
|
||||||
Gemla::merge_completed_nodes(l)?;
|
Gemla::merge_completed_nodes(l)?;
|
||||||
Gemla::merge_completed_nodes(r)?;
|
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 => {
|
(Some(l), None) if l.val.state() == GeneticState::Finish => {
|
||||||
trace!("Copying node {}", l.val.id());
|
trace!("Copying node {}", l.val.id());
|
||||||
|
|
||||||
|
@ -199,14 +240,15 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_unprocessed_node(&self, tree: &SimulationTree<T>) -> Option<GeneticNodeWrapper<T>> {
|
fn get_unprocessed_node(&self, tree: &SimulationTree<T>) -> Option<GeneticNodeWrapper<T>> {
|
||||||
|
// 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()) {
|
if tree.val.state() != GeneticState::Finish && !self.threads.contains_key(&tree.val.id()) {
|
||||||
match (&tree.left, &tree.right) {
|
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))
|
(Some(l), Some(r))
|
||||||
if l.val.state() == GeneticState::Finish
|
if l.val.state() == GeneticState::Finish
|
||||||
&& r.val.state() == GeneticState::Finish =>
|
&& r.val.state() == GeneticState::Finish => Some(tree.val.clone()),
|
||||||
{
|
|
||||||
Some(tree.val.clone())
|
|
||||||
}
|
|
||||||
(Some(l), Some(r)) => self
|
(Some(l), Some(r)) => self
|
||||||
.get_unprocessed_node(l)
|
.get_unprocessed_node(l)
|
||||||
.or_else(|| self.get_unprocessed_node(r)),
|
.or_else(|| self.get_unprocessed_node(r)),
|
||||||
|
@ -223,6 +265,7 @@ where
|
||||||
tree: &mut SimulationTree<T>,
|
tree: &mut SimulationTree<T>,
|
||||||
mut nodes: Vec<GeneticNodeWrapper<T>>,
|
mut nodes: Vec<GeneticNodeWrapper<T>>,
|
||||||
) -> Vec<GeneticNodeWrapper<T>> {
|
) -> Vec<GeneticNodeWrapper<T>> {
|
||||||
|
// Replacing nodes as we recurse through the tree
|
||||||
if let Some(i) = nodes.iter().position(|n| n.id() == tree.val.id()) {
|
if let Some(i) = nodes.iter().position(|n| n.id() == tree.val.id()) {
|
||||||
tree.val = nodes.remove(i);
|
tree.val = nodes.remove(i);
|
||||||
}
|
}
|
||||||
|
@ -243,15 +286,16 @@ where
|
||||||
if amount == 0 {
|
if amount == 0 {
|
||||||
tree
|
tree
|
||||||
} else {
|
} else {
|
||||||
let right_branch_height =
|
let left_branch_right =
|
||||||
tree.as_ref().map(|t| t.height() as u64).unwrap_or(0) + amount - 1;
|
tree.as_ref().map(|t| t.height() as u64).unwrap_or(0) + amount - 1;
|
||||||
|
|
||||||
Some(Box::new(Tree::new(
|
Some(Box::new(Tree::new(
|
||||||
GeneticNodeWrapper::new(config.generations_per_node),
|
GeneticNodeWrapper::new(config.generations_per_node),
|
||||||
Gemla::increase_height(tree, config, amount - 1),
|
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(
|
Some(Box::new(btree!(GeneticNodeWrapper::new(
|
||||||
right_branch_height * config.generations_per_node
|
left_branch_right * config.generations_per_node
|
||||||
))))
|
))))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -261,16 +305,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_completed(tree: &SimulationTree<T>) -> bool {
|
fn is_completed(tree: &SimulationTree<T>) -> bool {
|
||||||
if tree.val.state() == GeneticState::Finish {
|
// If the current node is finished, then by convention the children should all be finished as well
|
||||||
match (&tree.left, &tree.right) {
|
tree.val.state() == GeneticState::Finish
|
||||||
(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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_node(mut node: GeneticNodeWrapper<T>) -> Result<GeneticNodeWrapper<T>, Error> {
|
async fn process_node(mut node: GeneticNodeWrapper<T>) -> Result<GeneticNodeWrapper<T>, Error> {
|
||||||
|
@ -297,8 +333,33 @@ where
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::core::*;
|
use crate::core::*;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
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<F: FnOnce(&Path) -> 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)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
struct TestState {
|
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::<TestState>::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::<TestState>::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::<TestState>::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::<TestState>::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(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue