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
49 changes: 49 additions & 0 deletions ebuild.Tests/Unit/CliParserConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using NUnit.Framework;
using System.Collections.Generic;
using ebuild.cli;

namespace ebuild.Tests.Unit
{
[TestFixture]
public class CliParserConverterTests
{
// Simple converter used by tests
public class UpperCaseConverter : IStringConverter
{
public object Convert(string value) => value.ToUpperInvariant();
}

class TestConvRoot : Command
{
[Option("opt", ConverterType = typeof(UpperCaseConverter))]
public string? Opt;
}

[Test]
public void Uses_IStringConverter_for_field()
{
var parser = new CliParser(typeof(TestConvRoot));
parser.Parse(new[] { "cmd", "--opt=hello" });
parser.ApplyParsedToCommands();
var root = (TestConvRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Opt, Is.EqualTo("HELLO"));
}

class TestDictRoot : Command
{
[Option("map")]
public Dictionary<string, int>? Map;
}

[Test]
public void Dictionary_values_are_converted_to_int()
{
var parser = new CliParser(typeof(TestDictRoot));
parser.Parse(new[] { "cmd", "--map=foo=42", "--map=bar=7" });
parser.ApplyParsedToCommands();
var root = (TestDictRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Map.ContainsKey("foo") && root.Map["foo"] == 42);
Assert.That(root.Map.ContainsKey("bar") && root.Map["bar"] == 7);
}
}
}
96 changes: 96 additions & 0 deletions ebuild.Tests/Unit/CliParserSubcommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using NUnit.Framework;
using ebuild.cli;

namespace ebuild.Tests.Unit
{
[TestFixture]
public class CliParserSubcommandTests
{
[Command("sub", Aliases = new[] { "s" })]
class SubCommand : Command
{
[Option("flag")]
public bool Flag;
}

class RootWithSub : Command
{
// root has no options; subcommand added at runtime
}

class RootWithNested : Command
{
[Command("nested")]
public class Nested : Command
{
[Option("flag")]
public bool Flag;
}
}

class RootWithNestedOptOut : Command
{
[Command("noauto", AutoRegister = false)]
public class NoAuto : Command
{
}
}

[Test]
public void Subcommand_switches_and_parses_option()
{
var parser = new CliParser(typeof(RootWithSub));
// add a subcommand instance to the root created inside the parser
var root = (RootWithSub)parser.currentCommandChain.First!.Value;
var sub = new SubCommand();
root.AddSubCommand(sub);

parser.Parse(new[] { "cmd", "sub", "--flag" });
parser.ApplyParsedToCommands();

// ensure current command chain ended on the subcommand instance and its option was set
var last = parser.currentCommandChain.Last!.Value as SubCommand;
Assert.That(last, Is.Not.Null);
Assert.That(last!.Flag, Is.True);
}

[Test]
public void Subcommand_alias_is_recognized()
{
var parser = new CliParser(typeof(RootWithSub));
var root = (RootWithSub)parser.currentCommandChain.First!.Value;
var sub = new SubCommand();
root.AddSubCommand(sub);

parser.Parse(new[] { "cmd", "s", "--flag" });
parser.ApplyParsedToCommands();

var last = parser.currentCommandChain.Last!.Value as SubCommand;
Assert.That(last, Is.Not.Null);
Assert.That(last!.Flag, Is.True);
}

[Test]
public void Nested_subcommand_is_auto_registered()
{
var parser = new CliParser(typeof(RootWithNested));
parser.Parse(new[] { "cmd", "nested", "--flag" });
parser.ApplyParsedToCommands();

var last = parser.currentCommandChain.Last!.Value as RootWithNested.Nested;
Assert.That(last, Is.Not.Null);
Assert.That(last!.Flag, Is.True);
}

[Test]
public void Nested_subcommand_opt_out_prevents_registration()
{
var parser = new CliParser(typeof(RootWithNestedOptOut));
parser.Parse(new[] { "cmd", "noauto" });
parser.ApplyParsedToCommands();

// should remain on root because nested class opted out
Assert.That(parser.currentCommandChain.Last!.Value.GetType(), Is.EqualTo(typeof(RootWithNestedOptOut)));
}
}
}
121 changes: 121 additions & 0 deletions ebuild.Tests/Unit/CliParserUnitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using NUnit.Framework;
using System;
using ebuild.cli;

namespace ebuild.Tests.Unit
{
[TestFixture]
public class CliParserTests
{
class TestRoot : Command
{
[Option("verbose", ShortName = "v")]
public bool Verbose;

[Option("define", ShortName = "D")]
public string? Define;
[Argument(0, Name = "input")]
public string? Input;
}

[Test]
public void Parses_long_option_with_equals()
{
var parser = new CliParser(typeof(TestRoot));
parser.Parse(new[] { "cmd", "--define=Z=1" });
parser.ApplyParsedToCommands();
var root = (TestRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Define, Is.EqualTo("Z=1"));
}

[Test]
public void Negative_number_is_argument_when_no_option()
{
var parser = new CliParser(typeof(TestRoot));
parser.Parse(new[] { "cmd", "-42", "foo" });
parser.ApplyParsedToCommands();
Assert.That(parser.ParsedOptions.Count, Is.EqualTo(0));
Assert.That(parser.ParsedArguments.Count, Is.EqualTo(2));
Assert.That(parser.ParsedArguments[0].Value, Is.EqualTo("-42"));
}

[Test]
public void Positional_argument_maps_to_field()
{
var parser = new CliParser(typeof(TestRoot));
parser.Parse(new[] { "cmd", "input.txt" });
parser.ApplyParsedToCommands();
var root = (TestRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Input, Is.EqualTo("input.txt"));
}

[Test]
public void Duplicate_option_policy_error_throws()
{
var parser = new CliParser(typeof(TestRoot), DuplicateOptionPolicy.Error);
parser.Parse(new[] { "cmd", "--define=one", "--define=two" });
Assert.Throws<InvalidOperationException>(() => parser.ApplyParsedToCommands());
}

[Test]
public void Duplicate_option_policy_warn_last_wins()
{
var parser = new CliParser(typeof(TestRoot), DuplicateOptionPolicy.Warn);
parser.Parse(new[] { "cmd", "--define=one", "--define=two" });
parser.ApplyParsedToCommands();
var root = (TestRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Define, Is.EqualTo("two"));
}

[Test]
public void Duplicate_option_policy_ignore_last_wins()
{
var parser = new CliParser(typeof(TestRoot), DuplicateOptionPolicy.Ignore);
parser.Parse(new[] { "cmd", "--define=one", "--define=two" });
parser.ApplyParsedToCommands();
var root = (TestRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Define, Is.EqualTo("two"));
}

[Test]
public void Disallow_combined_short_flags()
{
var parser = new CliParser(typeof(TestRoot));
parser.Parse(new[] { "cmd", "-abc" });
parser.ApplyParsedToCommands();
// Should treat as a single option name 'abc'
Assert.That(parser.ParsedOptions.Count, Is.EqualTo(1));
Assert.That(parser.ParsedOptions[0].Name, Is.EqualTo("abc"));
}

[Test]
public void Short_option_attached_value_with_equals_parses()
{
var parser = new CliParser(typeof(TestRoot));
parser.Parse(new[] { "cmd", "-DZLIB=1.2.11" });
parser.ApplyParsedToCommands();
var root = (TestRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Define, Is.EqualTo("ZLIB=1.2.11"));
}

[Test]
public void Short_option_attached_quoted_value_parses()
{
var parser = new CliParser(typeof(TestRoot));
parser.Parse(new[] { "cmd", "-D\"USE_ZLIB\"" });
parser.ApplyParsedToCommands();
var root = (TestRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Define, Is.EqualTo("USE_ZLIB"));
}

[Test]
public void Short_option_attached_quoted_value_with_spaces_parses()
{
var parser = new CliParser(typeof(TestRoot));
parser.Parse(new[] { "cmd", "-D\"Use ZLIB Data\"" });
parser.ApplyParsedToCommands();
var root = (TestRoot)parser.currentCommandChain.First!.Value;
Assert.That(root.Define, Is.EqualTo("Use ZLIB Data"));
}
}
}
1 change: 1 addition & 0 deletions ebuild.Tests/ebuild.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="../ebuild/ebuild.csproj" />
<ProjectReference Include="../ebuild.api/ebuild.api.csproj" />
<ProjectReference Include="../ebuild.cli/ebuild.cli.csproj" />
</ItemGroup>
<PropertyGroup>
<Nullable>enable</Nullable>
Expand Down
2 changes: 1 addition & 1 deletion ebuild.api/Definition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace ebuild.api
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
/// <summary>
/// Represents a single preprocessor/definition entry typically specified as
/// a string in the form <c>NAME=VALUE</c> or simply <c>NAME</c>.
Expand All @@ -15,6 +14,7 @@ namespace ebuild.api
/// </summary>
/// <param name="inValue">The raw definition string passed to the constructor.
/// Expected formats: <c>NAME=VALUE</c> or <c>NAME</c>.</param>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class Definition(string inValue)
{
/// <summary>
Expand Down
5 changes: 3 additions & 2 deletions ebuild.api/ModuleBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,10 @@ public string GetOutputTransformerName()
/// <returns>Absolute path to the module's binary output directory, always ending with a directory separator.</returns>
public string GetBinaryOutputDirectory()
{
var basePath = Path.Combine(Context.ModuleDirectory!.FullName, OutputDirectory, Context.Configuration, GetOutputTransformerName());
if (UseVariants)
return Path.TrimEndingDirectorySeparator(Path.Combine(Context.ModuleDirectory!.FullName, OutputDirectory, GetOutputTransformerName(), GetVariantId().ToString()) + Path.DirectorySeparatorChar);
return Path.TrimEndingDirectorySeparator(Path.Combine(Context.ModuleDirectory!.FullName, OutputDirectory, GetOutputTransformerName()) + Path.DirectorySeparatorChar);
return Path.TrimEndingDirectorySeparator(Path.Combine(basePath, GetVariantId().ToString()));
return Path.TrimEndingDirectorySeparator(basePath);
}


Expand Down
4 changes: 2 additions & 2 deletions ebuild.api/ModuleContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace ebuild.api
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
/// <summary>
/// Runtime context provided to modules when they are instantiated.
///
/// This class carries information about the calling environment such as the
/// module reference, target platform, toolchain, architecture, and option map.
/// Module implementations use this context during construction and initialization.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class ModuleContext
{
/// <summary>
Expand All @@ -27,6 +27,7 @@ public ModuleContext(ModuleContext m)
Toolchain = m.Toolchain;
TargetArchitecture = m.TargetArchitecture;
Options = m.Options;
Configuration = m.Configuration;
AdditionalDependencyPaths = m.AdditionalDependencyPaths;
}

Expand Down Expand Up @@ -107,7 +108,6 @@ public ModuleContext(ModuleReference reference, PlatformBase platform, IToolchai
/// </summary>
public string RequestedOutput => SelfReference.GetOutput();


/// <summary>
/// Simple message wrapper used to surface informational/warning/error messages produced
/// during module instantiation.
Expand Down
19 changes: 19 additions & 0 deletions ebuild.cli/ArgumentAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace ebuild.cli
{
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class ArgumentAttribute : Attribute
{
public int Order { get; }
public string? Name { get; set; }
public string? Description { get; set; }
public bool AllowMultiple { get; set; } = false;
public bool IsRequired { get; set; } = false;

public ArgumentAttribute(int order)
{
Order = order;
}
}
}
Loading
Loading