Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoped value formatters #2478

Closed
FLAMESpl opened this issue Nov 24, 2023 · 23 comments · Fixed by #2676
Closed

Scoped value formatters #2478

FLAMESpl opened this issue Nov 24, 2023 · 23 comments · Fixed by #2676
Assignees
Labels
api-approved API was approved, it can be implemented enhancement

Comments

@FLAMESpl
Copy link

FLAMESpl commented Nov 24, 2023

Background and motivation

This feature is about allowing adding custom IValueFormatters to AssertionScope. In my development I discovered that I need to selectively customize formatting of an object based on the test's context. There is an option to remove once added formatter but it will not play nicely if tests would be executed simultanously. Having scoped formatters have a side bonus of reusable parametrizing formatter instances.

API Proposal

First make IFormatterCollection interface.

public interface IFormatterCollection : ICollection<IValueFormatter>
{
}

Then add it to AssertionScope:

public class AssertionScope
{
    public IFormatterCollection Formatters { get; } = // initialize from parent scope
}

Formatter API remains unchanged. Internally it will utilize IFormatterCollection for sake of coherence.

API Usage

using (var scope = new AssertionScope())
{
   scope.Formatters .Add(new MyCustomFormatter())
   // some assertions
}

Alternative Designs

Alternative would be to add a custom key-value store in AssertionScope that could be read inside a IValueFormatter.

Risks

None I can think of.

Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?

Yes, please assign this issue to me.

@FLAMESpl FLAMESpl added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Nov 24, 2023
@dennisdoomen
Copy link
Member

Maybe scope.UsingFormatter(new MyCustomFormatter()) is nicer?

@FLAMESpl
Copy link
Author

It is also an option, this way collection of formatters could be internal.

@dennisdoomen
Copy link
Member

I'm fine with that. What about you @jnyrup ?

@jnyrup
Copy link
Member

jnyrup commented Feb 24, 2024

I like the proposal of having scoped formatters 👍

As the signature is Formatter.ToString(object value, FormattingOptions options = null) placing the scoped formatters on FormattingOptions might be a good place?

When retrieving all formatters in Formatter.Format it will then search in scoped custom formatters, static custom formatters and lastly static default formatters.

Perhaps we should also move the current Formatter.CustomFormatters to FormattingOptions?
This should be doable without introducing any breaking changes, by forwarding the existing AddFormatter and RemoveFormatter.
The statically set custom formatters will then be set on the static AssertionOptions.FormattingOptions and the scoped ones will be set on AssertionScope.FormattingOptions.

To set a scoped formatter you'll only have to do

using var scope = new AssertionScope();
scope.FormattingOptions.AddFormatter(new MyCustomFormatter());

@dennisdoomen
Copy link
Member

That sounds like a great idea. Let's go for that.

@dennisdoomen dennisdoomen added api-approved API was approved, it can be implemented and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Feb 24, 2024
@dennisdoomen dennisdoomen changed the title [API Proposal]: Scoped value formatters Scoped value formatters Feb 24, 2024
@dennisdoomen
Copy link
Member

@FLAMESpl are you still interested in this one?

@ChristoWolf
Copy link

@dennisdoomen: I would certainly be interested 😃

@dennisdoomen
Copy link
Member

I meant in implementing this.

@ChristoWolf
Copy link

Ah whoops, I misunderstood.

@IT-VBFK
Copy link
Contributor

IT-VBFK commented Mar 21, 2024

I am willing to take that, if @FLAMESpl doesn't want to.

@ITaluone
Copy link
Contributor

Should a nested AssertionScope pick up all previous defined scoped formatters?

@dennisdoomen
Copy link
Member

dennisdoomen commented Mar 22, 2024

Everything that is associated with an AssertionScope flows in the nested scopes.

@ITaluone
Copy link
Contributor

Ok, the tricky part is how to determine which scoped formatters to remove at scope dispose 🤔

@dennisdoomen
Copy link
Member

Yeah, it means that the scope needs some state on which scope has added a particular formatter.

@ITaluone
Copy link
Contributor

Since @FLAMESpl does not respond to this issue, I started the implementation.
I have one remaining question:
Should inner scope formatting options be allowed to modify outer scope scoped formatters?

For example: Should this be possible?

[Fact]
public void Test2()
{
    using var outerScope = new AssertionScope();

    var outerFormatter = new OuterFormatter();
    var innerFormatter = new InnerFormatter();
    outerScope.FormattingOptions.AddFormatter(outerFormatter);

    using var innerScope = new AssertionScope();
    innerScope.FormattingOptions.AddFormatter(innerFormatter);

    innerScope.FormattingOptions.RemoveFormatter(outerFormatter);
}

