From 59fbb2253beebee737aee0389c98873c5d79d88a Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:40:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(tests):=20=E6=B7=BB=E5=8A=A0=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E5=BC=8F=E7=BC=96=E7=A8=8B=E6=89=A9=E5=B1=95=E5=92=8C?= =?UTF-8?q?Option=E7=B1=BB=E5=9E=8B=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加ControlExtensions的TakeIfValue、TakeUnlessValue、When、RepeatUntil和Retry方法测试 - 添加FunctionExtensions的Compose、AndThen、Curry、Uncurry、Defer和Once方法测试 - 添加Option类型的完整测试套件,包括工厂方法、取值、变换、过滤、模式匹配等功能 - 添加PipeExtensions的Tap、Pipe、Let、PipeIf方法测试 - 移除AsyncExtensionsTests中的WhenAll相关测试 - 移除NumericExtensionsTests中的Clamp测试 - 更新ObjectExtensionsTests中Do方法为Also方法的测试 - 修复ControlExtensions文档中的XML代码标签格式 - 在AsyncExtensionsTests中添加对GFramework.Core.Functional.Async的引用 - 在ObjectExtensionsTests中添加对GFramework.Core.functional.pipe的引用 --- .../extensions/AsyncExtensionsTests.cs | 35 +- .../extensions/NumericExtensionsTests.cs | 51 +-- .../extensions/ObjectExtensionsTests.cs | 11 +- .../extensions/ResultExtensionsTests.cs | 2 +- .../extensions/StringExtensionsTests.cs | 8 +- .../functional/OptionTests.cs | 388 ++++++++++++++++++ .../control/ControlExtensionsTests.cs | 331 +++++++++++++++ .../functions/FunctionExtensionsTests.cs | 230 +++++++++++ .../functional/pipe/PipeExtensionsTests.cs | 208 ++++++++++ .../functional/control/ControlExtensions.cs | 4 +- 10 files changed, 1172 insertions(+), 96 deletions(-) create mode 100644 GFramework.Core.Tests/functional/OptionTests.cs diff --git a/GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs b/GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs index 7a0d80b..5a7ef23 100644 --- a/GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs +++ b/GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using GFramework.Core.extensions; +using GFramework.Core.Functional.Async; using NUnit.Framework; namespace GFramework.Core.Tests.extensions; @@ -237,40 +238,6 @@ public class AsyncExtensionsTests Assert.That(result.IsFaulted, Is.True); } - [Test] - public async Task WhenAll_Should_Wait_For_All_Tasks() - { - // Arrange - var task1 = Task.Delay(10); - var task2 = Task.Delay(20); - var task3 = Task.Delay(30); - var tasks = new[] { task1, task2, task3 }; - - // Act - await tasks.WhenAll(); - - // Assert - Assert.That(task1.IsCompleted, Is.True); - Assert.That(task2.IsCompleted, Is.True); - Assert.That(task3.IsCompleted, Is.True); - } - - [Test] - public async Task WhenAll_WithResults_Should_Return_All_Results() - { - // Arrange - var task1 = Task.FromResult(1); - var task2 = Task.FromResult(2); - var task3 = Task.FromResult(3); - var tasks = new[] { task1, task2, task3 }; - - // Act - var results = await tasks.WhenAll(); - - // Assert - Assert.That(results, Is.EqualTo(new[] { 1, 2, 3 })); - } - [Test] public async Task WithFallback_Should_Return_Result_When_Task_Succeeds() { diff --git a/GFramework.Core.Tests/extensions/NumericExtensionsTests.cs b/GFramework.Core.Tests/extensions/NumericExtensionsTests.cs index c85bf5c..32465d6 100644 --- a/GFramework.Core.Tests/extensions/NumericExtensionsTests.cs +++ b/GFramework.Core.Tests/extensions/NumericExtensionsTests.cs @@ -9,55 +9,6 @@ namespace GFramework.Core.Tests.extensions; [TestFixture] public class NumericExtensionsTests { - [Test] - public void Clamp_Should_Return_Min_When_Value_Is_Less_Than_Min() - { - // Arrange - var value = -10; - - // Act - var result = value.Clamp(0, 100); - - // Assert - Assert.That(result, Is.EqualTo(0)); - } - - [Test] - public void Clamp_Should_Return_Max_When_Value_Is_Greater_Than_Max() - { - // Arrange - var value = 150; - - // Act - var result = value.Clamp(0, 100); - - // Assert - Assert.That(result, Is.EqualTo(100)); - } - - [Test] - public void Clamp_Should_Return_Value_When_Value_Is_Within_Range() - { - // Arrange - var value = 50; - - // Act - var result = value.Clamp(0, 100); - - // Assert - Assert.That(result, Is.EqualTo(50)); - } - - [Test] - public void Clamp_Should_Throw_ArgumentException_When_Min_Is_Greater_Than_Max() - { - // Arrange - var value = 50; - - // Act & Assert - Assert.Throws(() => value.Clamp(100, 0)); - } - [Test] public void Between_Should_Return_True_When_Value_Is_Within_Range() { @@ -212,4 +163,4 @@ public class NumericExtensionsTests // Arrange & Act & Assert Assert.Throws(() => 50f.InverseLerp(100f, 100f)); } -} +} \ No newline at end of file diff --git a/GFramework.Core.Tests/extensions/ObjectExtensionsTests.cs b/GFramework.Core.Tests/extensions/ObjectExtensionsTests.cs index 519cff1..2883e34 100644 --- a/GFramework.Core.Tests/extensions/ObjectExtensionsTests.cs +++ b/GFramework.Core.Tests/extensions/ObjectExtensionsTests.cs @@ -1,4 +1,5 @@ using GFramework.Core.extensions; +using GFramework.Core.functional.pipe; using NUnit.Framework; namespace GFramework.Core.Tests.extensions; @@ -138,29 +139,29 @@ public class ObjectExtensionsTests } /// - /// 验证Do方法执行操作并返回对象本身 + /// 验证Also方法执行操作并返回对象本身 /// [Test] public void Do_Should_Execute_Action_And_Return_Object() { var obj = new TestClass { Value = 5 }; - var result = obj.Do(x => x.Value = 10); + var result = obj.Also(x => x.Value = 10); Assert.That(result, Is.SameAs(obj)); Assert.That(obj.Value, Is.EqualTo(10)); } /// - /// 验证Do方法支持链式调用 + /// 验证Also方法支持链式调用 /// [Test] public void Do_Should_Support_Chaining() { var obj = new TestClass { Value = 1, Name = "A" }; - obj.Do(x => x.Value = 2) - .Do(x => x.Name = "B"); + obj.Also(x => x.Value = 2) + .Also(x => x.Name = "B"); Assert.That(obj.Value, Is.EqualTo(2)); Assert.That(obj.Name, Is.EqualTo("B")); diff --git a/GFramework.Core.Tests/extensions/ResultExtensionsTests.cs b/GFramework.Core.Tests/extensions/ResultExtensionsTests.cs index a65a2c4..22e2195 100644 --- a/GFramework.Core.Tests/extensions/ResultExtensionsTests.cs +++ b/GFramework.Core.Tests/extensions/ResultExtensionsTests.cs @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using GFramework.Core.Extensions; using GFramework.Core.Functional; +using GFramework.Core.functional.result; using NUnit.Framework; namespace GFramework.Core.Tests.Extensions; diff --git a/GFramework.Core.Tests/extensions/StringExtensionsTests.cs b/GFramework.Core.Tests/extensions/StringExtensionsTests.cs index c650014..789820c 100644 --- a/GFramework.Core.Tests/extensions/StringExtensionsTests.cs +++ b/GFramework.Core.Tests/extensions/StringExtensionsTests.cs @@ -55,7 +55,7 @@ public class StringExtensionsTests string? text = null; // Act - var result = text.IsNullOrWhiteSpace(); + var result = string.IsNullOrWhiteSpace(text); // Assert Assert.That(result, Is.True); @@ -68,7 +68,7 @@ public class StringExtensionsTests var text = " "; // Act - var result = text.IsNullOrWhiteSpace(); + var result = string.IsNullOrWhiteSpace(text); // Assert Assert.That(result, Is.True); @@ -81,7 +81,7 @@ public class StringExtensionsTests var text = "Hello"; // Act - var result = text.IsNullOrWhiteSpace(); + var result = string.IsNullOrWhiteSpace(text); // Assert Assert.That(result, Is.False); @@ -230,4 +230,4 @@ public class StringExtensionsTests // Act & Assert Assert.Throws(() => words.Join(null!)); } -} +} \ No newline at end of file diff --git a/GFramework.Core.Tests/functional/OptionTests.cs b/GFramework.Core.Tests/functional/OptionTests.cs new file mode 100644 index 0000000..be86809 --- /dev/null +++ b/GFramework.Core.Tests/functional/OptionTests.cs @@ -0,0 +1,388 @@ +// Copyright (c) 2025 GeWuYou +// Licensed 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. + +using GFramework.Core.Functional; +using NUnit.Framework; + +namespace GFramework.Core.Tests.Functional; + +/// +/// Option<T> 类型测试类 +/// +[TestFixture] +public class OptionTests +{ + [Test] + public void Some_WithValue_Should_Create_Some_Option() + { + var option = Option.Some(42); + Assert.That(option.IsSome, Is.True); + Assert.That(option.IsNone, Is.False); + } + + [Test] + public void Some_WithNull_Should_Throw_ArgumentNullException() + { + Assert.Throws(() => Option.Some(null!)); + } + + [Test] + public void None_Should_Create_None_Option() + { + var option = Option.None; + Assert.That(option.IsSome, Is.False); + Assert.That(option.IsNone, Is.True); + } + + [Test] + public void GetOrElse_WithSome_Should_Return_Value() + { + var option = Option.Some(42); + var result = option.GetOrElse(0); + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void GetOrElse_WithNone_Should_Return_Default_Value() + { + var option = Option.None; + var result = option.GetOrElse(99); + Assert.That(result, Is.EqualTo(99)); + } + + [Test] + public void GetOrElse_WithFactory_WithSome_Should_Return_Value_Without_Calling_Factory() + { + var option = Option.Some(42); + var factoryCalled = false; + + var result = option.GetOrElse(() => + { + factoryCalled = true; + return 99; + }); + + Assert.That(result, Is.EqualTo(42)); + Assert.That(factoryCalled, Is.False); + } + + [Test] + public void GetOrElse_WithFactory_WithNone_Should_Call_Factory() + { + var option = Option.None; + var result = option.GetOrElse(() => 99); + Assert.That(result, Is.EqualTo(99)); + } + + [Test] + public void GetOrElse_WithNullFactory_Should_Throw_ArgumentNullException() + { + var option = Option.None; + Assert.Throws(() => option.GetOrElse(null!)); + } + + [Test] + public void Map_WithSome_Should_Map_Value() + { + var option = Option.Some(42); + var mapped = option.Map(x => x.ToString()); + Assert.That(mapped.IsSome, Is.True); + Assert.That(mapped.GetOrElse(""), Is.EqualTo("42")); + } + + [Test] + public void Map_WithNone_Should_Return_None() + { + var option = Option.None; + var mapped = option.Map(x => x.ToString()); + Assert.That(mapped.IsNone, Is.True); + } + + [Test] + public void Map_WithNullMapper_Should_Throw_ArgumentNullException() + { + var option = Option.Some(42); + Assert.Throws(() => option.Map(null!)); + } + + [Test] + public void Bind_WithSome_Should_Bind_Value() + { + var option = Option.Some("42"); + var bound = option.Bind(s => int.TryParse(s, out var i) + ? Option.Some(i) + : Option.None); + + Assert.That(bound.IsSome, Is.True); + Assert.That(bound.GetOrElse(0), Is.EqualTo(42)); + } + + [Test] + public void Bind_WithSome_Can_Return_None() + { + var option = Option.Some("invalid"); + var bound = option.Bind(s => int.TryParse(s, out var i) + ? Option.Some(i) + : Option.None); + + Assert.That(bound.IsNone, Is.True); + } + + [Test] + public void Bind_WithNone_Should_Return_None() + { + var option = Option.None; + var bound = option.Bind(s => Option.Some(42)); + Assert.That(bound.IsNone, Is.True); + } + + [Test] + public void Bind_WithNullBinder_Should_Throw_ArgumentNullException() + { + var option = Option.Some(42); + Assert.Throws(() => option.Bind(null!)); + } + + [Test] + public void Filter_WithSome_PredicateTrue_Should_Return_Some() + { + var option = Option.Some(42); + var filtered = option.Filter(x => x > 0); + Assert.That(filtered.IsSome, Is.True); + Assert.That(filtered.GetOrElse(0), Is.EqualTo(42)); + } + + [Test] + public void Filter_WithSome_PredicateFalse_Should_Return_None() + { + var option = Option.Some(42); + var filtered = option.Filter(x => x < 0); + Assert.That(filtered.IsNone, Is.True); + } + + [Test] + public void Filter_WithNone_Should_Return_None() + { + var option = Option.None; + var filtered = option.Filter(x => true); + Assert.That(filtered.IsNone, Is.True); + } + + [Test] + public void Filter_WithNullPredicate_Should_Throw_ArgumentNullException() + { + var option = Option.Some(42); + Assert.Throws(() => option.Filter(null!)); + } + + [Test] + public void Match_WithSome_Should_Call_Some_Function() + { + var option = Option.Some(42); + var result = option.Match( + some: value => $"Value: {value}", + none: () => "No value" + ); + Assert.That(result, Is.EqualTo("Value: 42")); + } + + [Test] + public void Match_WithNone_Should_Call_None_Function() + { + var option = Option.None; + var result = option.Match( + some: value => $"Value: {value}", + none: () => "No value" + ); + Assert.That(result, Is.EqualTo("No value")); + } + + [Test] + public void Match_WithNullSomeFunction_Should_Throw_ArgumentNullException() + { + var option = Option.Some(42); + Assert.Throws(() => + option.Match(null!, () => "")); + } + + [Test] + public void Match_WithNullNoneFunction_Should_Throw_ArgumentNullException() + { + var option = Option.Some(42); + Assert.Throws(() => + option.Match(value => "", null!)); + } + + [Test] + public void Match_Action_WithSome_Should_Call_Some_Action() + { + var option = Option.Some(42); + var someCalled = false; + var noneCalled = false; + + option.Match( + some: _ => someCalled = true, + none: () => noneCalled = true + ); + + Assert.That(someCalled, Is.True); + Assert.That(noneCalled, Is.False); + } + + [Test] + public void Match_Action_WithNone_Should_Call_None_Action() + { + var option = Option.None; + var someCalled = false; + var noneCalled = false; + + option.Match( + some: _ => someCalled = true, + none: () => noneCalled = true + ); + + Assert.That(someCalled, Is.False); + Assert.That(noneCalled, Is.True); + } + + [Test] + public void ToResult_WithSome_Should_Return_Success_Result() + { + var option = Option.Some(42); + var result = option.ToResult("Error"); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Match(succ: v => v, fail: _ => 0), Is.EqualTo(42)); + } + + [Test] + public void ToResult_WithNone_Should_Return_Failure_Result() + { + var option = Option.None; + var result = option.ToResult("Value not found"); + Assert.That(result.IsFaulted, Is.True); + Assert.That(result.Exception, Is.TypeOf()); + Assert.That(result.Exception.Message, Is.EqualTo("Value not found")); + } + + [Test] + public void ToResult_WithNullOrWhiteSpaceMessage_Should_Throw_ArgumentException() + { + var option = Option.None; + Assert.Throws(() => option.ToResult("")); + Assert.Throws(() => option.ToResult(" ")); + } + + [Test] + public void ToEnumerable_WithSome_Should_Return_Sequence_With_One_Element() + { + var option = Option.Some(42); + var enumerable = option.ToEnumerable().ToList(); + Assert.That(enumerable, Has.Count.EqualTo(1)); + Assert.That(enumerable[0], Is.EqualTo(42)); + } + + [Test] + public void ToEnumerable_WithNone_Should_Return_Empty_Sequence() + { + var option = Option.None; + var enumerable = option.ToEnumerable().ToList(); + Assert.That(enumerable, Is.Empty); + } + + [Test] + public void ImplicitConversion_FromValue_Should_Create_Some_Option() + { + Option option = 42; + Assert.That(option.IsSome, Is.True); + Assert.That(option.GetOrElse(0), Is.EqualTo(42)); + } + + [Test] + public void ImplicitConversion_FromNull_Should_Create_None_Option() + { + Option option = null!; + Assert.That(option.IsNone, Is.True); + } + + [Test] + public void Equals_TwoSomeWithSameValue_Should_Return_True() + { + var option1 = Option.Some(42); + var option2 = Option.Some(42); + Assert.That(option1.Equals(option2), Is.True); + Assert.That(option1 == option2, Is.True); + Assert.That(option1 != option2, Is.False); + } + + [Test] + public void Equals_TwoSomeWithDifferentValue_Should_Return_False() + { + var option1 = Option.Some(42); + var option2 = Option.Some(99); + Assert.That(option1.Equals(option2), Is.False); + Assert.That(option1 == option2, Is.False); + Assert.That(option1 != option2, Is.True); + } + + [Test] + public void Equals_TwoNone_Should_Return_True() + { + var option1 = Option.None; + var option2 = Option.None; + Assert.That(option1.Equals(option2), Is.True); + Assert.That(option1 == option2, Is.True); + Assert.That(option1 != option2, Is.False); + } + + [Test] + public void Equals_SomeAndNone_Should_Return_False() + { + var option1 = Option.Some(42); + var option2 = Option.None; + Assert.That(option1.Equals(option2), Is.False); + Assert.That(option1 == option2, Is.False); + Assert.That(option1 != option2, Is.True); + } + + [Test] + public void GetHashCode_TwoSomeWithSameValue_Should_Return_Same_HashCode() + { + var option1 = Option.Some(42); + var option2 = Option.Some(42); + Assert.That(option1.GetHashCode(), Is.EqualTo(option2.GetHashCode())); + } + + [Test] + public void GetHashCode_TwoNone_Should_Return_Same_HashCode() + { + var option1 = Option.None; + var option2 = Option.None; + Assert.That(option1.GetHashCode(), Is.EqualTo(option2.GetHashCode())); + } + + [Test] + public void ToString_WithSome_Should_Return_Formatted_String() + { + var option = Option.Some(42); + var result = option.ToString(); + Assert.That(result, Is.EqualTo("Some(42)")); + } + + [Test] + public void ToString_WithNone_Should_Return_None() + { + var option = Option.None; + var result = option.ToString(); + Assert.That(result, Is.EqualTo("None")); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/functional/control/ControlExtensionsTests.cs b/GFramework.Core.Tests/functional/control/ControlExtensionsTests.cs index e646cb1..b9996a5 100644 --- a/GFramework.Core.Tests/functional/control/ControlExtensionsTests.cs +++ b/GFramework.Core.Tests/functional/control/ControlExtensionsTests.cs @@ -72,4 +72,335 @@ public class ControlExtensionsTests // Assert Assert.That(result, Is.Null); } + + [Test] + public void TakeIfValue_Should_Return_Value_When_Condition_Is_True() + { + // Arrange + var value = 42; + + // Act + var result = value.TakeIfValue(x => x > 0); + + // Assert + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void TakeIfValue_Should_Return_Null_When_Condition_Is_False() + { + // Arrange + var value = -5; + + // Act + var result = value.TakeIfValue(x => x > 0); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void TakeIfValue_WithNullPredicate_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => value.TakeIfValue(null!)); + } + + [Test] + public void TakeUnlessValue_Should_Return_Value_When_Condition_Is_False() + { + // Arrange + var value = 42; + + // Act + var result = value.TakeUnlessValue(x => x < 0); + + // Assert + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void TakeUnlessValue_Should_Return_Null_When_Condition_Is_True() + { + // Arrange + var value = -5; + + // Act + var result = value.TakeUnlessValue(x => x < 0); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void TakeUnlessValue_WithNullPredicate_Should_Throw_ArgumentNullE() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => value.TakeUnlessValue(null!)); + } + + [Test] + public void When_Should_Execute_Action_When_Condition_Is_True() + { + // Arrange + var value = 42; + var executed = false; + + // Act + var result = value.When(x => x > 0, x => executed = true); + + // Assert + Assert.That(result, Is.EqualTo(42)); + Assert.That(executed, Is.True); + } + + [Test] + public void When_Should_Not_Executehen_Condition_Is_False() + { + // Arrange + var value = -5; + var executed = false; + + // Act + var result = value.When(x => x > 0, x => executed = true); + + // Assert + Assert.That(result, Is.EqualTo(-5)); + Assert.That(executed, Is.False); + } + + [Test] + public void When_WithNullPredicate_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => + value.When(null!, x => { })); + } + + [Test] + public void When_WithNullAction_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => + value.When(x => true, null!)); + } + + [Test] + public void When_Should_Allow_Chaining() + { + // Arrange + var value = 10; + var log = new List(); + + // Act + var result = value + .When(x => x > 5, x => log.Add("Greater than 5")) + .When(x => x % 2 == 0, x => log.Add("Even number")); + + // Assert + Assert.That(result, Is.EqualTo(10)); + Assert.That(log, Has.Count.EqualTo(2)); + } + + [Test] + public void RepeatUntil_Should_Repeat_Until_Condition_Is_Met() + { + // Arrange + var value = 1; + + // Act + var result = value.RepeatUntil( + x => x * 2, + x => x >= 100, + maxIterations: 10 + ); + + // Assert + Assert.That(result, Is.EqualTo(128)); + } + + [Test] + public void RepeatUntil_Should_Return_Initial_Value_If_Condition_Already_Met() + { + // Arrange + var value = 100; + + // Act + var result = value.RepeatUntil( + x => x * 2, + x => x >= 100, + maxIterations: 10 + ); + + // Assert + Assert.That(result, Is.EqualTo(100)); + } + + [Test] + public void RepeatUntil_Should_Throw_When_Max_Iterations_Reached() + { + // Arrange + var value = 1; + + // Act & Assert + Assert.Throws(() => + value.RepeatUntil( + x => x + 1, + x => x > 1000, + maxIterations: 10 + )); + } + + [Test] + public void RepeatUntil_WithNullFunc_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 1; + + // Act & Assert + Assert.Throws(() => + value.RepeatUntil(null!, x => true)); + } + + [Test] + public void RepeatUntil_WithNullPredicate_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 1; + + // Act & Assert + Assert.Throws(() => + value.RepeatUntil(x => x, null!)); + } + + [Test] + public void RepeatUntil_WithInvalidMaxIterations_Should_Throw_ArgumentOutOfRangeException() + { + // Arrange + var value = 1; + + // Act & Assert + Assert.Throws(() => + value.RepeatUntil(x => x, x => true, maxIterations: 0)); + } + + [Test] + public void Retry_Should_Return_Result_On_First_Success() + { + // Arrange + var counter = 0; + Func func = () => ++counter; + + // Act + var result = ControlExtensions.Retry(func, maxRetries: 3); + + // Assert + Assert.That(result, Is.EqualTo(1)); + Assert.That(counter, Is.EqualTo(1)); + } + + [Test] + public void Retry_Should_Retry_On_Failure() + { + // Arrange + var counter = 0; + Func func = () => + { + counter++; + if (counter < 3) + throw new InvalidOperationException("Not ready"); + return counter; + }; + + // Act + var result = ControlExtensions.Retry(func, maxRetries: 3); + + // Assert + Assert.That(result, Is.EqualTo(3)); + Assert.That(counter, Is.EqualTo(3)); + } + + [Test] + public void Retry_Should_Throw_AggregateException_When_All_Retries_Fail() + { + // Arrange + var counter = 0; + Func func = () => + { + counter++; + throw new InvalidOperationException($"Attempt {counter}"); + }; + + // Act & Assert + var ex = Assert.Throws(() => + ControlExtensions.Retry(func, maxRetries: 2)); + + Assert.That(counter, Is.EqualTo(3)); // 1 initial + 2 retries + Assert.That(ex!.InnerExceptions, Has.Count.EqualTo(3)); + } + + [Test] + public void Retry_WithNullFunc_Should_Throw_ArgumentNullException() + { + // Act & Assert + Assert.Throws(() => + ControlExtensions.Retry(null!, maxRetries: 3)); + } + + [Test] + public void Retry_WithNegativeMaxRetries_Should_Throw_ArgumentOutOfRangeException() + { + // Arrange + Func func = () => 42; + + // Act & Assert + Assert.Throws(() => + ControlExtensions.Retry(func, maxRetries: -1)); + } + + [Test] + public void Retry_WithNegativeDelay_Should_Throw_ArgumentOutOfRangeException() + { + // Arrange + Func func = () => 42; + + // Act & Assert + Assert.Throws(() => + ControlExtensions.Retry(func, maxRetries: 3, delayMilliseconds: -1)); + } + + [Test] + public void Retry_Should_Delay_Between_Retries() + { + // Arrange + var counter = 0; + var startTime = DateTime.UtcNow; + Func func = () => + { + counter++; + if (counter < 3) + throw new InvalidOperationException("Not ready"); + return counter; + }; + + // Act + var result = ControlExtensions.Retry(func, maxRetries: 3, delayMilliseconds: 50); + var elapsed = DateTime.UtcNow - startTime; + + // Assert + Assert.That(result, Is.EqualTo(3)); + Assert.That(elapsed.TotalMilliseconds, Is.GreaterThanOrEqualTo(100)); // 2 delays of 50ms + } } \ No newline at end of file diff --git a/GFramework.Core.Tests/functional/functions/FunctionExtensionsTests.cs b/GFramework.Core.Tests/functional/functions/FunctionExtensionsTests.cs index 0523f59..3651689 100644 --- a/GFramework.Core.Tests/functional/functions/FunctionExtensionsTests.cs +++ b/GFramework.Core.Tests/functional/functions/FunctionExtensionsTests.cs @@ -43,4 +43,234 @@ public class FunctionExtensionsTests // 2 -> 4 -> 8 -> 16 (3次重复) Assert.That(result, Is.EqualTo(16)); } + + [Test] + public void Compose_Should_Apply_Functions_In_Reverse_Order() + { + // Arrange + int AddOne(int x) => x + 1; + Func multiplyTwo = x => x * 2; + + // Act + var composed = multiplyTwo.Compose((Func)AddOne); // (x + 1) * 2 + + // Assert + Assert.That(composed(5), Is.EqualTo(12)); // (5 + 1) * 2 = 12 + } + + [Test] + public void Compose_WithNullOuterFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func addOne = x => x + 1; + + // Act & Assert + Assert.Throws(() => + ((Func)null!).Compose(addOne)); + } + + [Test] + public void Compose_WithNullInnerFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func multiplyTwo = x => x * 2; + + // Act & Assert + Assert.Throws(() => + multiplyTwo.Compose(null!)); + } + + [Test] + public void AndThen_Should_Apply_Functions_In_Order() + { + // Arrange + Func addOne = x => x + 1; + int MultiplyTwo(int x) => x * 2; + + // Act + var chained = addOne.AndThen((Func)MultiplyTwo); // (x + 1) * 2 + + // Assert + Assert.That(chained(5), Is.EqualTo(12)); // (5 + 1) * 2 = 12 + } + + [Test] + public void AndThen_WithNullFirstFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func multiplyTwo = x => x * 2; + + // Act & Assert + Assert.Throws(() => + ((Func)null!).AndThen(multiplyTwo)); + } + + [Test] + public void AndThen_WithNullSecondFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func addOne = x => x + 1; + + // Act & Assert + Assert.Throws(() => + addOne.AndThen(null!)); + } + + [Test] + public void Curry_TwoParameters_Should_Return_Nested_Functions() + { + // Arrange + Func add = (x, y) => x + y; + + // Act + var curriedAdd = add.Curry(); + var add5 = curriedAdd(5); + var result = add5(3); + + // Assert + Assert.That(result, Is.EqualTo(8)); + } + + [Test] + public void Curry_ThreeParameters_Should_Return_Nested_Functions() + { + // Arrange + Func add3 = (x, y, z) => x + y + z; + + // Act + var curriedAdd = add3.Curry(); + var result = curriedAdd(1)(2)(3); + + // Assert + Assert.That(result, Is.EqualTo(6)); + } + + [Test] + public void Curry_WithNullFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func func = null!; + + // Act & Assert + Assert.Throws(() => func.Curry()); + } + + [Test] + public void Uncurry_Should_Restore_Multi_Parameter_Function() + { + // Arrange + Func> curriedAdd = x => y => x + y; + + // Act + var add = curriedAdd.Uncurry(); + var result = add(5, 3); + + // Assert + Assert.That(result, Is.EqualTo(8)); + } + + [Test] + public void Uncurry_WithNullFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func> func = null!; + + // Act & Assert + Assert.Throws(() => func.Uncurry()); + } + + [Test] + public void Curry_Then_Uncurry_Should_Be_Identity() + { + // Arrange + Func original = (x, y) => x * y; + + // Act + var restored = original.Curry().Uncurry(); + var result = restored(6, 7); + + // Assert + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void Defer_Should_Not_Execute_Immediately() + { + // Arrange + var executed = false; + Func func = () => + { + executed = true; + return 42; + }; + + // Act + var lazy = func.Defer(); + + // Assert + Assert.That(executed, Is.False); + Assert.That(lazy.Value, Is.EqualTo(42)); + Assert.That(executed, Is.True); + } + + [Test] + public void Defer_WithNullFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func func = null!; + + // Act & Assert + Assert.Throws(() => func.Defer()); + } + + [Test] + public void Once_Should_Execute_Function_Only_Once() + { + // Arrange + var counter = 0; + Func func = () => ++counter; + + // Act + var once = func.Once(); + var result1 = once(); + var result2 = once(); + var result3 = once(); + + // Assert + Assert.That(result1, Is.EqualTo(1)); + Assert.That(result2, Is.EqualTo(1)); + Assert.That(result3, Is.EqualTo(1)); + Assert.That(counter, Is.EqualTo(1)); + } + + [Test] + public void Once_WithNullFunction_Should_Throw_ArgumentNullException() + { + // Arrange + Func func = null!; + + // Act & Assert + Assert.Throws(() => func.Once()); + } + + [Test] + public void Once_Should_Be_Thread_Safe() + { + // Arrange + var counter = 0; + Func func = () => Interlocked.Increment(ref counter); + + var once = func.Once(); + + // Act + var tasks = Enumerable.Range(0, 10) + .Select(_ => Task.Run(() => once())) + .ToArray(); + + Task.WaitAll(tasks.Cast().ToArray()); + + // Assert + Assert.That(counter, Is.EqualTo(1)); + Assert.That(tasks.Select(t => t.Result).Distinct().Count(), Is.EqualTo(1)); + } } \ No newline at end of file diff --git a/GFramework.Core.Tests/functional/pipe/PipeExtensionsTests.cs b/GFramework.Core.Tests/functional/pipe/PipeExtensionsTests.cs index 3db2114..dc9d820 100644 --- a/GFramework.Core.Tests/functional/pipe/PipeExtensionsTests.cs +++ b/GFramework.Core.Tests/functional/pipe/PipeExtensionsTests.cs @@ -27,4 +27,212 @@ public class PipeExtensionsTests Assert.That(result, Is.EqualTo(42)); Assert.That(capturedValue, Is.EqualTo(42)); } + + [Test] + public void Tap_Should_Execute_Action_And_Return_Original_Value() + { + // Arrange + var value = 42; + var capturedValue = 0; + + // Act + var result = value.Tap(x => capturedValue = x); + + // Assert + Assert.That(result, Is.EqualTo(42)); + Assert.That(capturedValue, Is.EqualTo(42)); + } + + [Test] + public void Tap_WithNullAction_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => value.Tap(null!)); + } + + [Test] + public void Tap_Should_Allow_Chaining() + { + // Arrange + var value = 10; + var log = new List(); + + // Act + var result = value + .Tap(x => log.Add($"Step 1: {x}")) + .Tap(x => log.Add($"Step 2: {x}")); + + // Assert + Assert.That(result, Is.EqualTo(10)); + Assert.That(log, Has.Count.EqualTo(2)); + Assert.That(log[0], Is.EqualTo("Step 1: 10")); + Assert.That(log[1], Is.EqualTo("Step 2: 10")); + } + + [Test] + public void Pipe_Should_Transform_Value() + { + // Arrange + var value = 42; + + // Act + var result = value.Pipe(x => x * 2); + + // Assert + Assert.That(result, Is.EqualTo(84)); + } + + [Test] + public void Pipe_WithNullFunction_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => value.Pipe(null!)); + } + + [Test] + public void Pipe_Should_Allow_Chaining() + { + // Arrange + var value = 5; + + // Act + var result = value + .Pipe(x => x * 2) + .Pipe(x => x + 10) + .Pipe(x => x.ToString()); + + // Assert + Assert.That(result, Is.EqualTo("20")); + } + + [Test] + public void Let_Should_Transform_Value() + { + // Arrange + var value = 42; + + // Act + var result = value.Let(x => x.ToString()); + + // Assert + Assert.That(result, Is.EqualTo("42")); + } + + [Test] + public void Let_WithNullTransform_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => value.Let(null!)); + } + + [Test] + public void Let_Should_Allow_Complex_Transformations() + { + // Arrange + var value = "hello"; + + // Act + var result = value.Let(s => new + { + Original = s, + Upper = s.ToUpper(), + Length = s.Length + }); + + // Assert + Assert.That(result.Original, Is.EqualTo("hello")); + Assert.That(result.Upper, Is.EqualTo("HELLO")); + Assert.That(result.Length, Is.EqualTo(5)); + } + + [Test] + public void PipeIf_WithTruePredicate_Should_Apply_IfTrue_Function() + { + // Arrange + var value = 42; + + // Act + var result = value.PipeIf( + x => x > 0, + x => $"Positive: {x}", + x => $"Non-positive: {x}" + ); + + // Assert + Assert.That(result, Is.EqualTo("Positive: 42")); + } + + [Test] + public void PipeIf_WithFalsePredicate_Should_Apply_IfFalse_Function() + { + // Arrange + var value = -5; + + // Act + var result = value.PipeIf( + x => x > 0, + x => $"Positive: {x}", + x => $"Non-positive: {x}" + ); + + // Assert + Assert.That(result, Is.EqualTo("Non-positive: -5")); + } + + [Test] + public void PipeIf_WithNullPredicate_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => + value.PipeIf(null!, x => "", x => "")); + } + + [Test] + public void PipeIf_WithNullIfTrue_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => + value.PipeIf(x => true, null!, x => "")); + } + + [Test] + public void PipeIf_WithNullIfFalse_Should_Throw_ArgumentNullException() + { + // Arrange + var value = 42; + + // Act & Assert + Assert.Throws(() => + value.PipeIf(x => true, x => "", null!)); + } + + [Test] + public void PipeIf_Should_Allow_Chaining() + { + // Arrange + var value = 10; + + // Act + var result = value + .PipeIf(x => x > 5, x => x * 2, x => x + 10) + .PipeIf(x => x > 15, x => $"Large: {x}", x => $"Small: {x}"); + + // Assert + Assert.That(result, Is.EqualTo("Large: 20")); + } } \ No newline at end of file diff --git a/GFramework.Core/functional/control/ControlExtensions.cs b/GFramework.Core/functional/control/ControlExtensions.cs index 412c9fc..64b3ff5 100644 --- a/GFramework.Core/functional/control/ControlExtensions.cs +++ b/GFramework.Core/functional/control/ControlExtensions.cs @@ -57,10 +57,10 @@ public static class ControlExtensions /// 条件为真时返回原值,否则返回 null /// 当 predicate 为 null 时抛出 /// - /// + /// x > 0); // 42 /// var result2 = -5.TakeIfValue(x => x > 0); // null - /// + /// ]]> /// public static TSource? TakeIfValue( this TSource value,