From 32522585aa71c0d00cc87542598972b7923d28a3 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Sun, 26 Jan 2025 21:24:48 -0500 Subject: [PATCH] Add unit tests for Collections, #1116 --- .../Support/TestCollections.cs | 274 ++++++++++++++++++ src/Lucene.Net/Support/Collections.cs | 86 ++---- 2 files changed, 299 insertions(+), 61 deletions(-) create mode 100644 src/Lucene.Net.Tests/Support/TestCollections.cs diff --git a/src/Lucene.Net.Tests/Support/TestCollections.cs b/src/Lucene.Net.Tests/Support/TestCollections.cs new file mode 100644 index 0000000000..feefa1e77e --- /dev/null +++ b/src/Lucene.Net.Tests/Support/TestCollections.cs @@ -0,0 +1,274 @@ +// Some tests adapted from Apache Harmony: +// https://github.com/apache/harmony/blob/02970cb7227a335edd2c8457ebdde0195a735733/classlib/modules/luni/src/test/api/common/org/apache/harmony/luni/tests/java/util/CollectionsTest.java + +using Lucene.Net.Attributes; +using Lucene.Net.Util; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Lucene.Net.Support +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [TestFixture] + public class TestCollections : LuceneTestCase + { + private List ll = null!; // LUCENENET specific: was LinkedList in Harmony tests, !: will be initialized in SetUp + + // LUCENENET - omitting unused fields + + private static object[] objArray = LoadObjArray(); // LUCENENET - use static loader method instead of static ctor + + private static object[] LoadObjArray() + { + object[] objArray = new object[1000]; + for (int i = 0; i < objArray.Length; i++) + { + objArray[i] = i; + } + + return objArray; + } + + [Test, LuceneNetSpecific] + public void TestEmptyList() + { + IList list = Collections.EmptyList(); + + Assert.AreEqual(0, list.Count); + Assert.IsTrue(list.IsReadOnly); + Assert.Throws(() => list.Add(new object())); + + IList list2 = Collections.EmptyList(); + + Assert.AreSame(list, list2); // ensure it does not allocate + } + + [Test, LuceneNetSpecific] + public void TestEmptyMap() + { + IDictionary map = Collections.EmptyMap(); + + Assert.AreEqual(0, map.Count); + Assert.IsTrue(map.IsReadOnly); + Assert.Throws(() => map.Add(new object(), new object())); + + IDictionary map2 = Collections.EmptyMap(); + + Assert.AreSame(map, map2); // ensure it does not allocate + } + + [Test, LuceneNetSpecific] + public void TestEmptySet() + { + ISet set = Collections.EmptySet(); + + Assert.AreEqual(0, set.Count); + Assert.IsTrue(set.IsReadOnly); + Assert.Throws(() => set.Add(new object())); + + ISet set2 = Collections.EmptySet(); + + Assert.AreSame(set, set2); // ensure it does not allocate + } + + /// + /// Adapted from Harmony test_reverseLjava_util_List() + /// + [Test] + public void TestReverse() + { + // Test for method void java.util.Collections.reverse(java.util.List) + try + { + Collections.Reverse(null!); + fail("Expected NullPointerException for null list parameter"); + } + catch (Exception e) when (e.IsNullPointerException()) + { + //Expected + } + + Collections.Reverse(ll); + using var i = ll.GetEnumerator(); + int count = objArray.Length - 1; + while (i.MoveNext()) + { + assertEquals("Failed to reverse collection", objArray[count], i.Current); + --count; + } + + var myList = new List + { + null, + 20, + }; + Collections.Reverse(myList); + assertEquals($"Did not reverse correctly--first element is: {myList[0]}", 20, myList[0]); + assertNull($"Did not reverse correctly--second element is: {myList[1]}", myList[1]); + } + + /// + /// Adapted from Harmony test_reverseOrder() + /// + [Test] + public void TestReverseOrder() { + // Test for method IComparer + // Collections.ReverseOrder() + // assumes no duplicates in ll + IComparer comp = Collections.ReverseOrder(); + var list2 = new List(ll); // LUCENENET - was LinkedList in Harmony + list2.Sort(comp); + int llSize = ll.Count; + for (int counter = 0; counter < llSize; counter++) + { + assertEquals("New comparator does not reverse sorting order", list2[llSize - counter - 1], ll[counter]); + } + } + + [Test, LuceneNetSpecific] + public void TestReverseOrder_WithComparer() + { + IComparer comp = Collections.ReverseOrder(StringComparer.OrdinalIgnoreCase); + var list = new List { "B", "c", "a", "D" }; + list.Sort(comp); + Assert.AreEqual(4, list.Count); + Assert.AreEqual("D", list[0]); + Assert.AreEqual("c", list[1]); + Assert.AreEqual("B", list[2]); + Assert.AreEqual("a", list[3]); + } + + [Test, LuceneNetSpecific] + public void TestSingletonMap() + { + IDictionary map = Collections.SingletonMap("key", "value"); + + Assert.AreEqual(1, map.Count); + Assert.IsTrue(map.IsReadOnly); + Assert.Throws(() => map.Add("key2", "value2")); + Assert.Throws(() => map["key"] = "value2"); + + Assert.AreEqual("value", map["key"]); + } + + [Test, LuceneNetSpecific] + public void TestToString_Collection_Null() + { + Assert.AreEqual("null", Collections.ToString(null!)); + } + + [Test, LuceneNetSpecific] + public void TestToString_Collection_Empty() + { + Assert.AreEqual("[]", Collections.ToString(new List())); + } + + [Test, LuceneNetSpecific] + public void TestToString_Collection() + { + var list = new List(); + list.Add(list); + list.Add(1); + list.Add('a'); + list.Add(2.1); + list.Add("xyz"); + list.Add(new List { 1, 2, 3 }); + list.Add(null); + + Assert.AreEqual("[(this Collection), 1, a, 2.1, xyz, [1, 2, 3], null]", Collections.ToString(list)); + } + + [Test, LuceneNetSpecific] + public void TestToString_Dictionary() + { + var dict = new Dictionary() + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", 'a' }, + { "key4", 3.1 }, + { "key5", new List { 1, 2, 3 } }, + { "key6", null } + }; + + Assert.AreEqual("{key1=value1, key2=2, key3=a, key4=3.1, key5=[1, 2, 3], key6=null}", Collections.ToString(dict)); + } + + [Test, LuceneNetSpecific] + public void TestToString_Object_Null() + { + Assert.AreEqual("null", Collections.ToString(null)); + } + + [Test, LuceneNetSpecific] + public void TestToString_Object() + { + Assert.AreEqual("1", Collections.ToString(1)); + Assert.AreEqual("a", Collections.ToString('a')); + Assert.AreEqual("2.1", Collections.ToString(2.1)); + Assert.AreEqual("xyz", Collections.ToString("xyz")); + Assert.AreEqual("[1, 2, 3]", Collections.ToString(new List { 1, 2, 3 })); + } + + [Test, LuceneNetSpecific] + public void TestAsReadOnly_List() + { + var list = new List { 1, 2, 3 }; + IList readOnlyList = Collections.AsReadOnly(list); + + Assert.AreEqual(3, readOnlyList.Count); + Assert.IsTrue(readOnlyList.IsReadOnly); + Assert.Throws(() => readOnlyList.Add(4)); + Assert.Throws(() => readOnlyList[0] = 5); + } + + [Test, LuceneNetSpecific] + public void TestAsReadOnly_Dictionary() + { + var dict = new Dictionary + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", 'a' } + }; + IDictionary readOnlyDict = Collections.AsReadOnly(dict); + + Assert.AreEqual(3, readOnlyDict.Count); + Assert.IsTrue(readOnlyDict.IsReadOnly); + Assert.Throws(() => readOnlyDict.Add("key4", 4)); + Assert.Throws(() => readOnlyDict["key1"] = "value2"); + } + + public override void SetUp() + { + base.SetUp(); + + ll = new List(); + // LUCENENET - omitting unused fields + + for (int i = 0; i < objArray.Length; i++) + { + ll.Add(objArray[i]); + } + } + } +} diff --git a/src/Lucene.Net/Support/Collections.cs b/src/Lucene.Net/Support/Collections.cs index 87d33daa7a..40c637b35c 100644 --- a/src/Lucene.Net/Support/Collections.cs +++ b/src/Lucene.Net/Support/Collections.cs @@ -1,14 +1,13 @@ using J2N; using J2N.Collections.Generic.Extensions; using J2N.Collections.ObjectModel; -using J2N.Globalization; using Lucene.Net.Diagnostics; using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; using JCG = J2N.Collections.Generic; +#nullable enable namespace Lucene.Net.Support { @@ -72,10 +71,10 @@ public static void Reverse(IList list) public static IComparer ReverseOrder() { - return (IComparer)ReverseComparer.REVERSE_ORDER; + return ReverseComparer.REVERSE_ORDER; } - public static IComparer ReverseOrder(IComparer cmp) + public static IComparer ReverseOrder(IComparer? cmp) { if (cmp is null) return ReverseOrder(); @@ -87,16 +86,16 @@ public static IComparer ReverseOrder(IComparer cmp) } public static IDictionary SingletonMap(TKey key, TValue value) + where TKey : notnull { return AsReadOnly(new Dictionary { { key, value } }); } - /// /// This is the same implementation of ToString from Java's AbstractCollection /// (the default implementation for all sets and lists) /// - public static string ToString(ICollection collection) + public static string ToString(ICollection? collection) { if (collection is null) return "null"; @@ -114,7 +113,7 @@ public static string ToString(ICollection collection) while (true) { T e = it.Current; - sb.Append(object.ReferenceEquals(e, collection) ? "(this Collection)" : (isValueType ? e.ToString() : ToString(e))); + sb.Append(ReferenceEquals(e, collection) ? "(this Collection)" : (isValueType ? Convert.ToString(e, CultureInfo.InvariantCulture) : ToString(e))); if (!it.MoveNext()) { return sb.Append(']').ToString(); @@ -123,23 +122,11 @@ public static string ToString(ICollection collection) } } - /// - /// This is the same implementation of ToString from Java's AbstractCollection - /// (the default implementation for all sets and lists), plus the ability - /// to specify culture for formatting of nested numbers and dates. Note that - /// this overload will change the culture of the current thread. - /// - public static string ToString(ICollection collection, CultureInfo culture) - { - using var context = new CultureContext(culture); - return ToString(collection); - } - /// /// This is the same implementation of ToString from Java's AbstractMap /// (the default implementation for all dictionaries) /// - public static string ToString(IDictionary dictionary) + public static string ToString(IDictionary? dictionary) { if (dictionary is null) return "null"; @@ -160,9 +147,9 @@ public static string ToString(IDictionary dictionary KeyValuePair e = i.Current; TKey key = e.Key; TValue value = e.Value; - sb.Append(object.ReferenceEquals(key, dictionary) ? "(this Dictionary)" : (keyIsValueType ? key.ToString() : ToString(key))); + sb.Append(ReferenceEquals(key, dictionary) ? "(this Dictionary)" : (keyIsValueType ? Convert.ToString(key, CultureInfo.InvariantCulture) : ToString(key))); sb.Append('='); - sb.Append(object.ReferenceEquals(value, dictionary) ? "(this Dictionary)" : (valueIsValueType ? value.ToString() : ToString(value))); + sb.Append(ReferenceEquals(value, dictionary) ? "(this Dictionary)" : (valueIsValueType ? Convert.ToString(value, CultureInfo.InvariantCulture) : ToString(value))); if (!i.MoveNext()) { return sb.Append('}').ToString(); @@ -171,24 +158,17 @@ public static string ToString(IDictionary dictionary } } - /// - /// This is the same implementation of ToString from Java's AbstractMap - /// (the default implementation for all dictionaries), plus the ability - /// to specify culture for formatting of nested numbers and dates. Note that - /// this overload will change the culture of the current thread. - /// - public static string ToString(IDictionary dictionary, CultureInfo culture) - { - using var context = new CultureContext(culture); - return ToString(dictionary); - } - /// /// This is a helper method that assists with recursively building /// a string of the current collection and all nested collections. /// - public static string ToString(object obj) + public static string? ToString(object? obj) { + if (obj is null) + { + return "null"; + } + Type t = obj.GetType(); if (t.IsGenericType && (t.ImplementsGenericInterface(typeof(ICollection<>))) @@ -198,19 +178,7 @@ public static string ToString(object obj) return ToString(genericType); } - return obj.ToString(); - } - - /// - /// This is a helper method that assists with recursively building - /// a string of the current collection and all nested collections, plus the ability - /// to specify culture for formatting of nested numbers and dates. Note that - /// this overload will change the culture of the current thread. - /// - public static string ToString(object obj, CultureInfo culture) - { - using var context = new CultureContext(culture); - return ToString(obj); + return Convert.ToString(obj, CultureInfo.InvariantCulture); } public static ReadOnlyList AsReadOnly(IList list) @@ -227,14 +195,14 @@ public static ReadOnlyDictionary AsReadOnly(IDiction #region ReverseComparer - private class ReverseComparer : IComparer + private class ReverseComparer : IComparer { internal static readonly ReverseComparer REVERSE_ORDER = new ReverseComparer(); - public int Compare(T x, T y) + public int Compare(T? x, T? y) { // LUCENENET specific: Use J2N's Comparer to mimic Java comparison behavior - return JCG.Comparer.Default.Compare(y, x); + return JCG.Comparer.Default.Compare(y, x); } } @@ -242,7 +210,7 @@ public int Compare(T x, T y) #region ReverseComparer2 - private class ReverseComparer2 : IComparer + private class ReverseComparer2 : IComparer { /** @@ -252,20 +220,21 @@ private class ReverseComparer2 : IComparer * * @serial */ - internal readonly IComparer cmp; + internal readonly IComparer cmp; public ReverseComparer2(IComparer cmp) { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (Debugging.AssertsEnabled) Debugging.Assert(cmp != null); - this.cmp = cmp; + this.cmp = cmp!; } - public int Compare(T t1, T t2) + public int Compare(T? t1, T? t2) { return cmp.Compare(t2, t1); } - public override bool Equals(object o) + public override bool Equals(object? o) { return (o == this) || (o is ReverseComparer2 reverseComparer2 && @@ -276,11 +245,6 @@ public override int GetHashCode() { return cmp.GetHashCode() ^ int.MinValue; } - - public IComparer Reversed() - { - return cmp; - } } #endregion ReverseComparer2