Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,30 @@ fake execute [flags]
await Assert.That(tag.CSharpType).IsEqualTo("IEnumerable<string>?");
}

[Test]
[Arguments("Accepts multiple values")]
[Arguments("One or more label selectors")]
public async Task SharedShapeInference_Preserves_Common_Repeatability_Phrases(string description)
{
var helpText = $"""
Execute a command.

Usage:
fake execute [flags]

Flags:
--tag string {description}
""";
var scraper = new TestCobraScraper(new StubExecutor(
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)));

var command = await scraper.Parse(["fake", "execute"], helpText);
var tag = command!.Options.Single();

await Assert.That(tag.AcceptsMultipleValues).IsTrue();
await Assert.That(tag.CSharpType).IsEqualTo("IEnumerable<string>?");
}

[Test]
public async Task SharedShapeInference_Models_Optional_Cobra_Option_Values()
{
Expand Down Expand Up @@ -663,13 +687,42 @@ fake [flags]
{
["--help"] = helpText,
});
var scraper = new ShapeMismatchScraper(executor);
var scraper = new OptionShapeScraper(executor);

var commands = await ScrapeAsync(scraper);

await Assert.That(commands).IsEmpty();
}

[Test]
public async Task SharedTraversal_Preserves_Boolean_Value_Options_With_Repeatability_Prose()
{
const string helpText = """
Execute a command.

Usage:
fake [flags]

Flags:
--tag=<true|false> May be specified multiple times
""";
var executor = new StubExecutor(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["--help"] = helpText,
});
var scraper = new OptionShapeScraper(executor, "bool?");

var command = (await ScrapeAsync(scraper)).Single();
var option = command.Options.Single();

using (Assert.Multiple())
{
await Assert.That(option.CSharpType).IsEqualTo("bool?");
await Assert.That(option.IsFlag).IsFalse();
await Assert.That(option.AcceptsMultipleValues).IsFalse();
}
}

