| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| //! Property-based randomized testing for the core float layout algorithm. |
| |
| use std::f32::INFINITY; |
| use std::ops::Range; |
| use std::panic::{self, PanicHookInfo}; |
| use std::sync::{Mutex, MutexGuard}; |
| use std::{thread, u32}; |
| |
| use app_units::Au; |
| use layout::flow::float::{ |
| Clear, ContainingBlockPositionInfo, FloatBand, FloatBandNode, FloatBandTree, FloatContext, |
| FloatSide, PlacementInfo, |
| }; |
| use layout::geom::{LogicalRect, LogicalVec2}; |
| use num_traits::identities::Zero; |
| use quickcheck::{Arbitrary, Gen}; |
| |
| static PANIC_HOOK_MUTEX: Mutex<()> = Mutex::new(()); |
| |
| // Suppresses panic messages. Some tests need to fail and we don't want them to spam the console. |
| // Note that, because the panic hook is process-wide, tests that are expected to fail might |
| // suppress panic messages from other failing tests. To work around this, run failing tests one at |
| // a time or use only a single test thread. |
| struct PanicMsgSuppressor<'a> { |
| #[allow(dead_code)] |
| mutex_guard: MutexGuard<'a, ()>, |
| prev_hook: Option<Box<dyn Fn(&PanicHookInfo<'_>) + 'static + Sync + Send>>, |
| } |
| |
| impl<'a> PanicMsgSuppressor<'a> { |
| fn new(mutex_guard: MutexGuard<'a, ()>) -> PanicMsgSuppressor<'a> { |
| let prev_hook = panic::take_hook(); |
| panic::set_hook(Box::new(|_| ())); |
| PanicMsgSuppressor { |
| mutex_guard, |
| prev_hook: Some(prev_hook), |
| } |
| } |
| } |
| |
| impl<'a> Drop for PanicMsgSuppressor<'a> { |
| fn drop(&mut self) { |
| panic::set_hook(self.prev_hook.take().unwrap()) |
| } |
| } |
| |
| // AA tree helpers |
| |
| #[derive(Clone, Debug)] |
| struct FloatBandWrapper(FloatBand); |
| |
| impl Arbitrary for FloatBandWrapper { |
| fn arbitrary(generator: &mut Gen) -> FloatBandWrapper { |
| let top: u32 = u32::arbitrary(generator); |
| let inline_start: Option<u32> = Some(u32::arbitrary(generator)); |
| let inline_end: Option<u32> = Some(u32::arbitrary(generator)); |
| |
| FloatBandWrapper(FloatBand { |
| top: Au::from_f32_px(top as f32), |
| inline_start: inline_start.map(|value| Au::from_f32_px(value as f32)), |
| inline_end: inline_end.map(|value| Au::from_f32_px(value as f32)), |
| }) |
| } |
| } |
| |
| #[derive(Clone, Debug)] |
| struct FloatRangeInput { |
| start_index: u32, |
| side: FloatSide, |
| length: u32, |
| } |
| |
| impl Arbitrary for FloatRangeInput { |
| fn arbitrary(generator: &mut Gen) -> FloatRangeInput { |
| let start_index: u32 = Arbitrary::arbitrary(generator); |
| let is_left: bool = Arbitrary::arbitrary(generator); |
| let length: u32 = Arbitrary::arbitrary(generator); |
| FloatRangeInput { |
| start_index, |
| side: if is_left { |
| FloatSide::InlineStart |
| } else { |
| FloatSide::InlineEnd |
| }, |
| length, |
| } |
| } |
| } |
| |
| // AA tree predicates |
| |
| fn check_node_ordering(node: &FloatBandNode) { |
| let mid = node.band.top; |
| if let Some(ref left) = node.left.0 { |
| assert!(left.band.top < mid); |
| } |
| if let Some(ref right) = node.right.0 { |
| assert!(right.band.top > mid); |
| } |
| if let Some(ref left) = node.left.0 { |
| check_node_ordering(left); |
| } |
| if let Some(ref right) = node.right.0 { |
| check_node_ordering(right); |
| } |
| } |
| |
| // https://en.wikipedia.org/wiki/AA_tree#Balancing_rotations |
| fn check_node_balance(node: &FloatBandNode) { |
| // 1. The level of every leaf node is one. |
| if node.left.0.is_none() && node.right.0.is_none() { |
| assert_eq!(node.level, 1); |
| } |
| // 2. The level of every left child is exactly one less than that of its parent. |
| if let Some(ref left) = node.left.0 { |
| assert_eq!(left.level, node.level - 1); |
| } |
| // 3. The level of every right child is equal to or one less than that of its parent. |
| if let Some(ref right) = node.right.0 { |
| assert!(right.level == node.level || right.level == node.level - 1); |
| } |
| // 4. The level of every right grandchild is strictly less than that of its grandparent. |
| if let Some(ref right) = node.right.0 { |
| if let Some(ref right_right) = right.right.0 { |
| assert!(right_right.level < node.level); |
| } |
| } |
| // 5. Every node of level greater than one has two children. |
| if node.level > 1 { |
| assert!(node.left.0.is_some() && node.right.0.is_some()); |
| } |
| } |
| |
| fn check_tree_ordering(tree: FloatBandTree) { |
| if let Some(ref root) = tree.root.0 { |
| check_node_ordering(root); |
| } |
| } |
| |
| fn check_tree_balance(tree: FloatBandTree) { |
| if let Some(ref root) = tree.root.0 { |
| check_node_balance(root); |
| } |
| } |
| |
| fn check_tree_find(tree: &FloatBandTree, block_position: Au, sorted_bands: &[FloatBand]) { |
| let found_band = tree |
| .find(block_position) |
| .expect("Couldn't find the band in the tree!"); |
| let reference_band_index = sorted_bands |
| .iter() |
| .position(|band| band.top > block_position) |
| .expect("Couldn't find the reference band!") - |
| 1; |
| let reference_band = &sorted_bands[reference_band_index]; |
| assert_eq!(found_band.top, reference_band.top); |
| assert_eq!(found_band.inline_start, reference_band.inline_start); |
| assert_eq!(found_band.inline_end, reference_band.inline_end); |
| } |
| |
| fn check_tree_find_next(tree: &FloatBandTree, block_position: Au, sorted_bands: &[FloatBand]) { |
| let found_band = tree |
| .find_next(block_position) |
| .expect("Couldn't find the band in the tree!"); |
| let reference_band_index = sorted_bands |
| .iter() |
| .position(|band| band.top > block_position) |
| .expect("Couldn't find the reference band!"); |
| let reference_band = &sorted_bands[reference_band_index]; |
| assert_eq!(found_band.top, reference_band.top); |
| assert_eq!(found_band.inline_start, reference_band.inline_start); |
| assert_eq!(found_band.inline_end, reference_band.inline_end); |
| } |
| |
| fn check_node_range_setting( |
| node: &FloatBandNode, |
| block_range: &Range<Au>, |
| side: FloatSide, |
| value: Au, |
| ) { |
| if node.band.top >= block_range.start && node.band.top < block_range.end { |
| match side { |
| FloatSide::InlineStart => assert!(node.band.inline_start.unwrap() >= value), |
| FloatSide::InlineEnd => assert!(node.band.inline_end.unwrap() <= value), |
| } |
| } |
| |
| if let Some(ref left) = node.left.0 { |
| check_node_range_setting(left, block_range, side, value) |
| } |
| if let Some(ref right) = node.right.0 { |
| check_node_range_setting(right, block_range, side, value) |
| } |
| } |
| |
| fn check_tree_range_setting( |
| tree: &FloatBandTree, |
| block_range: &Range<Au>, |
| side: FloatSide, |
| value: Au, |
| ) { |
| if let Some(ref root) = tree.root.0 { |
| check_node_range_setting(root, block_range, side, value) |
| } |
| } |
| |
| // AA tree unit tests |
| |
| // Tests that the tree is a properly-ordered binary tree. |
| #[test] |
| fn test_tree_ordering() { |
| let f: fn(Vec<FloatBandWrapper>) = check; |
| quickcheck::quickcheck(f); |
| fn check(bands: Vec<FloatBandWrapper>) { |
| let mut tree = FloatBandTree::new(); |
| for FloatBandWrapper(band) in bands { |
| tree = tree.insert(band); |
| } |
| check_tree_ordering(tree); |
| } |
| } |
| |
| // Tests that the tree is balanced (i.e. AA tree invariants are maintained). |
| #[test] |
| fn test_tree_balance() { |
| let f: fn(Vec<FloatBandWrapper>) = check; |
| quickcheck::quickcheck(f); |
| fn check(bands: Vec<FloatBandWrapper>) { |
| let mut tree = FloatBandTree::new(); |
| for FloatBandWrapper(band) in bands { |
| tree = tree.insert(band); |
| } |
| check_tree_balance(tree); |
| } |
| } |
| |
| // Tests that the `find()` method works. |
| #[test] |
| fn test_tree_find() { |
| let f: fn(Vec<FloatBandWrapper>, Vec<u16>) = check; |
| quickcheck::quickcheck(f); |
| fn check(bands: Vec<FloatBandWrapper>, lookups: Vec<u16>) { |
| let mut bands: Vec<FloatBand> = bands.into_iter().map(|band| band.0).collect(); |
| bands.push(FloatBand { |
| top: Au::zero(), |
| inline_start: None, |
| inline_end: None, |
| }); |
| bands.push(FloatBand { |
| top: Au::from_f32_px(INFINITY), |
| inline_start: None, |
| inline_end: None, |
| }); |
| let mut tree = FloatBandTree::new(); |
| for band in &bands { |
| tree = tree.insert(*band); |
| } |
| bands.sort_by(|a, b| a.top.partial_cmp(&b.top).unwrap()); |
| for lookup in lookups { |
| check_tree_find(&tree, Au::from_f32_px(lookup as f32), &bands); |
| } |
| } |
| } |
| |
| // Tests that the `find_next()` method works. |
| #[test] |
| fn test_tree_find_next() { |
| let f: fn(Vec<FloatBandWrapper>, Vec<u16>) = check; |
| quickcheck::quickcheck(f); |
| fn check(bands: Vec<FloatBandWrapper>, lookups: Vec<u16>) { |
| let mut bands: Vec<FloatBand> = bands.into_iter().map(|band| band.0).collect(); |
| bands.push(FloatBand { |
| top: Au::zero(), |
| inline_start: None, |
| inline_end: None, |
| }); |
| bands.push(FloatBand { |
| top: Au::from_f32_px(INFINITY), |
| inline_start: None, |
| inline_end: None, |
| }); |
| bands.sort_by(|a, b| a.top.partial_cmp(&b.top).unwrap()); |
| bands.dedup_by(|a, b| a.top == b.top); |
| let mut tree = FloatBandTree::new(); |
| for band in &bands { |
| tree = tree.insert(*band); |
| } |
| for lookup in lookups { |
| check_tree_find_next(&tree, Au::from_f32_px(lookup as f32), &bands); |
| } |
| } |
| } |
| |
| // Tests that `set_range()` works. |
| #[test] |
| fn test_tree_range_setting() { |
| let f: fn(Vec<FloatBandWrapper>, Vec<FloatRangeInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(bands: Vec<FloatBandWrapper>, ranges: Vec<FloatRangeInput>) { |
| let mut tree = FloatBandTree::new(); |
| for FloatBandWrapper(band) in &bands { |
| tree = tree.insert(*band); |
| } |
| |
| let mut tops: Vec<Au> = bands.iter().map(|band| band.0.top).collect(); |
| tops.push(Au::from_f32_px(INFINITY)); |
| tops.sort_by(|a, b| a.to_px().partial_cmp(&b.to_px()).unwrap()); |
| |
| for range in ranges { |
| let start = range.start_index.min(tops.len() as u32 - 1); |
| let end = (range.start_index as u64 + range.length as u64).min(tops.len() as u64 - 1); |
| let block_range = tops[start as usize]..tops[end as usize]; |
| let length = Au::from_px(range.length as i32); |
| let new_tree = tree.set_range(&block_range, range.side, length); |
| check_tree_range_setting(&new_tree, &block_range, range.side, length); |
| } |
| } |
| } |
| |
| // Float predicates |
| |
| #[derive(Clone, Debug)] |
| struct FloatInput { |
| // Information needed to place the float. |
| info: PlacementInfo, |
| // The float may be placed no higher than this line. This simulates the effect of line boxes |
| // per CSS 2.1 ยง 9.5.1 rule 6. |
| ceiling: Au, |
| /// Containing block positioning information, which is used to track the current offsets |
| /// from the float containing block formatting context to the current containing block. |
| containing_block_info: ContainingBlockPositionInfo, |
| } |
| |
| impl Arbitrary for FloatInput { |
| fn arbitrary(generator: &mut Gen) -> FloatInput { |
| // See #29819: Limit the maximum size of all f32 values here because |
| // massive float values will start to introduce very bad floating point |
| // errors. |
| // TODO: This should be be addressed in a better way. Perhaps we should |
| // reintroduce the use of app_units in Layout 2020. |
| let width = u32::arbitrary(generator) % 12345; |
| let height = u32::arbitrary(generator) % 12345; |
| let is_left = bool::arbitrary(generator); |
| let ceiling = u32::arbitrary(generator) % 12345; |
| let left = u32::arbitrary(generator) % 12345; |
| let containing_block_width = u32::arbitrary(generator) % 12345; |
| let clear = u8::arbitrary(generator); |
| FloatInput { |
| info: PlacementInfo { |
| size: LogicalVec2 { |
| inline: Au::from_f32_px(width as f32), |
| block: Au::from_f32_px(height as f32), |
| }, |
| side: if is_left { |
| FloatSide::InlineStart |
| } else { |
| FloatSide::InlineEnd |
| }, |
| clear: new_clear(clear), |
| }, |
| ceiling: Au::from_f32_px(ceiling as f32), |
| containing_block_info: ContainingBlockPositionInfo::new_with_inline_offsets( |
| Au::from_f32_px(left as f32), |
| Au::from_f32_px(left as f32 + containing_block_width as f32), |
| ), |
| } |
| } |
| |
| fn shrink(&self) -> Box<dyn Iterator<Item = FloatInput>> { |
| let mut this = (*self).clone(); |
| let mut shrunk = false; |
| if let Some(inline_size) = self.info.size.inline.to_px().shrink().next() { |
| this.info.size.inline = Au::from_px(inline_size); |
| shrunk = true; |
| } |
| if let Some(block_size) = self.info.size.block.to_px().shrink().next() { |
| this.info.size.block = Au::from_px(block_size); |
| shrunk = true; |
| } |
| if let Some(clear) = (self.info.clear as u8).shrink().next() { |
| this.info.clear = new_clear(clear); |
| shrunk = true; |
| } |
| if let Some(left) = self |
| .containing_block_info |
| .inline_start |
| .to_px() |
| .shrink() |
| .next() |
| { |
| this.containing_block_info.inline_start = Au::from_px(left); |
| shrunk = true; |
| } |
| if let Some(right) = self |
| .containing_block_info |
| .inline_end |
| .to_px() |
| .shrink() |
| .next() |
| { |
| this.containing_block_info.inline_end = Au::from_px(right); |
| shrunk = true; |
| } |
| if let Some(ceiling) = self.ceiling.to_px().shrink().next() { |
| this.ceiling = Au::from_px(ceiling); |
| shrunk = true; |
| } |
| if shrunk { |
| quickcheck::single_shrinker(this) |
| } else { |
| quickcheck::empty_shrinker() |
| } |
| } |
| } |
| |
| fn new_clear(value: u8) -> Clear { |
| match value & 3 { |
| 0 => Clear::None, |
| 1 => Clear::InlineStart, |
| 2 => Clear::InlineEnd, |
| _ => Clear::Both, |
| } |
| } |
| |
| #[derive(Clone)] |
| struct FloatPlacement { |
| float_context: FloatContext, |
| placed_floats: Vec<PlacedFloat>, |
| } |
| |
| // Information about the placement of a float. |
| #[derive(Clone)] |
| struct PlacedFloat { |
| origin: LogicalVec2<Au>, |
| info: PlacementInfo, |
| ceiling: Au, |
| containing_block_info: ContainingBlockPositionInfo, |
| } |
| |
| impl Drop for FloatPlacement { |
| fn drop(&mut self) { |
| if !thread::panicking() { |
| return; |
| } |
| |
| // Dump the float context for debugging. |
| eprintln!("Failing float placement:"); |
| for placed_float in &self.placed_floats { |
| eprintln!( |
| " * {:?} @ {:?}, T {:?} L {:?} R {:?}", |
| placed_float.info, |
| placed_float.origin, |
| placed_float.ceiling, |
| placed_float.containing_block_info.inline_start, |
| placed_float.containing_block_info.inline_end, |
| ); |
| } |
| eprintln!("Bands:\n{:?}\n", self.float_context.bands); |
| } |
| } |
| |
| impl PlacedFloat { |
| fn rect(&self) -> LogicalRect<Au> { |
| LogicalRect { |
| start_corner: self.origin, |
| size: self.info.size, |
| } |
| } |
| } |
| |
| impl FloatPlacement { |
| fn place(floats: Vec<FloatInput>) -> FloatPlacement { |
| let mut float_context = FloatContext::new(Au::from_f32_px(INFINITY)); |
| let mut placed_floats = vec![]; |
| for float in floats { |
| let ceiling = float.ceiling; |
| float_context.set_ceiling_from_non_floats(ceiling); |
| float_context.containing_block_info = float.containing_block_info; |
| placed_floats.push(PlacedFloat { |
| origin: float_context.add_float(&float.info), |
| info: float.info, |
| ceiling, |
| containing_block_info: float.containing_block_info, |
| }) |
| } |
| FloatPlacement { |
| float_context, |
| placed_floats, |
| } |
| } |
| } |
| |
| // From CSS 2.1 ยง 9.5.1 [1]. |
| // |
| // [1]: https://www.w3.org/TR/CSS2/visuren.html#float-position |
| |
| // 1. The left outer edge of a left-floating box may not be to the left of the left edge of its |
| // containing block. An analogous rule holds for right-floating elements. |
| fn check_floats_rule_1(placement: &FloatPlacement) { |
| for placed_float in &placement.placed_floats { |
| match placed_float.info.side { |
| FloatSide::InlineStart => assert!( |
| placed_float.origin.inline >= placed_float.containing_block_info.inline_start |
| ), |
| FloatSide::InlineEnd => { |
| assert!( |
| placed_float.rect().max_inline_position() <= |
| placed_float.containing_block_info.inline_end |
| ) |
| }, |
| } |
| } |
| } |
| |
| // 2. If the current box is left-floating, and there are any left-floating boxes generated by |
| // elements earlier in the source document, then for each such earlier box, either the left |
| // outer edge of the current box must be to the right of the right outer edge of the earlier |
| // box, or its top must be lower than the bottom of the earlier box. Analogous rules hold for |
| // right-floating boxes. |
| fn check_floats_rule_2(placement: &FloatPlacement) { |
| for (this_float_index, this_float) in placement.placed_floats.iter().enumerate() { |
| for prev_float in &placement.placed_floats[0..this_float_index] { |
| match (this_float.info.side, prev_float.info.side) { |
| (FloatSide::InlineStart, FloatSide::InlineStart) => { |
| assert!( |
| this_float.origin.inline >= prev_float.rect().max_inline_position() || |
| this_float.origin.block >= prev_float.rect().max_block_position() |
| ); |
| }, |
| (FloatSide::InlineEnd, FloatSide::InlineEnd) => { |
| assert!( |
| this_float.rect().max_inline_position() <= prev_float.origin.inline || |
| this_float.origin.block >= prev_float.rect().max_block_position() |
| ); |
| }, |
| (FloatSide::InlineStart, FloatSide::InlineEnd) | |
| (FloatSide::InlineEnd, FloatSide::InlineStart) => {}, |
| } |
| } |
| } |
| } |
| |
| // 3. The right outer edge of a left-floating box may not be to the right of the left outer edge of |
| // any right-floating box that is next to it. Analogous rules hold for right-floating elements. |
| fn check_floats_rule_3(placement: &FloatPlacement) { |
| for (this_float_index, this_float) in placement.placed_floats.iter().enumerate() { |
| for other_float in &placement.placed_floats[0..this_float_index] { |
| // This logic to check intersection is complicated by the fact that we need to treat |
| // zero-height floats later in the document as "next to" floats earlier in the |
| // document. Otherwise we might end up with a situation like: |
| // |
| // <div id="a" style="float: left; width: 32px; height: 32px"></div> |
| // <div id="b" style="float: right; width: 0px; height: 0px"></div> |
| // |
| // Where the top of `b` should probably be 32px per Rule 3, but unless this distinction |
| // is made the top of `b` could legally be 0px. |
| if this_float.origin.block >= other_float.rect().max_block_position() || |
| (this_float.info.size.block.is_zero() && |
| this_float.rect().max_block_position() < other_float.origin.block) || |
| (this_float.info.size.block > Au::zero() && |
| this_float.rect().max_block_position() <= other_float.origin.block) |
| { |
| continue; |
| } |
| |
| match (this_float.info.side, other_float.info.side) { |
| (FloatSide::InlineStart, FloatSide::InlineEnd) => { |
| assert!(this_float.rect().max_inline_position() <= other_float.origin.inline); |
| }, |
| (FloatSide::InlineEnd, FloatSide::InlineStart) => { |
| assert!(this_float.origin.inline >= other_float.rect().max_inline_position()); |
| }, |
| (FloatSide::InlineStart, FloatSide::InlineStart) | |
| (FloatSide::InlineEnd, FloatSide::InlineEnd) => {}, |
| } |
| } |
| } |
| } |
| |
| // 4. A floating box's outer top may not be higher than the top of its containing block. When the |
| // float occurs between two collapsing margins, the float is positioned as if it had an |
| // otherwise empty anonymous block parent taking part in the flow. The position of such a parent |
| // is defined by the rules in the section on margin collapsing. |
| fn check_floats_rule_4(placement: &FloatPlacement) { |
| for placed_float in &placement.placed_floats { |
| assert!(placed_float.origin.block >= Au::zero()); |
| } |
| } |
| |
| // 5. The outer top of a floating box may not be higher than the outer top of any block or floated |
| // box generated by an element earlier in the source document. |
| fn check_floats_rule_5(placement: &FloatPlacement) { |
| let mut block_position = Au::zero(); |
| for placed_float in &placement.placed_floats { |
| assert!(placed_float.origin.block >= block_position); |
| block_position = placed_float.origin.block; |
| } |
| } |
| |
| // 6. The outer top of an element's floating box may not be higher than the top of any line-box |
| // containing a box generated by an element earlier in the source document. |
| fn check_floats_rule_6(placement: &FloatPlacement) { |
| for placed_float in &placement.placed_floats { |
| assert!(placed_float.origin.block >= placed_float.ceiling); |
| } |
| } |
| |
| // 7. A left-floating box that has another left-floating box to its left may not have its right |
| // outer edge to the right of its containing block's right edge. (Loosely: a left float may not |
| // stick out at the right edge, unless it is already as far to the left as possible.) An |
| // analogous rule holds for right-floating elements. |
| fn check_floats_rule_7(placement: &FloatPlacement) { |
| for (placed_float_index, placed_float) in placement.placed_floats.iter().enumerate() { |
| // Only consider floats that stick out. |
| match placed_float.info.side { |
| FloatSide::InlineStart => { |
| if placed_float.rect().max_inline_position() <= |
| placed_float.containing_block_info.inline_end |
| { |
| continue; |
| } |
| }, |
| FloatSide::InlineEnd => { |
| if placed_float.origin.inline >= placed_float.containing_block_info.inline_start { |
| continue; |
| } |
| }, |
| } |
| |
| // Make sure there are no previous floats to the left or right. |
| for prev_float in &placement.placed_floats[0..placed_float_index] { |
| assert!( |
| prev_float.info.side != placed_float.info.side || |
| prev_float.rect().max_block_position() <= placed_float.origin.block || |
| prev_float.origin.block >= placed_float.rect().max_block_position() |
| ); |
| } |
| } |
| } |
| |
| // 8. A floating box must be placed as high as possible. |
| fn check_floats_rule_8(floats_and_perturbations: Vec<(FloatInput, u32)>) { |
| let floats = floats_and_perturbations |
| .iter() |
| .map(|(float, _)| (*float).clone()) |
| .collect(); |
| let placement = FloatPlacement::place(floats); |
| |
| for (float_index, &(_, perturbation)) in floats_and_perturbations.iter().enumerate() { |
| if perturbation == 0 { |
| continue; |
| } |
| |
| let mut placement = placement.clone(); |
| placement.placed_floats[float_index].origin.block -= Au::from_f32_px(perturbation as f32); |
| |
| let result = { |
| let mutex_guard = PANIC_HOOK_MUTEX.lock().unwrap(); |
| let _suppressor = PanicMsgSuppressor::new(mutex_guard); |
| panic::catch_unwind(|| check_basic_float_rules(&placement)) |
| }; |
| assert!(result.is_err()); |
| } |
| } |
| |
| // 9. A left-floating box must be put as far to the left as possible, a right-floating box as far |
| // to the right as possible. A higher position is preferred over one that is further to the |
| // left/right. |
| fn check_floats_rule_9(floats_and_perturbations: Vec<(FloatInput, u32)>) { |
| let floats = floats_and_perturbations |
| .iter() |
| .map(|(float, _)| (*float).clone()) |
| .collect(); |
| let placement = FloatPlacement::place(floats); |
| |
| for (float_index, &(_, perturbation)) in floats_and_perturbations.iter().enumerate() { |
| if perturbation == 0 { |
| continue; |
| } |
| |
| let mut placement = placement.clone(); |
| { |
| let placed_float = &mut placement.placed_floats[float_index]; |
| let perturbation = Au::from_f32_px(perturbation as f32); |
| match placed_float.info.side { |
| FloatSide::InlineStart => placed_float.origin.inline -= perturbation, |
| FloatSide::InlineEnd => placed_float.origin.inline += perturbation, |
| } |
| } |
| |
| let result = { |
| let mutex_guard = PANIC_HOOK_MUTEX.lock().unwrap(); |
| let _suppressor = PanicMsgSuppressor::new(mutex_guard); |
| panic::catch_unwind(|| check_basic_float_rules(&placement)) |
| }; |
| assert!(result.is_err()); |
| } |
| } |
| |
| // From CSS 2.1 ยง 9.5.2 (https://www.w3.org/TR/CSS2/visuren.html#propdef-clear): |
| // |
| // 10. The top outer edge of the float must be below the bottom outer edge of all earlier |
| // left-floating boxes (in the case of 'clear: left'), or all earlier right-floating boxes (in |
| // the case of 'clear: right'), or both ('clear: both'). |
| fn check_floats_rule_10(placement: &FloatPlacement) { |
| let mut block_position = Au::zero(); |
| for placed_float in &placement.placed_floats { |
| assert!(placed_float.origin.block >= block_position); |
| block_position = placed_float.origin.block; |
| } |
| |
| for (this_float_index, this_float) in placement.placed_floats.iter().enumerate() { |
| if this_float.info.clear == Clear::None { |
| continue; |
| } |
| |
| for other_float in &placement.placed_floats[0..this_float_index] { |
| // This logic to check intersection is complicated by the fact that we need to treat |
| // zero-height floats later in the document as "next to" floats earlier in the |
| // document. Otherwise we might end up with a situation like: |
| // |
| // <div id="a" style="float: left; width: 32px; height: 32px"></div> |
| // <div id="b" style="float: right; width: 0px; height: 0px"></div> |
| // |
| // Where the top of `b` should probably be 32px per Rule 3, but unless this distinction |
| // is made the top of `b` could legally be 0px. |
| if this_float.origin.block >= other_float.rect().max_block_position() || |
| (this_float.info.size.block.is_zero() && |
| this_float.rect().max_block_position() < other_float.origin.block) || |
| (this_float.info.size.block > Au::zero() && |
| this_float.rect().max_block_position() <= other_float.origin.block) |
| { |
| continue; |
| } |
| |
| match this_float.info.clear { |
| Clear::InlineStart => assert_ne!(other_float.info.side, FloatSide::InlineStart), |
| Clear::InlineEnd => assert_ne!(other_float.info.side, FloatSide::InlineEnd), |
| Clear::Both => assert!(false), |
| Clear::None => unreachable!(), |
| } |
| } |
| } |
| } |
| |
| // Checks that rule 1-7 and rule 10 hold (i.e. all rules that don't specify that floats are placed |
| // "as far as possible" in some direction). |
| fn check_basic_float_rules(placement: &FloatPlacement) { |
| check_floats_rule_1(placement); |
| check_floats_rule_2(placement); |
| check_floats_rule_3(placement); |
| check_floats_rule_4(placement); |
| check_floats_rule_5(placement); |
| check_floats_rule_6(placement); |
| check_floats_rule_7(placement); |
| check_floats_rule_10(placement); |
| } |
| |
| // Float unit tests |
| |
| #[test] |
| fn test_floats_rule_1() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_1(&FloatPlacement::place(floats)); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_2() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_2(&FloatPlacement::place(floats)); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_3() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_3(&FloatPlacement::place(floats)); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_4() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_4(&FloatPlacement::place(floats)); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_5() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_5(&FloatPlacement::place(floats)); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_6() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_6(&FloatPlacement::place(floats)); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_7() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_7(&FloatPlacement::place(floats)); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_8() { |
| let f: fn(Vec<(FloatInput, u32)>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<(FloatInput, u32)>) { |
| check_floats_rule_8(floats); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_9() { |
| let f: fn(Vec<(FloatInput, u32)>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<(FloatInput, u32)>) { |
| check_floats_rule_9(floats); |
| } |
| } |
| |
| #[test] |
| fn test_floats_rule_10() { |
| let f: fn(Vec<FloatInput>) = check; |
| quickcheck::quickcheck(f); |
| fn check(floats: Vec<FloatInput>) { |
| check_floats_rule_10(&FloatPlacement::place(floats)); |
| } |
| } |