Increasing code coverage

This commit is contained in:
vandomej 2025-09-08 11:55:32 -07:00
parent 95fdad1034
commit 32a6813cf4
3 changed files with 282 additions and 48 deletions

View file

@ -19,5 +19,7 @@ num_cpus = "1.17.0"
rand = "0.9.2" rand = "0.9.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.143" serde_json = "1.0.143"
tempfile = "3.21.0"
tokio = { version = "1.47.1", features = ["full"] } tokio = { version = "1.47.1", features = ["full"] }
tokio-test = "0.4.4"
uuid = "1.18.1" uuid = "1.18.1"

View file

@ -935,6 +935,74 @@ async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::
#[cfg(test)] #[cfg(test)]
pub mod test { pub mod test {
use super::*; use super::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
use tokio_test::block_on;
use uuid::Uuid;
#[test]
fn test_get_individual_id_format() {
let nn = FighterNN {
id: Uuid::new_v4(),
folder: PathBuf::new(),
generation: 0,
population_size: 10,
scores: vec![],
nn_shapes: vec![],
crossbreed_segments: 2,
weight_initialization_range: -0.5..0.5,
minor_mutation_rate: 0.1,
major_mutation_rate: 0.05,
mutation_weight_range: -0.1..0.1,
id_mapping: vec![],
lerp_amount: 0.0,
generational_lenience: 3,
survival_rate: 0.5,
};
let id_str = nn.get_individual_id(42);
assert!(id_str.contains("fighter_nn_"));
assert!(id_str.ends_with("_42"));
assert!(id_str.starts_with(&format!("{:06}_", nn.id)));
}
#[test]
fn test_read_score_from_file_found() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("score.txt");
let nn_id = "test_nn";
let mut file = File::create(&file_path).unwrap();
writeln!(file, "{}: 123.45", nn_id).unwrap();
let score = block_on(read_score_from_file(&file_path, nn_id)).unwrap();
assert!((score - 123.45).abs() < 1e-5);
}
#[test]
fn test_read_score_from_file_not_found() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("score.txt");
let nn_id = "not_in_file";
let mut file = File::create(&file_path).unwrap();
writeln!(file, "other_nn: 1.0").unwrap();
let result = block_on(read_score_from_file(&file_path, nn_id));
assert!(result.is_err());
}
#[test]
fn test_run_1v1_simulation_reads_existing_score() {
let dir = tempdir().unwrap();
let nn_id1 = "nn1";
let nn_id2 = "nn2";
let file_path = dir.path().join(format!("{}_vs_{}.txt", nn_id1, nn_id2));
let mut file = File::create(&file_path).unwrap();
writeln!(file, "{}: 10.0", nn_id1).unwrap();
writeln!(file, "{}: 20.0", nn_id2).unwrap();
let nn_path_1 = dir.path().join(format!("{}.net", nn_id1));
let nn_path_2 = dir.path().join(format!("{}.net", nn_id2));
let result = block_on(run_1v1_simulation(&nn_path_1, &nn_path_2, false)).unwrap();
assert_eq!(result, (10.0, 20.0));
}
#[test] #[test]
fn test_weighted_random_selection() { fn test_weighted_random_selection() {

View file

@ -409,11 +409,8 @@ pub fn crossbreed_neuron_arrays(
current_layer += 1; current_layer += 1;
} }
new_neurons.push((*neuron_id, is_primary, current_layer, 0)); new_neurons.push((*neuron_id, is_primary, current_layer, 0));
if is_primary { // The first segment is always from the primary network, so we can set primary_last_layer here
primary_last_layer = current_layer; primary_last_layer = current_layer;
} else {
secondary_last_layer = current_layer;
}
} else { } else {
break; break;
} }
@ -452,22 +449,22 @@ pub fn crossbreed_neuron_arrays(
let highest_id = earlier_layer_neurons let highest_id = earlier_layer_neurons
.iter() .iter()
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0))); .max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
if let Some(highest_id) = highest_id { if let Some(highest_id) = highest_id
if highest_id.1 == is_primary { && highest_id.1 == is_primary
let neurons_to_add = target_neurons {
.iter() let neurons_to_add = target_neurons
.filter(|(id, l)| { .iter()
id > &highest_id.0 && id < neuron_id && l == layer .filter(|(id, l)| {
}) id > &highest_id.0 && id < neuron_id && l == layer
.collect::<Vec<_>>(); })
for (neuron_id, layer) in neurons_to_add { .collect::<Vec<_>>();
new_neurons.push((*neuron_id, is_primary, *layer, 0)); for (neuron_id, layer) in neurons_to_add {
new_neurons.push((*neuron_id, is_primary, *layer, 0));
if is_primary { if is_primary {
primary_last_layer = *layer; primary_last_layer = *layer;
} else { } else {
secondary_last_layer = *layer; secondary_last_layer = *layer;
}
} }
} }
} }
@ -489,25 +486,20 @@ pub fn crossbreed_neuron_arrays(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let highest_id = let highest_id =
current_layer_neurons.iter().max_by_key(|(id, _, _, _)| id); current_layer_neurons.iter().max_by_key(|(id, _, _, _)| id);
if let Some(highest_id) = highest_id { if let Some(highest_id) = highest_id
if highest_id.1 == is_primary { && highest_id.1 == is_primary
let neurons_to_add = target_neurons {
.iter() let neurons_to_add = target_neurons
.filter(|(id, l)| id > &highest_id.0 && *l == layer - 1) .iter()
.collect::<Vec<_>>(); .filter(|(id, l)| id > &highest_id.0 && *l == layer - 1)
for (neuron_id, _) in neurons_to_add { .collect::<Vec<_>>();
new_neurons.push(( for (neuron_id, _) in neurons_to_add {
*neuron_id, new_neurons.push((*neuron_id, is_primary, current_layer, 0));
is_primary,
current_layer,
0,
));
if is_primary { if is_primary {
primary_last_layer = current_layer; primary_last_layer = current_layer;
} else { } else {
secondary_last_layer = current_layer; secondary_last_layer = current_layer;
}
} }
} }
} }
@ -563,7 +555,9 @@ pub fn crossbreed_neuron_arrays(
new_neurons.push((*neuron_id, is_primary, current_layer, 0)); new_neurons.push((*neuron_id, is_primary, current_layer, 0));
} }
break; break;
} else if *neuron_id == &segments.last().unwrap().1 + 1 { }
// If the neuron id is exactly one more than the last neuron id, we need to ensure that there's at least one neuron from the same individual in the previous layer
else if *neuron_id == &segments.last().unwrap().1 + 1 {
let target_layer = if is_primary { let target_layer = if is_primary {
primary_last_layer primary_last_layer
} else { } else {
@ -579,15 +573,15 @@ pub fn crossbreed_neuron_arrays(
let highest_id = earlier_layer_neurons let highest_id = earlier_layer_neurons
.iter() .iter()
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0))); .max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
if let Some(highest_id) = highest_id { if let Some(highest_id) = highest_id
if highest_id.1 == is_primary { && highest_id.1 == is_primary
let neurons_to_add = target_neurons {
.iter() let neurons_to_add = target_neurons
.filter(|(id, _)| id > &highest_id.0 && id < neuron_id) .iter()
.collect::<Vec<_>>(); .filter(|(id, _)| id > &highest_id.0 && id < neuron_id)
for (neuron_id, l) in neurons_to_add { .collect::<Vec<_>>();
new_neurons.push((*neuron_id, is_primary, *l, 0)); for (neuron_id, l) in neurons_to_add {
} new_neurons.push((*neuron_id, is_primary, *l, 0));
} }
} }
@ -808,6 +802,45 @@ mod tests {
use super::*; use super::*;
#[test]
fn crossbreed_basic_test() -> Result<(), Box<dyn std::error::Error>> {
// Create a dummy FighterNN
let fighter_nn = FighterNN {
id: uuid::Uuid::new_v4(),
folder: std::path::PathBuf::from("/tmp"),
population_size: 2,
generation: 0,
scores: vec![],
nn_shapes: vec![],
crossbreed_segments: 1,
weight_initialization_range: -0.5..0.5,
minor_mutation_rate: 0.1,
major_mutation_rate: 0.1,
mutation_weight_range: -0.5..0.5,
id_mapping: vec![],
lerp_amount: 0.5,
generational_lenience: 1,
survival_rate: 0.5,
};
// Use very small networks to avoid FANN memory/resource issues
let primary = Fann::new(&[5, 3, 3])?;
let secondary = Fann::new(&[5, 3, 3])?;
// Run crossbreed
let result = crossbreed(&fighter_nn, &primary, &secondary, 3)?;
// Check that the result has the correct input and output size
let shape = result.get_layer_sizes();
assert_eq!(shape[0], 5);
assert_eq!(*shape.last().unwrap(), 3);
// All hidden layers should have at least 1 neuron
for (i, &layer_size) in shape.iter().enumerate().skip(1).take(shape.len() - 2) {
assert!(layer_size > 0, "Hidden layer {} has zero neurons", i);
}
Ok(())
}
#[test] #[test]
fn major_mutation_test() -> Result<(), Box<dyn std::error::Error>> { fn major_mutation_test() -> Result<(), Box<dyn std::error::Error>> {
// Assign // Assign
@ -1493,6 +1526,51 @@ mod tests {
assert_eq!(result_set, expected); assert_eq!(result_set, expected);
} }
#[test]
fn crossbreed_neuron_arrays_secondary_last_layer_final_segment() {
// Use 3 segments and larger neuron arrays to ensure the else-if branch is covered
let segments = vec![(0, 2), (3, 5), (6, 8)];
// secondary_neurons: (id, layer)
let primary_neurons = generate_neuron_datastructure(&vec![3, 3, 3, 3, 2]);
let secondary_neurons = generate_neuron_datastructure(&vec![3, 3, 3, 3, 3]);
// The last segment is (6, 8), so neuron 9 in secondary_neurons will trigger the else-if branch
let result = crossbreed_neuron_arrays(segments, primary_neurons, secondary_neurons);
// Assert: The result should contain a secondary neuron with id 9 and layer 3
let has_secondary_9_layer_3 = result
.iter()
.any(|&(id, is_primary, layer, _)| id == 9 && !is_primary && layer == 3);
assert!(
has_secondary_9_layer_3,
"Expected a secondary neuron with id 9 in layer 3, indicating secondary_last_layer was used"
);
}
#[test]
fn crossbreed_neuron_arrays_prune_layer_exceeds_max() {
// Use the real constant from the module
let max = NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX;
let layer = 1;
// Create more than max neurons in layer 1, alternating primary/secondary
let primary_neurons = generate_neuron_datastructure(&vec![1, (max + 3) as u32, 1]);
let secondary_neurons = generate_neuron_datastructure(&vec![1, (max + 3) as u32, 1]);
// Segments: one for input, one for the large layer, one for output
let segments = vec![
(0, 0),
(1, (max + 3) as u32),
((max + 4) as u32, (max + 4) as u32),
];
let result = crossbreed_neuron_arrays(segments, primary_neurons, secondary_neurons);
// Count neurons in layer 1
let count = result.iter().filter(|(_, _, l, _)| *l == layer).count();
assert!(
count <= max,
"Layer should be pruned to NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX neurons"
);
}
#[test] #[test]
fn generate_neuron_datastructure_test() { fn generate_neuron_datastructure_test() {
// Assign // Assign
@ -1866,6 +1944,92 @@ mod tests {
} }
} }
// Setup for bias connection fallback
// We'll target hidden layer 3 (layer index 3), neuron 20 (first in that layer, is_primary = false)
let bias_layer = 3;
let bias_neuron_primary = get_bias_neuron_for_layer(bias_layer, &primary_shape).unwrap();
// Add a bias connection in primary if not present
let mut primary_connections = primary_fann.get_connections();
for connection in primary_connections.iter_mut() {
if connection.from_neuron == bias_neuron_primary {
// Set to a unique weight to verify it's not used
connection.weight = 12345.0;
}
}
primary_fann.set_connections(&primary_connections);
// Secondary network needs to have only 1 hidden layer, so it won't have a bias connection for layer 3
let secondary_shape = vec![4, 8, 6];
let mut secondary_fann = Fann::new(&secondary_shape)?;
let mut secondary_connections = secondary_fann.get_connections();
for connection in secondary_connections.iter_mut() {
connection.weight = ((connection.from_neuron * 100) + connection.to_neuron) as f32;
connection.weight = connection.weight * -1.0;
}
secondary_fann.set_connections(&secondary_connections);
let new_neurons = vec![
// Input layer: Expect 4
(0, true, 0, 0),
(1, true, 0, 1),
(2, true, 0, 2),
(3, true, 0, 3),
// Hidden Layer 1: Expect 8
(4, false, 1, 4),
(5, false, 1, 5),
(6, false, 1, 6),
(7, true, 1, 7),
(8, true, 1, 8),
(9, true, 1, 9),
(10, true, 1, 10),
(11, true, 1, 11),
// Hidden Layer 2: Expect 6
(12, true, 2, 12),
(13, true, 2, 13),
(14, true, 2, 14),
(15, true, 2, 15),
(16, true, 2, 16),
(17, true, 2, 17),
// Output Layer: Expect 4
(18, true, 3, 18),
(19, true, 3, 19),
(20, false, 3, 20),
(21, true, 3, 21),
];
let new_shape = vec![4, 8, 6, 4];
let mut new_fann = Fann::new(&new_shape)?;
// Initialize weights to 0
let mut new_connections = new_fann.get_connections();
for connection in new_connections.iter_mut() {
connection.weight = 0.0;
}
new_fann.set_connections(&new_connections);
// Act
consolidate_old_connections(
&primary_fann,
&secondary_fann,
new_shape,
new_neurons,
&mut new_fann,
);
// Assert that the fallback bias connection was used
let new_connections = new_fann.get_connections();
for connection in new_connections.iter() {
println!("{:?}", connection);
}
let found = new_connections
.iter()
.any(|c| c.from_neuron == bias_neuron_primary && c.weight == 12345.0);
assert!(
found,
"Expected fallback bias connection from primary network to be used"
);
Ok(()) Ok(())
} }
} }