Skip to content

Commit 98f8346

Browse files
committed
add support for CliAction nonexclusivity
1 parent eebf461 commit 98f8346

File tree

9 files changed

+300
-138
lines changed

9 files changed

+300
-138
lines changed

src/System.CommandLine.Tests/DirectiveTests.cs

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4-
using System.Collections.Generic;
54
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
67
using FluentAssertions;
8+
using FluentAssertions.Execution;
79
using Xunit;
810

911
namespace System.CommandLine.Tests
@@ -31,6 +33,16 @@ public void Raw_tokens_still_hold_directives()
3133
result.Tokens.Should().Contain(t => t.Value == "[parse]");
3234
}
3335

36+
[Fact]
37+
public void Directives_must_precede_other_symbols()
38+
{
39+
CliDirective directive = new("parse");
40+
41+
ParseResult result = Parse(new CliOption<bool>("-y"), directive, "-y [parse]");
42+
43+
result.FindResultFor(directive).Should().BeNull();
44+
}
45+
3446
[Fact]
3547
public void Multiple_directives_are_allowed()
3648
{
@@ -47,14 +59,111 @@ public void Multiple_directives_are_allowed()
4759
result.FindResultFor(suggestDirective).Should().NotBeNull();
4860
}
4961

50-
[Fact]
51-
public void Directives_must_be_the_first_argument()
62+
[Theory]
63+
[InlineData(true)]
64+
[InlineData(false)]
65+
public async Task Multiple_instances_of_the_same_directive_can_be_invoked(bool invokeAsync)
5266
{
53-
CliDirective directive = new("parse");
67+
var commandActionWasCalled = false;
68+
var directiveCallCount = 0;
69+
70+
var testDirective = new TestDirective("test")
71+
{
72+
Action = new NonexclusiveTestAction(_ => directiveCallCount++)
73+
};
74+
75+
var config = new CliConfiguration(new CliRootCommand
76+
{
77+
Action = new NonexclusiveTestAction(_ => commandActionWasCalled = true)
78+
})
79+
{
80+
Directives = { testDirective }
81+
};
82+
83+
if (invokeAsync)
84+
{
85+
await config.InvokeAsync("[test:1] [test:2]");
86+
}
87+
else
88+
{
89+
config.Invoke("[test:1] [test:2]");
90+
}
91+
92+
using var _ = new AssertionScope();
93+
94+
commandActionWasCalled.Should().BeTrue();
95+
directiveCallCount.Should().Be(2);
96+
}
5497

55-
ParseResult result = Parse(new CliOption<bool>("-y"), directive, "-y [parse]");
98+
[Theory]
99+
[InlineData(true)]
100+
[InlineData(false)]
101+
public async Task Multiple_different_directives_can_be_invoked(bool invokeAsync)
102+
{
103+
bool commandActionWasCalled = false;
104+
bool directiveOneActionWasCalled = false;
105+
bool directiveTwoActionWasCalled = false;
106+
107+
var directiveOne = new TestDirective("one")
108+
{
109+
Action = new NonexclusiveTestAction(_ => directiveOneActionWasCalled = true)
110+
};
111+
var directiveTwo = new TestDirective("two")
112+
{
113+
Action = new NonexclusiveTestAction(_ => directiveTwoActionWasCalled = true)
114+
};
115+
var config = new CliConfiguration(new CliRootCommand
116+
{
117+
Action = new NonexclusiveTestAction(_ => commandActionWasCalled = true)
118+
})
119+
{
120+
Directives = { directiveOne, directiveTwo }
121+
};
122+
123+
if (invokeAsync)
124+
{
125+
await config.InvokeAsync("[one] [two]");
126+
}
127+
else
128+
{
129+
config.Invoke("[one] [two]");
130+
}
131+
132+
using var _ = new AssertionScope();
133+
134+
commandActionWasCalled.Should().BeTrue();
135+
directiveOneActionWasCalled.Should().BeTrue();
136+
directiveTwoActionWasCalled.Should().BeTrue();
137+
}
56138

57-
result.FindResultFor(directive).Should().BeNull();
139+
public class TestDirective : CliDirective
140+
{
141+
public TestDirective(string name) : base(name)
142+
{
143+
}
144+
}
145+
146+
private class NonexclusiveTestAction : CliAction
147+
{
148+
private readonly Action<ParseResult> _invoke;
149+
150+
public NonexclusiveTestAction(Action<ParseResult> invoke)
151+
{
152+
_invoke = invoke;
153+
Exclusive = false;
154+
}
155+
156+
public override int Invoke(ParseResult parseResult)
157+
{
158+
_invoke(parseResult);
159+
return 0;
160+
}
161+
162+
public override Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
163+
{
164+
;
165+
return Task.FromResult(Invoke(parseResult));
166+
}
58167
}
59168

