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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="Npgsql" Version="10.0.3" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" />
<PackageVersion Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.9" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageVersion Include="Dapper" Version="2.1.79" />
<PackageVersion Include="MySqlConnector" Version="2.6.0" />
Expand Down
538 changes: 300 additions & 238 deletions src/grate/Commands/MigrateCommand.cs

Large diffs are not rendered by default.

81 changes: 32 additions & 49 deletions src/grate/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.CommandLine.NamingConventionBinder;
using System.CommandLine.Parsing;
using System.Reflection;
using grate.Commands;
using grate.Configuration;
Expand All @@ -28,7 +23,7 @@ public static async Task<int> Main(string[] args)
{
// Temporarily parse the configuration, to get the verbosity level, and potentially set parameters
// to support the "IsUpToDate" check.
var cfg = await ParseGrateConfiguration(args);
var cfg = ParseGrateConfiguration(args);
if (cfg.UpToDateCheck)
{
cfg = cfg with { Verbosity = LogLevel.Critical, DryRun = true };
Expand All @@ -37,64 +32,56 @@ public static async Task<int> Main(string[] args)
_serviceProvider = BuildServiceProvider(cfg).CreateAsyncScope().ServiceProvider;

var rootCommand = Create<MigrateCommand>();
rootCommand.Add(Verbosity());

rootCommand.Description = $"grate v{GetVersion()} - sql for the 20s";

var parser = new CommandLineBuilder(rootCommand)
// These are all the CommandLine features enabled by default
// .UseVersionOption() //but we don't want version (as we use the --version option ourselves)
.UseHelp()
.UseEnvironmentVariableDirective()
.UseParseDirective()
.UseSuggestDirective()
.RegisterWithDotnetSuggest()
.UseTypoCorrections()
.UseParseErrorReporting()
.UseExceptionHandler(ExceptionHandler)
.CancelOnProcessTermination()
.Build();

var result = await parser.InvokeAsync(args);
// System.CommandLine enables help, suggestions, typo corrections and parse-error reporting
// by default. We handle exceptions ourselves (see below), so disable the built-in handler.
var configuration = new InvocationConfiguration
{
EnableDefaultExceptionHandler = false
};

var parseResult = rootCommand.Parse(args);

int result;
try
{
result = await parseResult.InvokeAsync(configuration);
}
catch (Exception ex)
{
result = ExceptionHandler(ex, configuration);
}

await WaitForLoggerToFinish();

return result;
}
private static void ExceptionHandler(Exception ex, InvocationContext context)

private static int ExceptionHandler(Exception ex, InvocationConfiguration configuration)
{
// Log the error message at the highest level, and the exception at debug level.
// Avoids logging the exception stack trace to the end user, if logging level is not set to debug.

var logger = _serviceProvider.GetRequiredService<ILogger<GrateMigrator>>();
context.Console.Error.CreateTextWriter().WriteColoredMessage("An error occurred: ", GrateConsoleColor.Foreground.Red);

configuration.Error.WriteColoredMessage("An error occurred: ", GrateConsoleColor.Foreground.Red);

logger.LogDebug(ex, "{ErrorMessage}", ex.Message);
logger.LogError("{ErrorMessage}", ex.Message);
context.ExitCode = 1;

return 1;
}


private static string GetVersion() => Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "1.0.0.1";

private static async Task<CommandLineGrateConfiguration> ParseGrateConfiguration(IReadOnlyList<string> commandline)
private static CommandLineGrateConfiguration ParseGrateConfiguration(IReadOnlyList<string> commandline)
{
CommandLineGrateConfiguration cfg = new CommandLineGrateConfiguration();
var handler = CommandHandler.Create((CommandLineGrateConfiguration config) => cfg = config);

var cmd = new MigrateCommand(null!)
{
Verbosity(),
};

ParseResult p =
new Parser(cmd).Parse(commandline);
await handler.InvokeAsync(new InvocationContext(p));

return cfg;
var cmd = new MigrateCommand(null!);
var parseResult = cmd.Parse(commandline);
return cmd.GetConfiguration(parseResult);
}


Expand Down Expand Up @@ -154,9 +141,5 @@ private static ServiceProvider BuildServiceProvider(CommandLineGrateConfiguratio
return services.BuildServiceProvider();
}

internal static Option<LogLevel> Verbosity() => new(
["-v", "--verbosity"],
"Verbosity level (as defined here: https://docs.microsoft.com/dotnet/api/Microsoft.Extensions.Logging.LogLevel)");

private static T Create<T>() where T : notnull => _serviceProvider.GetRequiredService<T>();
}
2 changes: 1 addition & 1 deletion src/grate/grate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.CommandLine.NamingConventionBinder" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Dapper" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.CommandLine.Parsing;
using System.Configuration;
using System.Configuration;
using FluentAssertions;
using grate.Commands;
using grate.Configuration;
using grate.Exceptions;
using grate.Infrastructure;

