Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageVersion Include="CliWrap" Version="3.10.0" />
<PackageVersion Include="EnumerableAsyncProcessor" Version="3.8.4" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageVersion Include="FsCheck" Version="3.3.2" />
<PackageVersion Include="FSharp.Core" Version="10.0.101" />
<PackageVersion Include="Humanizer" Version="3.0.1" />
<PackageVersion Include="MessagePack" Version="3.1.4" />
Expand Down
91 changes: 91 additions & 0 deletions TUnit.Example.FsCheck.TestProject/PropertyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using FsCheck;
using FsCheck.Fluent;
using TUnit.FsCheck;

namespace TUnit.Example.FsCheck.TestProject;

public class PropertyTests
{
[Test, FsCheckProperty]
public bool ReverseReverseIsOriginal(int[] array)
{
var reversed = array.AsEnumerable().Reverse().Reverse().ToArray();
return array.SequenceEqual(reversed);
}

[Test, FsCheckProperty]
public bool AbsoluteValueIsNonNegative(int value)
{
return Math.Abs((long)value) >= 0;
}

[Test, FsCheckProperty]
public bool StringConcatenationLength(string a, string b)
{
if (a == null || b == null)
{
return true; // Skip null cases
}

return (a + b).Length == a.Length + b.Length;
}

[Test, FsCheckProperty(MaxTest = 50)]
public bool ListConcatenationPreservesElements(int[] first, int[] second)
{
var combined = first.Concat(second).ToArray();
return combined.Length == first.Length + second.Length;
}

[Test, FsCheckProperty]
public void AdditionIsCommutative(int a, int b)
{
var result1 = a + b;
var result2 = b + a;

if (result1 != result2)
{
throw new InvalidOperationException($"Addition is not commutative: {a} + {b} = {result1}, {b} + {a} = {result2}");
}
}

[Test, FsCheckProperty]
public async Task AsyncPropertyTest(int value)
{
await Task.Delay(1); // Simulate async work

if (value * 0 != 0)
{
throw new InvalidOperationException("Multiplication by zero should always be zero");
}
}

[Test, FsCheckProperty]
public bool MultiplicationIsAssociative(int a, int b, int c)
{
// Using long to avoid overflow
var left = (long)a * ((long)b * c);
var right = ((long)a * b) * c;
return left == right;
}

[Test, FsCheckProperty]
public bool SumOfFourNumbersIsCommutative(int a, int b, int c, int d)
{
var sum1 = a + b + c + d;
var sum2 = d + c + b + a;
return sum1 == sum2;
}

[Test, FsCheckProperty]
public Property StringReversalProperty()
{
return Prop.ForAll<string>(str =>
{
var reversed = new string(str.Reverse().ToArray());
var doubleReversed = new string(reversed.Reverse().ToArray());
return str == doubleReversed;
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\TestProject.props" />

<ItemGroup>
<ProjectReference Include="..\TUnit.FsCheck\TUnit.FsCheck.csproj" />
</ItemGroup>

<Import Project="..\TestProject.targets" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TUnit.Example.FsCheck.TestProject", "TUnit.Example.FsCheck.TestProject.csproj", "{41C48729-CBC0-9C84-9E2E-AD18967D3F54}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{41C48729-CBC0-9C84-9E2E-AD18967D3F54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41C48729-CBC0-9C84-9E2E-AD18967D3F54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41C48729-CBC0-9C84-9E2E-AD18967D3F54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41C48729-CBC0-9C84-9E2E-AD18967D3F54}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {29D619C4-A78D-44B6-9D5C-304546DF9B20}
EndGlobalSection
EndGlobal
100 changes: 100 additions & 0 deletions TUnit.FsCheck/FsCheckPropertyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using TUnit.Core;
using TUnit.Core.Interfaces;

namespace TUnit.FsCheck;

/// <summary>
/// Marks a test method as an FsCheck property-based test.
/// The test method parameters will be generated by FsCheck and the test
/// will be run multiple times with different generated values.
/// </summary>
/// <remarks>
/// This attribute must be used together with the <see cref="TestAttribute"/>.
/// Example:
/// <code>
/// [Test, FsCheckProperty]
/// public bool MyProperty(int value) => value * 0 == 0;
/// </code>
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public class FsCheckPropertyAttribute : Attribute, ITestRegisteredEventReceiver, IDataSourceAttribute
{

/// <summary>
/// The maximum number of tests to run. Default is 100.
/// </summary>
public int MaxTest { get; set; } = 100;

/// <summary>
/// The maximum number of rejected tests (tests that failed the precondition) before failing.
/// </summary>
public int MaxFail { get; set; } = 1000;

/// <summary>
/// The starting size for test generation. Size increases linearly between StartSize and EndSize.
/// </summary>
public int StartSize { get; set; } = 1;

/// <summary>
/// The ending size for test generation. Size increases linearly between StartSize and EndSize.
/// </summary>
public int EndSize { get; set; } = 100;

/// <summary>
/// If set, replay the test using this seed. Format: "seed1,seed2" or just "seed1".
/// Useful for reproducing failures.
/// </summary>
public string? Replay { get; set; }

/// <summary>
/// If true, output all generated arguments to the test output.
/// </summary>
public bool Verbose { get; set; }

/// <summary>
/// If true, suppress output on passing tests.
/// </summary>
public bool QuietOnSuccess { get; set; }

/// <summary>
/// The level of parallelism to use when running tests.
/// Default is 1 (no parallelism within property execution).
/// </summary>
public int Parallelism { get; set; } = 1;

/// <summary>
/// Types containing Arbitrary instances to use for generating test data.
/// </summary>
public Type[]? Arbitrary { get; set; }

/// <summary>
/// Gets the order in which this event receiver is executed.
/// </summary>
public int Order => 0;

/// <summary>
/// Not used - FsCheck generates its own data during test execution.
/// This property exists to satisfy the IDataSourceAttribute interface.
/// </summary>
public bool SkipIfEmpty { get; set; }

/// <summary>
/// Called when the test is registered. Sets up the FsCheck property executor.
/// </summary>
public ValueTask OnTestRegistered(TestRegisteredContext context)
{
context.SetTestExecutor(new FsCheckPropertyTestExecutor(this));
return default;
}

/// <summary>
/// Returns placeholder data - actual test data is generated by FsCheck during test execution.
/// </summary>
#pragma warning disable CS1998 // Async method lacks 'await' operators
async IAsyncEnumerable<Func<Task<object?[]?>>> IDataSourceAttribute.GetDataRowsAsync(DataGeneratorMetadata dataGeneratorMetadata)
#pragma warning restore CS1998
{
// Return null array as placeholder - the FsCheckPropertyTestExecutor will generate actual data
yield return () => Task.FromResult<object?[]?>(null);
}
}
Loading
Loading