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

Implement Get-Random -Count without specifying an InputObject list #9111

Merged
merged 2 commits into from
Mar 26, 2019

Conversation

pougetat
Copy link

@pougetat pougetat commented Mar 11, 2019

PR Summary

The purpose of this PR is to implement the following feature for the Get-Random commandlet :

Get-Random -Maximum 10 -Count 3
5
1
7

PR Context

This PR is tied to the following issue :
fix #8986

PR Checklist

Copy link
Collaborator

@vexx32 vexx32 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, been wanting to get this done, so thanks! 😄

int randomNumber = this.Generator.Next(min, max);
Debug.Assert(min <= randomNumber, "lower bound <= random number");
Debug.Assert(randomNumber < max, "random number < upper bound");
for (int i = 0; i < Math.Max(1, this.Count); i++)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than need a call to Math.Max() you could simply initialize the value of this parameter to 1 by default.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -281,7 +281,7 @@ private double ConvertToDouble(object o, double defaultIfNull)
/// <summary>
/// Number of items to output (number of list items or of numbers).
/// </summary>
[Parameter(ParameterSetName = GetRandomCommand.RandomListItemParameterSet)]
[Parameter]
[ValidateRange(1, int.MaxValue)]
public int Count { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public int Count { get; set; }
public int Count { get; set; } = 1;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

for (int i = 0; i < Math.Max(1, this.Count); i++)
{
int randomNumber = this.Generator.Next(min, max);
Debug.Assert(min <= randomNumber, "lower bound <= random number");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we generally use Debug.Assert() in cmdlets. This seems like something that should only fail if the Random class has a fatal regression for its Next() code path(s), which is outside the purview of our debug code in general, I would think.

I think if we want to test this at all, we should implement this as a Pester test so that we can see it in CI rather than waiting for someone to stumble across it. 🙂

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm not too sure I agree that Debug.Assert isn't used since I have found it in quite a few cmdlets throughout the codebase.
I do agree that ideally this type of thing should surface as a result of a failing CI test but I don't know how important the consequence of removing it would be (scripts not crashing and using potentially invalid random values). 🙂

@JamesWTruher ? @PaulHigin ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember rightly @PaulHigin, aren't Debug.Assert() calls ignored / no-op in release builds anyway? 🤔 Or am I misremembering? 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this is the wrong place for debug asserts and the correct place is in the random number generator adapter. Debug asserts are only evaluated in debug builds and are intended to verify expected (and assumed) behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done :)

Debug.Assert(randomNumber < max, "random number < upper bound");
for (int i = 0; i < Math.Max(1, this.Count); i++)
{
int randomNumber = this.Generator.Next(min, max);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this. is usually needed here. Is there a naming conflict that requires this? If not, we don't generally use it where it's not explicitly required. 🙂

There are a couple of these, just tagging this one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I've removed all this. invocations from this commandlet. :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cheers! 😄

Copy link
Contributor

@PaulHigin PaulHigin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

for (int i = 0; i < Math.Max(1, this.Count); i++)
{
int randomNumber = this.Generator.Next(min, max);
Debug.Assert(min <= randomNumber, "lower bound <= random number");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this is the wrong place for debug asserts and the correct place is in the random number generator adapter. Debug asserts are only evaluated in debug builds and are intended to verify expected (and assumed) behavior.

@iSazonov
Copy link
Collaborator

iSazonov commented Mar 14, 2019

@pougetat Please move all style changes in new PR - we should not mix style and function changes.

Also please open new issue in PowerShell-Docs repo and add reference to the PR description (current link points to PowerShell repo issue that is wrong).

@iSazonov iSazonov self-assigned this Mar 14, 2019
@iSazonov iSazonov added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Mar 14, 2019
@pougetat pougetat mentioned this pull request Mar 14, 2019
11 tasks
@pougetat
Copy link
Author

pougetat commented Mar 14, 2019

@iSazonov
I have opened #9133 to fix all the coding style issues. As soon as that one is merged I will checkout this one and rebase master.

@pougetat
Copy link
Author

Hey @iSazonov ,
I rebased master containing the PR you just merged with the codestyle changes. This PR now only contains a single clean commit with the Feature implementation. Also, I haven't touched the asserts. :)


$result = Get-Random -Maximum $maximum -Minimum $minimum -Count 1
$result | Should -BeGreaterThan $greaterThan
$result | Should -BeLessThan $lessThan
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking asserts above it seems it should be BeLessOrEqual in all tests. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you. I've made the modifications.

@iSazonov
Copy link
Collaborator

@PaulHigin @adityapatwardhan Please confirm latest changes in tests.

@{ Name = 'max is double with plus sign and enclosed in quote'; Maximum = '+100.0'; Minimum = 0; GreaterThan = -1.0; LessThan = 100.0; Type = 'System.Double' }
@{ Name = 'both set to the special numbers as 1.0e+xx '; Maximum = $null; Minimum = 1.0e+100; GreaterThan = 1.0e+99; LessThan = ([double]::MaxValue); Type = 'System.Double' }
@{ Name = 'max is Double.MaxValue, min is Double.MinValue'; Maximum = ([double]::MaxValue); Minimum = ([double]::MinValue); GreaterThan = ([double]::MinValue); LessThan = ([double]::MaxValue); Type = 'System.Double' }
@{ Name = 'no params'; Maximum = $null; Minimum = $null; GreaterThan = -1; LessOrEqual = ([int32]::MaxValue); Type = 'System.Int32' }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was LessThan changed to LessOrEqual?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug.assert statements in the code a exlusive for lower bounds and inclusive for higher bounds thus we have changed LessThan into LessOrEqual to make it consistent with the current implementation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, our tests do not match the code. One of the two must be corrected.

Copy link
Author

@pougetat pougetat Mar 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait sorry i mixed things up. The assert statements are exclusive for higher bounds and inclusive for lower bounds. In any case I think I should just revert the changes made to the tests for the following reason :

  • The debug.assert statements should be moved into the actual random number generators but for that it also means looking at what numbers can be generated (inclusive bounds VS exclusive bounds) since some of the bounding conditions will be incorrect as they stand today.
    I think that discussion can be had in another PR and this one can simply remain very pointed in scope :)
    I have no issue opening the extra PR myself btw.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pougetat Let's wait @adityapatwardhan conclusion.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pougetat Sorry I reverted the commit when I wrote my previous comment. Having said that I still have it on a local branch so I can put it back easily depending on his conclusion. I should have simply reverted with an actual commit instead of dropping the commit altogether. In any case yes I agree we can wait for his conclusion :).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with having a separate PR for that. I just signed off.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pougetat for future reference, I do believe the random generators are inclusive on the lower bound, but exclusive on the higher bound. Don't recall the exact reason for it, but it's a pretty common pattern.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iSazonov
Copy link
Collaborator

@mklement0 Does this meet your expectations?

@@ -163,6 +178,7 @@ Describe "Get-Random" -Tags "CI" {
$secondRandomNumber = Get-Random 34359738367 -SetSeed 20
$firstRandomNumber | Should -Be @secondRandomNumber
}

It "Should throw an error because the hexadecimal number is to large " {
{ Get-Random 0x07FFFFFFFFFFFFFFFF } | Should -Throw "Value was either too large or too small for a UInt32"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't dug deeper, but the full error message is self-contradictory (Int32 vs. UInt32): Cannot convert value "0x07FFFFFFFFFFFFFFFF" to type "System.Int32". Error: "Value was either too large or too small for a UInt32." Where does the UInt come from?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I actually fixed this back when I was toying with the tokenizer, and then it was requested I re-break this for consistency with prior implementations. This can be fixed, but the fix would be in the tokenizer and how hex numbers get parsed.

This number is an extra digit beyond the widest permitted for uint64, and a hex number with a leading zero is forced to be an unsigned numeral when using the conversion paths we have in the tokenizer (which are still done with Convert.ToInt32() I think?). I think the default path goes to (u)int32 because it checks for (u)int64 lengths and then defaults to (u)int32 for all other cases iirc.

I remember I originally did some fun things in my tokenizer PRs where I would automatically upcast between uint/int64 if needed, but it differed too much from the previous implementation I think and we decided to keep the behaviour consistent. I think my solution was to have it upcast up to decimal (or even biginteger) if needed, which worked well.

I think due to the refactoring in #7993 this error message might actually change, but it's been a little while and I don't quite recall.

Copy link
Contributor

@mklement0 mklement0 Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see: if the pre-parameter-binding typing can't convert a "number-looking" argument to an actual number, it passes a string, and line baseObject = System.Management.Automation.Language.Parser.ScanNumber((string)baseObject, typeof(int)); then re-attempts conversion, which fails.

Again, not in the scope of this PR, but I wonder if the error message could be made friendlier (and it's still curious that UInt32 is referenced, given that hex. literals seem to top out at Int64 [see previous comment]).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, @vexx32 - wires crossed - reading your comment now.

Copy link
Contributor

@mklement0 mklement0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following is unrelated to this specific PR, but it's worth pointing out:

Tests such as the following are flawed, because they always pass:

$randomNumber[0] | Should -Be (1 -or 2 -or 3 -or 5 -or 8 -or 13)

The above should be:

$randomNumber[0] | Should -BeIn 1, 2, 3, 5, 8, 13

@mklement0
Copy link
Contributor

Thanks for asking, @iSazonov: looks great. Thanks for taking this on, @pougetat.

@TravisEz13 TravisEz13 added this to the 6.3.0-preview.1 milestone Mar 22, 2019
@iSazonov iSazonov merged commit e553aef into PowerShell:master Mar 26, 2019
@iSazonov
Copy link
Collaborator

@pougetat Thanks for your contribution!

@pougetat
Copy link
Author

@iSazonov Thanks for the review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow combining Get-Random -Count with (implied) -Minimum / -Maximum to create multiple random values
8 participants