TUnit icon indicating copy to clipboard operation
TUnit copied to clipboard

Dynamic Assertion Generation

Open AnnaSasDev opened this issue 8 months ago • 11 comments

Very early draft on dynamic assertion generation like aluded to in #2315 .

Will update the below text when ready for actual review:

Please check the following before creating a Pull Request

  • If this is a new feature or piece of functionality, have you started a discussion and gotten agreement on it?
  • If it fixes a bug or problem, is there an issue to track it? If not, create one first and link it please so there's clear visibility.
  • Did you write tests to ensure you code works properly?

AnnaSasDev avatar May 02 '25 16:05 AnnaSasDev

Currently the styling and conventions are not yet compatible with TUnit yet, this will be changed soon, but I had to close out early today due to irl things.

I've mainly opened up this as a very early PR draft for the following reasons:

  • Is the project name and location of TUnit.Assertions.SourceGenerators correct
  • Are new external dependencies like IsExternalInit allowed
  • Am I free to follow my own design pattern when it comes to the generators, or is there a set pattern I should follow

AnnaSasDev avatar May 02 '25 16:05 AnnaSasDev

  • Is the project name and location of TUnit.Assertions.SourceGenerators correct

Works for me!

  • Are new external dependencies like IsExternalInit allowed

I've been using Polyfill library. Bringing that in should suffice.

  • Am I free to follow my own design pattern when it comes to the generators, or is there a set pattern I should follow

Go for it. I'll just add comments if I'm not sure about something :smile:

thomhurst avatar May 03 '25 00:05 thomhurst

I've been hit with something of a roadblock trying to use Polyfill, when trying to use PolyFill as a package in the generator I get the build errors that it adding polyfill source generated files twice, and when I remove it again I get the following error when trying to run my own generator

CSC: Error CS8785 : Generator 'GenerateAssertionsGenerator' failed to generate source. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'ArgumentException' with message 'The hintName 'Polyfills.Polyfill.g.cs' of the added source file must be unique within a generator. (Parameter 'hintName')'

I feel I'm missing something in either the setup of my own package with Polyfill or with how my generator is setup all together? (yes I know polyfill is currently not added to the current iteration of the TUnit.Assertions.SourceGenerators but I get errors either way, and searching for answers isnt helping...)

AnnaSasDev avatar May 03 '25 09:05 AnnaSasDev

I've not experienced that. Is it set to private assets etc?

Like in here: https://github.com/thomhurst/TUnit/blob/main/TUnit.Core.SourceGenerator/TUnit.Core.SourceGenerator.csproj

thomhurst avatar May 03 '25 10:05 thomhurst

The errors have now gone away after I fully reinstalled the repo... C# generators sometimes be a strange beast. There must have gone something wrong during an installation of a package. Sorry for the unnecessary ping

AnnaSasDev avatar May 03 '25 10:05 AnnaSasDev

Still needs a lot of lifting to work with something like a span<T>, or basically anything that uses a generic type argument(s), and testing, and proper diagnostic names, better structure, etc.. but it is a start

But I already have the following implemented

  • Special types like char, string etc should use the char.IsDigit(value) format instead of the normal value.IsDigit()
  • Is and IsNot variants
  • objects like Uri, which have Is... propeties are used as properties and not tried as methods

example

[GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsAbsoluteUri))]
// [GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsBaseOf))] // TODO requires extra options
[GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsDefaultPort))]
[GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsFile))]
[GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsLoopback))]
[GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsUnc))]
[GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsWellFormedOriginalString))]
// [GenerateAssertion<Uri>(AssertionType.Is, nameof(Uri.IsWellFormedUriString))] //  TODO requires extra options
public static partial class UriIsExtensions {
    
}

generates =>

// <auto-generated />
using TUnit.Assertions.Extensions;
using TUnit.Assertions.AssertConditions;
using TUnit.Assertions.AssertConditions.Interfaces;
using TUnit.Assertions.AssertionBuilders;
using TUnit.Assertions.AssertionBuilders.Wrappers;
namespace TUnit.Assertions.Assertions.Uris;

public static partial class UriIsExtensions
{
    public static InvokableValueAssertionBuilder<Uri> IsAbsoluteUri(this IValueSource<Uri> valueSource)
    {
        return valueSource.RegisterAssertion(new FuncValueAssertCondition<Uri, int>(0,
            (value, _, self) =>
            {
                if (value is null)
                {
                    self.FailWithMessage("Actual Uri is null");
                    return false;
                }
                return value.IsAbsoluteUri;
            },
            (s, _, _) => "'{s}' was not an AbsoluteUri",
            "to be a AbsoluteUri"),
            []
        ); 
    }

    public static InvokableValueAssertionBuilder<Uri> IsDefaultPort(this IValueSource<Uri> valueSource)
    {
        return valueSource.RegisterAssertion(new FuncValueAssertCondition<Uri, int>(0,
            (value, _, self) =>
            {
                if (value is null)
                {
                    self.FailWithMessage("Actual Uri is null");
                    return false;
                }
                return value.IsDefaultPort;
            },
            (s, _, _) => "'{s}' was not a DefaultPort",
            "to be a DefaultPort"),
            []
        ); 
    }

    public static InvokableValueAssertionBuilder<Uri> IsFile(this IValueSource<Uri> valueSource)
    {
        return valueSource.RegisterAssertion(new FuncValueAssertCondition<Uri, int>(0,
            (value, _, self) =>
            {
                if (value is null)
                {
                    self.FailWithMessage("Actual Uri is null");
                    return false;
                }
                return value.IsFile;
            },
            (s, _, _) => "'{s}' was not a File",
            "to be a File"),
            []
        ); 
    }

    public static InvokableValueAssertionBuilder<Uri> IsLoopback(this IValueSource<Uri> valueSource)
    {
        return valueSource.RegisterAssertion(new FuncValueAssertCondition<Uri, int>(0,
            (value, _, self) =>
            {
                if (value is null)
                {
                    self.FailWithMessage("Actual Uri is null");
                    return false;
                }
                return value.IsLoopback;
            },
            (s, _, _) => "'{s}' was not a Loopback",
            "to be a Loopback"),
            []
        ); 
    }

    public static InvokableValueAssertionBuilder<Uri> IsUnc(this IValueSource<Uri> valueSource)
    {
        return valueSource.RegisterAssertion(new FuncValueAssertCondition<Uri, int>(0,
            (value, _, self) =>
            {
                if (value is null)
                {
                    self.FailWithMessage("Actual Uri is null");
                    return false;
                }
                return value.IsUnc;
            },
            (s, _, _) => "'{s}' was not an Unc",
            "to be a Unc"),
            []
        ); 
    }

    public static InvokableValueAssertionBuilder<Uri> IsWellFormedOriginalString(this IValueSource<Uri> valueSource)
    {
        return valueSource.RegisterAssertion(new FuncValueAssertCondition<Uri, int>(0,
            (value, _, self) =>
            {
                if (value is null)
                {
                    self.FailWithMessage("Actual Uri is null");
                    return false;
                }
                return value.IsWellFormedOriginalString();
            },
            (s, _, _) => "'{s}' was not a WellFormedOriginalString",
            "to be a WellFormedOriginalString"),
            []
        ); 
    }

}

AnnaSasDev avatar May 04 '25 12:05 AnnaSasDev

Nice work! Looking good 😄

thomhurst avatar May 04 '25 12:05 thomhurst

Sory for the inactivity, have been ill for a week and then there was my own wedding. Back to programming now, and I wanted to ask what the norm is for updating this out of date branch, "update with merge commit" or "update with rebase"?

AnnaSasDev avatar May 14 '25 16:05 AnnaSasDev

Hope you're feeling better and congratulations!! :)

I normally just do a merge, I find rebases annoying when conflicts start

thomhurst avatar May 14 '25 17:05 thomhurst

@AnnaSasDev do you want a hand with this at all?

thomhurst avatar May 20 '25 18:05 thomhurst

@AnnaSasDev do you want a hand with this at all?

Sorry for the inactivity. Had been trying to make methods with more parameters work as well, like the Uri.IsBaseOf(Uri other) but this proved a bit too out of scope for this pr maybe?

Currently trying to do the next things (incomplete list)

  • [ ] reformat the code so it adheres to the existing codebase style
  • [ ] proper diagnostic reporting on the AssertionHolder (partial etc...)
  • [ ] correct naming of the diagnostics already present, and checking if more are required
  • [ ] testing of the newly created methods, to ensure the logical output on positive and negative branches as those which they are based upon.

I dont mind the help, especially when it comes to diagnostics. In my own projects I have limited experience with them as I dont have such a userbase as TUnit, by very very far.

Something else I noticed is that the editorconfig file is rather unpopulated? Maybe having a built out editorconfig file might help with the coding style?

AnnaSasDev avatar May 20 '25 21:05 AnnaSasDev

This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days.

github-actions[bot] avatar Jun 20 '25 00:06 github-actions[bot]

This PR was closed because it has been stalled for 10 days with no activity.

github-actions[bot] avatar Jun 30 '25 00:06 github-actions[bot]