My main concern is, that this would become pretty "slow" when traversing back a lot of parent AssertionScopes, like:

[Fact]
public void Test2()
{
    using var outerScope = new AssertionScope();

    var outerFormatter = new OuterFormatter();
    var innerFormatter = new InnerFormatter();
    outerScope.FormattingOptions.AddFormatter(outerFormatter);

    using var innerScope = new AssertionScope();
    innerScope.FormattingOptions.AddFormatter(innerFormatter);

    using var innerScope1 = new AssertionScope();
    innerScope1.FormattingOptions.AddFormatter(innerFormatter);

    // ...
    using var innerScope100 = new AssertionScope();
    innerScope100.FormattingOptions.AddFormatter(innerFormatter);

    innerScope100.FormattingOptions.RemoveFormatter(outerFormatter);

}

That said: my current implementation does not allow such a thing.

@dennisdoomen
Copy link
Member

Should inner scope formatting options be allowed to modify outer scope scoped formatters?

No, but inner scope options should be able to override the formatters used by the outer scope. So if the inner options remove a formatter that was added by the outer scope, that should only affect the inner scope.

@ITaluone
Copy link
Contributor

ITaluone commented Jun 19, 2024

I struggle to understand, why the scoped formatters are not tied to the AssertionScope?

The fact that the formatters are mainly static and we forward Formatter.AddFormatter() to FormattingOptions.AddFormatter I cannot distinguish whether this should go to the static custom formatters or to the scoped formatters.

Second, with the current implementation I am able to do the following: AssertionOption.Formatters.AddFormatter() which will then add a scoped formatter and not a custom formatter..

So: long story short I wonder what you are thinking about this:

using var outerScope = new AssertionScope();

var outerFormatter = new OuterFormatter();
outerScope.AddFormatter(outerFormatter);

which adds a scoped formatter, and

// as suggested forwarding the first to the second
Formatter.AddFormatter(); 
AssertionOptions.FormattingOptions.AddFormatter(); 

to add a custom formatter.

Edit: After a few times back-and-forth I finally managed this.. Ever day a new challenge 🎉🎉

@dennisdoomen
Copy link
Member

This usually happens only if I run all tests. (Please note, that this couldn't be a issue on my side, because this obviously happens on GHA too: see here: https://github.com/fluentassertions/fluentassertions/actions/runs/9565943493/job/26370017856

I've never seen this before, but it does seem to be related to formatters.

@dennisdoomen
Copy link
Member

I struggle to understand, why the scoped formatters are not tied to the AssertionScope?

Well, that's what you are trying to change, right?

The fact that the formatters are mainly static and we forward Formatter.AddFormatter() to FormattingOptions.AddFormatter I cannot distinguish whether this should go to the static custom formatters or to the scoped formatters.

I don't understand the question. When you use Formatter.AddFormatter, with the proposed changes, you're adding a global formatter to the AssertionOptions.FormattingOptions

Second, with the current implementation I am able to do the following: AssertionOption.Formatters.AddFormatter() which will then add a scoped formatter and not a custom formatter..

This should add a global formatter that is available to all assertion scopes.

So: long story short I wonder what you are thinking about this:

using var outerScope = new AssertionScope();

var outerFormatter = new OuterFormatter();
outerScope.AddFormatter(outerFormatter);

which adds a scoped formatter, and

// as suggested forwarding the first to the second
Formatter.AddFormatter(); 
AssertionOptions.FormattingOptions.AddFormatter(); 

to add a custom formatter.

I thought we said to use outerScope.FormattingOptions.AddFormatter(formatter)?

@ITaluone
Copy link
Contributor

I have to admit, I had blinders on.. After I rethought the API it suddenly made sense.. Sorry for the spam..

I struggle to understand, why the scoped formatters are not tied to the AssertionScope?

Well, that's what you are trying to change, right?

What I meant was: why are the scoped formatters tied to FormattingOptions and not to AssertionScope directly. But like I said.. I rethought this and it made sense

@dennisdoomen
Copy link
Member

Don't worry about that. I had to study the entire thread and look at the code to build a good understanding as well.

@ChristoWolf
Copy link

When will this be released?

@jnyrup
Copy link
Member

jnyrup commented Jul 12, 2024

When will this be released?

With v7 - we don't have an ETA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved, it can be implemented enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants