use super::*;
fn id(byte: u8) -> MachineId {
MachineId::try_from(&[byte; MachineId::LEN][..]).unwrap()
}
fn rect(byte: u8, x: i32, y: i32) -> Placement {
Placement {
id: id(byte),
x,
y,
width: 100,
height: 100,
blocked: 0,
}
}
#[test]
fn validate_invalid_rectangles_rejects_and_touching_accepts() {
let cases = [
(
vec![Placement {
width: 0,
..rect(1, 0, 0)
}],
false,
),
(
vec![Placement {
height: 0,
..rect(1, 0, 0)
}],
false,
),
(vec![rect(1, 0, 0), rect(1, 100, 0)], false),
(vec![rect(1, 0, 0), rect(2, 50, 50)], false),
(vec![rect(1, i32::MAX, 0)], false),
(vec![rect(1, 0, i32::MAX)], false),
(vec![rect(1, 0, 0), rect(2, 100, 0)], true),
(vec![rect(1, -100, -100), rect(2, 0, -100)], true),
(vec![rect(1, 0, 0); limits::LAYOUT_MACHINES_MAX + 1], false),
];
for (placements, valid) in cases {
let result = validate(&placements);
assert_eq!(result.is_ok(), valid, "{placements:?}");
}
}
#[test]
fn merge_version_and_author_orders_and_reports_lost_edit() {
let cases = [
(2, 1, 1, 2, Merge::Unchanged),
(
1,
1,
2,
2,
Merge::Replaced {
lost_own_edit: false,
},
),
(1, 2, 1, 1, Merge::Unchanged),
(
1,
1,
1,
2,
Merge::Replaced {
lost_own_edit: true,
},
),
(
1,
2,
1,
3,
Merge::Replaced {
lost_own_edit: false,
},
),
(1, 1, 1, 1, Merge::Unchanged),
];
for (version, author, next_version, next_author, expected) in cases {
let mut layout = Layout {
version,
by: id(author),
placements: vec![],
};
let incoming = Layout {
version: next_version,
by: id(next_author),
placements: vec![],
};
let before = layout.clone();
let wanted = if expected == Merge::Unchanged {
before
} else {
incoming.clone()
};
let outcome = layout.merge(incoming, id(1));
assert_eq!(outcome, expected);
assert_eq!(layout, wanted);
}
}
#[test]
fn exit_touching_edges_maps_offset_and_gaps_are_walls() {
let layout = Layout::empty(id(1))
.edit(vec![rect(1, 0, 0), rect(2, 100, 50)], id(1))
.unwrap();
let cases = [
(0.75, Some(0.25)),
(0.5, Some(0.0)),
(0.25, None),
(1.5, Some(0.5)),
(-1.0, None),
(f32::NAN, None),
];
for (along, offset) in cases {
let landing = layout.exit(id(1), Edge::Right, along, |_| true);
assert_eq!(
landing.map(|(target, landing)| (target, landing.edge, landing.offset)),
offset.map(|offset| (id(2), Edge::Left as i32, offset))
);
}
}
#[test]
fn edit_bumps_version_and_round_trips_wire_but_exhausted_version_errors() {
let layout = Layout::empty(id(1));
let placements = vec![rect(1, 0, 0)];
let exhausted = Layout {
version: u64::MAX,
by: id(1),
placements: vec![],
};
let edited = layout.edit(placements.clone(), id(2)).unwrap();
let overflow = exhausted.edit(vec![], id(1));
assert!(matches!(overflow, Err(LayoutError::VersionExhausted)));
assert_eq!(edited.version(), 1);
assert_eq!(edited.by(), id(2));
assert_eq!(edited.placements(), placements);
assert_eq!(Layout::try_from(&edited.to_proto()).unwrap(), edited);
}
#[test]
fn try_from_malformed_ids_and_overlapping_wire_rejects() {
let valid = Layout::empty(id(1))
.edit(vec![rect(1, 0, 0), rect(2, 100, 0)], id(1))
.unwrap()
.to_proto();
let mut bad_author = valid.clone();
bad_author.by.clear();
let mut bad_machine = valid.clone();
bad_machine.machines[0].id.clear();
let mut overlap = valid.clone();
overlap.machines[1].x = 50;
let mut huge_version = valid;
huge_version.version = limits::COUNTER_MAX + 1;
for wire in [bad_author, bad_machine, overlap, huge_version] {
let result = Layout::try_from(&wire);
assert!(result.is_err());
}
}
#[test]
fn exit_blocked_thirds_only_prevent_source_segment_crossings() {
for (edge, x, y, index) in [
(Edge::Left, -100, 0, 0),
(Edge::Top, 0, -100, 1),
(Edge::Right, 100, 0, 2),
(Edge::Bottom, 0, 100, 3),
] {
for third in 0..3 {
let source = Placement {
blocked: 1 << (index * 3 + third),
..rect(1, 0, 0)
};
let target = Placement {
blocked: u16::MAX,
..rect(2, x, y)
};
let layout = Layout::empty(id(1))
.edit(vec![source, target], id(1))
.unwrap();
for (segment, along) in [0.1, 0.5, 0.9].into_iter().enumerate() {
let exit = layout.exit(id(1), edge, along, |_| true);
assert_eq!(exit.is_none(), segment == third);
}
}
}
}
#[test]
fn exit_corner_diagonal_neighbours_land_on_opposite_edge_at_shared_corner() {
let layout = Layout::empty(id(1))
.edit(
vec![
rect(1, 0, 0),
rect(2, -100, -100),
rect(3, 100, -100),
rect(4, -100, 100),
rect(5, 100, 100),
],
id(1),
)
.unwrap();
for (corner, edge, target, opposite, offset) in [
(Corner::TopLeft, Edge::Left, 2, Edge::Right, 1.0),
(Corner::TopLeft, Edge::Top, 2, Edge::Bottom, 1.0),
(Corner::TopRight, Edge::Right, 3, Edge::Left, 1.0),
(Corner::TopRight, Edge::Top, 3, Edge::Bottom, 0.0),
(Corner::BottomLeft, Edge::Left, 4, Edge::Right, 0.0),
(Corner::BottomLeft, Edge::Bottom, 4, Edge::Top, 1.0),
(Corner::BottomRight, Edge::Right, 5, Edge::Left, 0.0),
(Corner::BottomRight, Edge::Bottom, 5, Edge::Top, 0.0),
] {
let (target_id, landing) = layout.exit_corner(id(1), edge, corner, |_| true).unwrap();
assert_eq!(
(target_id, landing.edge, landing.offset),
(id(target), opposite as i32, offset)
);
}
}
#[test]
fn try_from_unknown_and_absent_blocked_bits_masks_to_known_segments() {
use a7n_proto::{FrameKind, decode_frame, encode_frame};
for (blocked, expected) in [(u32::MAX, BLOCKED_MASK), (0, 0)] {
let mut wire = Layout::empty(id(1))
.edit(vec![rect(1, 0, 0)], id(1))
.unwrap()
.to_proto();
wire.machines[0].blocked = blocked;
let mut bytes = Vec::new();
encode_frame(&wire, &mut bytes);
let (decoded, _) = decode_frame::<v1::Layout>(&bytes, FrameKind::Control)
.unwrap()
.unwrap();
let layout = Layout::try_from(&decoded).unwrap();
assert_eq!(u32::from(layout.placements()[0].blocked), expected);
assert_eq!(layout.to_proto().machines[0].blocked, expected);
}
}