-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Conversation
// If the index is less than 0, the && below will short-circuit, and self.Length will | ||
// not be evaluated. Then we will get an ArgumentOutOfRangeException. | ||
// Otherwise, we will get a NullReferenceException when trying to evaluate self.Length. | ||
|
||
var self = this; | ||
Requires.Range(index >= 0 && index <= self.Length, nameof(index)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a problem here where if self
is default and index
is negative, an ArgumentOutOfRangeException will be thrown. But if index
is positive or length
is negative, a NullReferenceException will be thrown. Would it be acceptable to add a self.ThrowNullRefIfNotInitialized()
before this line to make the behavior consistent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either exception should never be caught and is a sign of an error in the caller. So consistently prioritizing one over the other sounds like (mild) goodness to me.
But unless it's blocking your test development, I suggest you fix this in a separate PR as well.
{ | ||
Assert.Throws<ArgumentNullException>("items", () => ImmutableArray.CreateRange((IEnumerable<int>)null)); | ||
// Once https://github.com/xunit/assert.xunit/pull/5 comes into corefx, all the StrongBox stuff can be removed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
xUnit tries to format the collection for each test case by calling GetEnumerator
and stringifying each of the values. For default immutable arrays, this is problematic because that methods throws an InvalidOperationException. This was fixed in xunit/assert.xunit#5, but it looks like corefx hasn't picked up that change yet. I've worked around this issue temporarily by wrapping each enumerable in a StrongBox<T>
so it won't get formatted.
} | ||
|
||
[Fact] | ||
public void CreateRangeFromImmutableArrayWithSelector() | ||
public void CreateEnumerableElementType() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added extra tests here to ensure Create
creates an immutable array of collections for non-arrays.
Assert.Equal(new double[] { }, copy1); | ||
[Theory] | ||
[MemberData(nameof(CreateRangeWithSelectorData))] | ||
public void CreateRangeWithSelector<TResult>(IEnumerable<int> source, Func<int, TResult> selector, TResult dummy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another xUnit issue that has been fixed, but not made it into corefx yet: the type of TResult
cannot be inferred unless there is a TResult
parameter that isn't wrapped. xunit/xunit#965
|
||
[Theory] | ||
[MemberData(nameof(ContainsEqualityComparerData))] | ||
public void ContainsEqualityComparer<T>(IEnumerable<T> source, T value, IEqualityComparer<T> comparer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After cleaning up this test I found out this was actually calling a method in LINQ, so it isn't really relevant to ImmutableArray (aside from testing GetEnumerator, which is covered below). Should I remove this?
|
||
// TODO: The parameter names in CoreCLR need to be better. | ||
|
||
Assert.Throws<ArgumentNullException>("dest", () => array.CopyTo(null)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter names in Array.Copy
actually match that of ImmutableArray.CopyTo
(sourceIndex
, destinationIndex
, destination
, etc.) but the names in the exceptions do not match up. I will look into changing the names in CoreCLR in a later PR.
OK, I have just finished my first pass, so un-marking this as WIP. I still have some comments/skipped tests labeled with TODO on issues I am looking for guidance on. |
@@ -728,6 +728,9 @@ public ImmutableArray<T> Replace(T oldValue, T newValue) | |||
public ImmutableArray<T> Replace(T oldValue, T newValue, IEqualityComparer<T> equalityComparer) | |||
{ | |||
var self = this; | |||
// TODO: This is calling the IImmutableList<T> overload which is silently boxing the ImmutableArray<T>. | |||
// It's also causing inconsistencies where calling Replace on an IsDefault instance will throw an | |||
// InvalidOperationException rather than a NullReferenceException. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some extra test coverage that called this method on a default ImmutableArray
. That caused problems because the IndexOf
call here is actually boxing this to an IImmutableList<T>
and calling the extension method in ImmutableList
.
The fix would be to simply add self.ThrowNullRefIfNotInitialized()
before this line, or modify the IndexOf
call so it calls the ImmutableArray
overload and avoids boxing. This would be a behavioral change from InvalidOperationException
-> NullReferenceException
for default arrays, but I am assuming throwing IOE in the first place because normally everything throws NRE for default arrays. Is this OK to change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoiding boxing sounds reasonable. But let's make that change in another PR and keep this one about testing.
I would suggest you revert the TODO comment and file an issue to track.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AArnott OK, I have reverted the comment and opened https://github.com/dotnet/corefx/issues/14961 to be addresed after the test changes go in.
{ | ||
Assert.Throws<NullReferenceException>(() => s_emptyDefault.LastIndexOf(5)); | ||
Assert.Throws<NullReferenceException>(() => s_emptyDefault.LastIndexOf(5, 0)); | ||
Assert.Throws<NullReferenceException>(() => s_emptyDefault.LastIndexOf(5, 0, 0)); | ||
} | ||
|
||
// TODO: Cleanup IndexOf/LastIndexOf tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-note: Remove. This PR is big enough already and I can do this separately.
|
||
Assert.Equal(new[] { 12345 }, s_oneElemen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here as with the Contains
tests above. These are calling the Linq methods, Concat
doesn't exist on ImmutableArray
. Can I delete these?
@@ -764,6 +767,7 @@ public ImmutableArray<T> Remove(T item, IEqualityComparer<T> equalityComparer) | |||
{ | |||
var self = this; | |||
self.ThrowNullRefIfNotInitialized(); | |||
// TODO: This is silently boxing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I discovered that there was silent boxing here too.
Assert.Equal(1, removed13.Length); | ||
Assert.Equal(new[] { 2 }, removed13); | ||
[Theory] | ||
[InlineData(-1, Skip = "TODO. See comment in ImmutableArray<T>.RemoveRange(int, int).")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tl;dr RemoveRange
on a default array is throwing AOORE when the index is negative, but when other parameters are invalid it's throwing NRE because it evaluates self.Length
. Can I fix this?
var array = source.ToImmutableArray(); | ||
IEnumerable<int> expected = items.Aggregate( | ||
seed: source.ToImmutableArray(), | ||
func: (a, i) => a.Remove(i, comparer)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-note: This is the cleanest way to calculate expected
but it seems uncomfortably close to the code I'm actually testing.
|
||
Assert.Equal(new[] { 12345 }, s_oneElemen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was the one where the IImmutableList
box was causing an IOE to be thrown for default immutable arrays.
|
||
Assert.Equal(new[] { 12345 }, s_oneElemen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a review note in SetItem
. Requires.Range(index >= 0 && index < self.Length)
-- if index is negative the condition short-circuits.
Assert.Equal(array.GetHashCode(EverythingEqual<int>.Default), immArray.GetHashCode(EverythingEqual<int>.Default)); | ||
return SharedComparers<int>() | ||
.OfType<IEqualityComparer>() | ||
.Except(new IEqualityComparer[] { null }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-nit: I think all of the Except(new[] { null })
s may not be necessary. I found out (while writing this PR) what OfType
does not include nulls.
IStructuralComparable equalArray = new int[3] { 1, 2, 3 }; | ||
IStructuralComparable immArray = ImmutableArray.Create((int[])array); | ||
IStructuralComparable equalImmArray = ImmutableArray.Create((int[])equalArray); | ||
var array = source?.ToImmutableArray() ?? s_emptyDefault; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got a bit lazy here and used null
for source/other to represent a default immutable array, instead of a StrongBox
.
Assert.Equal((object)derivedImmutable, baseImmutable, EqualityComparer<object>.Default); | ||
|
||
// Make sure we can reverse that, as a means to verify the underlying array is the same instance. | ||
ImmutableArray<string> derivedImmutable2 = baseImmutable.As<string>(); | ||
Assert.False(derivedImmutable2.IsDefault); | ||
Assert.Equal(derivedImmutable, derivedImmutable2); | ||
Assert.True(derivedImmutable == derivedImmutable2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the change from Assert.Equal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ianhays When I first saw Assert.Equal
I was confused because I thought it was calling the Equal(IEnumerable<T>, IEnumerable<T>)
overload which compares the contents of the arrays. Hovering over the method in VS showed that this was not the case, but I still think this is better because the tests will fail loudly if you accidentally write Assert.True(expected == source)
(where source
is an enumerable and expected
is an ImmutableArray) whereas they will silently pass if you write Assert.Equal(expected, source)
.
Assert.Throws<NullReferenceException>(() => s_emptyDefault.Insert(1, 10)); | ||
Assert.Throws<NullReferenceException>(() => s_emptyDefault.Insert(0, 10)); | ||
} | ||
// Once https://github.com/xunit/assert.xunit/pull/5 comes into corefx, all the StrongBox stuff can be removed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of doing this workaround, why don't we bring those necessary changes into buildtools?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice idea. I will look into making a PR to buildtools that updates xunit.
{ | ||
var array = source.ToImmutableArray(); | ||
|
||
Assert.Throws<ArgumentNullException>("match", () => array.RemoveAll(match: null)); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, lets look into bringing this into buildtools so we don't have to work around its absence.
Thanks @jamesqo, these look good. Sorry for the delay in the review, there are quite a few changes here :) Did you do a coverage comparison to make sure no coverage of value was lost? |
I looked into the code coverage reports and all lines in the type are still covered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no way I'll be confident from comparing the before and after review. But as you say code coverage is no worse, and as I like the theories you've built, it looks like a good change. Thank you.
Cleanup ImmutableArray tests Commit migrated from dotnet/corefx@bf0b6e6
This is still WIP, but since it's a rather large PR and almost finished I thought I'd submit it. There are still a few TODOs floating around, but feel free to review.
Changes:
IList<T>
, lazy enumerables, etc.)Assert.Equal(immutableArray1, immutableArray2)
withAssert.True(immutableArray1 == immutableArray2)
Note: I tried to keep many of the existing test cases we have, so that's why you'll see a lot of (sometimes redundant) cases in the MemberData.
/cc @AArnott @ianhays @stephentoub