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
@@ -0,0 +1,100 @@
using ModularPipelines.OptionsGenerator.Tests.Scrapers.Cli;

namespace ModularPipelines.OptionsGenerator.Tests.Generators;

public partial class RequiredConstructorValidationTests
{
[Test]
public async Task Gcloud_Build_Trigger_Updates_Preserve_Documented_Nested_Choices()
{
var help = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "Fixtures", "Gcloud", "585.0.0",
"gcloud-builds-triggers-update-github.txt"));
var command = (await new GcloudResourceArgumentTests.TestScraper().Parse(["gcloud", "builds", "triggers", "update", "github"], help))!;
var group = command.RequiredAlternativeGroups.Single(group => group.PropertyNames.Contains("TriggerConfig"));
await ValidateCapturedGroup(command, group,
[
("", false),
("TriggerConfig", true),
("BranchPattern", true),
("TagPattern", true),
("BuildConfig", true),
("UpdateSubstitutions", true),
("Description", true),
("Description,BranchPattern,BuildConfig", true),
("TriggerConfig,BranchPattern", false),
("BranchPattern,TagPattern", false),
("BuildConfig,InlineConfig", false),
]);
}

[Test]
public async Task Gcloud_Storage_Source_Branches_Keep_Bucket_And_Filter_Choices_Together()
{
var command = await GcloudCapturedSemanticsTests.Scrape("storage batch-operations jobs create");
var group = command.RequiredAlternativeGroups.Single(group => group.PropertyNames.Contains("Bucket"));
await ValidateCapturedGroup(command, group,
[
("", false),
("Bucket,ManifestLocation", true),
("BucketList,ManifestLocation", true),
("Bucket,IncludedObjectPrefixes", true),
("BucketList,IncludedObjectPrefixes", true),
("Bucket", false),
("ManifestLocation", false),
("Bucket,BucketList,ManifestLocation", false),
("Bucket,ManifestLocation,IncludedObjectPrefixes", false),
("DryRunJobId", true),
("InsightsDataSetConfig,TargetProject", true),
("InsightsDataSetConfig", false),
("TargetProject", false),
("InsightsDataSetConfig,TargetProject,TargetLocations,TargetSnapshotTime", true),
("Bucket,ManifestLocation,DryRunJobId", false),
]);
}

[Test]
public async Task Gcloud_Storage_Custom_Context_Alternatives_Preserve_Documented_Choices()
{
var command = await GcloudCapturedSemanticsTests.Scrape("storage batch-operations jobs create");
var group = command.RequiredAlternativeGroups.Single(group => group.PropertyNames.Contains("ClearAllObjectCustomContexts"));
await ValidateCapturedGroup(command, group,
[
("", false),
("ClearAllObjectCustomContexts", true),
("ClearObjectCustomContexts,UpdateObjectCustomContexts", true),
("ClearObjectCustomContexts", true),
("UpdateObjectCustomContexts", true),
("ClearObjectCustomContexts,UpdateObjectCustomContextsFile", true),
("UpdateObjectCustomContexts,UpdateObjectCustomContextsFile", false),
("UpdateObjectCustomContextsFile", true),
("ClearAllObjectCustomContexts,UpdateObjectCustomContextsFile", false),
("DeleteObject", true),
("DeleteObject,EnablePermanentObjectDeletion", true),
("EnablePermanentObjectDeletion", false),
]);
}

