fscheck / FsCheck

Random Testing for .NET
https://fscheck.github.io/FsCheck/
BSD 3-Clause "New" or "Revised" License
1.15k stars 154 forks source link

FsCheck 3.0.0-rc1 in C# is ignoring Xunit property size values #655

Closed BennieCopeland closed 4 months ago

BennieCopeland commented 5 months ago

I just migrated my custom generators to 3.0 and I noticed it's ignoring the size requirements.

    [Property(StartSize = 50, EndSize = 100)]
    public bool Generator_Obeys_Max_Size_String(string str)
    {
        return str.Length >= 50 && str.Length <= 100;
    }

Edit: I noticed after looking at the code that this never worked to begin with. Only a few numeric types use sizes.

BennieCopeland commented 5 months ago

Ok, so the sizes actually work, it had to do with how I am writing my generators. I'm not sure why this works

    public override Gen<EmailAddress> Generator
    {
        get
        {
            return new SizedUnicodeStringGenerator()
                .Generator
                .Resize(EmailAddress.MaxSize)
                .Select(str => (string)str)
                .Where(str => !string.IsNullOrEmpty(str) && !str.Contains('\0'))
                .Select(EmailAddress.FromString)
                .Select(result => result.Value);
        }
    }

But this doesn't

    public override Gen<EmailAddress> Generator
    {
        get
        {
            return ArbMap.Default.GeneratorFor<SizedUnicodeString>()
                .Resize(EmailAddress.MaxSize)
                .Select(str => (string)str)
                .Where(str => !string.IsNullOrEmpty(str) && !str.Contains('\0'))
                .Select(EmailAddress.FromString)
                .Select(result => result.Value);
        }
    }
kurtschelfthout commented 4 months ago

The size is interpreted by generators in any way they choose. Most don't interpret it as a lower bound, but as an upper bound - the idea of size is not so much to put a hard constraint on the generated values, but to control how the size evolves through the test run. Tpically as the size gets bigger, the number of possibilities increases exponentially, and so you can only cover a tiny amount of it. So we start with generating "small" values, giving you a good chance of covering those as well as finding any "easy" bugs early in the test run. Then FsCheck increases the size to get some coverage of the bigger cases. It increases the size linearly between StartSize and EndSize over the course of however number of test cases you specify (100 by default)

In particular, the string generator in the end depends on Gen.arrayOf which does use size: https://github.com/fscheck/FsCheck/blob/master/src/FsCheck/FSharp.Gen.fs#L482 as you can see as an upper bound. That is, for size 50 it'll randomly choose a length between 0 and 50. That is, I assume, why your first code snippet fails.

I don't know exactly what difference you're seeing between the two last snippets, but perhaps Resize is causing some confusion here. Resize just overrides the size that is passed down from the runner to each generator, but that still doesn't force the generators to interpret the size any differently. So e.g. you may be at the last test case and FsCheck is setting a size of 100. All you're doing is overriding that to EmailAddress.MaxSize. That will just make Gen.arrayOf choose a length between 0 and EmailAddress.MaxSize.

In conclusion, if you want a guaranteed hard upper bound or fixed size array/list/string, don't rely on size. Build it into your generators directly.