#if NET6_0
using Dir = TestCommon.TestInfrastructure.Net6PolyFills.Directory;
#else
using Dir = System.IO.Directory;
#endif

namespace Basic_tests.CommandLineParsing;

// ReSharper disable once InconsistentNaming
public class Basic_CommandLineParsing
{
[Fact]
public void ParserIsConfiguredCorrectly()

[Theory]
[InlineData("")]
[InlineData("-ct=100")]
public void ParserIsConfiguredCorrectly(string commandline)
{
// Test that the parser configuration is valid, see https://github.com/dotnet/command-line-api/issues/1613
var command = new MigrateCommand(null!);
var configuration = new CommandLineConfiguration(command);
configuration.ThrowIfInvalid();
var parseResult = command.Parse(commandline);
Assert.NotNull(parseResult.Errors);
Assert.Single(parseResult.Errors);
Assert.Equal("Option '--connectionstring' is required.", parseResult.Errors[0].Message);
}

[Theory]
Expand Down Expand Up @@ -188,8 +183,8 @@ public async Task WithoutTransaction(string argName)

cfg?.Transaction.Should().Be(false);
}


/// <summary>
/// We can use multiple environments, separated by space, ; or ,
/// This makes it possible to create orhotogonal environments, and run scripts
Expand Down Expand Up @@ -221,16 +216,16 @@ public async Task WithoutTransaction(string argName)
/// <param name="expected"></param>

[Theory]
[InlineData("--env KASHMIR", new[] {"KASHMIR"})]
[InlineData("--env JALLA", new[] {"JALLA"})]
[InlineData("--env JALLA KASHMIR", new[] {"JALLA", "KASHMIR"})]
[InlineData("--env JALLA,BERGEN", new[] {"JALLA", "BERGEN"})]
[InlineData("--env Dev;Azure;OnlyOnMondays", new[] {"Dev", "Azure", "OnlyOnMondays"})]
[InlineData("--env Customer1;Azure;Dev", new[] {"Customer1", "Azure", "Dev"})]
[InlineData("--env Customer1;Azure;Test", new[] {"Customer1", "Azure", "Test"})]
[InlineData("--env Customer2;Azure;Dev", new[] {"Customer2", "Azure", "Dev"})]
[InlineData("--env Customer2;Aws;QA", new[] {"Customer2", "Aws", "QA"})]
[InlineData("--env Customer2;Azure;Prod", new[] {"Customer2", "Azure", "Prod"})]
[InlineData("--env KASHMIR", new[] { "KASHMIR" })]
[InlineData("--env JALLA", new[] { "JALLA" })]
[InlineData("--env JALLA KASHMIR", new[] { "JALLA", "KASHMIR" })]
[InlineData("--env JALLA,BERGEN", new[] { "JALLA", "BERGEN" })]
[InlineData("--env Dev;Azure;OnlyOnMondays", new[] { "Dev", "Azure", "OnlyOnMondays" })]
[InlineData("--env Customer1;Azure;Dev", new[] { "Customer1", "Azure", "Dev" })]
[InlineData("--env Customer1;Azure;Test", new[] { "Customer1", "Azure", "Test" })]
[InlineData("--env Customer2;Azure;Dev", new[] { "Customer2", "Azure", "Dev" })]
[InlineData("--env Customer2;Aws;QA", new[] { "Customer2", "Aws", "QA" })]
[InlineData("--env Customer2;Azure;Prod", new[] { "Customer2", "Azure", "Prod" })]
public async Task Environments(string argName, IEnumerable<string> expected)
{
var commandline = argName;
Expand Down Expand Up @@ -384,7 +379,7 @@ public async Task IgnoreDirectoryNames(string args, bool expected)
var cfg = await ParseGrateConfiguration(args);
cfg?.IgnoreDirectoryNames.Should().Be(expected);
}