60169
[Theory]
Lines changed: 56 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
using FluentAssertions;
2-
using System.CommandLine.Parsing;
1+
using System.CommandLine.Help;
2+
using FluentAssertions;
33
using System.Linq;
4+
using System.Threading;
45
using System.Threading.Tasks;
56

67
using Xunit;
@@ -9,167 +10,137 @@ namespace System.CommandLine.Tests
910
{
1011
public class EnvironmentVariableDirectiveTests
1112
{
12-
private static readonly Random randomizer = new(Seed: 456476756);
13-
private readonly string test_variable = $"TEST_ENVIRONMENT_VARIABLE{randomizer.Next()}";
13+
private static readonly Random _random = new();
14+
private readonly string _testVariableName = $"TEST_ENVIRONMENT_VARIABLE_{_random.Next()}";
1415

1516
[Fact]
1617
public async Task Sets_environment_variable_to_value()
1718
{
1819
bool asserted = false;
19-
string variable = test_variable;
20-
const string value = "This is a test";
20+
const string value = "hello";
2121
var rootCommand = new CliRootCommand();
22-
rootCommand.SetAction((_) =>
22+
rootCommand.SetAction(_ =>
2323
{
2424
asserted = true;
25-
Environment.GetEnvironmentVariable(variable).Should().Be(value);
25+
Environment.GetEnvironmentVariable(_testVariableName).Should().Be(value);
2626
});
2727

2828
var config = new CliConfiguration(rootCommand)
2929
{
30-
Directives = { new EnvironmentVariablesDirective() }
30+
Directives = { new EnvironmentVariablesDirective() },
31+
EnableDefaultExceptionHandler = false
3132
};
3233

33-
await config.InvokeAsync(new[] { $"[env:{variable}={value}]" });
34+
await config.InvokeAsync($"[env:{_testVariableName}={value}]");
3435

3536
asserted.Should().BeTrue();
3637
}
3738

3839
[Fact]
39-
public async Task Trims_environment_variable_name()
40+
public async Task Sets_environment_variable_value_containing_equals_sign()
4041
{
4142
bool asserted = false;
42-
string variable = test_variable;
43-
const string value = "This is a test";
43+
const string value = "1=2";
4444
var rootCommand = new CliRootCommand();
45-
rootCommand.SetAction((_) =>
45+
rootCommand.SetAction(_ =>
4646
{
4747
asserted = true;
48-
Environment.GetEnvironmentVariable(variable).Should().Be(value);
48+
Environment.GetEnvironmentVariable(_testVariableName).Should().Be(value);
4949
});
5050

5151
var config = new CliConfiguration(rootCommand)
5252
{
53-
Directives = { new EnvironmentVariablesDirective() }
53+
Directives = { new EnvironmentVariablesDirective() },
54+
EnableDefaultExceptionHandler = false
5455
};
5556

56-
await config.InvokeAsync(new[] { $"[env: {variable} ={value}]" });
57+
await config.InvokeAsync($"[env:{_testVariableName}={value}]" );
5758

5859
asserted.Should().BeTrue();
5960
}
6061

6162
[Fact]
62-
public async Task Trims_environment_variable_value()
63+
public async Task Ignores_environment_directive_without_equals_sign()
6364
{
6465
bool asserted = false;
65-
string variable = test_variable;
66-
const string value = "This is a test";
66+
string variable = _testVariableName;
6767
var rootCommand = new CliRootCommand();
68-
rootCommand.SetAction((_) =>
68+
rootCommand.SetAction(_ =>
6969
{
7070
asserted = true;
71-
Environment.GetEnvironmentVariable(variable).Should().Be(value);
71+
Environment.GetEnvironmentVariable(variable).Should().BeNull();
7272
});
7373

7474
var config = new CliConfiguration(rootCommand)
7575
{
76-
Directives = { new EnvironmentVariablesDirective() }
76+
Directives = { new EnvironmentVariablesDirective() },
77+
EnableDefaultExceptionHandler = false
7778
};
7879

79-
await config.InvokeAsync(new[] { $"[env:{variable}= {value} ]" });
80+
await config.InvokeAsync( $"[env:{variable}]" );
8081

8182
asserted.Should().BeTrue();
8283
}
8384

8485
[Fact]
85-
public async Task Sets_environment_variable_value_containing_equals_sign()
86+
public static async Task Ignores_environment_directive_with_empty_variable_name()
8687
{
8788
bool asserted = false;
88-
string variable = test_variable;
89-
const string value = "This is = a test containing equals";
89+
string value = "value";
9090
var rootCommand = new CliRootCommand();
91-
rootCommand.SetAction((_) =>
91+
rootCommand.SetAction(_ =>
9292
{
9393
asserted = true;
94-
Environment.GetEnvironmentVariable(variable).Should().Be(value);
94+
var env = Environment.GetEnvironmentVariables();
95+
env.Values.Cast<string>().Should().NotContain(value);
9596
});
9697

9798
var config = new CliConfiguration(rootCommand)
9899
{
99-
Directives = { new EnvironmentVariablesDirective() }
100+
Directives = { new EnvironmentVariablesDirective() },
101+
EnableDefaultExceptionHandler = false
100102
};
101103

102-
await config.InvokeAsync(new[] { $"[env:{variable}={value}]" });
104+
var result = config.Parse($"[env:={value}]");
103105

104-
asserted.Should().BeTrue();
105-
}
106-
107-
[Fact]
108-
public async Task Ignores_environment_directive_without_equals_sign()
109-
{
110-
bool asserted = false;
111-
string variable = test_variable;
112-
var rootCommand = new CliRootCommand();
113-
rootCommand.SetAction((_) =>
114-
{
115-
asserted = true;
116-
Environment.GetEnvironmentVariable(variable).Should().BeNull();
117-
});
118-
119-
var config = new CliConfiguration(rootCommand)
120-
{
121-
Directives = { new EnvironmentVariablesDirective() }
122-
};
123-
124-
await config.InvokeAsync(new[] { $"[env:{variable}]" });
106+
await result.InvokeAsync();
125107

126108
asserted.Should().BeTrue();
127109
}
128110

129111
[Fact]
130-
public static async Task Ignores_environment_directive_with_empty_variable_name()
112+
public void It_does_not_prevent_help_from_being_invoked()
131113
{
132-
bool asserted = false;
133-
string value = $"This is a test, random: {randomizer.Next()}";
134-
var rootCommand = new CliRootCommand();
135-
rootCommand.SetAction((_) =>
136-
{
137-
asserted = true;
138-
var env = Environment.GetEnvironmentVariables();
139-
env.Values.Cast<string>().Should().NotContain(value);
140-
});
114+
var root = new CliRootCommand();
115+
root.SetAction(_ => { });
141116

142-
var config = new CliConfiguration(rootCommand)
143-
{
144-
Directives = { new EnvironmentVariablesDirective() }
145-
};
117+
var customHelpAction = new CustomHelpAction();
118+
root.Options.OfType<HelpOption>().Single().Action = customHelpAction;
146119

147-
await config.InvokeAsync(new[] { $"[env:={value}]" });
120+
var config = new CliConfiguration(root);
121+
config.Directives.Add(new EnvironmentVariablesDirective());
148122

149-
asserted.Should().BeTrue();
123+
root.Parse($"[env:{_testVariableName}=1] -h", config).Invoke();
124+
125+
customHelpAction.WasCalled.Should().BeTrue();
126+
Environment.GetEnvironmentVariable(_testVariableName).Should().Be("1");
150127
}
151128

152-
[Fact]
153-
public static async Task Ignores_environment_directive_with_whitespace_variable_name()
129+
private class CustomHelpAction : CliAction
154130
{
155-
bool asserted = false;
156-
string value = $"This is a test, random: {randomizer.Next()}";
157-
var rootCommand = new CliRootCommand();
158-
rootCommand.SetAction((_) =>
159-
{
160-
asserted = true;
161-
var env = Environment.GetEnvironmentVariables();
162-
env.Values.Cast<string>().Should().NotContain(value);
163-
});
131+
public bool WasCalled { get; private set; }
164132

165-
var config = new CliConfiguration(rootCommand)
133+
public override int Invoke(ParseResult parseResult)
166134
{
167-
Directives = { new EnvironmentVariablesDirective() }
168-
};
135+
WasCalled = true;
136+
return 0;
137+
}
169138

170-
await config.InvokeAsync(new[] { $"[env: ={value}]" });
171-
172-
asserted.Should().BeTrue();
139+
public override Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
140+
{
141+
WasCalled = true;
142+
return Task.FromResult(0);
143+
}
173144
}
174145
}
175146
}

src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
using System.CommandLine.Help;
66
using System.IO;
77
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
810
using FluentAssertions;
11+
using FluentAssertions.Common;
912
using Xunit;
1013
using static System.Environment;
1114

0 commit comments

Comments
 (0)