Skip to content
Open
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
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions src/dotnet-repl.sln → dotnet-repl.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31112.23
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-repl", "dotnet-repl\dotnet-repl.csproj", "{D84A3673-0973-4C16-B0A1-1E00BD9146A3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-repl", "src\dotnet-repl\dotnet-repl.csproj", "{D84A3673-0973-4C16-B0A1-1E00BD9146A3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-repl.Tests", "dotnet-repl.Tests\dotnet-repl.Tests.csproj", "{77D12413-604B-47ED-85B0-3CC253AFA9A4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-repl.Tests", "src\dotnet-repl.Tests\dotnet-repl.Tests.csproj", "{77D12413-604B-47ED-85B0-3CC253AFA9A4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7D05BFEB-F647-47E1-B50A-BACE5005B627}"
EndProject
Expand Down
File renamed without changes.
File renamed without changes.
18 changes: 9 additions & 9 deletions src/dotnet-repl.Tests/Automation/NotebookRunnerTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
Expand Down Expand Up @@ -49,22 +48,23 @@ public NotebookRunnerTests()
[Fact]
public async Task When_an_ipynb_is_run_and_no_error_is_produced_then_the_exit_code_is_0()
{
var parseResult =_rootCommand.Parse($"--run \"{_directory}/succeed.ipynb\" --exit-after-run");
parseResult.Configuration.Error = new StringWriter();
var result = await ((AsynchronousCommandLineAction)_rootCommand.Action).InvokeAsync(parseResult);
var error = new StringWriter();
var result = await _rootCommand
.Parse($"--run \"{_directory}/succeed.ipynb\" --exit-after-run")
.InvokeAsync(new() { Output = new StringWriter(), Error = error });

parseResult.Configuration.Error.ToString().Should().BeEmpty();
error.ToString().Should().BeEmpty();
result.Should().Be(0);
}

[Fact]
public async Task When_an_ipynb_is_run_and_an_error_is_produced_from_a_cell_then_the_exit_code_is_2()
{
var parseResult = _rootCommand.Parse($"--run \"{_directory}/fail.ipynb\" --exit-after-run");
parseResult.Configuration.Error = new StringWriter();
var result = await ((AsynchronousCommandLineAction)_rootCommand.Action).InvokeAsync(parseResult);
var error = new StringWriter();
var result = await _rootCommand
.Parse($"--run \"{_directory}/fail.ipynb\" --exit-after-run").InvokeAsync(new() { Output = new StringWriter(), Error = error });

parseResult.Configuration.Error.ToString().Should().BeEmpty();
error.ToString().Should().BeEmpty();
result.Should().Be(2);
}

Expand Down
28 changes: 13 additions & 15 deletions src/dotnet-repl.Tests/CommandLineParserTests.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using dotnet_repl.Tests.Utility;
using FluentAssertions;
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

namespace dotnet_repl.Tests;

public class CommandLineParserTests
public partial class CommandLineParserTests
{
private readonly RootCommand _rootCommand;

Expand All @@ -22,10 +21,9 @@ public CommandLineParserTests()
public void Help_is_snazzy()
{
var parseResult = _rootCommand.Parse("-h");
parseResult.Configuration.Output = new StringWriter();
((SynchronousCommandLineAction)parseResult.Action).Invoke(parseResult);
parseResult.Invoke(new() { Output = new StringWriter() });

var outputLines = parseResult.Configuration
var outputLines = parseResult.InvocationConfiguration
.Output
.ToString()
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
Expand All @@ -34,19 +32,19 @@ public void Help_is_snazzy()
string.Join('\n', outputLines)
.Should().Contain(
"""
 _ _ _____ _____ ____ _____ ____ _ 
 | \ | | | ____| |_ _| | _ \ | ____| | _ \ | | 
 | \| | | _| | | | |_) | | _| | |_) | | | 
 _ | |\ | | |___ | | | _ < | |___ | __/ | |___ 
 (_) |_| \_| |_____| |_| |_| \_\ |_____| |_| |_____|
 
""".Replace("\r", ""));
 _ _ _____ _____ ____ _____ ____ _ 
 | \ | | | ____| |_ _| | _ \ | ____| | _ \ | | 
 | \| | | _| | | | |_) | | _| | |_) | | | 
 _ | |\ | | |___ | | | _ < | |___ | __/ | |___ 
 (_) |_| \_| |_____| |_| |_| \_\ |_____| |_| |_____|
 
""".Replace("\r", ""));
}

[Fact]
public void Parser_configuration_is_valid()
{
_rootCommand.Parse("").Configuration.ThrowIfInvalid();
_rootCommand.ThrowIfInvalid();
}

[Fact]
Expand Down
75 changes: 75 additions & 0 deletions src/dotnet-repl.Tests/Utility/CommandExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Linq;

namespace dotnet_repl.Tests.Utility;

public static class CommandExtensions
{
/// <summary>
/// Throws an exception if the parser configuration is ambiguous or otherwise not valid.
/// </summary>
/// <remarks>Due to the performance cost of this method, it is recommended to be used in unit testing or in scenarios where the parser is configured dynamically at runtime.</remarks>
/// <exception cref="InvalidOperationException">Thrown if the configuration is found to be invalid.</exception>
public static void ThrowIfInvalid(this Command command)
{
if (command.Parents.FlattenBreadthFirst(c => c.Parents).Any(ancestor => ancestor == command))
{
throw new InvalidOperationException($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor.");
}

int count = command.Subcommands.Count + command.Options.Count;
for (var i = 0; i < count; i++)
{
Symbol symbol1 = GetChild(i, command, out HashSet<string> aliases1);

for (var j = i + 1; j < count; j++)
{
Symbol symbol2 = GetChild(j, command, out HashSet<string> aliases2);

if (symbol1.Name.Equals(symbol2.Name, StringComparison.Ordinal)
|| aliases1 is not null && aliases1.Contains(symbol2.Name))
{
throw new InvalidOperationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'.");
}
else if (aliases2 is not null && aliases2.Contains(symbol1.Name))
{
throw new InvalidOperationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'.");
}

if (aliases1 is not null && aliases2 is not null)
{
// take advantage of the fact that we are dealing with two hash sets
if (aliases1.Overlaps(aliases2))
{
foreach (string symbol2Alias in aliases2)
{
if (aliases1.Contains(symbol2Alias))
{
throw new InvalidOperationException($"Duplicate alias '{symbol2Alias}' found on command '{command.Name}'.");
}
}
}
}
}

if (symbol1 is Command childCommand)
{
childCommand.ThrowIfInvalid();
}
}

static Symbol GetChild(int index, Command command, out HashSet<string> aliases)
{
if (index < command.Subcommands.Count)
{
aliases = command.Subcommands[index].Aliases.ToHashSet();
return command.Subcommands[index];
}

aliases = command.Options[index - command.Subcommands.Count].Aliases.ToHashSet();
return command.Options[index - command.Subcommands.Count];
}
}
}
31 changes: 31 additions & 0 deletions src/dotnet-repl.Tests/Utility/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;

namespace dotnet_repl.Tests.Utility;

internal static class EnumerableExtensions
{
internal static IEnumerable<T> FlattenBreadthFirst<T>(
this IEnumerable<T> source,
Func<T, IEnumerable<T>> children)
{
var queue = new Queue<T>();

foreach (var item in source)
{
queue.Enqueue(item);
}

while (queue.Count > 0)
{
var current = queue.Dequeue();

foreach (var option in children(current))
{
queue.Enqueue(option);
}

yield return current;
}
}
}
Loading