Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ConcurrentDictionary Test Coverage | TestGetOrAddOrUpdate #83429

Open
Notheisz57 opened this issue Mar 14, 2023 · 5 comments
Open

ConcurrentDictionary Test Coverage | TestGetOrAddOrUpdate #83429

Notheisz57 opened this issue Mar 14, 2023 · 5 comments
Labels
area-System.Collections test-enhancement Improvements of test source code
Milestone

Comments

@Notheisz57
Copy link

Code for context:

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
public static void TestGetOrAdd()
{
TestGetOrAddOrUpdate(1, 1, 1, 10000, true);
TestGetOrAddOrUpdate(5, 1, 1, 10000, true);
TestGetOrAddOrUpdate(1, 1, 2, 5000, true);
TestGetOrAddOrUpdate(1, 1, 5, 2000, true);
TestGetOrAddOrUpdate(4, 0, 4, 2000, true);
TestGetOrAddOrUpdate(16, 31, 4, 2000, true);
TestGetOrAddOrUpdate(64, 5, 5, 5000, true);
TestGetOrAddOrUpdate(5, 5, 5, 25000, true);
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
public static void TestAddOrUpdate()
{
TestGetOrAddOrUpdate(1, 1, 1, 10000, false);
TestGetOrAddOrUpdate(5, 1, 1, 10000, false);
TestGetOrAddOrUpdate(1, 1, 2, 5000, false);
TestGetOrAddOrUpdate(1, 1, 5, 2000, false);
TestGetOrAddOrUpdate(4, 0, 4, 2000, false);
TestGetOrAddOrUpdate(16, 31, 4, 2000, false);
TestGetOrAddOrUpdate(64, 5, 5, 5000, false);
TestGetOrAddOrUpdate(5, 5, 5, 25000, false);
}
private static void TestGetOrAddOrUpdate(int cLevel, int initSize, int threads, int addsPerThread, bool isAdd)
{
ConcurrentDictionary<int, int> dict = new ConcurrentDictionary<int, int>(cLevel, 1);
int count = threads;
using (ManualResetEvent mre = new ManualResetEvent(false))
{
for (int i = 0; i < threads; i++)
{
int ii = i;
Task.Run(
() =>
{
for (int j = 0; j < addsPerThread; j++)
{
if (isAdd)
{
//call one of the overloads of GetOrAdd
switch (j % 3)
{
case 0:
dict.GetOrAdd(j, -j);
break;
case 1:
dict.GetOrAdd(j, x => -x);
break;
case 2:
dict.GetOrAdd(j, (x,m) => x * m, -1);
break;
}
}
else
{
switch (j % 3)
{
case 0:
dict.AddOrUpdate(j, -j, (k, v) => -j);
break;
case 1:
dict.AddOrUpdate(j, (k) => -k, (k, v) => -k);
break;
case 2:
dict.AddOrUpdate(j, (k,m) => k*m, (k, v, m) => k * m, -1);
break;
}
}
}
if (Interlocked.Decrement(ref count) == 0) mre.Set();
});
}
mre.WaitOne();
}
foreach (var pair in dict)
{
Assert.Equal(pair.Key, -pair.Value);
}
List<int> gotKeys = new List<int>();
foreach (var pair in dict)
gotKeys.Add(pair.Key);
gotKeys.Sort();
List<int> expectKeys = new List<int>();
for (int i = 0; i < addsPerThread; i++)
expectKeys.Add(i);
Assert.Equal(expectKeys.Count, gotKeys.Count);
for (int i = 0; i < expectKeys.Count; i++)
{
Assert.True(expectKeys[i].Equals(gotKeys[i]),
string.Format("* Test '{4}': Level={0}, initSize={1}, threads={2}, addsPerThread={3})" + Environment.NewLine +
"> FAILED. The set of keys in the dictionary is are not the same as the expected.",
cLevel, initSize, threads, addsPerThread, isAdd ? "GetOrAdd" : "GetOrUpdate"));
}
// Finally, let's verify that the count is reported correctly.
Assert.Equal(addsPerThread, dict.Count);
Assert.Equal(addsPerThread, dict.ToArray().Length);
}

While the other switch-case statements make use of the lambda arguments, the case statement on line 547 does not:

Should this test case be more thorough/complete in testing these overloads and their lambda arguments, or is this test case good enough as it is?

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Mar 14, 2023
@ghost
Copy link

ghost commented Mar 14, 2023

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

Issue Details

Code for context:

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
public static void TestGetOrAdd()
{
TestGetOrAddOrUpdate(1, 1, 1, 10000, true);
TestGetOrAddOrUpdate(5, 1, 1, 10000, true);
TestGetOrAddOrUpdate(1, 1, 2, 5000, true);
TestGetOrAddOrUpdate(1, 1, 5, 2000, true);
TestGetOrAddOrUpdate(4, 0, 4, 2000, true);
TestGetOrAddOrUpdate(16, 31, 4, 2000, true);
TestGetOrAddOrUpdate(64, 5, 5, 5000, true);
TestGetOrAddOrUpdate(5, 5, 5, 25000, true);
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
public static void TestAddOrUpdate()
{
TestGetOrAddOrUpdate(1, 1, 1, 10000, false);
TestGetOrAddOrUpdate(5, 1, 1, 10000, false);
TestGetOrAddOrUpdate(1, 1, 2, 5000, false);
TestGetOrAddOrUpdate(1, 1, 5, 2000, false);
TestGetOrAddOrUpdate(4, 0, 4, 2000, false);
TestGetOrAddOrUpdate(16, 31, 4, 2000, false);
TestGetOrAddOrUpdate(64, 5, 5, 5000, false);
TestGetOrAddOrUpdate(5, 5, 5, 25000, false);
}
private static void TestGetOrAddOrUpdate(int cLevel, int initSize, int threads, int addsPerThread, bool isAdd)
{
ConcurrentDictionary<int, int> dict = new ConcurrentDictionary<int, int>(cLevel, 1);
int count = threads;
using (ManualResetEvent mre = new ManualResetEvent(false))
{
for (int i = 0; i < threads; i++)
{
int ii = i;
Task.Run(
() =>
{
for (int j = 0; j < addsPerThread; j++)
{
if (isAdd)
{
//call one of the overloads of GetOrAdd
switch (j % 3)
{
case 0:
dict.GetOrAdd(j, -j);
break;
case 1:
dict.GetOrAdd(j, x => -x);
break;
case 2:
dict.GetOrAdd(j, (x,m) => x * m, -1);
break;
}
}
else
{
switch (j % 3)
{
case 0:
dict.AddOrUpdate(j, -j, (k, v) => -j);
break;
case 1:
dict.AddOrUpdate(j, (k) => -k, (k, v) => -k);
break;
case 2:
dict.AddOrUpdate(j, (k,m) => k*m, (k, v, m) => k * m, -1);
break;
}
}
}
if (Interlocked.Decrement(ref count) == 0) mre.Set();
});
}
mre.WaitOne();
}
foreach (var pair in dict)
{
Assert.Equal(pair.Key, -pair.Value);
}
List<int> gotKeys = new List<int>();
foreach (var pair in dict)
gotKeys.Add(pair.Key);
gotKeys.Sort();
List<int> expectKeys = new List<int>();
for (int i = 0; i < addsPerThread; i++)
expectKeys.Add(i);
Assert.Equal(expectKeys.Count, gotKeys.Count);
for (int i = 0; i < expectKeys.Count; i++)
{
Assert.True(expectKeys[i].Equals(gotKeys[i]),
string.Format("* Test '{4}': Level={0}, initSize={1}, threads={2}, addsPerThread={3})" + Environment.NewLine +
"> FAILED. The set of keys in the dictionary is are not the same as the expected.",
cLevel, initSize, threads, addsPerThread, isAdd ? "GetOrAdd" : "GetOrUpdate"));
}
// Finally, let's verify that the count is reported correctly.
Assert.Equal(addsPerThread, dict.Count);
Assert.Equal(addsPerThread, dict.ToArray().Length);
}

While the other switch-case statements make use of the lambda arguments, the case statement on line 547 does not:

Should this test case be more thorough/complete in testing these overloads and their lambda arguments, or is this test case good enough as it is?

Author: Notheisz57
Assignees: -
Labels:

area-System.Collections

Milestone: -

@stephentoub
Copy link
Member

Should this test case be more thorough/complete in testing these overloads and their lambda arguments, or is this test case good enough as it is?

Can you come up with a change to the source code for ConcurrentDictionary that would break its functionality but it would still pass all of these tests?

@eiriktsarpalis eiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Mar 15, 2023
@ghost
Copy link

ghost commented Mar 15, 2023

This issue has been marked needs-author-action and may be missing some important information.

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Mar 15, 2023
@Notheisz57
Copy link
Author

This seems to pass the existing tests in System.Collections.Concurrent.Tests without being too contrived:

Code for context:

public TValue AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory)
{
if (key is null)
{
ThrowHelper.ThrowKeyNullException();
}
if (updateValueFactory is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(updateValueFactory));
}
Tables tables = _tables;
IEqualityComparer<TKey>? comparer = tables._comparer;
int hashcode = GetHashCode(comparer, key);
while (true)
{
if (TryGetValueInternal(tables, key, hashcode, out TValue? oldValue))
{
// key exists, try to update
TValue newValue = updateValueFactory(key, oldValue);
if (TryUpdateInternal(tables, key, hashcode, newValue, oldValue))
{
return newValue;
}
}
else
{
// key doesn't exist, try to add
if (TryAddInternal(tables, key, hashcode, addValue, updateIfExists: false, acquireLock: true, out TValue resultingValue))
{
return resultingValue;
}
}
if (tables != _tables)
{
tables = _tables;
if (!ReferenceEquals(comparer, tables._comparer))
{
comparer = tables._comparer;
hashcode = GetHashCode(comparer, key);
}
}
}
}

This line:

Changed to something like this:

TValue newValue = updateValueFactory(key, addValue);

When key is already present, updateValueFactory would receive addValue instead of the existing oldValue.

@ghost ghost added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed needs-author-action An issue or pull request that requires more info or actions from the author. labels Mar 15, 2023
@stephentoub
Copy link
Member

If that's the case then please feel free to submit a PR that augments the tests in a way where they'd fail if that broke.

@eiriktsarpalis eiriktsarpalis added this to the Future milestone Mar 16, 2023
@eiriktsarpalis eiriktsarpalis added test-enhancement Improvements of test source code and removed needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration labels Mar 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Collections test-enhancement Improvements of test source code
Projects
None yet
Development

No branches or pull requests

3 participants