Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion library/alloc/src/collections/btree/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2416,7 +2416,7 @@ impl<K, V> Default for BTreeMap<K, V> {
#[stable(feature = "rust1", since = "1.0.0")]
impl<K: PartialEq, V: PartialEq, A: Allocator + Clone> PartialEq for BTreeMap<K, V, A> {
fn eq(&self, other: &BTreeMap<K, V, A>) -> bool {
self.iter().eq(other)
self.len() == other.len() && self.iter().zip(other).all(|(a, b)| a == b)
}
}

Expand Down
96 changes: 96 additions & 0 deletions library/alloctests/tests/collections/eq_diff_len.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//! Regression tests which fail if some collections' `PartialEq::eq` impls compare
//! elements when the collections have different sizes.
//! This behavior is not guaranteed either way, so regressing these tests is fine
//! if it is done on purpose.
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, LinkedList};

/// This intentionally has a panicking `PartialEq` impl, to test that various
/// collections' `PartialEq` impls don't actually compare elements if their sizes
/// are unequal.
///
/// This is not advisable in normal code.
#[derive(Debug, Clone, Copy, Hash)]
struct Evil;

impl PartialEq for Evil {
fn eq(&self, _: &Self) -> bool {
panic!("Evil::eq is evil");
}
}
impl Eq for Evil {}

impl PartialOrd for Evil {
fn partial_cmp(&self, _: &Self) -> Option<Ordering> {
Some(Ordering::Equal)
}
}

impl Ord for Evil {
fn cmp(&self, _: &Self) -> Ordering {
// Constructing a `BTreeSet`/`BTreeMap` uses `cmp` on the elements,
// but comparing it with with `==` uses `eq` on the elements,
// so Evil::cmp doesn't need to be evil.
Ordering::Equal
}
}

// check Evil works
#[test]
#[should_panic = "Evil::eq is evil"]
fn evil_eq_works() {
let v1 = vec![Evil];
let v2 = vec![Evil];

_ = v1 == v2;
}

// check various containers don't compare if their sizes are different

#[test]
fn vec_evil_eq() {
let v1 = vec![Evil];
let v2 = vec![Evil; 2];

assert_eq!(false, v1 == v2);
}

#[test]
fn hashset_evil_eq() {
let s1 = HashSet::from([(0, Evil)]);
let s2 = HashSet::from([(0, Evil), (1, Evil)]);
Comment on lines +60 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let s1 = HashSet::from([(0, Evil)]);
let s2 = HashSet::from([(0, Evil), (1, Evil)]);
let s1 = HashSet::from([Evil]);
let s2 = HashSet::from([Evil; 2]);

the various Set collections don't need to have (usize, Evil) tuples as elements.
Actually, I'm not even sure that having these tuples would catch the failures you're testing for, since the usize would be compared first and found unequal, so Evil::eq would never be called. No?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this test actually fail on beta?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that I think about it, I'm not completely sure the Map-collection tests would catch this regression either, since their keys would always compare as unequal and short-circuit the PartialEq implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the various Set collections don't need to have (usize, Evil) tuples as elements. Actually, I'm not even sure that having these tuples would catch the failures you're testing for, since the usize would be compared first and found unequal, so Evil::eq would never be called. No?

The suggested change makes the two sets have the same length, so would not be testing the case of comparing sets with different lengths (actually, it makes the test panic during HashSet::from([Evil; 2]), because HashSet will call Evil::eq to deduplicate during construction). The tuples are there specifically to avoid calling Evil::eq when constructing the sets/maps, since the first element/key makes the comparison short-circuit.

now that I think about it, I'm not completely sure the Map-collection tests would catch this regression either, since their keys would always compare as unequal and short-circuit the PartialEq implementation?

As I said in #149125 (comment) ,

(I think teeeechically a regression for (BTree,Hash)(Map,Set) could get past the test if they compare the (1, Evil) to the (0, Evil) first and bail early, but I think that's an unlikely change for BTree*, and would be nondeterministic for Hash* so would be caught eventually)

To expand on that: currently BTreeMap's eq impl goes in order, so if it did compare the elements, it would start with the two first elements which are both (0, Evil), so it would call Evil::eq. Similarly, HashMap's eq impl (checks the lengths are equal, and then) iterates over self and checks that all of self's keys exist in other with the equal values.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, missed that paragraph from the PR description. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A holistic test would probably need a struct Evil(usize); where the Hash implementation is guaranteed to return a different hash for Evil(0) and Evil(1), but panic in the impl for PartialEq::eq?
(just curious, not sure if it's worth the implementation, and even if it did, could definitely go in a follow-up PR)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it? was HashSet in the affected set?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it? was HashSet in the affected set?

Right, it shouldn't!
Brain went AWOL for a bit there :D


assert_eq!(false, s1 == s2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: assert_eq! with true or false is kinda weird, IMHO.

Suggested change
assert_eq!(false, s1 == s2);
assert_ne!(s1, s2);

(also pertinent to rest of the assertions in this file)

}

#[test]
fn hashmap_evil_eq() {
let m1 = HashMap::from([(0, Evil)]);
let m2 = HashMap::from([(0, Evil), (1, Evil)]);

assert_eq!(false, m1 == m2);
}

#[test]
fn btreeset_evil_eq() {
let s1 = BTreeSet::from([(0, Evil)]);
let s2 = BTreeSet::from([(0, Evil), (1, Evil)]);

assert_eq!(false, s1 == s2);
}

#[test]
fn btreemap_evil_eq() {
let m1 = BTreeMap::from([(0, Evil)]);
let m2 = BTreeMap::from([(0, Evil), (1, Evil)]);

assert_eq!(false, m1 == m2);
}

#[test]
fn linkedlist_evil_eq() {
let m1 = LinkedList::from([Evil]);
let m2 = LinkedList::from([Evil; 2]);

assert_eq!(false, m1 == m2);
}
1 change: 1 addition & 0 deletions library/alloctests/tests/collections/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod binary_heap;
mod eq_diff_len;
Loading