Skip to content

Commit 1f0c95b

Browse files
Merge pull request #2477 from KathleenDollard/symbolvalue-usage-validation-valueprovider
ValidationSubsystem
2 parents adcdf43 + 78e9a09 commit 1f0c95b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1687
-81
lines changed

OpenQuestions.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Open questions
2+
3+
Please also include TODO in appropriate locations. This is intended as a wrap up.
4+
5+
Also, include whether the question is to add or remove something, and add date/initials
6+
7+
## NotNulWhen on TryGetValue in ValueSource and ValueProvider
8+
9+
Things to consider:
10+
11+
* Within System.CommandLine all TryGetValue should probably be the same
12+
* TryGetValue on dictionary can return null
13+
* For nullable values, the actual value can be null
14+
* For nullable or non-nullable ref types, the default for the type is null
15+
* Allowing null out values keeps a single meaning to "not found" and allows "found but null". Conflating these blocks expressing which happened
16+
17+
The recovery is the same as with Dictionary.TryGetValue. The first line of the block that handles the return Boolean is a guard.
18+
19+
## The extensibility story for ValueSource
20+
21+
The proposal and current code seal our value sources and expect people to make additional ones based on ValueSource. The classes are public and sealed, the constructors are internal.
22+
23+
Reasons to reconsider: Aggregate value source has a logical precedence or an as entered one. If someone adds a new value source, it is always last in the logic precedence.There are likely to be other similar cases.
24+
25+
Possible resolution: Have this be case by case and allow aggregate values to be unsealed and have a mechanism for overriding. Providing a non-inheritance based solution could make this look like a normal operation when it is a rare one.
26+
27+
## Contexts [RESOLVED]
28+
29+
We had two different philosophies at different spots in subsystems. "Give folks everything they might need" and "Give folks only what we know they need".
30+
31+
The first probably means we pass around `PipelineResult`. The second means that each purpose needs a special context. Sharing contexts is likely to mean that something will be added to one context that is unneeded by the other. Known expected contexts are:
32+
33+
- `AnnotationProviderContext`
34+
- `ValueSourceContext`
35+
- `ValidationContext` (includes ability to report diagnostics)
36+
- `CompletionContext`
37+
- `HelpContext`
38+
39+
## Which contexts should allow diagnostic reporting?
40+
41+
## Should we have both Validators and IValidator on Conditions? [RESOLVED]
42+
43+
We started with `Validators` and then added the IValidator interface to allow conditions to do validation because they have the strong type. Checking for this first also avoids a dictionary lookup.
44+
45+
Our default validations will be on the Condition for the shortcut. Users can offer alternatives by creaing custom validators. The dictionary for custom validators will be lazy, and lookups will be pay for play when the user has custom validators. (This is not yet implemented.)
46+
47+
When present, custom validators have precedence. There is no cost when they are not present.
48+
49+
## Should conditions be public
50+
51+
Since there are factory methods and validators could still access them, current behavior could be supported with internal conditions.
52+
53+
However, the point of conditions is that they are a statement about the symbol and not an implementation. They are known to be used by completions and completions are expected to be extended. Thus, to get the values held in the condition (such as environment variable name) need to be available outside the external scope.
54+
55+
Suggestion: Use internal constructors and leave conditions public
56+
57+
## Should `ValueCondition` be called `Condition`?
58+
59+
They may apply to commands.

System.CommandLine.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1616
Directory.Packages.props = Directory.Packages.props
1717
global.json = global.json
1818
LICENSE.md = LICENSE.md
19+
OpenQuestions.md = OpenQuestions.md
1920
README.md = README.md
2021
restore.cmd = restore.cmd
2122
restore.sh = restore.sh

