Skip to content

Commit

Permalink
Initial commit of Xledger.Collections and tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
oconnor0 committed Oct 8, 2024
1 parent f11c5d5 commit 6cf5a22
Show file tree
Hide file tree
Showing 18 changed files with 1,472 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Build .NET assemblies and test

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet restore
- run: dotnet build --no-restore
- run: dotnet test --no-build --verbosity normal
36 changes: 36 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Upload .NET package

on:
release:
types: [created]

jobs:
release:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
env:
version: ${{ github.event.release.tag_name }}
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build and pack
run: |
echo $version
echo ${version:1}
case $version in v*) true;; *) echo "Git tag must start with a 'v'"; false;; esac
export version=${version:1}
echo $version
dotnet build /p:VersionPrefix=$version --configuration Release Xledger.Sql
dotnet pack /p:VersionPrefix=$version --configuration Release Xledger.Sql
# https://learn.microsoft.com/en-us/nuget/quickstart/create-and-publish-a-package-using-the-dotnet-cli
- name: Push package to nuget
run: dotnet nuget push Xledger.Sql/bin/Release/*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
# run: dotnet nuget push Xledger.Sql/bin/Release/*.nupkg --api-key "$NUGET_API_KEY" --source https://apiint.nugettest.org/v3/index.json
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
10 changes: 10 additions & 0 deletions Xledger.Collections.Test/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "This is testing the entire collection interface.", Scope = "namespaceanddescendants", Target = "~N:Xledger.Collections.Test")]
[assembly: SuppressMessage("Assertions", "xUnit2017:Do not use Contains() to check if a value exists in a collection", Justification = "This is testing the entire collection interface.", Scope = "namespaceanddescendants", Target = "~N:Xledger.Collections.Test")]
[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Polyfill.>", Scope = "namespace", Target = "~N:System.Runtime.CompilerServices")]
11 changes: 11 additions & 0 deletions Xledger.Collections.Test/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#if NETFRAMEWORK
namespace System.Runtime.CompilerServices;
/// <summary>
/// Allows compiling <c>init</c> properties for .NET Framework.
/// https://developercommunity.visualstudio.com/t/error-cs0518-predefined-type-systemruntimecompiler/1244809
/// https://stackoverflow.com/questions/64749385/predefined-type-system-runtime-compilerservices-isexternalinit-is-not-defined
/// https://stackoverflow.com/questions/62648189/testing-c-sharp-9-0-in-vs2019-cs0518-isexternalinit-is-not-defined-or-imported
/// </summary>
[ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)]
static class IsExternalInit { }
#endif
214 changes: 214 additions & 0 deletions Xledger.Collections.Test/TestImmArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
namespace Xledger.Collections.Test;

public class TestImmArray {
[Fact]
public void TestEmpty() {
var imm = ImmArray<string>.Empty;
Assert.Empty(imm);
Assert.Equal(0, imm.Count);
Assert.False(imm.GetEnumerator().MoveNext());
Assert.Equal(imm.GetHashCode(), new ImmArray<string>().GetHashCode());
Assert.Equal(imm, new ImmArray<string>());
Assert.Equal(imm, imm);
Assert.Equal(new ImmArray<string>(), imm);
Assert.Equal("[]", imm.ToString());

Assert.False(imm.Equals((object)null));
Assert.False(imm.Equals((ImmArray<string>)null));
}

[Fact]
public void TestSingle() {
var imm1 = new ImmArray<int>([1]);
Assert.NotEmpty(imm1);
Assert.Equal(1, imm1.Count);
Assert.Equal(imm1, imm1);
Assert.True(imm1.GetEnumerator().MoveNext());
Assert.Equal("[1]", imm1.ToString());

var imm2 = new ImmArray<int>([1]);
Assert.Equal(imm1.GetHashCode(), imm2.GetHashCode());
Assert.Equal(imm1, imm2);
Assert.Equal(imm2, imm1);
}

[Fact]
public void TestCopy() {
int[] arr = [1, 2, 3, 4, 5, 6];
var lst = new List<int>(arr);
var imm = arr.ToImmArray();

int[] target1 = new int[100];
lst.CopyTo(target1);
lst.CopyTo(target1, 12);
lst.CopyTo(2, target1, 30, 3);

int[] target2 = new int[100];
imm.CopyTo(target2);
imm.CopyTo(target2, 12);
imm.CopyTo(2, target2, 30, 3);

Assert.Equal(target1, target2);

target1 = lst.ToArray();
target1[0] = 99;
Assert.NotEqual(lst, target1);

target2 = imm.ToArray();
target2[0] = 99;
Assert.NotEqual(lst, target2);
}

[Fact]
public void TestContains() {
int[] arr = [1, 2, 3, 1, 2, 2];
var lst = new List<int>(arr);
var imm = arr.ToImmArray();

Assert.Equal(0, lst.IndexOf(1));
Assert.Equal(1, lst.IndexOf(2));
Assert.Equal(2, lst.IndexOf(3));
Assert.False(lst.Contains(9));
Assert.True(lst.Contains(3));

Assert.Equal(0, imm.IndexOf(1));
Assert.Equal(1, imm.IndexOf(2));
Assert.Equal(2, imm.IndexOf(3));
Assert.False(imm.Contains(9));
Assert.True(imm.Contains(3));

Assert.Equal(lst.IndexOf(-1), imm.IndexOf(-1));
Assert.Equal(lst.IndexOf(0), imm.IndexOf(0));
Assert.Equal(lst.IndexOf(1), imm.IndexOf(1));
Assert.Equal(lst.IndexOf(2), imm.IndexOf(2));
Assert.Equal(lst.IndexOf(3), imm.IndexOf(3));
Assert.Equal(lst.IndexOf(9), imm.IndexOf(9));
}

[Fact]
public void TestMutability() {
int[] arr = [1, 2, 3, 4, 5, 6];
var lst = new List<int>(arr);
Assert.Equal(arr, lst);

ImmArray<int> foo = ImmArray.Of(1, 2, 3);

// Are ImmArrays sane?
var imm1 = arr.ToImmArray();
var imm2 = new ImmArray<int>(arr);
var imm3 = lst.ToImmArray();
Assert.Equal(imm1, imm2);
Assert.Equal(imm1, imm3);
Assert.Equal(imm2, imm3);
Assert.Equal(arr, imm1);
Assert.Equal(arr, imm2);
Assert.Equal(arr, imm3);
Assert.Equal(lst, imm1);
Assert.Equal(lst, imm2);
Assert.Equal(lst, imm3);
Assert.True(imm1.Equals(imm2));
Assert.True(imm1.Equals(imm3));
Assert.True(imm2.Equals(imm1));
Assert.True(imm2.Equals(imm3));
Assert.True(imm3.Equals(imm1));
Assert.True(imm3.Equals(imm2));

Assert.True(imm1.Equals((object)imm2));
Assert.True(imm1.Equals((object)imm3));
Assert.True(imm2.Equals((object)imm1));
Assert.True(imm2.Equals((object)imm3));
Assert.True(imm3.Equals((object)imm1));
Assert.True(imm3.Equals((object)imm2));

Assert.True(imm1 == imm2);
Assert.True(imm2 == imm3);

// Yes.
// And now we mutate.

// Mutating the shared backing array is visible.
arr[0] = 0;
Assert.Equal(1, imm1[0]);
Assert.Equal(0, imm2[0]);
Assert.Equal(1, imm3[0]);

Assert.True(imm1.Equals(imm3));
Assert.True(imm3.Equals(imm1));
Assert.False(imm1.Equals(imm2));
Assert.False(imm2.Equals(imm1));
Assert.False(imm2.Equals(imm3));
Assert.False(imm3.Equals(imm2));

// Revert the bad change for sanity's sake.
arr[0] = 1;
Assert.True(imm1.Equals(imm2));
Assert.True(imm2.Equals(imm3));

// Mutating the list an ImmArray was created from is not visible.
lst[1] = 0;
Assert.Equal(2, imm1[1]);
Assert.Equal(2, imm2[1]);
Assert.Equal(2, imm3[1]);
Assert.True(imm1.Equals(imm2));
Assert.True(imm1.Equals(imm3));
Assert.True(imm2.Equals(imm1));
Assert.True(imm2.Equals(imm3));
Assert.True(imm3.Equals(imm1));
Assert.True(imm3.Equals(imm2));
}

[Fact]
public void TestNoOps() {
var imm = ImmArray.Of(7, 6, 5, 4);
IList<int> ilist = imm;
Assert.ThrowsAny<NotSupportedException>(() => ilist[0] = 1);
Assert.ThrowsAny<NotSupportedException>(() => ilist.Add(1));
Assert.ThrowsAny<NotSupportedException>(() => ilist.Clear());
Assert.ThrowsAny<NotSupportedException>(() => ilist.Insert(1, 2));
Assert.ThrowsAny<NotSupportedException>(() => ilist.Remove(4));
Assert.ThrowsAny<NotSupportedException>(() => ilist.RemoveAt(2));
Assert.ThrowsAny<NotSupportedException>(() => ((System.Collections.ICollection)ilist).CopyTo((Array)null, 0));
}

[Fact]
public void TestCompareTo() {
Assert.True(null <= (ImmArray<string>)null);

var imm1 = ImmArray.Of("foo", "bar", "baz");
Assert.True(null < imm1);
Assert.True(null <= imm1);
Assert.True(imm1 > null);
Assert.True(imm1 >= null);

var imm2 = ImmArray.Of("foo", "bar", "zip");
Assert.True(imm1 < imm2);
Assert.True(imm1 <= imm2);
Assert.True(imm2 > imm1);
Assert.True(imm2 >= imm1);
}


[Fact]
public void TestCompareToRecord() {
var imm1 = ImmArray.Of(
new Employee(1, "Bas Rutten", 10_000),
new Employee(2, "Conor McGregor", 200_000),
new Employee(3, "Frank Shamrock", 6_000));
Assert.True(null < imm1);
Assert.True(null <= imm1);
Assert.True(imm1 > null);
Assert.True(imm1 >= null);

var imm2 = ImmArray.Of(
new Employee(1, "Bas Rutten", 10_000),
new Employee(2, "Conor McGregor", 200_000),
new Employee(3, "Frank Shamrock", 6_000));
Assert.ThrowsAny<ArgumentException>(() => imm1 < imm2);
Assert.ThrowsAny<ArgumentException>(() => imm1 <= imm2);
Assert.ThrowsAny<ArgumentException>(() => imm2 > imm1);
Assert.ThrowsAny<ArgumentException>(() => imm2 >= imm1);
}

public record Employee(int Id, string Name, decimal Salary);
}

65 changes: 65 additions & 0 deletions Xledger.Collections.Test/TestImmDict.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace Xledger.Collections.Test;

public class TestImmDict {
[Fact]
public void TestEmpty() {
var imm = ImmDict<string, object>.Empty;
Assert.Empty(imm);
Assert.Equal(0, imm.Count);
Assert.False(imm.GetEnumerator().MoveNext());
Assert.Equal(imm.GetHashCode(), new ImmDict<string, object>().GetHashCode());
Assert.Equal(imm, new ImmDict<string, object>());
Assert.Equal(imm, imm);
Assert.Equal(new ImmDict<string, object>(), imm);
Assert.Equal("[]", imm.ToString());

Assert.False(imm.Equals((object)null));
Assert.False(imm.Equals((ImmDict<string, object>)null));
}

[Fact]
public void TestSingle() {
var imm1 = new ImmDict<int, object>(new Dictionary<int, object> {
[1] = "foo",
});
Assert.NotEmpty(imm1);
Assert.Equal(1, imm1.Count);
Assert.Equal(imm1, imm1);
Assert.True(imm1.GetEnumerator().MoveNext());
Assert.Equal("[1: foo]", imm1.ToString());

var imm2 = new ImmDict<int, object>(new Dictionary<int, object> {
[1] = "foo",
});
Assert.Equal(imm1.GetHashCode(), imm2.GetHashCode());
Assert.Equal(imm1, imm2);
Assert.Equal(imm2, imm1);
}

[Fact]
public void TestContains() {
var items = Enumerable.Range(-100, 1_000).ToDictionary(i => i, i => i * i);
var dct = items;
var imm = items.ToImmDict();

for (int i = -1000; i < 2000; ++i) {
Assert.Equal(dct.ContainsKey(i), imm.ContainsKey(i));
if (dct.TryGetValue(i, out var dctValue)) {
imm.TryGetValue(i, out var immValue);
Assert.Equal(dctValue, immValue);
}
}
}

[Fact]
public void TestNoOps() {
var imm = ImmDict.Of((7, 6), (5, 4));
IDictionary<int, int> idict = imm;
Assert.ThrowsAny<NotSupportedException>(() => idict.Add(1, 2));
Assert.ThrowsAny<NotSupportedException>(() => idict.Clear());
Assert.ThrowsAny<NotSupportedException>(() => idict.Remove(4));
Assert.ThrowsAny<NotSupportedException>(() => ((System.Collections.ICollection)idict).CopyTo((Array)null, 0));
}
}


Loading

0 comments on commit 6cf5a22

Please sign in to comment.