Repository: rust-itertools/itertools (rust)

Issue:
multi_cartesian_product of zero iterators should produce one empty Vec
One would expect
```rust
for v in (0..n).map(|_| &[0, 1]).multi_cartesian_product() {
    println!("{:?}", v);
}
```
to enumerate all {0, 1} vectors of length `n`. However, it fails when `n == 0`, printing no vectors instead of the [expected](https://en.wikipedia.org/wiki/Empty_product#Nullary_Cartesian_product) single vector `[]`.

Pull request description:
Rewrite `MultiProduct`
Fixes #337. Closes #603 since this is an alternative.

#834 legitimately asked that we work on the bugfix #603. But after rebasing it, I saw that I could finally fuse the iterator (and therefore have a working specialization test). I thought of using #603 as a base but I eventually chose to make my own changes, starting over. I however credited @JakobDegen where it made sense.

I made commits as atomic as I could.

So this fuses the iterator which is a breaking change, which won't affect most people.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9361ee6..9140274 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -69,7 +69,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: obi1kenobi/cargo-semver-checks-action@v2.1
+      - uses: obi1kenobi/cargo-semver-checks-action@v2.3
         with:
           rust-toolchain: stable
           feature-group: all-features
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36fc274..3994e5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,42 @@
 # Changelog
 
+## 0.12.1
+
+### Added
+- Documented iteration order guarantee for `Itertools::[tuple_]combinations` (#822)
+- Documented possible panic in `iterate` (#842)
+- Implemented `Clone` and `Debug` for `Diff` (#845)
+- Implemented `Debug` for `WithPosition` (#859)
+- Implemented `Eq` for `MinMaxResult` (#838)
+- Implemented `From<EitherOrBoth<A, B>>` for `Option<Either<A, B>>` (#843)
+- Implemented `PeekingNext` for `RepeatN` (#855)
+
+### Changed
+- Made `CoalesceBy` lazy (#801)
+- Optimized `Filter[Map]Ok::next`, `Itertools::partition`, `Unique[By]::next[_back]` (#818)
+- Optimized `Itertools::find_position` (#837)
+- Optimized `Positions::next[_back]` (#816)
+- Optimized `ZipLongest::fold` (#854)
+- Relaxed `Debug` bounds for `GroupingMapBy` (#860)
+- Specialized `ExactlyOneError::fold` (#826)
+- Specialized `Interleave[Shortest]::fold` (#849)
+- Specialized `MultiPeek::fold` (#820)
+- Specialized `PadUsing::[r]fold` (#825)
+- Specialized `PeekNth::fold` (#824)
+- Specialized `Positions::[r]fold` (#813)
+- Specialized `PutBackN::fold` (#823)
+- Specialized `RepeatN::[r]fold` (#821)
+- Specialized `TakeWhileInclusive::fold` (#851)
+- Specialized `ZipLongest::rfold` (#848)
+
+### Notable Internal Changes
+- Added test coverage in CI (#847, #856)
+- Added semver check in CI (#784)
+- Enforced `clippy` in CI (#740)
+- Enforced `rustdoc` in CI (#840)
+- Improved specialization tests (#807)
+- More specialization benchmarks (#806)
+
 ## 0.12.0
 
 ### Breaking
diff --git a/Cargo.toml b/Cargo.toml
index 9903ed4..70af684 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "itertools"
-version = "0.12.0"
+version = "0.12.1"
 
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/rust-itertools/itertools"
diff --git a/README.md b/README.md
index e1c3f72..381a3e9 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ How to use with Cargo:
 
 ```toml
 [dependencies]
-itertools = "0.12.0"
+itertools = "0.12.1"
 ```
 
 How to use in your crate:
diff --git a/src/adaptors/coalesce.rs b/src/adaptors/coalesce.rs
index ad9df11..ab1ab52 100644
--- a/src/adaptors/coalesce.rs
+++ b/src/adaptors/coalesce.rs
@@ -33,7 +33,7 @@ where
     C: CountItem<I::Item>,
     C::CItem: fmt::Debug,
 {
-    debug_fmt_fields!(CoalesceBy, iter);
+    debug_fmt_fields!(CoalesceBy, iter, last);
 }
 
 pub trait CoalescePredicate<Item, T> {
diff --git a/src/adaptors/multi_product.rs b/src/adaptors/multi_product.rs
index 74cb06d..d551c0a 100644
--- a/src/adaptors/multi_product.rs
+++ b/src/adaptors/multi_product.rs
@@ -1,30 +1,52 @@
 #![cfg(feature = "use_alloc")]
-
-use crate::size_hint;
-use crate::Itertools;
+use Option::{self as State, None as ProductEnded, Some as ProductInProgress};
+use Option::{self as CurrentItems, None as NotYetPopulated, Some as Populated};
 
 use alloc::vec::Vec;
 
+use crate::size_hint;
+
 #[derive(Clone)]
 /// An iterator adaptor that iterates over the cartesian product of
 /// multiple iterators of type `I`.
 ///
-/// An iterator element type is `Vec<I>`.
+/// An iterator element type is `Vec<I::Item>`.
 ///
 /// See [`.multi_cartesian_product()`](crate::Itertools::multi_cartesian_product)
 /// for more information.
 #[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
-pub struct MultiProduct<I>(Vec<MultiProductIter<I>>)
+pub struct MultiProduct<I>(State<MultiProductInner<I>>)
 where
     I: Iterator + Clone,
     I::Item: Clone;
 
+#[derive(Clone)]
+/// Internals for `MultiProduct`.
+struct MultiProductInner<I>
+where
+    I: Iterator + Clone,
+    I::Item: Clone,
+{
+    /// Holds the iterators.
+    iters: Vec<MultiProductIter<I>>,
+    /// Not populated at the beginning then it holds the current item of each iterator.
+    cur: CurrentItems<Vec<I::Item>>,
+}
+
 impl<I> std::fmt::Debug for MultiProduct<I>
 where
     I: Iterator + Clone + std::fmt::Debug,
     I::Item: Clone + std::fmt::Debug,
 {
-    debug_fmt_fields!(CoalesceBy, 0);
+    debug_fmt_fields!(MultiProduct, 0);
+}
+
+impl<I> std::fmt::Debug for MultiProductInner<I>
+where
+    I: Iterator + Clone + std::fmt::Debug,
+    I::Item: Clone + std::fmt::Debug,
+{
+    debug_fmt_fields!(MultiProductInner, iters, cur);
 }
 
 /// Create a new cartesian product iterator over an arbitrary number
@@ -38,11 +60,13 @@ where
     <H::Item as IntoIterator>::IntoIter: Clone,
     <H::Item as IntoIterator>::Item: Clone,
 {
-    MultiProduct(
-        iters
+    let inner = MultiProductInner {
+        iters: iters
             .map(|i| MultiProductIter::new(i.into_iter()))
             .collect(),
-    )
+        cur: NotYetPopulated,
+    };
+    MultiProduct(ProductInProgress(inner))
 }
 
 #[derive(Clone, Debug)]
@@ -52,87 +76,10 @@ where
     I: Iterator + Clone,
     I::Item: Clone,
 {
-    cur: Option<I::Item>,
     iter: I,
     iter_orig: I,
 }
 
-/// Holds the current state during an iteration of a `MultiProduct`.
-#[derive(Debug)]
-enum MultiProductIterState {
-    StartOfIter,
-    MidIter { on_first_iter: bool },
-}
-
-impl<I> MultiProduct<I>
-where
-    I: Iterator + Clone,
-    I::Item: Clone,
-{
-    /// Iterates the rightmost iterator, then recursively iterates iterators
-    /// to the left if necessary.
-    ///
-    /// Returns true if the iteration succeeded, else false.
-    fn iterate_last(
-        multi_iters: &mut [MultiProductIter<I>],
-        mut state: MultiProductIterState,
-    ) -> bool {
-        use self::MultiProductIterState::*;
-
-        if let Some((last, rest)) = multi_iters.split_last_mut() {
-            let on_first_iter = match state {
-                StartOfIter => {
-                    let on_first_iter = !last.in_progress();
-                    state = MidIter { on_first_iter };
-                    on_first_iter
-                }
-                MidIter { on_first_iter } => on_first_iter,
-            };
-
-            if !on_first_iter {
-                last.iterate();
-            }
-
-            if last.in_progress() {
-                true
-            } else if Self::iterate_last(rest, state) {
-                last.reset();
-                last.iterate();
-                // If iterator is None twice consecutively, then iterator is
-                // empty; whole product is empty.
-                last.in_progress()
-            } else {
-                false
-            }
-        } else {
-            // Reached end of iterator list. On initialisation, return true.
-            // At end of iteration (final iterator finishes), finish.
-            match state {
-                StartOfIter => false,
-                MidIter { on_first_iter } => on_first_iter,
-            }
-        }
-    }
-
-    /// Returns the unwrapped value of the next iteration.
-    fn curr_iterator(&self) -> Vec<I::Item> {
-        self.0
-            .iter()
-            .map(|multi_iter| multi_iter.cur.clone().unwrap())
-            .collect()
-    }
-
-    /// Returns true if iteration has started and has not yet finished; false
-    /// otherwise.
-    fn in_progress(&self) -> bool {
-        if let Some(last) = self.0.last() {
-            last.in_progress()
-        } else {
-            false
-        }
-    }
-}
-
 impl<I> MultiProductIter<I>
 where
     I: Iterator + Clone,
@@ -140,27 +87,10 @@ where
 {
     fn new(iter: I) -> Self {
         Self {
-            cur: None,
             iter: iter.clone(),
             iter_orig: iter,
         }
     }
-
-    /// Iterate the managed iterator.
-    fn iterate(&mut self) {
-        self.cur = self.iter.next();
-    }
-
-    /// Reset the managed iterator.
-    fn reset(&mut self) {
-        self.iter = self.iter_orig.clone();
-    }
-
-    /// Returns true if the current iterator has been started and has not yet
-    /// finished; false otherwise.
-    fn in_progress(&self) -> bool {
-        self.cur.is_some()
-    }
 }
 
 impl<I> Iterator for MultiProduct<I>
@@ -171,81 +101,131 @@ where
     type Item = Vec<I::Item>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        if Self::iterate_last(&mut self.0, MultiProductIterState::StartOfIter) {
-            Some(self.curr_iterator())
-        } else {
-            None
+        // This fuses the iterator.
+        let inner = self.0.as_mut()?;
+        match &mut inner.cur {
+            Populated(values) => {
+                debug_assert!(!inner.iters.is_empty());
+                // Find (from the right) a non-finished iterator and
+                // reset the finished ones encountered.
+                for (iter, item) in inner.iters.iter_mut().zip(values.iter_mut()).rev() {
+                    if let Some(new) = iter.iter.next() {
+                        *item = new;
+                        return Some(values.clone());
+                    } else {
+                        iter.iter = iter.iter_orig.clone();
+                        // `cur` is populated so the untouched `iter_orig` can not be empty.
+                        *item = iter.iter.next().unwrap();
+                    }
+                }
+                self.0 = ProductEnded;
+                None
+            }
+            // Only the first time.
+            NotYetPopulated => {
+                let next: Option<Vec<_>> = inner.iters.iter_mut().map(|i| i.iter.next()).collect();
+                if next.is_none() || inner.iters.is_empty() {
+                    // This cartesian product had at most one item to generate and now ends.
+                    self.0 = ProductEnded;
+                } else {
+                    inner.cur = next.clone();
+                }
+                next
+            }
         }
     }
 
     fn count(self) -> usize {
-        if self.0.is_empty() {
-            return 0;
-        }
-
-        if !self.in_progress() {
-            return self
-                .0
+        match self.0 {
+            ProductEnded => 0,
+            // The iterator is fresh so the count is the product of the length of each iterator:
+            // - If one of them is empty, stop counting.
+            // - Less `count()` calls than the general case.
+            ProductInProgress(MultiProductInner {
+                iters,
+                cur: NotYetPopulated,
+            }) => iters
                 .into_iter()
-                .fold(1, |acc, multi_iter| acc * multi_iter.iter.count());
+                .map(|iter| iter.iter_orig.count())
+                .try_fold(1, |product, count| {
+                    if count == 0 {
+                        None
+                    } else {
+                        Some(product * count)
+                    }
+                })
+                .unwrap_or_default(),
+            // The general case.
+            ProductInProgress(MultiProductInner {
+                iters,
+                cur: Populated(_),
+            }) => iters.into_iter().fold(0, |mut acc, iter| {
+                if acc != 0 {
+                    acc *= iter.iter_orig.count();
+                }
+                acc + iter.iter.count()
+            }),
         }
-
-        self.0.into_iter().fold(
-            0,
-            |acc,
-             MultiProductIter {
-                 iter,
-                 iter_orig,
-                 cur: _,
-             }| {
-                let total_count = iter_orig.count();
-                let cur_count = iter.count();
-                acc * total_count + cur_count
-            },
-        )
     }
 
     fn size_hint(&self) -> (usize, Option<usize>) {
-        // Not ExactSizeIterator because size may be larger than usize
-        if self.0.is_empty() {
-            return (0, Some(0));
-        }
-
-        if !self.in_progress() {
-            return self.0.iter().fold((1, Some(1)), |acc, multi_iter| {
-                size_hint::mul(acc, multi_iter.iter.size_hint())
-            });
+        match &self.0 {
+            ProductEnded => (0, Some(0)),
+            ProductInProgress(MultiProductInner {
+                iters,
+                cur: NotYetPopulated,
+            }) => iters
+                .iter()
+                .map(|iter| iter.iter_orig.size_hint())
+                .fold((1, Some(1)), size_hint::mul),
+            ProductInProgress(MultiProductInner {
+                iters,
+                cur: Populated(_),
+            }) => {
+                if let [first, tail @ ..] = &iters[..] {
+                    tail.iter().fold(first.iter.size_hint(), |mut sh, iter| {
+                        sh = size_hint::mul(sh, iter.iter_orig.size_hint());
+                        size_hint::add(sh, iter.iter.size_hint())
+                    })
+                } else {
+                    // Since it is populated, this cartesian product has started so `iters` is not empty.
+                    unreachable!()
+                }
+            }
         }
-
-        self.0.iter().fold(
-            (0, Some(0)),
-            |acc,
-             MultiProductIter {
-                 iter,
-                 iter_orig,
-                 cur: _,
-             }| {
-                let cur_size = iter.size_hint();
-                let total_size = iter_orig.size_hint();
-                size_hint::add(size_hint::mul(acc, total_size), cur_size)
-            },
-        )
     }
 
     fn last(self) -> Option<Self::Item> {
-        let iter_count = self.0.len();
-
-        let lasts: Self::Item = self
-            .0
-            .into_iter()
-            .map(|multi_iter| multi_iter.iter.last())
-            .while_some()
-            .collect();
-
-        if lasts.len() == iter_count {
-            Some(lasts)
+        let MultiProductInner { iters, cur } = self.0?;
+        // Collect the last item of each iterator of the product.
+        if let Populated(values) = cur {
+            let mut count = iters.len();
+            let last = iters
+                .into_iter()
+                .zip(values)
+                .map(|(i, value)| {
+                    i.iter.last().unwrap_or_else(|| {
+                        // The iterator is empty, use its current `value`.
+                        count -= 1;
+                        value
+                    })
+                })
+                .collect();
+            if count == 0 {
+                // `values` was the last item.
+                None
+            } else {
+                Some(last)
+            }
         } else {
-            None
+            iters.into_iter().map(|i| i.iter.last()).collect()
         }
     }
 }
+
+impl<I> std::iter::FusedIterator for MultiProduct<I>
+where
+    I: Iterator + Clone,
+    I::Item: Clone,
+{
+}
diff --git a/src/combinations_with_replacement.rs b/src/combinations_with_replacement.rs
index 88d858b..ee34978 100644
--- a/src/combinations_with_replacement.rs
+++ b/src/combinations_with_replacement.rs
@@ -1,3 +1,4 @@
+use alloc::boxed::Box;
 use alloc::vec::Vec;
 use std::fmt;
 use std::iter::FusedIterator;
@@ -16,7 +17,7 @@ where
     I: Iterator,
     I::Item: Clone,
 {
-    indices: Vec<usize>,
+    indices: Box<[usize]>,
     pool: LazyBuffer<I>,
     first: bool,
 }
@@ -46,7 +47,7 @@ where
     I: Iterator,
     I::Item: Clone,
 {
-    let indices: Vec<usize> = alloc::vec![0; k];
+    let indices = alloc::vec![0; k].into_boxed_slice();
     let pool: LazyBuffer<I> = LazyBuffer::new(iter);
 
     CombinationsWithReplacement {
diff --git a/src/cons_tuples_impl.rs b/src/cons_tuples_impl.rs
index 3cae0b0..9ab3094 100644
--- a/src/cons_tuples_impl.rs
+++ b/src/cons_tuples_impl.rs
@@ -21,16 +21,6 @@ macro_rules! impl_cons_iter(
                 self.iter.fold(accum, move |acc, (($($B,)*), x)| f(acc, ($($B,)* x, )))
             }
         }
-
-        #[allow(non_snake_case)]
-        impl<X, Iter, $($B),*> DoubleEndedIterator for ConsTuples<Iter, (($($B,)*), X)>
-            where Iter: DoubleEndedIterator<Item = (($($B,)*), X)>,
-        {
-            fn next_back(&mut self) -> Option<Self::Item> {
-                self.iter.next().map(|(($($B,)*), x)| ($($B,)* x, ))
-            }
-        }
-
     );
 );
 
diff --git a/src/exactly_one_err.rs b/src/exactly_one_err.rs
index 90d8987..19b9e19 100644
--- a/src/exactly_one_err.rs
+++ b/src/exactly_one_err.rs
@@ -102,25 +102,17 @@ where
     I::Item: Debug,
 {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
+        let mut dbg = f.debug_struct("ExactlyOneError");
         match &self.first_two {
             Some(Either::Left([first, second])) => {
-                write!(
-                    f,
-                    "ExactlyOneError[First: {:?}, Second: {:?}, RemainingIter: {:?}]",
-                    first, second, self.inner
-                )
+                dbg.field("first", first).field("second", second);
             }
             Some(Either::Right(second)) => {
-                write!(
-                    f,
-                    "ExactlyOneError[Second: {:?}, RemainingIter: {:?}]",
-                    second, self.inner
-                )
-            }
-            None => {
-                write!(f, "ExactlyOneError[RemainingIter: {:?}]", self.inner)
+                dbg.field("second", second);
             }
+            None => {}
         }
+        dbg.field("inner", &self.inner).finish()
     }
 }
 
diff --git a/src/flatten_ok.rs b/src/flatten_ok.rs
index 21ae1f7..8531222 100644
--- a/src/flatten_ok.rs
+++ b/src/flatten_ok.rs
@@ -147,13 +147,7 @@ where
     T: IntoIterator,
     T::IntoIter: fmt::Debug,
 {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("FlattenOk")
-            .field("iter", &self.iter)
-            .field("inner_front", &self.inner_front)
-            .field("inner_back", &self.inner_back)
-            .finish()
-    }
+    debug_fmt_fields!(FlattenOk, iter, inner_front, inner_back);
 }
 
 /// Only the iterator being flattened needs to implement [`FusedIterator`].
diff --git a/src/grouping_map.rs b/src/grouping_map.rs
index aeb86f1..a01968d 100644
--- a/src/grouping_map.rs
+++ b/src/grouping_map.rs
@@ -3,14 +3,19 @@
 use crate::MinMaxResult;
 use std::cmp::Ordering;
 use std::collections::HashMap;
+use std::fmt;
 use std::hash::Hash;
 use std::iter::Iterator;
 use std::ops::{Add, Mul};
 
 /// A wrapper to allow for an easy [`into_grouping_map_by`](crate::Itertools::into_grouping_map_by)
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub struct MapForGrouping<I, F>(I, F);
 
+impl<I: fmt::Debug, F> fmt::Debug for MapForGrouping<I, F> {
+    debug_fmt_fields!(MapForGrouping, 0);
+}
+
 impl<I, F> MapForGrouping<I, F> {
     pub(crate) fn new(iter: I, key_mapper: F) -> Self {
         Self(iter, key_mapper)
diff --git a/src/lib.rs b/src/lib.rs
index a9977e1..126eb22 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1162,10 +1162,11 @@ pub trait Itertools: Iterator {
     /// the product of iterators yielding multiple types, use the
     /// [`iproduct`] macro instead.
     ///
-    ///
     /// The iterator element type is `Vec<T>`, where `T` is the iterator element
     /// of the subiterators.
     ///
+    /// Note that the iterator is fused.
+    ///
     /// ```
     /// use itertools::Itertools;
     /// let mut multi_prod = (0..3).map(|i| (i * 2)..(i * 2 + 2))
diff --git a/src/permutations.rs b/src/permutations.rs
index 534ca59..cf5973c 100644
--- a/src/permutations.rs
+++ b/src/permutations.rs
@@ -1,3 +1,4 @@
+use alloc::boxed::Box;
 use alloc::vec::Vec;
 use std::fmt;
 use std::iter::once;
@@ -33,8 +34,8 @@ enum PermutationState {
     Buffered { k: usize, min_n: usize },
     /// All values from the iterator are known so `n` is known.
     Loaded {
-        indices: Vec<usize>,
-        cycles: Vec<usize>,
+        indices: Box<[usize]>,
+        cycles: Box<[usize]>,
     },
     /// No permutation left to generate.
     End,
@@ -89,8 +90,8 @@ where
                 } else {
                     let n = *min_n;
                     let prev_iteration_count = n - *k + 1;
-                    let mut indices: Vec<_> = (0..n).collect();
-                    let mut cycles: Vec<_> = (n - k..n).rev().collect();
+                    let mut indices: Box<[_]> = (0..n).collect();
+                    let mut cycles: Box<[_]> = (n - k..n).rev().collect();
                     // Advance the state to the correct point.
                     for _ in 0..prev_iteration_count {
                         if advance(&mut indices, &mut cycles) {
diff --git a/src/take_while_inclusive.rs b/src/take_while_inclusive.rs
index 077b954..0d186d7 100644
--- a/src/take_while_inclusive.rs
+++ b/src/take_while_inclusive.rs
@@ -34,7 +34,7 @@ impl<I, F> fmt::Debug for TakeWhileInclusive<I, F>
 where
     I: Iterator + fmt::Debug,
 {
-    debug_fmt_fields!(TakeWhileInclusive, iter);
+    debug_fmt_fields!(TakeWhileInclusive, iter, done);
 }
 
 impl<I, F> Iterator for TakeWhileInclusive<I, F>
diff --git a/src/with_position.rs b/src/with_position.rs
index 89cddeb..2d56bb9 100644
--- a/src/with_position.rs
+++ b/src/with_position.rs
@@ -1,3 +1,4 @@
+use std::fmt;
 use std::iter::{Fuse, FusedIterator, Peekable};
 
 /// An iterator adaptor that wraps each element in an [`Position`].
@@ -14,6 +15,14 @@ where
     peekable: Peekable<Fuse<I>>,
 }
 
+impl<I> fmt::Debug for WithPosition<I>
+where
+    I: Iterator,
+    Peekable<Fuse<I>>: fmt::Debug,
+{
+    debug_fmt_fields!(WithPosition, handled_first, peekable);
+}
+
 impl<I> Clone for WithPosition<I>
 where
     I: Clone + Iterator,


Test patch:
diff --git a/tests/quick.rs b/tests/quick.rs
index dffcd22..954655a 100644
--- a/tests/quick.rs
+++ b/tests/quick.rs
@@ -450,6 +450,12 @@ quickcheck! {
         assert_eq!(answer.into_iter().last(), a.multi_cartesian_product().last());
     }
 
+    fn correct_empty_multi_product() -> () {
+        let empty = Vec::<std::vec::IntoIter<i32>>::new().into_iter().multi_cartesian_product();
+        assert!(correct_size_hint(empty.clone()));
+        itertools::assert_equal(empty, std::iter::once(Vec::new()))
+    }
+
     #[allow(deprecated)]
     fn size_step(a: Iter<i16, Exact>, s: usize) -> bool {
         let mut s = s;
diff --git a/tests/specializations.rs b/tests/specializations.rs
index e14b1b6..fd8801e 100644
--- a/tests/specializations.rs
+++ b/tests/specializations.rs
@@ -163,7 +163,6 @@ quickcheck! {
         TestResult::passed()
     }
 
-    #[ignore] // It currently fails because `MultiProduct` is not fused.
     fn multi_cartesian_product(a: Vec<u8>, b: Vec<u8>, c: Vec<u8>) -> TestResult {
         if a.len() * b.len() * c.len() > 100 {
             return TestResult::discard();