[Theory]
[InlineData("", false)]
[InlineData("--isuptodate", true)]
Expand All @@ -397,7 +392,7 @@ public async Task UpToDateCheck(string args, bool expected)
cfg?.UpToDateCheck.Should().Be(expected);
}

private static async Task<CommandLineGrateConfiguration?> ParseGrateConfiguration(string commandline)
private static Task<CommandLineGrateConfiguration?> ParseGrateConfiguration(string commandline)
{
// All parsing fails if the connectionstring is not supplied, so we need to add it here, if it's not in the commandline.
if (
Expand All @@ -409,19 +404,15 @@ public async Task UpToDateCheck(string args, bool expected)
commandline += " -c \"Server=.;Database=master;Trusted_Connection=True;\"";
}

CommandLineGrateConfiguration? cfg = null;
var cmd = CommandHandler.Create((CommandLineGrateConfiguration config) => cfg = config);

ParseResult p =
new Parser(new MigrateCommand(null!)).Parse(commandline);
var command = new MigrateCommand(null!);
var parseResult = command.Parse(commandline);

if (p.Errors.Any())
if (parseResult.Errors.Any())
{
var exceptions = p.Errors.Select(error => new ConfigurationErrorsException(error.Message)).ToList();
var exceptions = parseResult.Errors.Select(error => new ConfigurationErrorsException(error.Message)).ToList();
throw new MigrationFailed(exceptions);
}

await cmd.InvokeAsync(new InvocationContext(p));
return cfg;
return Task.FromResult<CommandLineGrateConfiguration?>(command.GetConfiguration(parseResult));
}
}
17 changes: 5 additions & 12 deletions unittests/Basic_tests/CommandLineParsing/FolderConfiguration_.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.Collections.Immutable;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.CommandLine.Parsing;
using System.Runtime.CompilerServices;
using FluentAssertions;
using grate.Commands;
Expand All @@ -25,15 +22,15 @@ public async Task Default()

AssertEquivalent(expected.Values, actual?.Values);
}

[Fact]
public async Task Default_with_overridden_transaction_handling_for_one_folder()
{
var cfg = await ParseGrateConfiguration("--folders=runAfterCreateDatabase=transactionHandling:autonomous");

var expected = FoldersConfiguration.Default();
expected[RunAfterCreateDatabase] = expected[RunAfterCreateDatabase]! with { TransactionHandling = TransactionHandling.Autonomous };

var actual = cfg?.Folders;
actual![RunAfterCreateDatabase]!.TransactionHandling.Should().Be(TransactionHandling.Autonomous);
AssertEquivalent(expected.Values, actual.Values);
Expand Down Expand Up @@ -117,16 +114,12 @@ private static void AssertEquivalent(MigrationsFolder? expected, MigrationsFolde
}


private static async Task<GrateConfiguration?> ParseGrateConfiguration(params string[] commandline)
private static Task<GrateConfiguration?> ParseGrateConfiguration(params string[] commandline)
{
GrateConfiguration? cfg = null;
var cmd = CommandHandler.Create((GrateConfiguration config) => cfg = config);

var migrateCommand = new MigrateCommand(null!);
ParseResult p = new Parser(migrateCommand).Parse(commandline);
await cmd.InvokeAsync(new InvocationContext(p));
var parseResult = migrateCommand.Parse(commandline);

return cfg;
return Task.FromResult<GrateConfiguration?>(migrateCommand.GetConfiguration(parseResult));
}

private static KnownFolderNamesWithDescription? Wrap(KnownFolderNames? names, [CallerArgumentExpression(nameof(names))] string description = "") =>
Expand Down
Loading
Loading