src/System.CommandLine.Subsystems.Tests/AlternateSubsystems.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal class AlternateSubsystems
1010
{
1111
internal class AlternateVersion : VersionSubsystem
1212
{
13-
protected override void Execute(PipelineResult pipelineResult)
13+
public override void Execute(PipelineResult pipelineResult)
1414
{
1515
pipelineResult.ConsoleHack.WriteLine($"***{CliExecutable.ExecutableVersion}***");
1616
pipelineResult.SetSuccess();
@@ -28,7 +28,7 @@ public VersionThatUsesHelpData(CliSymbol symbol)
2828

2929
private CliSymbol Symbol { get; }
3030

31-
protected override void Execute(PipelineResult pipelineResult)
31+
public override void Execute(PipelineResult pipelineResult)
3232
{
3333
TryGetAnnotation(Symbol, HelpAnnotations.Description, out string? description);
3434
pipelineResult.ConsoleHack.WriteLine(description);
@@ -43,20 +43,20 @@ internal class VersionWithInitializeAndTeardown : VersionSubsystem
4343
internal bool ExecutionWasRun;
4444
internal bool TeardownWasRun;
4545

46-
protected override void Initialize(InitializationContext context)
46+
protected internal override void Initialize(InitializationContext context)
4747
{
4848
base.Initialize(context);
4949
// marker hack needed because ConsoleHack not available in initialization
5050
InitializationWasRun = true;
5151
}
5252

53-
protected override void Execute(PipelineResult pipelineResult)
53+
public override void Execute(PipelineResult pipelineResult)
5454
{
5555
ExecutionWasRun = true;
5656
base.Execute(pipelineResult);
5757
}
5858

59-
protected override void TearDown(PipelineResult pipelineResult)
59+
protected internal override void TearDown(PipelineResult pipelineResult)
6060
{
6161
TeardownWasRun = true;
6262
base.TearDown(pipelineResult);

src/System.CommandLine.Subsystems.Tests/System.CommandLine.Subsystems.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
-->
3333
<Compile Include="AlternateSubsystems.cs" />
3434
<Compile Include="Constants.cs" />
35+
<Compile Include="ValueSourceTests.cs" />
36+
<Compile Include="ValidationSubsystemTests.cs" />
3537
<Compile Include="ValueSubsystemTests.cs" />
3638
<Compile Include="ResponseSubsystemTests.cs" />
3739
<Compile Include="DirectiveSubsystemTests.cs" />
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using FluentAssertions;
5+
using System.CommandLine.Parsing;
6+
using System.CommandLine.ValueSources;
7+
using Xunit;
8+
9+
namespace System.CommandLine.Subsystems.Tests;
10+
11+
public class ValidationSubsystemTests
12+
{
13+
// Running exactly the same code is important here because missing a step will result in a false positive. Ask me how I know
14+
private CliOption GetOptionWithSimpleRange<T>(T lowerBound, T upperBound)
15+
where T : IComparable<T>
16+
{
17+
var option = new CliOption<int>("--intOpt");
18+
option.SetRange(lowerBound, upperBound);
19+
return option;
20+
}
21+
22+
private CliOption GetOptionWithRangeBounds<T>(ValueSource<T> lowerBound, ValueSource<T> upperBound)
23+
where T : IComparable<T>
24+
{
25+
var option = new CliOption<int>("--intOpt");
26+
option.SetRange(lowerBound, upperBound);
27+
return option;
28+
}
29+
30+
private PipelineResult ExecutedPipelineResultForRangeOption(CliOption option, string input)
31+
{
32+
var command = new CliRootCommand { option };
33+
return ExecutedPipelineResultForCommand(command, input);
34+
}
35+
36+
private PipelineResult ExecutedPipelineResultForCommand(CliCommand command, string input)
37+
{
38+
var validationSubsystem = ValidationSubsystem.Create();
39+
var parseResult = CliParser.Parse(command, input, new CliConfiguration(command));
40+
var pipelineResult = new PipelineResult(parseResult, input, Pipeline.CreateEmpty());
41+
validationSubsystem.Execute(pipelineResult);
42+
return pipelineResult;
43+
}
44+
45+
[Fact]
46+
public void Int_values_in_specified_range_do_not_have_errors()
47+
{
48+
var option = GetOptionWithSimpleRange(0, 50);
49+
50+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
51+
52+
pipelineResult.Should().NotBeNull();
53+
pipelineResult.GetErrors().Should().BeEmpty();
54+
}
55+
56+
[Fact]
57+
public void Int_values_above_upper_bound_report_error()
58+
{
59+
var option = GetOptionWithSimpleRange(0, 5);
60+
61+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
62+
63+
pipelineResult.Should().NotBeNull();
64+
pipelineResult.GetErrors().Should().HaveCount(1);
65+
var error = pipelineResult.GetErrors().First();
66+
// TODO: Create test mechanism for CliDiagnostics
67+
}
68+
69+
[Fact]
70+
public void Int_below_lower_bound_report_error()
71+
{
72+
var option = GetOptionWithSimpleRange(0, 5);
73+
74+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt -42");
75+
76+
pipelineResult.Should().NotBeNull();
77+
pipelineResult.GetErrors().Should().HaveCount(1);
78+
var error = pipelineResult.GetErrors().First();
79+
// TODO: Create test mechanism for CliDiagnostics
80+
}
81+
82+
[Fact]
83+
public void Int_values_on_lower_range_bound_do_not_report_error()
84+
{
85+
var option = GetOptionWithSimpleRange(42, 50);
86+
87+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
88+
89+
pipelineResult.Should().NotBeNull();
90+
pipelineResult.GetErrors().Should().BeEmpty();
91+
}
92+
93+
[Fact]
94+
public void Int_values_on_upper_range_bound_do_not_report_error()
95+
{
96+
var option = GetOptionWithSimpleRange(0, 42);
97+
98+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
99+
100+
pipelineResult.Should().NotBeNull();
101+
pipelineResult.GetErrors().Should().BeEmpty();
102+
}
103+
104+
[Fact]
105+
public void Values_below_calculated_lower_bound_report_error()
106+
{
107+
var option = GetOptionWithRangeBounds<int>(ValueSource.Create(() => (true, 1)), 50);
108+
109+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 0");
110+
111+
pipelineResult.Should().NotBeNull();
112+
pipelineResult.GetErrors().Should().HaveCount(1);
113+
var error = pipelineResult.GetErrors().First();
114+
// TODO: Create test mechanism for CliDiagnostics
115+
}
116+
117+
118+
[Fact]
119+
public void Values_within_calculated_range_do_not_report_error()
120+
{
121+
var option = GetOptionWithRangeBounds(ValueSource<int>.Create(() => (true, 1)), ValueSource<int>.Create(() => (true, 50)));
122+
123+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
124+
125+
pipelineResult.Should().NotBeNull();
126+
pipelineResult.GetErrors().Should().BeEmpty();
127+
}
128+
129+
[Fact]
130+
public void Values_above_calculated_upper_bound_report_error()
131+
{
132+
var option = GetOptionWithRangeBounds(0, ValueSource<int>.Create(() => (true, 40)));
133+
134+
var pipelineResult = ExecutedPipelineResultForRangeOption(option, "--intOpt 42");
135+
136+
pipelineResult.Should().NotBeNull();
137+
pipelineResult.GetErrors().Should().HaveCount(1);
138+
var error = pipelineResult.GetErrors().First();
139+
// TODO: Create test mechanism for CliDiagnostics
140+
}
141+
142+
[Fact]
143+
public void Values_below_relative_lower_bound_report_error()
144+
{
145+
var otherOption = new CliOption<int>("-a");
146+
var option = GetOptionWithRangeBounds(ValueSource.Create(otherOption, o => (true, (int)o + 1)), 50);
147+
var command = new CliCommand("cmd") { option, otherOption };
148+
149+
var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 0 -a 0");
150+
151+
pipelineResult.Should().NotBeNull();
152+
pipelineResult.GetErrors().Should().HaveCount(1);
153+
var error = pipelineResult.GetErrors().First();
154+
// TODO: Create test mechanism for CliDiagnostics
155+
}
156+
157+
158+
[Fact]
159+
public void Values_within_relative_range_do_not_report_error()
160+
{
161+
var otherOption = new CliOption<int>("-a");
162+
var option = GetOptionWithRangeBounds(ValueSource<int>.Create(otherOption, o => (true, (int)o + 1)), ValueSource<int>.Create(otherOption, o => (true, (int)o + 10)));
163+
var command = new CliCommand("cmd") { option, otherOption };
164+
165+
var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 11 -a 3");
166+
167+
pipelineResult.Should().NotBeNull();
168+
pipelineResult.GetErrors().Should().BeEmpty();
169+
}
170+
171+
[Fact]
172+
public void Values_above_relative_upper_bound_report_error()
173+
{
174+
var otherOption = new CliOption<int>("-a");
175+
var option = GetOptionWithRangeBounds(0, ValueSource<int>.Create(otherOption, o => (true, (int)o + 10)));
176+
var command = new CliCommand("cmd") { option, otherOption };
177+
178+
var pipelineResult = ExecutedPipelineResultForCommand(command, "--intOpt 9 -a -2");
179+
180+
pipelineResult.Should().NotBeNull();
181+
pipelineResult.GetErrors().Should().HaveCount(1);
182+
var error = pipelineResult.GetErrors().First();
183+
// TODO: Create test mechanism for CliDiagnostics
184+
}
185+
186+
187+
}

0 commit comments

Comments
 (0)