From 27aa254ece0023c0b97dc1c4d10044680eb78e1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:35:43 +0000 Subject: [PATCH 1/3] Implement all empty test methods and add Util test project for improved test coverage - Implement 20 empty test methods in ImageTests.cs (constructor, equals, JSON, query/modify) - Implement 4 empty tests in PersonTests.cs (constructor, toString, toJson, invalidateId) - Implement 3 empty tests in PersonTagTests.cs (constructor, toString, invalidateIds) - Implement 2 empty tests in FaceTests.cs (constructor properties + descriptor validation, invalidateId) - Implement 3 empty tests in FaceInfoTests.cs (constructor, toJsonString, toJsonStringArray) - Implement 2 empty tests in FaceDistanceInfoTests.cs (fromJsonString, fromJsonStringArray) - Implement 5 empty tests in FileAndPersonTagTests.cs (constructor, arrays, toString) - Implement 2 empty tests in FixedPoint32Tests.cs (constructor + operators, toString) - Implement 2 empty tests in FixedPoint64Tests.cs (constructor + operators, toString) - Implement 4 empty tests in RectangleTests.cs (fromFloat, fromRawValues, constructor, toString) - Add new TCSystem.Util.Tests project with MathExtTests and EqualsUtilTests (10 new tests) - Update TCSystem.slnx and TCSystem.Util.csproj for new test project Agent-Logs-Url: https://github.com/tgoessler/TCSystemCS/sessions/6e83c422-f889-46e8-8505-fa25b23380c4 Co-authored-by: tgoessler <89462205+tgoessler@users.noreply.github.com> --- MetaData/Tests/FaceDistanceInfoTests.cs | 23 +- MetaData/Tests/FaceInfoTests.cs | 29 ++- MetaData/Tests/FaceTests.cs | 35 +++- MetaData/Tests/FileAndPersonTagTests.cs | 46 +++- MetaData/Tests/FixedPoint32Tests.cs | 35 +++- MetaData/Tests/FixedPoint64Tests.cs | 30 ++- MetaData/Tests/ImageTests.cs | 267 ++++++++++++++++++++++-- MetaData/Tests/PersonTagTests.cs | 21 +- MetaData/Tests/PersonTests.cs | 38 +++- MetaData/Tests/RectangleTests.cs | 42 +++- TCSystem.slnx | 1 + Util/TCSystem.Util.csproj | 6 + Util/Tests/EqualsUtilTests.cs | 66 ++++++ Util/Tests/MathExtTests.cs | 79 +++++++ Util/Tests/TCSystem.Util.Tests.csproj | 25 +++ 15 files changed, 691 insertions(+), 52 deletions(-) create mode 100644 Util/Tests/EqualsUtilTests.cs create mode 100644 Util/Tests/MathExtTests.cs create mode 100644 Util/Tests/TCSystem.Util.Tests.csproj diff --git a/MetaData/Tests/FaceDistanceInfoTests.cs b/MetaData/Tests/FaceDistanceInfoTests.cs index 1a8bf7b..74c2261 100644 --- a/MetaData/Tests/FaceDistanceInfoTests.cs +++ b/MetaData/Tests/FaceDistanceInfoTests.cs @@ -21,6 +21,7 @@ #region Usings using System; +using System.Linq; using NUnit.Framework; #endregion @@ -42,10 +43,28 @@ public void EqualsTest() } [Test] - public void FromJsonStringArrayTest() { } + public void FromJsonStringArrayTest() + { + var items = new[] { TestData.FaceDistanceInfo1, TestData.FaceDistanceInfo2 }; + string json = "[" + string.Join(",", items.Select(i => i.ToJsonString())) + "]"; + var parsed = FaceDistanceInfo.FromJsonStringArray(json).ToArray(); + Assert.That(parsed.Length, Is.EqualTo(2)); + Assert.That(parsed[0], Is.EqualTo(TestData.FaceDistanceInfo1)); + Assert.That(parsed[1], Is.EqualTo(TestData.FaceDistanceInfo2)); + + // Empty/null string returns empty + var empty = FaceDistanceInfo.FromJsonStringArray("").ToArray(); + Assert.That(empty.Length, Is.EqualTo(0)); + } [Test] - public void FromJsonStringTest() { } + public void FromJsonStringTest() + { + FaceDistanceInfo original = TestData.FaceDistanceInfo1; + string json = original.ToJsonString(); + FaceDistanceInfo parsed = FaceDistanceInfo.FromJsonString(json); + Assert.That(parsed, Is.EqualTo(original)); + } [Test] public void GetHashCodeTest() diff --git a/MetaData/Tests/FaceInfoTests.cs b/MetaData/Tests/FaceInfoTests.cs index 1018a30..9c1f43b 100644 --- a/MetaData/Tests/FaceInfoTests.cs +++ b/MetaData/Tests/FaceInfoTests.cs @@ -42,7 +42,20 @@ public void EqualsTest() } [Test] - public void FaceInfoTest() { } + public void FaceInfoTest() + { + FaceInfo fi = TestData.FaceInfo1; + Assert.That(fi.FileId, Is.EqualTo(1)); + Assert.That(fi.FaceId, Is.EqualTo(2)); + Assert.That(fi.PersonId, Is.EqualTo(3)); + Assert.That(fi.FaceMode, Is.EqualTo(FaceMode.DlibCnn)); + Assert.That(fi.FaceQuality, Is.EqualTo(FaceQuality.Normal)); + Assert.That(fi.FaceDescriptor, Is.Not.Null); + + // PersonId == EmptyPersonId should map to InvalidId + var fi2 = new FaceInfo(1, 2, Constants.EmptyPersonId, FaceMode.DlibFront, FaceQuality.Good, null); + Assert.That(fi2.PersonId, Is.EqualTo(Constants.InvalidId)); + } [Test] public void FromJsonStringTest() @@ -70,8 +83,18 @@ public void GetHashCodeTest() } [Test] - public void ToJsonStringArrayTest() { } + public void ToJsonStringArrayTest() + { + var faceInfos = new[] { TestData.FaceInfo1, TestData.FaceInfo2 }; + string json = FaceInfo.ToJsonStringArray(faceInfos); + Assert.That(json, Is.Not.Empty); + Assert.That(json, Does.StartWith("[")); + } [Test] - public void ToJsonStringTest() { } + public void ToJsonStringTest() + { + string json = TestData.FaceInfo1.ToJsonString(); + Assert.That(json, Is.Not.Empty); + } } \ No newline at end of file diff --git a/MetaData/Tests/FaceTests.cs b/MetaData/Tests/FaceTests.cs index cd540af..0d0e324 100644 --- a/MetaData/Tests/FaceTests.cs +++ b/MetaData/Tests/FaceTests.cs @@ -42,7 +42,37 @@ public void EqualsTest() } [Test] - public void FaceTest() { } + public void FaceTest() + { + Face face = TestData.Face1; + Assert.That(face.Id, Is.EqualTo(1)); + Assert.That(face.FaceMode, Is.EqualTo(FaceMode.DlibFront)); + Assert.That(face.FaceQuality, Is.EqualTo(FaceQuality.Good)); + Assert.That(face.Visible, Is.True); + Assert.That(face.IsFrontFace, Is.True); + Assert.That(face.HasFaceDescriptor, Is.True); + Assert.That(face.FaceDescriptor.Count, Is.EqualTo(128)); + + Face zero = TestData.FaceZero; + Assert.That(zero.IsFrontFace, Is.False); + Assert.That(zero.HasFaceDescriptor, Is.False); + + // Face descriptor with wrong length is discarded + var wrongDesc = new FixedPoint64[5]; + var faceWithWrongDesc = new Face(1, TestData.Rectangle1, FaceMode.DlibFront, FaceQuality.Normal, true, wrongDesc); + Assert.That(faceWithWrongDesc.HasFaceDescriptor, Is.False); + } + + [Test] + public void InvalidateIdTest() + { + Face invalidated = TestData.Face1.InvalidateId(); + Assert.That(invalidated.Id, Is.EqualTo(Constants.InvalidId)); + Assert.That(invalidated.Rectangle, Is.EqualTo(TestData.Face1.Rectangle)); + Assert.That(invalidated.FaceMode, Is.EqualTo(TestData.Face1.FaceMode)); + Assert.That(invalidated.FaceQuality, Is.EqualTo(TestData.Face1.FaceQuality)); + Assert.That(invalidated.Visible, Is.EqualTo(TestData.Face1.Visible)); + } [Test] public void FromJsonStringTest() @@ -68,7 +98,4 @@ public void GetHashCodeTest() TestUtil.GetHashCodeTest(TestData.FaceZero, TestData.Face1, TestData.Face2, copyOfData1); } - - [Test] - public void InvalidateIdTest() { } } \ No newline at end of file diff --git a/MetaData/Tests/FileAndPersonTagTests.cs b/MetaData/Tests/FileAndPersonTagTests.cs index 3803a5a..d20de53 100644 --- a/MetaData/Tests/FileAndPersonTagTests.cs +++ b/MetaData/Tests/FileAndPersonTagTests.cs @@ -21,6 +21,7 @@ #region Usings using System; +using System.Linq; using NUnit.Framework; #endregion @@ -42,10 +43,31 @@ public void EqualsTest() } [Test] - public void FileAndPersonTagTest() { } + public void FileAndPersonTagTest() + { + FileAndPersonTag fpt = TestData.FileAndPersonTag1; + Assert.That(fpt.FileName, Is.EqualTo("file1")); + Assert.That(fpt.PersonTag, Is.EqualTo(TestData.PersonTag1)); + + // null filename defaults to empty string + FileAndPersonTag zero = TestData.FileAndPersonTagZero; + Assert.That(zero.FileName, Is.EqualTo(string.Empty)); + } [Test] - public void FromJsonStringArrayTest() { } + public void FromJsonStringArrayTest() + { + var items = new[] { TestData.FileAndPersonTag1, TestData.FileAndPersonTag2 }; + string json = FileAndPersonTag.ToJsonStringArray(items); + var parsed = FileAndPersonTag.FromJsonStringArray(json).ToArray(); + Assert.That(parsed.Length, Is.EqualTo(2)); + Assert.That(parsed[0], Is.EqualTo(TestData.FileAndPersonTag1)); + Assert.That(parsed[1], Is.EqualTo(TestData.FileAndPersonTag2)); + + // Empty string returns empty + var empty = FileAndPersonTag.FromJsonStringArray("").ToArray(); + Assert.That(empty.Length, Is.EqualTo(0)); + } [Test] public void FromJsonStringTest() @@ -73,11 +95,25 @@ public void GetHashCodeTest() } [Test] - public void ToJsonStringArrayTest() { } + public void ToJsonStringArrayTest() + { + var items = new[] { TestData.FileAndPersonTag1, TestData.FileAndPersonTag2 }; + string json = FileAndPersonTag.ToJsonStringArray(items); + Assert.That(json, Is.Not.Empty); + Assert.That(json, Does.StartWith("[")); + } [Test] - public void ToJsonStringTest() { } + public void ToJsonStringTest() + { + string json = TestData.FileAndPersonTag1.ToJsonString(); + Assert.That(json, Is.Not.Empty); + } [Test] - public void ToStringTest() { } + public void ToStringTest() + { + string str = TestData.FileAndPersonTag1.ToString(); + Assert.That(str, Is.Not.Empty); + } } \ No newline at end of file diff --git a/MetaData/Tests/FixedPoint32Tests.cs b/MetaData/Tests/FixedPoint32Tests.cs index 02df399..eccaead 100644 --- a/MetaData/Tests/FixedPoint32Tests.cs +++ b/MetaData/Tests/FixedPoint32Tests.cs @@ -42,7 +42,32 @@ public void EqualsTest() } [Test] - public void FixedPoint32Test() { } + public void FixedPoint32Test() + { + // Constructor from raw int + var fp = new FixedPoint32(100); + Assert.That(fp.RawValue, Is.EqualTo(100)); + + // Constructor from float: value * (1 << 16) + var fpFloat = new FixedPoint32(1.0f); + Assert.That(fpFloat.RawValue, Is.EqualTo(1 << 16)); + Assert.That(fpFloat.Value, Is.EqualTo(1.0f).Within(1e-4f)); + + var fpNeg = new FixedPoint32(-2.0f); + Assert.That(fpNeg.Value, Is.EqualTo(-2.0f).Within(1e-4f)); + + // Zero + Assert.That(TestData.FixedPoint32Zero.Value, Is.EqualTo(0.0f)); + + // Comparison operators + Assert.That(TestData.FixedPoint321 < TestData.FixedPoint322, Is.True); + Assert.That(TestData.FixedPoint322 > TestData.FixedPoint321, Is.True); + var fp1Copy = new FixedPoint32(TestData.FixedPoint321.RawValue); + Assert.That(TestData.FixedPoint321 <= fp1Copy, Is.True); + Assert.That(TestData.FixedPoint321 >= fp1Copy, Is.True); + Assert.That(TestData.FixedPoint321 == fp1Copy, Is.True); + Assert.That(TestData.FixedPoint321 != TestData.FixedPoint322, Is.True); + } [Test] public void FromJsonStringTest() @@ -74,5 +99,11 @@ public void GetHashCodeTest() } [Test] - public void ToStringTest() { } + public void ToStringTest() + { + string s = new FixedPoint32(1.5f).ToString(); + Assert.That(s, Is.Not.Empty); + // Should use invariant culture (dot as decimal separator) + Assert.That(s, Does.Contain(".")); + } } \ No newline at end of file diff --git a/MetaData/Tests/FixedPoint64Tests.cs b/MetaData/Tests/FixedPoint64Tests.cs index 0e16c1e..0c1314c 100644 --- a/MetaData/Tests/FixedPoint64Tests.cs +++ b/MetaData/Tests/FixedPoint64Tests.cs @@ -42,7 +42,28 @@ public void EqualsTest() } [Test] - public void FixedPoint64Test() { } + public void FixedPoint64Test() + { + // Constructor from raw long + var fp = new FixedPoint64(100L); + Assert.That(fp.RawValue, Is.EqualTo(100L)); + + // Constructor from double: value * (1L << 32) + var fpDouble = new FixedPoint64(1.0); + Assert.That(fpDouble.RawValue, Is.EqualTo(1L << 32)); + Assert.That(fpDouble.Value, Is.EqualTo(1.0).Within(1e-9)); + + var fpNeg = new FixedPoint64(-2.0); + Assert.That(fpNeg.Value, Is.EqualTo(-2.0).Within(1e-9)); + + // Zero + Assert.That(TestData.FixedPoint64Zero.Value, Is.EqualTo(0.0)); + + // Operators + var fp641Copy = new FixedPoint64(TestData.FixedPoint641.RawValue); + Assert.That(TestData.FixedPoint641 == fp641Copy, Is.True); + Assert.That(TestData.FixedPoint641 != TestData.FixedPoint642, Is.True); + } [Test] public void FromJsonStringTest() @@ -75,5 +96,10 @@ public void GetHashCodeTest() } [Test] - public void ToStringTest() { } + public void ToStringTest() + { + string s = new FixedPoint64(1.5).ToString(); + Assert.That(s, Is.Not.Empty); + Assert.That(s, Does.Contain(".")); + } } \ No newline at end of file diff --git a/MetaData/Tests/ImageTests.cs b/MetaData/Tests/ImageTests.cs index 20c2c5b..ded169d 100644 --- a/MetaData/Tests/ImageTests.cs +++ b/MetaData/Tests/ImageTests.cs @@ -20,6 +20,9 @@ #region Usings +using System; +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; #endregion @@ -30,71 +33,289 @@ namespace TCSystem.MetaData.Tests; public class ImageTests { [Test] - public void AddPersonTagTest() { } + public void AddPersonTagTest() + { + Image image = CreateTestImage(); + Assert.That(image.PersonTags.Count, Is.EqualTo(1)); + + // Adding a person with a different name should succeed + Image updated = Image.AddPersonTag(image, TestData.PersonTag2); + Assert.That(updated.PersonTags.Count, Is.EqualTo(2)); + Assert.That(updated.HasPersonTag(TestData.PersonTag2), Is.True); + + // Adding a person whose name already exists should be skipped + Image updated2 = Image.AddPersonTag(updated, TestData.PersonTag1); + Assert.That(updated2.PersonTags.Count, Is.EqualTo(2)); + + // Adding an invalid person (empty name) is always allowed + var invalidPersonTag = new PersonTag(new Person(Constants.InvalidId, "", "", "", ""), TestData.FaceZero); + Image updated3 = Image.AddPersonTag(image, invalidPersonTag); + Assert.That(updated3.PersonTags.Count, Is.EqualTo(2)); + } [Test] - public void AddTagTest() { } + public void AddTagTest() + { + Image image = CreateTestImage(); + Assert.That(image.NumTags, Is.EqualTo(1)); + + // Adding a new tag should succeed + Image updated = Image.AddTag(image, "NewTag"); + Assert.That(updated.NumTags, Is.EqualTo(2)); + Assert.That(updated.HasTag("NewTag"), Is.True); + + // Adding a duplicate tag should be a no-op + Image updated2 = Image.AddTag(updated, "NewTag"); + Assert.That(updated2.NumTags, Is.EqualTo(2)); + + // Adding an empty tag should be a no-op + Image updated3 = Image.AddTag(image, ""); + Assert.That(updated3.NumTags, Is.EqualTo(1)); + } [Test] - public void ChangeDateTakenTest() { } + public void ChangeDateTakenTest() + { + Image image = CreateTestImage(); + var newDate = new DateTimeOffset(2022, 6, 15, 10, 30, 0, TimeSpan.Zero); + Image updated = Image.ChangeDateTaken(image, newDate); + Assert.That(updated.DateTaken.ToUnixTimeSeconds(), Is.EqualTo(newDate.ToUnixTimeSeconds())); + Assert.That(updated.FileName, Is.EqualTo(image.FileName)); + } [Test] - public void ChangeFileNameTest() { } + public void ChangeFileNameTest() + { + Image image = CreateTestImage(); + Image updated = Image.ChangeFileName(image, @"C:\New\Path\newfile.jpg"); + Assert.That(updated.FileName, Is.EqualTo(@"C:\New\Path\newfile.jpg")); + Assert.That(updated.Id, Is.EqualTo(image.Id)); + } [Test] - public void ChangeLocationTest() { } + public void ChangeLocationTest() + { + Image image = CreateTestImage(); + Image updated = Image.ChangeLocation(image, TestData.Location2); + Assert.That(updated.Location, Is.EqualTo(TestData.Location2)); + Assert.That(updated.FileName, Is.EqualTo(image.FileName)); + } [Test] - public void ChangePersonTagsTest() { } + public void ChangePersonTagsTest() + { + Image image = CreateTestImage(); + var newTags = new List { TestData.PersonTag1, TestData.PersonTag2 }; + Image updated = Image.ChangePersonTags(image, newTags); + Assert.That(updated.PersonTags.Count, Is.EqualTo(2)); + Assert.That(updated.PersonTags[0], Is.EqualTo(TestData.PersonTag1)); + Assert.That(updated.PersonTags[1], Is.EqualTo(TestData.PersonTag2)); + } [Test] - public void ChangePersonTagVisibleTest() { } + public void ChangePersonTagVisibleTest() + { + Image image = CreateTestImage(); + PersonTag originalTag = image.PersonTags[0]; + + Image updated = Image.ChangePersonTagVisible(image, originalTag, !originalTag.Face.Visible); + PersonTag changedTag = updated.PersonTags.First(pt => pt.Person.Name == originalTag.Person.Name); + Assert.That(changedTag.Face.Visible, Is.EqualTo(!originalTag.Face.Visible)); + + // Changing visibility of a tag not in the image is a no-op + Image unchanged = Image.ChangePersonTagVisible(image, TestData.PersonTag2, false); + Assert.That(unchanged.PersonTags.Count, Is.EqualTo(image.PersonTags.Count)); + } [Test] - public void ChangeProcessingInfoTest() { } + public void ChangeProcessingInfoTest() + { + Image image = CreateTestImage(); + Image updated = Image.ChangeProcessingInfo(image, ProcessingInfos.DlibFrontalFaceDetection2000); + Assert.That(updated.ProcessingInfos, Is.EqualTo(ProcessingInfos.DlibFrontalFaceDetection2000)); + Assert.That(updated.FileName, Is.EqualTo(image.FileName)); + } [Test] - public void ChangeTagsTest() { } + public void ChangeTagsTest() + { + Image image = CreateTestImage(); + var newTags = new List { "Tag1", "Tag2", "Tag3" }; + Image updated = Image.ChangeTags(image, newTags); + Assert.That(updated.NumTags, Is.EqualTo(3)); + Assert.That(updated.HasTag("Tag1"), Is.True); + Assert.That(updated.HasTag("Tag2"), Is.True); + Assert.That(updated.HasTag("Tag3"), Is.True); + } [Test] - public void EqualsTest() { } + public void EqualsTest() + { + Image image1 = CreateTestImage(); + Image image2 = CreateTestImage(); + + Assert.That(image1.Equals(image1), Is.True); + Assert.That(image1.Equals(image2), Is.True); + Assert.That(image1.Equals(null), Is.False); + Assert.That(image1, Is.Not.EqualTo(string.Empty)); + + Image different = Image.ChangeFileName(image1, "other.jpg"); + Assert.That(image1.Equals(different), Is.False); + } [Test] - public void FromJsonStringTest() { } + public void FromJsonStringTest() + { + Image image = CreateTestImage(); + + string ToJson(Image d) => d.ToJsonString(); + Image fromJson(string s) => Image.FromJsonString(s); + + TestUtil.FromJsonStringTest(image, ToJson, fromJson); + } [Test] - public void GetHashCodeTest() { } + public void GetHashCodeTest() + { + Image image1 = CreateTestImage(); + + // Same instance always returns same hash + Assert.That(image1.GetHashCode(), Is.EqualTo(image1.GetHashCode())); + + // Different images should have different hashes + Image different = Image.ChangeFileName(image1, "other.jpg"); + Assert.That(image1.GetHashCode(), Is.Not.EqualTo(different.GetHashCode())); + } [Test] - public void GetPersonTagTest() { } + public void GetPersonTagTest() + { + Image image = CreateTestImage(); + PersonTag found = image.GetPersonTag(TestData.Person1.Name); + Assert.That(found, Is.EqualTo(TestData.PersonTag1)); + + PersonTag notFound = image.GetPersonTag("NoSuchPerson"); + Assert.That(notFound, Is.Null); + } [Test] - public void HasPersonTagTest() { } + public void HasPersonTagTest() + { + Image image = CreateTestImage(); + Assert.That(image.HasPersonTag(TestData.PersonTag1), Is.True); + Assert.That(image.HasPersonTag(TestData.PersonTag2), Is.False); + } [Test] - public void HasPersonTest() { } + public void HasPersonTest() + { + Image image = CreateTestImage(); + Assert.That(image.HasPerson(TestData.Person1.Name), Is.True); + Assert.That(image.HasPerson("NoSuchPerson"), Is.False); + } [Test] - public void HasTagTest() { } + public void HasTagTest() + { + Image image = CreateTestImage(); + Assert.That(image.HasTag("TestTag"), Is.True); + Assert.That(image.HasTag("NonExistentTag"), Is.False); + } [Test] - public void ImageTest() { } + public void ImageTest() + { + Image image = CreateTestImage(); + Assert.That(image.Id, Is.EqualTo(42)); + Assert.That(image.FileName, Is.EqualTo(@"C:\photos\test.jpg")); + Assert.That(image.ProcessingInfos, Is.EqualTo(ProcessingInfos.None)); + Assert.That(image.Width, Is.EqualTo(1920)); + Assert.That(image.Height, Is.EqualTo(1080)); + Assert.That(image.Orientation, Is.EqualTo(OrientationMode.Normal)); + Assert.That(image.Title, Is.EqualTo("")); + Assert.That(image.Location, Is.EqualTo(TestData.Location1)); + Assert.That(image.PersonTags.Count, Is.EqualTo(1)); + Assert.That(image.NumTags, Is.EqualTo(1)); + } [Test] - public void InvalidateIdsTest() { } + public void InvalidateIdsTest() + { + Image image = CreateTestImage(); + Image invalidated = image.InvalidateId(); + + Assert.That(invalidated.Id, Is.EqualTo(Constants.InvalidId)); + Assert.That(invalidated.FileName, Is.EqualTo(image.FileName)); + foreach (PersonTag pt in invalidated.PersonTags) + { + Assert.That(pt.Person.Id, Is.EqualTo(Constants.InvalidId)); + Assert.That(pt.Face.Id, Is.EqualTo(Constants.InvalidId)); + } + } [Test] - public void RemovePersonTagTest() { } + public void RemovePersonTagTest() + { + Image image = CreateTestImage(); + Assert.That(image.PersonTags.Count, Is.EqualTo(1)); + + Image updated = Image.RemovePersonTag(image, TestData.PersonTag1); + Assert.That(updated.PersonTags.Count, Is.EqualTo(0)); + + // Removing a non-existent tag is a no-op + Image unchanged = Image.RemovePersonTag(image, TestData.PersonTag2); + Assert.That(unchanged.PersonTags.Count, Is.EqualTo(1)); + } [Test] - public void RemovePersonWithNameTest() { } + public void RemovePersonWithNameTest() + { + Image image = CreateTestImage(); + Image updated = Image.RemovePersonWithName(image, TestData.Person1.Name); + Assert.That(updated.PersonTags.Count, Is.EqualTo(0)); + + // Removing a non-existent name is a no-op + Image unchanged = Image.RemovePersonWithName(image, "NoSuchPerson"); + Assert.That(unchanged.PersonTags.Count, Is.EqualTo(1)); + } [Test] - public void RemoveTagTest() { } + public void RemoveTagTest() + { + Image image = CreateTestImage(); + Image updated = Image.RemoveTag(image, "TestTag"); + Assert.That(updated.NumTags, Is.EqualTo(0)); + Assert.That(updated.HasTag("TestTag"), Is.False); + + // Removing a non-existent tag is a no-op + Image unchanged = Image.RemoveTag(image, "NonExistentTag"); + Assert.That(unchanged.NumTags, Is.EqualTo(1)); + } [Test] - public void ToJsonStringTest() { } + public void ToJsonStringTest() + { + Image image = CreateTestImage(); + string json = image.ToJsonString(); + Assert.That(json, Is.Not.Empty); + } [Test] - public void ToStringTest() { } + public void ToStringTest() + { + Image image = CreateTestImage(); + string str = image.ToString(); + Assert.That(str, Is.Not.Empty); + } + + private static Image CreateTestImage() + { + return new(42, @"C:\photos\test.jpg", ProcessingInfos.None, + 1920, 1080, OrientationMode.Normal, + new DateTimeOffset(2021, 5, 1, 12, 0, 0, TimeSpan.Zero), + null, + TestData.Location1, + new[] { TestData.PersonTag1 }, + new[] { "TestTag" }); + } } \ No newline at end of file diff --git a/MetaData/Tests/PersonTagTests.cs b/MetaData/Tests/PersonTagTests.cs index 4a05dc7..7337e7e 100644 --- a/MetaData/Tests/PersonTagTests.cs +++ b/MetaData/Tests/PersonTagTests.cs @@ -67,11 +67,26 @@ public void GetHashCodeTest() } [Test] - public void InvalidateIdsTest() { } + public void InvalidateIdsTest() + { + PersonTag invalidated = TestData.PersonTag1.InvalidateId(); + Assert.That(invalidated.Person.Id, Is.EqualTo(Constants.InvalidId)); + Assert.That(invalidated.Face.Id, Is.EqualTo(Constants.InvalidId)); + Assert.That(invalidated.Person.Name, Is.EqualTo(TestData.PersonTag1.Person.Name)); + } [Test] - public void PersonTagTest() { } + public void PersonTagTest() + { + PersonTag tag = TestData.PersonTag1; + Assert.That(tag.Person, Is.EqualTo(TestData.Person1)); + Assert.That(tag.Face, Is.EqualTo(TestData.Face1)); + } [Test] - public void ToStringTest() { } + public void ToStringTest() + { + string str = TestData.PersonTag1.ToString(); + Assert.That(str, Is.Not.Empty); + } } \ No newline at end of file diff --git a/MetaData/Tests/PersonTests.cs b/MetaData/Tests/PersonTests.cs index ef9902f..ab0abcc 100644 --- a/MetaData/Tests/PersonTests.cs +++ b/MetaData/Tests/PersonTests.cs @@ -67,14 +67,44 @@ public void GetHashCodeTest() } [Test] - public void InvalidateIdTest() { } + public void InvalidateIdTest() + { + Person invalidated = TestData.Person1.InvalidateId(); + Assert.That(invalidated.Id, Is.EqualTo(Constants.InvalidId)); + Assert.That(invalidated.Name, Is.EqualTo(TestData.Person1.Name)); + Assert.That(invalidated.EmailDigest, Is.EqualTo(TestData.Person1.EmailDigest)); + Assert.That(invalidated.LiveId, Is.EqualTo(TestData.Person1.LiveId)); + Assert.That(invalidated.SourceId, Is.EqualTo(TestData.Person1.SourceId)); + } [Test] - public void PersonTest() { } + public void PersonTest() + { + Person person = TestData.Person1; + Assert.That(person.Id, Is.EqualTo(1)); + Assert.That(person.Name, Is.EqualTo("Thomas")); + Assert.That(person.EmailDigest, Is.EqualTo("thomas@email.com")); + Assert.That(person.LiveId, Is.EqualTo("123")); + Assert.That(person.SourceId, Is.EqualTo("456")); + Assert.That(person.IsValid, Is.True); + Assert.That(person.AllAttributesDefined, Is.True); + + Person empty = TestData.PersonZero; + Assert.That(empty.IsValid, Is.False); + Assert.That(empty.AllAttributesDefined, Is.False); + } [Test] - public void ToJsonStringTest() { } + public void ToJsonStringTest() + { + string json = TestData.Person1.ToJsonString(); + Assert.That(json, Is.Not.Empty); + } [Test] - public void ToStringTest() { } + public void ToStringTest() + { + string str = TestData.Person1.ToString(); + Assert.That(str, Is.Not.Empty); + } } \ No newline at end of file diff --git a/MetaData/Tests/RectangleTests.cs b/MetaData/Tests/RectangleTests.cs index 7ae38ac..ce48a4c 100644 --- a/MetaData/Tests/RectangleTests.cs +++ b/MetaData/Tests/RectangleTests.cs @@ -42,7 +42,14 @@ public void EqualsTest() } [Test] - public void FromFloatTest() { } + public void FromFloatTest() + { + Rectangle r = Rectangle.FromFloat(1.0f, 2.0f, 3.0f, 4.0f); + Assert.That(r.X, Is.EqualTo(new FixedPoint32(1.0f))); + Assert.That(r.Y, Is.EqualTo(new FixedPoint32(2.0f))); + Assert.That(r.W, Is.EqualTo(new FixedPoint32(3.0f))); + Assert.That(r.H, Is.EqualTo(new FixedPoint32(4.0f))); + } [Test] public void FromJsonStringTest() @@ -60,7 +67,18 @@ string ToJson(Rectangle d) } [Test] - public void FromRawValuesTest() { } + public void FromRawValuesTest() + { + Rectangle r = Rectangle.FromRawValues(10, 20, 30, 40); + Assert.That(r.X, Is.EqualTo(new FixedPoint32(10))); + Assert.That(r.Y, Is.EqualTo(new FixedPoint32(20))); + Assert.That(r.W, Is.EqualTo(new FixedPoint32(30))); + Assert.That(r.H, Is.EqualTo(new FixedPoint32(40))); + Assert.That(r.Left, Is.EqualTo(new FixedPoint32(10))); + Assert.That(r.Top, Is.EqualTo(new FixedPoint32(20))); + Assert.That(r.Right, Is.EqualTo(new FixedPoint32(40))); + Assert.That(r.Bottom, Is.EqualTo(new FixedPoint32(60))); + } [Test] public void GetHashCodeTest() @@ -133,8 +151,24 @@ public void RectangleDiameterTest() } [Test] - public void RectangleTest() { } + public void RectangleTest() + { + var r = new Rectangle(new FixedPoint32(1), new FixedPoint32(2), new FixedPoint32(3), new FixedPoint32(4)); + Assert.That(r.X, Is.EqualTo(new FixedPoint32(1))); + Assert.That(r.Y, Is.EqualTo(new FixedPoint32(2))); + Assert.That(r.W, Is.EqualTo(new FixedPoint32(3))); + Assert.That(r.H, Is.EqualTo(new FixedPoint32(4))); + var rCopy = new Rectangle(r.X, r.Y, r.W, r.H); + Assert.That(r == rCopy, Is.True); + Assert.That(r != TestData.RectangleZero, Is.True); + } [Test] - public void ToStringTest() { } + public void ToStringTest() + { + string str = TestData.Rectangle1.ToString(); + Assert.That(str, Is.Not.Empty); + // Format is "{X}, {Y}, {W}, {H}" + Assert.That(str, Does.Contain(",")); + } } \ No newline at end of file diff --git a/TCSystem.slnx b/TCSystem.slnx index 80633d5..7db108a 100644 --- a/TCSystem.slnx +++ b/TCSystem.slnx @@ -15,6 +15,7 @@ + diff --git a/Util/TCSystem.Util.csproj b/Util/TCSystem.Util.csproj index d26f898..5c2d532 100644 --- a/Util/TCSystem.Util.csproj +++ b/Util/TCSystem.Util.csproj @@ -9,4 +9,10 @@ + + + + + + \ No newline at end of file diff --git a/Util/Tests/EqualsUtilTests.cs b/Util/Tests/EqualsUtilTests.cs new file mode 100644 index 0000000..d2a40a3 --- /dev/null +++ b/Util/Tests/EqualsUtilTests.cs @@ -0,0 +1,66 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using NUnit.Framework; + +#endregion + +namespace TCSystem.Util.Tests; + +[TestFixture] +public class EqualsUtilTests +{ + [Test] + public void Equals_BothSameReference_ReturnsTrue() + { + var obj = new TestClass("test"); + Assert.That(EqualsUtil.Equals(obj, obj, _ => false), Is.True); + } + + [Test] + public void Equals_SecondIsNull_ReturnsFalse() + { + var obj = new TestClass("test"); + Assert.That(EqualsUtil.Equals(obj, null, _ => true), Is.False); + } + + [Test] + public void Equals_DifferentObjectsEqualsReturnsTrue_ReturnsTrue() + { + var obj1 = new TestClass("test"); + var obj2 = new TestClass("test"); + Assert.That(EqualsUtil.Equals(obj1, obj2, other => other.Value == obj1.Value), Is.True); + } + + [Test] + public void Equals_DifferentObjectsEqualsReturnsFalse_ReturnsFalse() + { + var obj1 = new TestClass("test1"); + var obj2 = new TestClass("test2"); + Assert.That(EqualsUtil.Equals(obj1, obj2, other => other.Value == obj1.Value), Is.False); + } + + private sealed class TestClass(string value) + { + public string Value { get; } = value; + } +} diff --git a/Util/Tests/MathExtTests.cs b/Util/Tests/MathExtTests.cs new file mode 100644 index 0000000..15439b0 --- /dev/null +++ b/Util/Tests/MathExtTests.cs @@ -0,0 +1,79 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using NUnit.Framework; + +#endregion + +namespace TCSystem.Util.Tests; + +[TestFixture] +public class MathExtTests +{ + [Test] + public void ToNextPowerOf2_Zero_ReturnsZero() + { + Assert.That(0.ToNextPowerOf2(), Is.EqualTo(0)); + } + + [Test] + public void ToNextPowerOf2_One_ReturnsOne() + { + Assert.That(1.ToNextPowerOf2(), Is.EqualTo(1)); + } + + [Test] + public void ToNextPowerOf2_PowerOf2_ReturnsSameValue() + { + Assert.That(2.ToNextPowerOf2(), Is.EqualTo(2)); + Assert.That(4.ToNextPowerOf2(), Is.EqualTo(4)); + Assert.That(8.ToNextPowerOf2(), Is.EqualTo(8)); + Assert.That(16.ToNextPowerOf2(), Is.EqualTo(16)); + Assert.That(1024.ToNextPowerOf2(), Is.EqualTo(1024)); + } + + [Test] + public void ToNextPowerOf2_NonPowerOf2_ReturnsNextPower() + { + Assert.That(3.ToNextPowerOf2(), Is.EqualTo(4)); + Assert.That(5.ToNextPowerOf2(), Is.EqualTo(8)); + Assert.That(7.ToNextPowerOf2(), Is.EqualTo(8)); + Assert.That(9.ToNextPowerOf2(), Is.EqualTo(16)); + Assert.That(17.ToNextPowerOf2(), Is.EqualTo(32)); + Assert.That(100.ToNextPowerOf2(), Is.EqualTo(128)); + Assert.That(1000.ToNextPowerOf2(), Is.EqualTo(1024)); + } + + [Test] + public void ToNextPowerOf2_NegativeValue_ReturnsZero() + { + Assert.That((-1).ToNextPowerOf2(), Is.EqualTo(0)); + Assert.That((-100).ToNextPowerOf2(), Is.EqualTo(0)); + } + + [Test] + public void ToNextPowerOf2_LargeValues() + { + Assert.That(65536.ToNextPowerOf2(), Is.EqualTo(65536)); + Assert.That(65537.ToNextPowerOf2(), Is.EqualTo(131072)); + } +} diff --git a/Util/Tests/TCSystem.Util.Tests.csproj b/Util/Tests/TCSystem.Util.Tests.csproj new file mode 100644 index 0000000..6361670 --- /dev/null +++ b/Util/Tests/TCSystem.Util.Tests.csproj @@ -0,0 +1,25 @@ + + + + TCSystem.Util.Tests + TCSystem.Util.Tests + net8.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + From ab46ea184a4a7720a38f6463070063a98d1c7d73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:10:27 +0000 Subject: [PATCH 2/3] Add TCSystem.Thread.Tests project with 38 test cases - FactoryTests: CreateWorkerThread, CreateAsyncUpdateHelper, CreateMultipleTasksExecute - AsyncUpdateHelperTests: initial state, BeginUpdateAsync/EndUpdate round trips, ShouldStop and IsUpdatePending under contention, sequential updates - AsyncUpdateHelperExtTests: BeginUpdateScopeAsync and WaitScopeAsync dispose correctly - SemaphoreSlimExtTests: Lock/LockAsync acquire+release, blocking, sequential locks - WorkerThreadTests: ExecuteCommand, ExecuteCommandAsync, ClearOpenCommands, NumOpenActions, IsBusy, OnInitThread, OnDeInitThread, CancellationToken, StopThread, IdleEvent - MultipleTasksExecuteTests: single/multiple actions, WaitAllDone, parallelism limit, cancellation token - Update Thread.csproj to exclude Tests/** - Add Thread/Tests project to TCSystem.slnx under /50 Tests/ Agent-Logs-Url: https://github.com/tgoessler/TCSystemCS/sessions/45f03805-4f5a-4ea6-ab23-685d8c654f77 Co-authored-by: tgoessler <89462205+tgoessler@users.noreply.github.com> --- TCSystem.slnx | 1 + Thread/TCSystem.Thread.csproj | 6 + Thread/Tests/AsyncUpdateHelperExtTests.cs | 83 +++++++ Thread/Tests/AsyncUpdateHelperTests.cs | 123 ++++++++++ Thread/Tests/FactoryTests.cs | 54 ++++ Thread/Tests/MultipleTasksExecuteTests.cs | 174 +++++++++++++ Thread/Tests/SemaphoreSlimExtTests.cs | 117 +++++++++ Thread/Tests/TCSystem.Thread.Tests.csproj | 25 ++ Thread/Tests/WorkerThreadTests.cs | 287 ++++++++++++++++++++++ 9 files changed, 870 insertions(+) create mode 100644 Thread/Tests/AsyncUpdateHelperExtTests.cs create mode 100644 Thread/Tests/AsyncUpdateHelperTests.cs create mode 100644 Thread/Tests/FactoryTests.cs create mode 100644 Thread/Tests/MultipleTasksExecuteTests.cs create mode 100644 Thread/Tests/SemaphoreSlimExtTests.cs create mode 100644 Thread/Tests/TCSystem.Thread.Tests.csproj create mode 100644 Thread/Tests/WorkerThreadTests.cs diff --git a/TCSystem.slnx b/TCSystem.slnx index 7db108a..178ea17 100644 --- a/TCSystem.slnx +++ b/TCSystem.slnx @@ -15,6 +15,7 @@ + diff --git a/Thread/TCSystem.Thread.csproj b/Thread/TCSystem.Thread.csproj index efa2405..9834602 100644 --- a/Thread/TCSystem.Thread.csproj +++ b/Thread/TCSystem.Thread.csproj @@ -10,6 +10,12 @@ + + + + + + diff --git a/Thread/Tests/AsyncUpdateHelperExtTests.cs b/Thread/Tests/AsyncUpdateHelperExtTests.cs new file mode 100644 index 0000000..49b55a1 --- /dev/null +++ b/Thread/Tests/AsyncUpdateHelperExtTests.cs @@ -0,0 +1,83 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using System; +using System.Threading.Tasks; +using NUnit.Framework; + +#endregion + +namespace TCSystem.Thread.Tests; + +[TestFixture] +public class AsyncUpdateHelperExtTests +{ + [SetUp] + public void SetUp() + { + _helper = Factory.CreateAsyncUpdateHelper(); + } + + [Test] + public async Task BeginUpdateScopeAsync_Dispose_ReleasesLock() + { + IDisposable scope = await _helper.BeginUpdateScopeAsync(); + + // While scope is held, a new BeginUpdateAsync should block — ShouldStop == false only after acquire + Assert.That(_helper.ShouldStop, Is.False); + + // Dispose must release the internal lock + scope.Dispose(); + + // After dispose another acquire should succeed immediately + await _helper.BeginUpdateAsync(); + _helper.EndUpdate(); + } + + [Test] + public async Task WaitScopeAsync_Dispose_ReleasesLock() + { + IDisposable scope = await _helper.WaitScopeAsync(); + + Assert.That(_helper.IsUpdatePending, Is.False); + + scope.Dispose(); + + // After dispose another acquire should succeed immediately + await _helper.WaitAsync(); + _helper.EndUpdate(); + } + + [Test] + public async Task BeginUpdateScopeAsync_UsedWithUsing_ReleasesOnExit() + { + IDisposable scope = await _helper.BeginUpdateScopeAsync(); + // Lock held + scope.Dispose(); + + // Lock released; next acquire must be immediate + await _helper.BeginUpdateAsync(); + _helper.EndUpdate(); + } + + private IAsyncUpdateHelper _helper; +} diff --git a/Thread/Tests/AsyncUpdateHelperTests.cs b/Thread/Tests/AsyncUpdateHelperTests.cs new file mode 100644 index 0000000..0ae7b10 --- /dev/null +++ b/Thread/Tests/AsyncUpdateHelperTests.cs @@ -0,0 +1,123 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +#endregion + +namespace TCSystem.Thread.Tests; + +[TestFixture] +public class AsyncUpdateHelperTests +{ + [SetUp] + public void SetUp() + { + _helper = Factory.CreateAsyncUpdateHelper(); + } + + [Test] + public void InitialState_ShouldStop_IsFalse() + { + Assert.That(_helper.ShouldStop, Is.False); + } + + [Test] + public void InitialState_IsUpdatePending_IsFalse() + { + Assert.That(_helper.IsUpdatePending, Is.False); + } + + [Test] + public async Task BeginUpdateAsync_ThenEndUpdate_Succeeds() + { + await _helper.BeginUpdateAsync(); + Assert.That(_helper.ShouldStop, Is.False); + Assert.That(_helper.IsUpdatePending, Is.False); + _helper.EndUpdate(); + } + + [Test] + public async Task WaitAsync_ThenEndUpdate_Succeeds() + { + await _helper.WaitAsync(); + Assert.That(_helper.ShouldStop, Is.False); + Assert.That(_helper.IsUpdatePending, Is.False); + _helper.EndUpdate(); + } + + [Test] + public async Task ShouldStop_TrueWhileSecondBeginUpdatePending() + { + // Acquire the lock so that a second BeginUpdateAsync will block. + await _helper.BeginUpdateAsync(); + + // Start a second update in the background — it will block until the semaphore is released. + Task secondUpdate = _helper.BeginUpdateAsync(); + + // Give the second task a moment to block on the semaphore. + await Task.Delay(50); + Assert.That(_helper.ShouldStop, Is.True); + + // Release so the second update can acquire. + _helper.EndUpdate(); + await secondUpdate; + Assert.That(_helper.ShouldStop, Is.False); + + // Release the second update. + _helper.EndUpdate(); + } + + [Test] + public async Task IsUpdatePending_TrueWhileSecondWaitPending() + { + // Acquire the lock so that WaitAsync will block. + await _helper.BeginUpdateAsync(); + + Task waitTask = _helper.WaitAsync(); + + await Task.Delay(50); + Assert.That(_helper.IsUpdatePending, Is.True); + + _helper.EndUpdate(); + await waitTask; + Assert.That(_helper.IsUpdatePending, Is.False); + _helper.EndUpdate(); + } + + [Test] + public async Task SequentialBeginEndUpdate_WorksMultipleTimes() + { + for (int i = 0; i < 5; i++) + { + await _helper.BeginUpdateAsync(); + _helper.EndUpdate(); + } + + Assert.That(_helper.ShouldStop, Is.False); + Assert.That(_helper.IsUpdatePending, Is.False); + } + + private IAsyncUpdateHelper _helper; +} diff --git a/Thread/Tests/FactoryTests.cs b/Thread/Tests/FactoryTests.cs new file mode 100644 index 0000000..d929312 --- /dev/null +++ b/Thread/Tests/FactoryTests.cs @@ -0,0 +1,54 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using System.Threading; +using NUnit.Framework; + +#endregion + +namespace TCSystem.Thread.Tests; + +[TestFixture] +public class FactoryTests +{ + [Test] + public void CreateWorkerThread_ReturnsNonNull() + { + IWorkerThread worker = Factory.CreateWorkerThread("TestThread", ThreadPriority.Normal); + Assert.That(worker, Is.Not.Null); + worker.StopThread(); + } + + [Test] + public void CreateAsyncUpdateHelper_ReturnsNonNull() + { + IAsyncUpdateHelper helper = Factory.CreateAsyncUpdateHelper(); + Assert.That(helper, Is.Not.Null); + } + + [Test] + public void CreateMultipleTasksExecute_ReturnsNonNull() + { + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(2); + Assert.That(executor, Is.Not.Null); + } +} diff --git a/Thread/Tests/MultipleTasksExecuteTests.cs b/Thread/Tests/MultipleTasksExecuteTests.cs new file mode 100644 index 0000000..15f777d --- /dev/null +++ b/Thread/Tests/MultipleTasksExecuteTests.cs @@ -0,0 +1,174 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +#endregion + +namespace TCSystem.Thread.Tests; + +[TestFixture] +public class MultipleTasksExecuteTests +{ + [Test] + public void ExecuteCommand_RunsAction() + { + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(2); + bool executed = false; + + executor.ExecuteCommand(() => executed = true); + executor.WaitAllDone(); + + Assert.That(executed, Is.True); + } + + [Test] + public void ExecuteCommand_WithCancellationToken_RunsAction() + { + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(2); + bool executed = false; + using var cts = new CancellationTokenSource(); + + executor.ExecuteCommand(() => executed = true, cts.Token); + executor.WaitAllDone(); + + Assert.That(executed, Is.True); + } + + [Test] + public void WaitAllDone_WaitsForAllTasksToComplete() + { + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(4); + int counter = 0; + + for (int i = 0; i < 8; i++) + { + executor.ExecuteCommand(() => Interlocked.Increment(ref counter)); + } + + executor.WaitAllDone(); + Assert.That(counter, Is.EqualTo(8)); + } + + [Test] + public void WaitAllDone_WithCancellationToken_WaitsForAllTasksToComplete() + { + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(3); + int counter = 0; + using var cts = new CancellationTokenSource(); + + for (int i = 0; i < 6; i++) + { + executor.ExecuteCommand(() => Interlocked.Increment(ref counter)); + } + + executor.WaitAllDone(cts.Token); + Assert.That(counter, Is.EqualTo(6)); + } + + [Test] + public void ExecuteCommand_LimitsParallelism() + { + const int maxParallel = 2; + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(maxParallel); + + int currentlyRunning = 0; + int maxObserved = 0; + var syncLock = new object(); + + using var holdEvent = new ManualResetEventSlim(false); + const int taskCount = 6; + + for (int i = 0; i < taskCount; i++) + { + executor.ExecuteCommand(() => + { + int running = Interlocked.Increment(ref currentlyRunning); + lock (syncLock) + { + if (running > maxObserved) + { + maxObserved = running; + } + } + + // Simulate some work while holding the slot + holdEvent.Wait(TimeSpan.FromMilliseconds(50)); + Interlocked.Decrement(ref currentlyRunning); + }); + } + + holdEvent.Set(); + executor.WaitAllDone(); + + Assert.That(maxObserved, Is.LessThanOrEqualTo(maxParallel)); + } + + [Test] + public void ExecuteCommand_MultipleActions_AllRun() + { + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(3); + var results = new List(); + var lockObj = new object(); + + for (int i = 0; i < 10; i++) + { + int value = i; + executor.ExecuteCommand(() => + { + lock (lockObj) + { + results.Add(value); + } + }); + } + + executor.WaitAllDone(); + + Assert.That(results.Count, Is.EqualTo(10)); + } + + [Test] + public void WaitAllDone_CancelledToken_ThrowsOperationCanceled() + { + IMultipleTasksExecute executor = Factory.CreateMultipleTasksExecute(1); + using var cts = new CancellationTokenSource(); + + // Block all slots + using var holdEvent = new ManualResetEventSlim(false); + executor.ExecuteCommand(() => holdEvent.Wait(TimeSpan.FromSeconds(5))); + + // Wait a bit for the slot to be acquired + Task.Delay(20).Wait(); + + cts.Cancel(); + + Assert.Throws(() => executor.WaitAllDone(cts.Token)); + + holdEvent.Set(); + executor.WaitAllDone(); + } +} diff --git a/Thread/Tests/SemaphoreSlimExtTests.cs b/Thread/Tests/SemaphoreSlimExtTests.cs new file mode 100644 index 0000000..3c75cea --- /dev/null +++ b/Thread/Tests/SemaphoreSlimExtTests.cs @@ -0,0 +1,117 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +#endregion + +namespace TCSystem.Thread.Tests; + +[TestFixture] +public class SemaphoreSlimExtTests +{ + [Test] + public void Lock_AcquiresAndReleasesOnDispose() + { + using var sem = new SemaphoreSlim(1, 1); + + IDisposable lockHandle = sem.Lock(); + Assert.That(sem.CurrentCount, Is.EqualTo(0)); + + lockHandle.Dispose(); + Assert.That(sem.CurrentCount, Is.EqualTo(1)); + } + + [Test] + public async Task LockAsync_AcquiresAndReleasesOnDispose() + { + using var sem = new SemaphoreSlim(1, 1); + + IDisposable lockHandle = await sem.LockAsync(); + Assert.That(sem.CurrentCount, Is.EqualTo(0)); + + lockHandle.Dispose(); + Assert.That(sem.CurrentCount, Is.EqualTo(1)); + } + + [Test] + public void Lock_BlocksUntilDisposed() + { + using var sem = new SemaphoreSlim(1, 1); + bool secondAcquired = false; + + IDisposable first = sem.Lock(); + + // Try to acquire in background — will block until first is disposed. + Task background = Task.Run(() => + { + using IDisposable second = sem.Lock(); + secondAcquired = true; + }); + + System.Threading.Thread.Sleep(50); + Assert.That(secondAcquired, Is.False); + + first.Dispose(); + background.Wait(TimeSpan.FromSeconds(2)); + Assert.That(secondAcquired, Is.True); + } + + [Test] + public async Task LockAsync_BlocksUntilDisposed() + { + using var sem = new SemaphoreSlim(1, 1); + bool secondAcquired = false; + + IDisposable first = await sem.LockAsync(); + + Task background = Task.Run(async () => + { + using IDisposable second = await sem.LockAsync(); + secondAcquired = true; + }); + + await Task.Delay(50); + Assert.That(secondAcquired, Is.False); + + first.Dispose(); + await background.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.That(secondAcquired, Is.True); + } + + [Test] + public void Lock_MultipleSequentialLocks_Work() + { + using var sem = new SemaphoreSlim(1, 1); + + for (int i = 0; i < 5; i++) + { + using IDisposable _ = sem.Lock(); + Assert.That(sem.CurrentCount, Is.EqualTo(0)); + } + + Assert.That(sem.CurrentCount, Is.EqualTo(1)); + } +} diff --git a/Thread/Tests/TCSystem.Thread.Tests.csproj b/Thread/Tests/TCSystem.Thread.Tests.csproj new file mode 100644 index 0000000..77bd336 --- /dev/null +++ b/Thread/Tests/TCSystem.Thread.Tests.csproj @@ -0,0 +1,25 @@ + + + + TCSystem.Thread.Tests + TCSystem.Thread.Tests + net8.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Thread/Tests/WorkerThreadTests.cs b/Thread/Tests/WorkerThreadTests.cs new file mode 100644 index 0000000..6d373ad --- /dev/null +++ b/Thread/Tests/WorkerThreadTests.cs @@ -0,0 +1,287 @@ +// ******************************************************************************* +// +// ******* *** *** * +// * * * * +// * * * ***** +// * * *** * * ** * ** *** +// * * * * * * * **** * * * +// * * * * * * * * * * * +// * *** *** * ** ** ** * * +// * +// ******************************************************************************* +// see https://github.com/tgoessler/TCSystemCS for details. +// Copyright (C) 2003 - 2026 Thomas Goessler. All Rights Reserved. +// ******************************************************************************* +// +// TCSystem is the legal property of its developers. +// Please refer to the COPYRIGHT file distributed with this source distribution. +// +// ******************************************************************************* + +#region Usings + +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +#endregion + +namespace TCSystem.Thread.Tests; + +[TestFixture] +public class WorkerThreadTests +{ + [SetUp] + public void SetUp() + { + _worker = Factory.CreateWorkerThread("TestWorkerThread", ThreadPriority.Normal); + } + + [TearDown] + public void TearDown() + { + _worker.StopThread(TimeSpan.FromSeconds(5)); + } + + [Test] + public void ExecuteCommand_RunsAction() + { + bool executed = false; + using var done = new ManualResetEventSlim(false); + + _worker.ExecuteCommand(() => + { + executed = true; + done.Set(); + }); + + Assert.That(done.Wait(TimeSpan.FromSeconds(5)), Is.True, "Action did not execute in time"); + Assert.That(executed, Is.True); + } + + [Test] + public void ExecuteCommand_WithMessage_RunsAction() + { + bool executed = false; + using var done = new ManualResetEventSlim(false); + + _worker.ExecuteCommand(() => + { + executed = true; + done.Set(); + }, "TestMessage"); + + Assert.That(done.Wait(TimeSpan.FromSeconds(5)), Is.True); + Assert.That(executed, Is.True); + } + + [Test] + public async Task ExecuteCommandAsync_RunsAction() + { + bool executed = false; + using var done = new ManualResetEventSlim(false); + + await _worker.ExecuteCommandAsync(() => + { + executed = true; + done.Set(); + }); + + Assert.That(done.Wait(TimeSpan.FromSeconds(5)), Is.True); + Assert.That(executed, Is.True); + } + + [Test] + public async Task ExecuteCommandAsync_WithMessage_RunsAction() + { + bool executed = false; + using var done = new ManualResetEventSlim(false); + + await _worker.ExecuteCommandAsync(() => + { + executed = true; + done.Set(); + }, "TestMessageAsync"); + + Assert.That(done.Wait(TimeSpan.FromSeconds(5)), Is.True); + Assert.That(executed, Is.True); + } + + [Test] + public void ClearOpenCommands_RemovesQueuedActions() + { + // Hold the worker busy so queued commands stay in the queue + using var holdEvent = new ManualResetEventSlim(false); + using var workerBusy = new ManualResetEventSlim(false); + + _worker.ExecuteCommand(() => + { + workerBusy.Set(); + holdEvent.Wait(TimeSpan.FromSeconds(5)); + }); + + // Wait until worker is executing the blocking action + workerBusy.Wait(TimeSpan.FromSeconds(5)); + + // Queue two more actions + _worker.ExecuteCommand(() => { }); + _worker.ExecuteCommand(() => { }); + Assert.That(_worker.NumOpenActions, Is.EqualTo(2)); + + _worker.ClearOpenCommands(); + Assert.That(_worker.NumOpenActions, Is.EqualTo(0)); + + // Unblock worker + holdEvent.Set(); + } + + [Test] + public async Task ClearOpenCommandsAsync_RemovesQueuedActions() + { + using var holdEvent = new ManualResetEventSlim(false); + using var workerBusy = new ManualResetEventSlim(false); + + _worker.ExecuteCommand(() => + { + workerBusy.Set(); + holdEvent.Wait(TimeSpan.FromSeconds(5)); + }); + + workerBusy.Wait(TimeSpan.FromSeconds(5)); + + _worker.ExecuteCommand(() => { }); + _worker.ExecuteCommand(() => { }); + + await _worker.ClearOpenCommandsAsync(); + Assert.That(_worker.NumOpenActions, Is.EqualTo(0)); + + holdEvent.Set(); + } + + [Test] + public void NumOpenActions_ReturnsCorrectCount() + { + // Block the worker thread so actions accumulate in the queue + using var holdEvent = new ManualResetEventSlim(false); + using var workerBusy = new ManualResetEventSlim(false); + + _worker.ExecuteCommand(() => + { + workerBusy.Set(); + holdEvent.Wait(TimeSpan.FromSeconds(5)); + }); + + workerBusy.Wait(TimeSpan.FromSeconds(5)); + + _worker.ExecuteCommand(() => { }); + _worker.ExecuteCommand(() => { }); + Assert.That(_worker.NumOpenActions, Is.EqualTo(2)); + + holdEvent.Set(); + } + + [Test] + public void IsBusy_TrueWhileExecuting() + { + using var holdEvent = new ManualResetEventSlim(false); + using var workerBusy = new ManualResetEventSlim(false); + + _worker.ExecuteCommand(() => + { + workerBusy.Set(); + holdEvent.Wait(TimeSpan.FromSeconds(5)); + }); + + workerBusy.Wait(TimeSpan.FromSeconds(5)); + Assert.That(_worker.IsBusy, Is.True); + + holdEvent.Set(); + } + + [Test] + public void OnInitThread_CalledBeforeFirstAction() + { + bool initCalled = false; + using var done = new ManualResetEventSlim(false); + + _worker.OnInitThread += () => initCalled = true; + _worker.ExecuteCommand(() => done.Set()); + + done.Wait(TimeSpan.FromSeconds(5)); + Assert.That(initCalled, Is.True); + } + + [Test] + public void OnDeInitThread_CalledAfterStop() + { + bool deinitCalled = false; + using var done = new ManualResetEventSlim(false); + + _worker.OnDeInitThread += () => + { + deinitCalled = true; + done.Set(); + }; + + // Execute at least one action so init runs + using var firstAction = new ManualResetEventSlim(false); + _worker.ExecuteCommand(() => firstAction.Set()); + firstAction.Wait(TimeSpan.FromSeconds(5)); + + _worker.StopThread(TimeSpan.FromSeconds(5)); + + Assert.That(done.Wait(TimeSpan.FromSeconds(5)), Is.True); + Assert.That(deinitCalled, Is.True); + } + + [Test] + public void CancellationToken_IsValid() + { + CancellationToken token = _worker.CancellationToken; + Assert.That(token.CanBeCanceled, Is.True); + Assert.That(token.IsCancellationRequested, Is.False); + } + + [Test] + public void StopThread_StopsExecution() + { + int count = 0; + using var done = new ManualResetEventSlim(false); + + _worker.ExecuteCommand(() => + { + count++; + done.Set(); + }); + + done.Wait(TimeSpan.FromSeconds(5)); + _worker.StopThread(TimeSpan.FromSeconds(5)); + + // After stop no further actions should run — verify count stays at 1 + Assert.That(count, Is.EqualTo(1)); + } + + [Test] + public void IdleEvent_FiredWhenWorkerBecomesIdle() + { + bool idleFired = false; + using var idleEvent = new ManualResetEventSlim(false); + + _worker.IdleEvent += idle => + { + if (idle) + { + idleFired = true; + idleEvent.Set(); + } + }; + + _worker.ExecuteCommand(() => { }); + + Assert.That(idleEvent.Wait(TimeSpan.FromSeconds(5)), Is.True); + Assert.That(idleFired, Is.True); + } + + private IWorkerThread _worker; +} From cfd3041e99120d7726e183cd53c2286131beb5e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:44:46 +0000 Subject: [PATCH 3/3] Fix suspicious issues in PR #80: align test project TargetFrameworks (net8.0;net10.0), Microsoft.NET.Test.Sdk version (18.4.0), and Import tag style Agent-Logs-Url: https://github.com/tgoessler/TCSystemCS/sessions/ed01ea44-3110-4dc0-8761-c5697e69a9d7 Co-authored-by: tgoessler <89462205+tgoessler@users.noreply.github.com> --- Thread/Tests/TCSystem.Thread.Tests.csproj | 6 +++--- Util/Tests/TCSystem.Util.Tests.csproj | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Thread/Tests/TCSystem.Thread.Tests.csproj b/Thread/Tests/TCSystem.Thread.Tests.csproj index 77bd336..94be5c4 100644 --- a/Thread/Tests/TCSystem.Thread.Tests.csproj +++ b/Thread/Tests/TCSystem.Thread.Tests.csproj @@ -3,7 +3,7 @@ TCSystem.Thread.Tests TCSystem.Thread.Tests - net8.0 + net8.0;net10.0 @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -20,6 +20,6 @@ - + diff --git a/Util/Tests/TCSystem.Util.Tests.csproj b/Util/Tests/TCSystem.Util.Tests.csproj index 6361670..4cda69f 100644 --- a/Util/Tests/TCSystem.Util.Tests.csproj +++ b/Util/Tests/TCSystem.Util.Tests.csproj @@ -3,7 +3,7 @@ TCSystem.Util.Tests TCSystem.Util.Tests - net8.0 + net8.0;net10.0 @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -20,6 +20,6 @@ - +