[Test]
public async Task Gcloud_Agent_Identity_Oauth_Requires_Every_Selected_Branch_Member()
{
var command = await GcloudCapturedSemanticsTests.Scrape("agent-identity auth-providers create");
var group = command.RequiredAlternativeGroups.Single(group => group.PropertyNames.Contains("ApiKey"));
const string threeLegged = "ThreeLeggedOauthAuthorizationUrl,ThreeLeggedOauthClientId,ThreeLeggedOauthClientSecret,ThreeLeggedOauthDefaultContinueUri,ThreeLeggedOauthEnablePkce,ThreeLeggedOauthTokenUrl";
const string twoLegged = "TwoLeggedOauthClientId,TwoLeggedOauthClientSecret,TwoLeggedOauthTokenUrl";
var cases = new List<(string Properties, bool Valid)>
{
("", false),
("ApiKey", true),
(threeLegged, true),
(twoLegged, true),
("ApiKey," + twoLegged, false),
};
foreach (var branch in new[] { threeLegged, twoLegged })
{
var members = branch.Split(',');
cases.AddRange(members.Select(missing => (string.Join(',', members.Where(member => member != missing)), false)));
}

await ValidateCapturedGroup(command, group, [.. cases]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ private static async Task ValidateCapturedGroup(CliCommandDefinition command, Cl
{
value = 1;
}
else if (property.PropertyType == typeof(IEnumerable<string>))
{
value = new[] { "value" };
}
else if (property.PropertyType == typeof(IReadOnlyList<ModularPipelines.Models.KeyValue>))
{
value = new[] { new ModularPipelines.Models.KeyValue("key", "value", "=") };
}
property.SetValue(instance, value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ internal static async Task<List<CliCommandDefinition>> ScrapeFixture(string path
return commands;
}

private sealed class TestScraper() : GcloudCliScraper(
internal sealed class TestScraper() : GcloudCliScraper(
new UnusedExecutor(),
new HelpTextCache(NullLogger<HelpTextCache>.Instance),
NullLogger<GcloudCliScraper>.Instance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,91 @@ namespace ModularPipelines.OptionsGenerator.Tests.Scrapers.Cli;

public class GcloudSynopsisGroupReconcilerTests
{
[Test]
public async Task Required_Option_Constraints_Follow_Option_Resolved_Synopsis_Selection()
{
const string help = """
NAME
gcloud example run - run an example
SYNOPSIS
gcloud example run --output VALUE (--first=FIRST --second=SECOND | --third=THIRD)
gcloud example run ITEM (--first=FIRST | --second=SECOND --third=THIRD)
POSITIONAL ARGUMENTS
[ITEM]
Optional item.
FLAGS
--output=OUTPUT
Output path.
Exactly one of these must be specified:
--first=FIRST
First value.
--second=SECOND
Second value.
--third=THIRD
Third value.
""";
var command = (await GcloudResourceArgumentTests.ScrapeFixture("example run", help)).Single();
var group = command.RequiredAlternativeGroups.Single(group => group.PropertyNames.Contains("First"));
await Assert.That(command.UsageSynopsis).Contains("ITEM");
await Assert.That(command.PositionalArguments.Select(argument => argument.PropertyName)).IsEquivalentTo(["Item"]);
await Assert.That(group.Members.Select(member => member.PropertyName)).IsEquivalentTo(["First"]);
await Assert.That(group.Groups.Single().Members.Select(member => member.PropertyName)).IsEquivalentTo(["Second", "Third"]);
}

[Test]
public async Task Required_Option_Constraints_Use_The_Selected_Synopsis_Form()
{
const string help = """
NAME
gcloud example run - run an example
SYNOPSIS
gcloud example run ITEM (--first=FIRST --second=SECOND | --third=THIRD)
gcloud example run (--first=FIRST | --second=SECOND --third=THIRD)
POSITIONAL ARGUMENTS
[ITEM]
Optional item.
FLAGS
Exactly one of these must be specified:
--first=FIRST
First value.
--second=SECOND
Second value.
--third=THIRD
Third value.
""";
var command = (await GcloudResourceArgumentTests.ScrapeFixture("example run", help)).Single();
var group = command.RequiredAlternativeGroups.Single(group => group.PropertyNames.Contains("First"));
await Assert.That(command.UsageSynopsis).Contains("ITEM");
await Assert.That(group.Members.Select(member => member.PropertyName)).IsEquivalentTo(["Third"]);
await Assert.That(group.Groups.Single().Members.Select(member => member.PropertyName)).IsEquivalentTo(["First", "Second"]);
}

[Test]
[Arguments("tool run [--first=FIRST | --second=SECOND]")]
[Arguments("tool run (--first=FIRST : --selector=SELECTOR | --second=SECOND)")]
[Arguments("tool run (--first=FIRST:--selector=SELECTOR|--second=SECOND)")]
[Arguments("tool run (--first=FIRST | OPERAND)")]
[Arguments("tool run (--first=FIRST; default=one | --second=SECOND)")]
public async Task Required_Option_Constraints_Exclude_Optional_Ambiguous_And_Operand_Syntax(string synopsis)
{
await Assert.That(UsageSynopsisParser.GetRequiredOptionChoiceGroups(synopsis)).IsEmpty();
}

[Test]
public async Task Required_Option_Constraints_Preserve_Nested_Choices_And_Optional_Selectors()
{
var group = UsageSynopsisParser.GetRequiredOptionChoiceGroups(
"tool run ((--first=FIRST | --second=SECOND) (--file=FILE | --prefix=PREFIX) | [--resource=RESOURCE : --selector=SELECTOR])").Single();
await Assert.That(group.IsChoice).IsTrue();
await Assert.That(group.IsRequired).IsTrue();
await Assert.That(group.Groups[0].Groups.Count).IsEqualTo(2);
await Assert.That(group.Groups[0].Groups.All(choice => choice.IsChoice && choice.IsRequired)).IsTrue();
var resource = group.Groups[1];
await Assert.That(resource.IsRequired).IsFalse();
await Assert.That(resource.Members[0].IsRequired).IsTrue();
await Assert.That(resource.Members[1].IsRequired).IsFalse();
}

[Test]
public async Task Resource_Bundles_Identify_Their_Single_Primary_Option()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1176,7 +1176,7 @@ protected IReadOnlyList<CliPositionalArgument> GetPositionalArguments(
.Select(argument => argument with { AssociatedOptionSwitch = null })];
}

private IReadOnlyList<CliOptionDefinition> GetUsageOptions(IReadOnlyList<CliOptionDefinition> options)
protected IReadOnlyList<CliOptionDefinition> GetUsageOptions(IReadOnlyList<CliOptionDefinition> options)
{
var globalOptions = EffectiveGlobalOptions;
return globalOptions.Count == 0 ? options : [.. options, .. globalOptions];
Expand Down
Loading
Loading