[Test]
public async Task SharedTraversal_Propagates_Invalid_Operand_Coverage()
{
Expand All @@ -694,7 +747,7 @@ await Assert.That(Scrape)
[Test]
public async Task Shared_Skip_Filter_Preserves_Uppercase_Subcommands()
{
var scraper = new ShapeMismatchScraper(new StubExecutor(
var scraper = new OptionShapeScraper(new StubExecutor(
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)));

await Assert.That(scraper.Skips("SSH")).IsFalse();
Expand Down Expand Up @@ -875,14 +928,17 @@ public Task<bool> IsAvailableAsync(
Task.FromResult(true);
}

private sealed class ShapeMismatchScraper : CliScraperBase
private sealed class OptionShapeScraper : CliScraperBase
{
public ShapeMismatchScraper(ICliCommandExecutor executor)
private readonly string _csharpType;

public OptionShapeScraper(ICliCommandExecutor executor, string csharpType = "string?")
: base(
executor,
new HelpTextCache(NullLogger<HelpTextCache>.Instance),
NullLogger<ShapeMismatchScraper>.Instance)
NullLogger<OptionShapeScraper>.Instance)
{
_csharpType = csharpType;
}

public override string ToolName => "fake";
Expand Down Expand Up @@ -914,7 +970,7 @@ public ShapeMismatchScraper(ICliCommandExecutor executor)
{
SwitchName = "--tag",
PropertyName = "Tag",
CSharpType = "string?",
CSharpType = _csharpType,
Description = "May be specified multiple times",
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
using Microsoft.Extensions.Logging.Abstractions;
using ModularPipelines.OptionsGenerator.Models;
using ModularPipelines.OptionsGenerator.Scrapers.Cli;
using ModularPipelines.OptionsGenerator.TypeDetection;

namespace ModularPipelines.OptionsGenerator.Tests.Scrapers;

public class RepeatableOptionAdapterTests
{
private static ICliCommandExecutor Executor { get; } =
new ProcessCliCommandExecutor(NullLogger<ProcessCliCommandExecutor>.Instance);

private static IHelpTextCache Cache { get; } =
new HelpTextCache(NullLogger<HelpTextCache>.Instance);

[Test]
public async Task Terraform_Recognizes_Multiple_Times_Prose()
{
const string helpText = """
Usage: terraform apply [options]

Options:
-var-file=path Set variables from a file. This flag can be used multiple times.
""";
var command = await new TestTerraformCliScraper().Parse(["terraform", "apply"], helpText);

await AssertRepeatable(command, "-var-file");
}

[Test]
public async Task Pip_Recognizes_Multiline_Multiple_Times_Prose()
{
const string helpText = """
Usage: pip freeze [options]

General Options:
-r, --requirement <file> Install from the given requirements file.
This option can be used multiple times.
""";
var command = await new TestPipCliScraper().Parse(["pip", "freeze"], helpText);

await AssertRepeatable(command, "--requirement");
}

[Test]
public async Task Packer_Recognizes_Repeatable_Prose()
{
const string helpText = """
Usage: packer build [options]

Options:
-var-file=path Set a variable file; repeatable for additional files.
""";
var command = await new TestPackerCliScraper().Parse(["packer", "build"], helpText);

await AssertRepeatable(command, "--var-file");
}

[Test]
public async Task Packer_Recognizes_Quoted_Repeatable_Value()
{
const string helpText = """
Usage: packer build [options]

Options:
-var 'key=value' Set a variable. Can be repeated.
""";
var command = await new TestPackerCliScraper().Parse(["packer", "build"], helpText);

await AssertRepeatable(command, "--var");
}

[Test]
public async Task Packer_Recognizes_Multiline_Repeatable_Prose()
{
const string helpText = """
Usage: packer build [options]

Options:
-var-file=path Set a variable file.
Can be repeated for additional files.
""";
var command = await new TestPackerCliScraper().Parse(["packer", "build"], helpText);

await AssertRepeatable(command, "--var-file");
}

[Test]
public async Task Packer_Does_Not_Treat_Operational_Repetition_As_Repeatable()
{
const string helpText = """
Usage: packer build [options]

Options:
-retry-count=count Retry the operation multiple times before failing.
""";
var command = await new TestPackerCliScraper().Parse(["packer", "build"], helpText);
var option = command!.Options.Single(item => item.SwitchName == "--retry-count");

using (Assert.Multiple())
{
await Assert.That(option.AcceptsMultipleValues).IsFalse();
await Assert.That(option.CSharpType).IsEqualTo("string?");
}
}

[Test]
[Arguments("This argument must be repeated for each entry.", true)]
[Arguments("The value is repeated across runs.", true)]
[Arguments("Provide one or more paths to scan.", true)]
[Arguments("Validates multiple values against a schema.", false)]
[Arguments("Retry the operation one or more times before failing.", false)]
[Arguments("This operation runs one or more attempts depending on configuration.", false)]
[Arguments("One or more attempts are made before failure.", false)]
[Arguments("One or more times may be required.", false)]
[Arguments("One or more retries may be attempted.", false)]
[Arguments("Provide one or more attempts before failing.", false)]
[Arguments("Accepts one or more retries.", false)]
[Arguments("Give one or more times to retry.", false)]
[Arguments("One or more retry attempts before failing.", false)]
[Arguments("Zero or more retry attempts may be made.", false)]
[Arguments("One or more connection retries before giving up.", false)]
[Arguments("One or more polling attempts before timing out.", false)]
[Arguments("This option can be specified one or more times.", true)]
[Arguments("May be provided zero or more times.", true)]
[Arguments("This option should be used more than once.", true)]
[Arguments("Supports multiple values.", true)]
[Arguments("Takes multiple values.", true)]
[Arguments("Value can contain multiple values.", true)]
public async Task Packer_Classifies_Explicit_Repeatability_Prose(
string description,
bool expected)
{
var helpText = $"""
Usage: packer build [options]

Options:
-var-file=path {description}
""";
var command = await new TestPackerCliScraper().Parse(["packer", "build"], helpText);
var option = command!.Options.Single(item => item.SwitchName == "--var-file");

await Assert.That(option.AcceptsMultipleValues).IsEqualTo(expected);
}

[Test]
public async Task Snyk_Recognizes_Repeated_Prose()
{
const string helpText = """
Usage: snyk monitor [<OPTIONS>]

Options
--project-environment=<ENVIRONMENT>
Set the project environment. Can be repeated.
""";
var command = await new TestSnykCliScraper().Parse(["snyk", "monitor"], helpText);

await AssertRepeatable(command, "--project-environment");
}

[Test]
public async Task Snyk_Preserves_Numeric_Element_Type_For_Repeatable_Options()
{
const string helpText = """
Usage: snyk test [<OPTIONS>]

Options
--max-depth=<DEPTH>
Set the maximum dependency depth. Can be repeated.
""";
var command = await new TestSnykCliScraper().Parse(["snyk", "test"], helpText);

await AssertRepeatable(command, "--max-depth", "IEnumerable<int>?");
}

[Test]
public async Task Snyk_Preserves_Enum_Element_Type_For_Repeatable_Options()
{
const string helpText = """
Usage: snyk test [<OPTIONS>]

Options
--severity-threshold=<low|medium|high|critical>
Report only vulnerabilities at the selected threshold. Can be repeated.
""";
var command = await new TestSnykCliScraper().Parse(["snyk", "test"], helpText);

await AssertRepeatable(
command,
"--severity-threshold",
"IEnumerable<SnykSeverityThreshold>?");
}

private static async Task AssertRepeatable(
CliCommandDefinition? command,
string switchName,
string expectedType = "IEnumerable<string>?")
{
var option = command!.Options.Single(item => item.SwitchName == switchName);
using (Assert.Multiple())
{
await Assert.That(option.AcceptsMultipleValues).IsTrue();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
await Assert.That(option.CSharpType).IsEqualTo(expectedType);
}
}

private sealed class TestTerraformCliScraper()
: TerraformCliScraper(
RepeatableOptionAdapterTests.Executor,
RepeatableOptionAdapterTests.Cache,
NullLogger<TerraformCliScraper>.Instance)
{
public Task<CliCommandDefinition?> Parse(string[] commandPath, string helpText) =>
ParseCommandAsync(
commandPath,
helpText,
ParseUsageSynopsis(commandPath, helpText),
CancellationToken.None);
}

private sealed class TestPipCliScraper()
: PipCliScraper(
RepeatableOptionAdapterTests.Executor,
RepeatableOptionAdapterTests.Cache,
NullLogger<PipCliScraper>.Instance)
{
public Task<CliCommandDefinition?> Parse(string[] commandPath, string helpText) =>
ParseCommandAsync(
commandPath,
helpText,
ParseUsageSynopsis(commandPath, helpText),
CancellationToken.None);
}

private sealed class TestPackerCliScraper()
: PackerCliScraper(
RepeatableOptionAdapterTests.Executor,
RepeatableOptionAdapterTests.Cache,
NullLogger<PackerCliScraper>.Instance)
{
public Task<CliCommandDefinition?> Parse(string[] commandPath, string helpText) =>
ParseCommandAsync(
commandPath,
helpText,
ParseUsageSynopsis(commandPath, helpText),
CancellationToken.None);
}

private sealed class TestSnykCliScraper()
: SnykCliScraper(
RepeatableOptionAdapterTests.Executor,
RepeatableOptionAdapterTests.Cache,
NullLogger<SnykCliScraper>.Instance)
{
public Task<CliCommandDefinition?> Parse(string[] commandPath, string helpText) =>
ParseCommandAsync(
commandPath,
helpText,
ParseUsageSynopsis(commandPath, helpText),
CancellationToken.None);
}
}
Loading
Loading