diff options
-rw-r--r-- | Cargo.lock | 14 | ||||
-rw-r--r-- | components/layout_2020/Cargo.toml | 4 | ||||
-rw-r--r-- | components/layout_2020/flow/float.rs | 473 | ||||
-rw-r--r-- | components/layout_2020/flow/mod.rs | 4 | ||||
-rw-r--r-- | components/layout_2020/geom.rs | 22 | ||||
-rw-r--r-- | components/layout_2020/lib.rs | 6 | ||||
-rw-r--r-- | components/layout_2020/tests/floats.rs | 823 | ||||
-rw-r--r-- | python/servo/testing_commands.py | 13 | ||||
-rw-r--r-- | servo-tidy.toml | 1 | ||||
-rw-r--r-- | tests/unit/style/parsing/mod.rs | 14 |
10 files changed, 1354 insertions, 20 deletions
diff --git a/Cargo.lock b/Cargo.lock index d096614da3c..546e53460b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2912,12 +2912,14 @@ dependencies = [ "gfx_traits", "html5ever", "ipc-channel", + "lazy_static", "libc", "log", "mitochondria", "msg", "net_traits", "parking_lot 0.10.2", + "quickcheck", "range", "rayon", "rayon_croissant", @@ -4366,6 +4368,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda5fe9b71976e62bc81b781206aaa076401769b2143379d3eb2118388babac4" [[package]] +name = "quickcheck" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +dependencies = [ + "env_logger", + "log", + "rand", + "rand_core", +] + +[[package]] name = "quote" version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/components/layout_2020/Cargo.toml b/components/layout_2020/Cargo.toml index 886549971d6..6ca3d709627 100644 --- a/components/layout_2020/Cargo.toml +++ b/components/layout_2020/Cargo.toml @@ -44,3 +44,7 @@ style = { path = "../style", features = ["servo", "servo-layout-2020"] } style_traits = { path = "../style_traits" } unicode-script = { version = "0.3", features = ["harfbuzz"] } webrender_api = { git = "https://github.com/servo/webrender" } + +[dev-dependencies] +lazy_static = "1" +quickcheck = "0.9" diff --git a/components/layout_2020/flow/float.rs b/components/layout_2020/flow/float.rs index 22b0b1bb66e..5cb349310e3 100644 --- a/components/layout_2020/flow/float.rs +++ b/components/layout_2020/flow/float.rs @@ -2,29 +2,494 @@ * 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/. */ +//! Float layout. +//! +//! See CSS 2.1 § 9.5.1: https://www.w3.org/TR/CSS2/visuren.html#float-position + use crate::context::LayoutContext; use crate::dom_traversal::{Contents, NodeAndStyleInfo, NodeExt}; use crate::formatting_contexts::IndependentFormattingContext; +use crate::geom::flow_relative::{Rect, Vec2}; use crate::style_ext::DisplayInside; +use euclid::num::Zero; +use servo_arc::Arc; +use std::f32; +use std::ops::Range; +use style::values::computed::Length; use style::values::specified::text::TextDecorationLine; +/// A floating box. #[derive(Debug, Serialize)] pub(crate) struct FloatBox { + /// The formatting context that makes up the content of this box. pub contents: IndependentFormattingContext, } /// Data kept during layout about the floats in a given block formatting context. -pub(crate) struct FloatContext { - // TODO +/// +/// This is a persistent data structure. Each float has its own private copy of the float context, +/// although such copies may share portions of the `bands` tree. +#[derive(Clone, Debug)] +pub struct FloatContext { + /// A persistent AA tree of float bands. + /// + /// This tree is immutable; modification operations return the new tree, which may share nodes + /// with previous versions of the tree. + pub bands: FloatBandTree, + /// The logical width of this context. No floats may extend outside the width of this context + /// unless they are as far (logically) left or right as possible. + pub inline_size: Length, + /// The current (logically) vertical position. No new floats may be placed (logically) above + /// this line. + pub ceiling: Length, } impl FloatContext { - pub fn new() -> Self { - FloatContext {} + /// Returns a new float context representing a containing block with the given content + /// inline-size. + pub fn new(inline_size: Length) -> Self { + let mut bands = FloatBandTree::new(); + bands = bands.insert(FloatBand { + top: Length::zero(), + left: None, + right: None, + }); + bands = bands.insert(FloatBand { + top: Length::new(f32::INFINITY), + left: None, + right: None, + }); + FloatContext { + bands, + inline_size, + ceiling: Length::zero(), + } + } + + /// Returns the current ceiling value. No new floats may be placed (logically) above this line. + pub fn ceiling(&self) -> Length { + self.ceiling + } + + /// (Logically) lowers the ceiling to at least `new_ceiling` units. + /// + /// If the ceiling is already logically lower (i.e. larger) than this, does nothing. + pub fn lower_ceiling(&mut self, new_ceiling: Length) { + self.ceiling = self.ceiling.max(new_ceiling); + } + + /// Returns the highest block position that is both logically below the current ceiling and + /// clear of floats on the given side or sides. + pub fn clearance(&self, side: ClearSide) -> Length { + let mut band = self.bands.find(self.ceiling).unwrap(); + while !band.is_clear(side) { + let next_band = self.bands.find_next(band.top).unwrap(); + if next_band.top.px().is_infinite() { + break; + } + band = next_band; + } + band.top.max(self.ceiling) + } + + /// Places a new float and adds it to the list. Returns the start corner of its margin box. + pub fn add_float(&mut self, new_float: FloatInfo) -> Vec2<Length> { + // Find the first band this float fits in. + let mut first_band = self.bands.find(self.ceiling).unwrap(); + while !first_band.float_fits(&new_float, self.inline_size) { + let next_band = self.bands.find_next(first_band.top).unwrap(); + if next_band.top.px().is_infinite() { + break; + } + first_band = next_band; + } + + // The float fits perfectly here. Place it. + let (new_float_origin, new_float_extent); + match new_float.side { + FloatSide::Left => { + new_float_origin = Vec2 { + inline: first_band.left.unwrap_or(Length::zero()), + block: first_band.top.max(self.ceiling), + }; + new_float_extent = new_float_origin.inline + new_float.size.inline; + }, + FloatSide::Right => { + new_float_origin = Vec2 { + inline: first_band.right.unwrap_or(self.inline_size) - new_float.size.inline, + block: first_band.top.max(self.ceiling), + }; + new_float_extent = new_float_origin.inline; + }, + }; + let new_float_rect = Rect { + start_corner: new_float_origin, + size: new_float.size, + }; + + // Split the first band if necessary. + first_band.top = new_float_rect.start_corner.block; + self.bands = self.bands.insert(first_band); + + // Split the last band if necessary. + let mut last_band = self + .bands + .find(new_float_rect.max_block_position()) + .unwrap(); + last_band.top = new_float_rect.max_block_position(); + self.bands = self.bands.insert(last_band); + + // Update all bands that contain this float to reflect the new available size. + let block_range = new_float_rect.start_corner.block..new_float_rect.max_block_position(); + self.bands = self + .bands + .set_range(&block_range, new_float.side, new_float_extent); + + // CSS 2.1 § 9.5.1 rule 6: 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. + self.ceiling = self.ceiling.max(new_float_rect.start_corner.block); + new_float_rect.start_corner + } +} + +/// Information needed to place a float. +#[derive(Clone, Debug)] +pub struct FloatInfo { + /// The *margin* box size of the float. + pub size: Vec2<Length>, + /// Whether the float is left or right. + pub side: FloatSide, + /// Which side or sides to clear floats on. + pub clear: ClearSide, +} + +/// Whether the float is left or right. +/// +/// See CSS 2.1 § 9.5.1: https://www.w3.org/TR/CSS2/visuren.html#float-position +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FloatSide { + Left, + Right, +} + +/// Which side or sides to clear floats on. +/// +/// See CSS 2.1 § 9.5.2: https://www.w3.org/TR/CSS2/visuren.html#flow-control +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ClearSide { + None = 0, + Left = 1, + Right = 2, + Both = 3, +} + +/// Internal data structure that describes a nonoverlapping vertical region in which floats may be +/// placed. Floats must go between "left edge + `left`" and "right edge - `right`". +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FloatBand { + /// The logical vertical position of the top of this band. + pub top: Length, + /// The distance from the left edge to the first legal (logically) horizontal position where + /// floats may be placed. If `None`, there are no floats to the left; distinguishing between + /// the cases of "a zero-width float is present" and "no floats at all are present" is + /// necessary to, for example, clear past zero-width floats. + pub left: Option<Length>, + /// The distance from the right edge to the first legal (logically) horizontal position where + /// floats may be placed. If `None`, there are no floats to the right; distinguishing between + /// the cases of "a zero-width float is present" and "no floats at all are present" is + /// necessary to, for example, clear past zero-width floats. + pub right: Option<Length>, +} + +impl FloatBand { + fn is_clear(&self, side: ClearSide) -> bool { + match (side, self.left, self.right) { + (ClearSide::Left, Some(_), _) | + (ClearSide::Right, _, Some(_)) | + (ClearSide::Both, Some(_), _) | + (ClearSide::Both, _, Some(_)) => false, + (ClearSide::None, _, _) | + (ClearSide::Left, None, _) | + (ClearSide::Right, _, None) | + (ClearSide::Both, None, None) => true, + } + } + + fn float_fits(&self, new_float: &FloatInfo, container_inline_size: Length) -> bool { + let available_space = + self.right.unwrap_or(container_inline_size) - self.left.unwrap_or(Length::zero()); + self.is_clear(new_float.clear) && + (new_float.size.inline <= available_space || + (self.left.is_none() && self.right.is_none())) + } +} + +// Float band storage + +/// A persistent AA tree for float band storage. +/// +/// Bands here are nonoverlapping, and there is guaranteed to be a band at block-position 0 and +/// another band at block-position infinity. +/// +/// AA trees were chosen for simplicity. +/// +/// See: https://en.wikipedia.org/wiki/AA_tree +/// https://arxiv.org/pdf/1412.4882.pdf +#[derive(Clone, Debug)] +pub struct FloatBandTree { + pub root: FloatBandLink, +} + +/// A single edge (or lack thereof) in the float band tree. +#[derive(Clone, Debug)] +pub struct FloatBandLink(pub Option<Arc<FloatBandNode>>); + +/// A single node in the float band tree. +#[derive(Clone, Debug)] +pub struct FloatBandNode { + /// The actual band. + pub band: FloatBand, + /// The left child. + pub left: FloatBandLink, + /// The right child. + pub right: FloatBandLink, + /// The level, which increases as you go up the tree. + /// + /// This value is needed for tree balancing. + pub level: i32, +} + +impl FloatBandTree { + /// Creates a new float band tree. + pub fn new() -> FloatBandTree { + FloatBandTree { + root: FloatBandLink(None), + } + } + + /// Returns the first band whose top is less than or equal to the given `block_position`. + pub fn find(&self, block_position: Length) -> Option<FloatBand> { + self.root.find(block_position) + } + + /// Returns the first band whose top is strictly greater than to the given `block_position`. + pub fn find_next(&self, block_position: Length) -> Option<FloatBand> { + self.root.find_next(block_position) + } + + /// Sets the side values of all bands within the given half-open range to be at least + /// `new_value`. + #[must_use] + pub fn set_range( + &self, + range: &Range<Length>, + side: FloatSide, + new_value: Length, + ) -> FloatBandTree { + FloatBandTree { + root: FloatBandLink( + self.root + .0 + .as_ref() + .map(|root| root.set_range(range, side, new_value)), + ), + } + } + + /// Inserts a new band into the tree. If the band has the same level as a pre-existing one, + /// replaces the existing band with the new one. + #[must_use] + pub fn insert(&self, band: FloatBand) -> FloatBandTree { + FloatBandTree { + root: self.root.insert(band), + } + } +} + +impl FloatBandNode { + fn new(band: FloatBand) -> FloatBandNode { + FloatBandNode { + band, + left: FloatBandLink(None), + right: FloatBandLink(None), + level: 1, + } + } + + /// Sets the side values of all bands within the given half-open range to be at least + /// `new_value`. + fn set_range( + &self, + range: &Range<Length>, + side: FloatSide, + new_value: Length, + ) -> Arc<FloatBandNode> { + let mut new_band = self.band.clone(); + if self.band.top >= range.start && self.band.top < range.end { + match side { + FloatSide::Left => match new_band.left { + None => new_band.left = Some(new_value), + Some(ref mut old_value) => *old_value = old_value.max(new_value), + }, + FloatSide::Right => match new_band.right { + None => new_band.right = Some(new_value), + Some(ref mut old_value) => *old_value = old_value.min(new_value), + }, + } + } + + let new_left = match self.left.0 { + None => FloatBandLink(None), + Some(ref old_left) if range.start < new_band.top => { + FloatBandLink(Some(old_left.set_range(range, side, new_value))) + }, + Some(ref old_left) => FloatBandLink(Some((*old_left).clone())), + }; + + let new_right = match self.right.0 { + None => FloatBandLink(None), + Some(ref old_right) if range.end > new_band.top => { + FloatBandLink(Some(old_right.set_range(range, side, new_value))) + }, + Some(ref old_right) => FloatBandLink(Some((*old_right).clone())), + }; + + Arc::new(FloatBandNode { + band: new_band, + left: new_left, + right: new_right, + level: self.level, + }) + } +} + +impl FloatBandLink { + /// Returns the first band whose top is less than or equal to the given `block_position`. + fn find(&self, block_position: Length) -> Option<FloatBand> { + let this = match self.0 { + None => return None, + Some(ref node) => node, + }; + + if block_position < this.band.top { + return this.left.find(block_position); + } + + // It's somewhere in this subtree, but we aren't sure whether it's here or in the right + // subtree. + if let Some(band) = this.right.find(block_position) { + return Some(band); + } + + Some(this.band.clone()) + } + + /// Returns the first band whose top is strictly greater than the given `block_position`. + fn find_next(&self, block_position: Length) -> Option<FloatBand> { + let this = match self.0 { + None => return None, + Some(ref node) => node, + }; + + if block_position >= this.band.top { + return this.right.find_next(block_position); + } + + // It's somewhere in this subtree, but we aren't sure whether it's here or in the left + // subtree. + if let Some(band) = this.left.find_next(block_position) { + return Some(band); + } + + Some(this.band.clone()) + } + + // Inserts a new band into the tree. If the band has the same level as a pre-existing one, + // replaces the existing band with the new one. + fn insert(&self, band: FloatBand) -> FloatBandLink { + let mut this = match self.0 { + None => return FloatBandLink(Some(Arc::new(FloatBandNode::new(band)))), + Some(ref this) => (**this).clone(), + }; + + if band.top < this.band.top { + this.left = this.left.insert(band); + return FloatBandLink(Some(Arc::new(this))).skew().split(); + } + if band.top > this.band.top { + this.right = this.right.insert(band); + return FloatBandLink(Some(Arc::new(this))).skew().split(); + } + + this.band = band; + FloatBandLink(Some(Arc::new(this))) + } + + // Corrects tree balance: + // + // T L + // / \ / \ + // L R → A T if level(T) = level(L) + // / \ / \ + // A B B R + fn skew(&self) -> FloatBandLink { + if let Some(ref this) = self.0 { + if let Some(ref left) = this.left.0 { + if this.level == left.level { + return FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level, + left: left.left.clone(), + band: left.band.clone(), + right: FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level, + left: left.right.clone(), + band: this.band.clone(), + right: this.right.clone(), + }))), + }))); + } + } + } + + (*self).clone() + } + + // Corrects tree balance: + // + // T R + // / \ / \ + // A R → T X if level(T) = level(X) + // / \ / \ + // B X A B + fn split(&self) -> FloatBandLink { + if let Some(ref this) = self.0 { + if let Some(ref right) = this.right.0 { + if let Some(ref right_right) = right.right.0 { + if this.level == right_right.level { + return FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level + 1, + left: FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level, + left: this.left.clone(), + band: this.band.clone(), + right: right.left.clone(), + }))), + band: right.band.clone(), + right: right.right.clone(), + }))); + } + } + } + } + + (*self).clone() } } +// Float boxes + impl FloatBox { + /// Creates a new float box. pub fn construct<'dom>( context: &LayoutContext, info: &NodeAndStyleInfo<impl NodeExt<'dom>>, diff --git a/components/layout_2020/flow/mod.rs b/components/layout_2020/flow/mod.rs index 9af17e0a4b1..f042f814dd3 100644 --- a/components/layout_2020/flow/mod.rs +++ b/components/layout_2020/flow/mod.rs @@ -30,7 +30,7 @@ use style::values::computed::{Length, LengthOrAuto}; use style::Zero; mod construct; -mod float; +pub mod float; pub mod inline; mod root; @@ -80,7 +80,7 @@ impl BlockFormattingContext { ) -> IndependentLayout { let mut float_context; let float_context = if self.contains_floats { - float_context = FloatContext::new(); + float_context = FloatContext::new(containing_block.inline_size); Some(&mut float_context) } else { None diff --git a/components/layout_2020/geom.rs b/components/layout_2020/geom.rs index ad8166296b2..7d687069d49 100644 --- a/components/layout_2020/geom.rs +++ b/components/layout_2020/geom.rs @@ -19,21 +19,21 @@ pub type PhysicalSides<U> = euclid::SideOffsets2D<U, CSSPixel>; pub type LengthOrAuto = AutoOr<Length>; pub type LengthPercentageOrAuto<'a> = AutoOr<&'a LengthPercentage>; -pub(crate) mod flow_relative { +pub mod flow_relative { #[derive(Clone, Serialize)] - pub(crate) struct Vec2<T> { + pub struct Vec2<T> { pub inline: T, pub block: T, } #[derive(Clone, Serialize)] - pub(crate) struct Rect<T> { + pub struct Rect<T> { pub start_corner: Vec2<T>, pub size: Vec2<T>, } #[derive(Clone, Serialize)] - pub(crate) struct Sides<T> { + pub struct Sides<T> { pub inline_start: T, pub inline_end: T, pub block_start: T, @@ -325,6 +325,20 @@ where } impl<T> flow_relative::Rect<T> { + pub fn max_inline_position(&self) -> T + where + T: Add<Output = T> + Copy, + { + self.start_corner.inline + self.size.inline + } + + pub fn max_block_position(&self) -> T + where + T: Add<Output = T> + Copy, + { + self.start_corner.block + self.size.block + } + pub fn inflate(&self, sides: &flow_relative::Sides<T>) -> Self where T: Add<Output = T> + Copy, diff --git a/components/layout_2020/lib.rs b/components/layout_2020/lib.rs index 24dc902bdd4..96fcbb448fa 100644 --- a/components/layout_2020/lib.rs +++ b/components/layout_2020/lib.rs @@ -16,10 +16,10 @@ pub mod display_list; mod dom_traversal; pub mod element_data; mod flexbox; -mod flow; +pub mod flow; mod formatting_contexts; mod fragments; -mod geom; +pub mod geom; #[macro_use] pub mod layout_debug; mod opaque_node; @@ -37,7 +37,7 @@ use crate::geom::flow_relative::Vec2; use style::properties::ComputedValues; use style::values::computed::{Length, LengthOrAuto}; -struct ContainingBlock<'a> { +pub struct ContainingBlock<'a> { inline_size: Length, block_size: LengthOrAuto, style: &'a ComputedValues, diff --git a/components/layout_2020/tests/floats.rs b/components/layout_2020/tests/floats.rs new file mode 100644 index 00000000000..212ed8026bb --- /dev/null +++ b/components/layout_2020/tests/floats.rs @@ -0,0 +1,823 @@ +/* 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. + +#[macro_use] +extern crate lazy_static; + +use euclid::num::Zero; +use layout::flow::float::{ClearSide, FloatBand, FloatBandNode, FloatBandTree, FloatContext}; +use layout::flow::float::{FloatInfo, FloatSide}; +use layout::geom::flow_relative::{Rect, Vec2}; +use quickcheck::{Arbitrary, Gen}; +use std::f32; +use std::ops::Range; +use std::panic::{self, PanicInfo}; +use std::sync::{Mutex, MutexGuard}; +use std::thread; +use std::u32; +use style::values::computed::Length; + +lazy_static! { + static ref 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(&PanicInfo<'_>) + '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<G>(generator: &mut G) -> FloatBandWrapper + where + G: Gen, + { + let top: u32 = Arbitrary::arbitrary(generator); + let left: Option<u32> = Arbitrary::arbitrary(generator); + let right: Option<u32> = Arbitrary::arbitrary(generator); + FloatBandWrapper(FloatBand { + top: Length::new(top as f32), + left: left.map(|value| Length::new(value as f32)), + right: right.map(|value| Length::new(value as f32)), + }) + } +} + +#[derive(Clone, Debug)] +struct FloatRangeInput { + start_index: u32, + band_count: u32, + side: FloatSide, + length: u32, +} + +impl Arbitrary for FloatRangeInput { + fn arbitrary<G>(generator: &mut G) -> FloatRangeInput + where + G: Gen, + { + let start_index: u32 = Arbitrary::arbitrary(generator); + let band_count: u32 = Arbitrary::arbitrary(generator); + let is_left: bool = Arbitrary::arbitrary(generator); + let length: u32 = Arbitrary::arbitrary(generator); + FloatRangeInput { + start_index, + band_count, + side: if is_left { + FloatSide::Left + } else { + FloatSide::Right + }, + 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: Length, 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.left, reference_band.left); + assert_eq!(found_band.right, reference_band.right); +} + +fn check_tree_find_next(tree: &FloatBandTree, block_position: Length, 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.left, reference_band.left); + assert_eq!(found_band.right, reference_band.right); +} + +fn check_node_range_setting( + node: &FloatBandNode, + block_range: &Range<Length>, + side: FloatSide, + value: Length, +) { + if node.band.top >= block_range.start && node.band.top < block_range.end { + match side { + FloatSide::Left => assert!(node.band.left.unwrap() >= value), + FloatSide::Right => assert!(node.band.right.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<Length>, + side: FloatSide, + value: Length, +) { + 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<u32>) = check; + quickcheck::quickcheck(f); + fn check(bands: Vec<FloatBandWrapper>, lookups: Vec<u32>) { + let mut bands: Vec<FloatBand> = bands.into_iter().map(|band| band.0).collect(); + bands.push(FloatBand { + top: Length::zero(), + left: None, + right: None, + }); + bands.push(FloatBand { + top: Length::new(f32::INFINITY), + left: None, + right: None, + }); + let mut tree = FloatBandTree::new(); + for ref band in &bands { + tree = tree.insert((*band).clone()); + } + bands.sort_by(|a, b| a.top.partial_cmp(&b.top).unwrap()); + for lookup in lookups { + check_tree_find(&tree, Length::new(lookup as f32), &bands); + } + } +} + +// Tests that the `find_next()` method works. +#[test] +fn test_tree_find_next() { + let f: fn(Vec<FloatBandWrapper>, Vec<u32>) = check; + quickcheck::quickcheck(f); + fn check(bands: Vec<FloatBandWrapper>, lookups: Vec<u32>) { + let mut bands: Vec<FloatBand> = bands.into_iter().map(|band| band.0).collect(); + bands.push(FloatBand { + top: Length::zero(), + left: None, + right: None, + }); + bands.push(FloatBand { + top: Length::new(f32::INFINITY), + left: None, + right: 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 ref band in &bands { + tree = tree.insert((*band).clone()); + } + for lookup in lookups { + check_tree_find_next(&tree, Length::new(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(ref band) in &bands { + tree = tree.insert((*band).clone()); + } + + let mut tops: Vec<Length> = bands.iter().map(|band| band.0.top).collect(); + tops.push(Length::new(f32::INFINITY)); + tops.sort_by(|a, b| a.px().partial_cmp(&b.px()).unwrap()); + + for range in ranges { + let start = range.start_index.min(tops.len() as u32 - 1); + let end = (range.start_index + range.length).min(tops.len() as u32 - 1); + let block_range = tops[start as usize]..tops[end as usize]; + let length = Length::new(range.length as f32); + 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: FloatInfo, + // 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: u32, +} + +impl Arbitrary for FloatInput { + fn arbitrary<G>(generator: &mut G) -> FloatInput + where + G: Gen, + { + let width: u32 = Arbitrary::arbitrary(generator); + let height: u32 = Arbitrary::arbitrary(generator); + let is_left: bool = Arbitrary::arbitrary(generator); + let ceiling: u32 = Arbitrary::arbitrary(generator); + let clear: u8 = Arbitrary::arbitrary(generator); + FloatInput { + info: FloatInfo { + size: Vec2 { + inline: Length::new(width as f32), + block: Length::new(height as f32), + }, + side: if is_left { + FloatSide::Left + } else { + FloatSide::Right + }, + clear: new_clear_side(clear), + }, + ceiling, + } + } + + 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.px().shrink().next() { + this.info.size.inline = Length::new(inline_size); + shrunk = true; + } + if let Some(block_size) = self.info.size.block.px().shrink().next() { + this.info.size.block = Length::new(block_size); + shrunk = true; + } + if let Some(clear_side) = (self.info.clear as u8).shrink().next() { + this.info.clear = new_clear_side(clear_side); + shrunk = true; + } + if let Some(ceiling) = self.ceiling.shrink().next() { + this.ceiling = ceiling; + shrunk = true; + } + if shrunk { + quickcheck::single_shrinker(this) + } else { + quickcheck::empty_shrinker() + } + } +} + +fn new_clear_side(value: u8) -> ClearSide { + match value & 3 { + 0 => ClearSide::None, + 1 => ClearSide::Left, + 2 => ClearSide::Right, + _ => ClearSide::Both, + } +} + +#[derive(Clone)] +struct FloatPlacement { + float_context: FloatContext, + placed_floats: Vec<PlacedFloat>, +} + +// Information about the placement of a float. +#[derive(Clone)] +struct PlacedFloat { + origin: Vec2<Length>, + info: FloatInfo, + ceiling: Length, +} + +impl Drop for FloatPlacement { + fn drop(&mut self) { + if !thread::panicking() { + return; + } + + // Dump the float context for debugging. + eprintln!( + "Failing float placement (inline size: {:?}):", + self.float_context.inline_size + ); + for placed_float in &self.placed_floats { + eprintln!(" * {:?} @ {:?}", placed_float.info, placed_float.origin); + } + eprintln!("Bands:\n{:?}\n", self.float_context.bands); + } +} + +impl PlacedFloat { + fn rect(&self) -> Rect<Length> { + Rect { + start_corner: self.origin.clone(), + size: self.info.size.clone(), + } + } +} + +impl FloatPlacement { + fn place(inline_size: u32, floats: Vec<FloatInput>) -> FloatPlacement { + let mut float_context = FloatContext::new(Length::new(inline_size as f32)); + let mut placed_floats = vec![]; + for float in floats { + let ceiling = Length::new(float.ceiling as f32); + float_context.lower_ceiling(ceiling); + placed_floats.push(PlacedFloat { + origin: float_context.add_float(float.info.clone()), + info: float.info, + ceiling, + }) + } + 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::Left => assert!(placed_float.origin.inline >= Length::zero()), + FloatSide::Right => assert!( + placed_float.rect().max_inline_position() <= placement.float_context.inline_size + ), + } + } +} + +// 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::Left, FloatSide::Left) => { + assert!( + this_float.origin.inline >= prev_float.rect().max_inline_position() || + this_float.origin.block >= prev_float.rect().max_block_position() + ); + }, + (FloatSide::Right, FloatSide::Right) => { + assert!( + this_float.rect().max_inline_position() <= prev_float.origin.inline || + this_float.origin.block >= prev_float.rect().max_block_position() + ); + }, + (FloatSide::Left, FloatSide::Right) | (FloatSide::Right, FloatSide::Left) => {}, + } + } + } +} + +// 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 == Length::zero() && + this_float.rect().max_block_position() < other_float.origin.block) || + (this_float.info.size.block > Length::zero() && + this_float.rect().max_block_position() <= other_float.origin.block) + { + continue; + } + + match (this_float.info.side, other_float.info.side) { + (FloatSide::Left, FloatSide::Right) => { + assert!(this_float.rect().max_inline_position() <= other_float.origin.inline); + }, + (FloatSide::Right, FloatSide::Left) => { + assert!(this_float.origin.inline >= other_float.rect().max_inline_position()); + }, + (FloatSide::Left, FloatSide::Left) | (FloatSide::Right, FloatSide::Right) => {}, + } + } + } +} + +// 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 >= Length::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 = Length::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::Left => { + if placed_float.rect().max_inline_position() <= placement.float_context.inline_size + { + continue; + } + }, + FloatSide::Right => { + if placed_float.origin.inline >= Length::zero() { + 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(inline_size: u32, floats_and_perturbations: Vec<(FloatInput, u32)>) { + let floats = floats_and_perturbations + .iter() + .map(|&(ref float, _)| (*float).clone()) + .collect(); + let placement = FloatPlacement::place(inline_size, 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 = + placement.placed_floats[float_index].origin.block - Length::new(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(inline_size: u32, floats_and_perturbations: Vec<(FloatInput, u32)>) { + let floats = floats_and_perturbations + .iter() + .map(|&(ref float, _)| (*float).clone()) + .collect(); + let placement = FloatPlacement::place(inline_size, floats); + + for (float_index, &(_, perturbation)) in floats_and_perturbations.iter().enumerate() { + if perturbation == 0 { + continue; + } + + let mut placement = placement.clone(); + { + let mut placed_float = &mut placement.placed_floats[float_index]; + let perturbation = Length::new(perturbation as f32); + match placed_float.info.side { + FloatSide::Left => { + placed_float.origin.inline = placed_float.origin.inline - perturbation + }, + FloatSide::Right => { + placed_float.origin.inline = 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 = Length::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 == ClearSide::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 == Length::zero() && + this_float.rect().max_block_position() < other_float.origin.block) || + (this_float.info.size.block > Length::zero() && + this_float.rect().max_block_position() <= other_float.origin.block) + { + continue; + } + + match this_float.info.clear { + ClearSide::Left => assert_ne!(other_float.info.side, FloatSide::Left), + ClearSide::Right => assert_ne!(other_float.info.side, FloatSide::Right), + ClearSide::Both => assert!(false), + ClearSide::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(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_1(&FloatPlacement::place(inline_size, floats)); + } +} + +#[test] +fn test_floats_rule_2() { + let f: fn(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_2(&FloatPlacement::place(inline_size, floats)); + } +} + +#[test] +fn test_floats_rule_3() { + let f: fn(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_3(&FloatPlacement::place(inline_size, floats)); + } +} + +#[test] +fn test_floats_rule_4() { + let f: fn(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_4(&FloatPlacement::place(inline_size, floats)); + } +} + +#[test] +fn test_floats_rule_5() { + let f: fn(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_5(&FloatPlacement::place(inline_size, floats)); + } +} + +#[test] +fn test_floats_rule_6() { + let f: fn(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_6(&FloatPlacement::place(inline_size, floats)); + } +} + +#[test] +fn test_floats_rule_7() { + let f: fn(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_7(&FloatPlacement::place(inline_size, floats)); + } +} + +#[test] +fn test_floats_rule_8() { + let f: fn(u32, Vec<(FloatInput, u32)>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<(FloatInput, u32)>) { + check_floats_rule_8(inline_size, floats); + } +} + +#[test] +fn test_floats_rule_9() { + let f: fn(u32, Vec<(FloatInput, u32)>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<(FloatInput, u32)>) { + check_floats_rule_9(inline_size, floats); + } +} + +#[test] +fn test_floats_rule_10() { + let f: fn(u32, Vec<FloatInput>) = check; + quickcheck::quickcheck(f); + fn check(inline_size: u32, floats: Vec<FloatInput>) { + check_floats_rule_10(&FloatPlacement::place(inline_size, floats)); + } +} diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index ec3a74ffdfa..f5784ce4f4d 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -222,7 +222,7 @@ class MachCommands(CommandBase): @CommandArgument('--nocapture', default=False, action="store_true", help="Run tests with nocapture ( show test stdout )") @CommandBase.build_like_command_arguments - def test_unit(self, test_name=None, package=None, bench=False, nocapture=False, **kwargs): + def test_unit(self, test_name=None, package=None, bench=False, nocapture=False, with_layout_2020=False, **kwargs): if test_name is None: test_name = [] @@ -255,7 +255,6 @@ class MachCommands(CommandBase): self_contained_tests = [ "background_hang_monitor", "gfx", - "layout_2013", "msg", "net", "net_traits", @@ -263,6 +262,10 @@ class MachCommands(CommandBase): "servo_config", "servo_remutex", ] + if with_layout_2020: + self_contained_tests.append("layout_2020") + else: + self_contained_tests.append("layout_2013") if not packages: packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store']) packages |= set(self_contained_tests) @@ -298,7 +301,11 @@ class MachCommands(CommandBase): if nocapture: args += ["--", "--nocapture"] - err = self.run_cargo_build_like_command("bench" if bench else "test", args, env=env, **kwargs) + err = self.run_cargo_build_like_command("bench" if bench else "test", + args, + env=env, + with_layout_2020=with_layout_2020, + **kwargs) if err: return err diff --git a/servo-tidy.toml b/servo-tidy.toml index 6893dac4ba8..935ba0a205f 100644 --- a/servo-tidy.toml +++ b/servo-tidy.toml @@ -18,6 +18,7 @@ rand = [ "hashglobe", # Only used in tests "ipc-channel", "phf_generator", + "quickcheck", # Only used in tests "servo_rand", "tempfile", "uuid", diff --git a/tests/unit/style/parsing/mod.rs b/tests/unit/style/parsing/mod.rs index ce4f7a0f600..80a2595caeb 100644 --- a/tests/unit/style/parsing/mod.rs +++ b/tests/unit/style/parsing/mod.rs @@ -112,16 +112,22 @@ macro_rules! parse_longhand { }; } -mod background; -mod border; mod box_; -mod column; mod effects; mod image; mod inherited_text; mod outline; mod selectors; mod supports; -mod text_overflow; mod transition_duration; mod transition_timing_function; + +// These tests test features that are only available in 2013 layout. +#[cfg(feature = "layout_2013")] +mod background; +#[cfg(feature = "layout_2013")] +mod border; +#[cfg(feature = "layout_2013")] +mod column; +#[cfg(feature = "layout_2013")] +mod text_overflow; |