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
57 changes: 57 additions & 0 deletions src/DiffEngine.Tests/ToolOrderDuplicateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/// <summary>
/// A repeated name in DiffEngine_ToolOrder.
/// <para>
/// Sort looked each requested tool up in Definitions, which holds every tool whether it is
/// installed or not - so the lookup only ever failed on a second occurrence, because the first had
/// removed it from the list. That was then reported as "is not installed", which was untrue, and
/// since DiffTools resolves the order in a static constructor it was also permanent: every later
/// use of DiffTools in that process threw TypeInitializationException.
/// </para>
/// </summary>
[NotInParallel]
public class ToolOrderDuplicateTests :
IDisposable
{
[Test]
public async Task ARepeatedToolIsNotAnUninstalledOne()
{
// An installed one, chosen from what this machine actually has, since throwForNoTool is
// the flag under test and naming an absent tool would throw for the right reason
var installed = DiffTools.Resolved.FirstOrDefault(_ => _.Tool != null)?.Tool;
if (installed == null)
{
// No built in tool resolved here, so there is nothing to repeat
return;
}

DiffTools.UseOrder(true, installed.Value, installed.Value);

// Still ordered, rather than dropped along with the duplicate
await Assert.That(DiffTools.Resolved.Any(_ => _.Tool == installed)).IsTrue();
}

/// <summary>
/// And the flag still means what it says for a tool that genuinely is not here.
/// </summary>
[Test]
public async Task AnUninstalledToolStillThrows()
{
var installed = DiffTools.Resolved
.Where(_ => _.Tool != null)
.Select(_ => _.Tool!.Value)
.ToHashSet();

var absent = Enum.GetValues<DiffTool>().FirstOrDefault(_ => !installed.Contains(_));
if (installed.Count == Enum.GetValues<DiffTool>().Length)
{
// Every tool installed, which no real machine is
return;
}

await Assert.That(() => DiffTools.UseOrder(true, absent))
.Throws<Exception>();
}

public void Dispose() =>
DiffTools.Reset();
}
14 changes: 12 additions & 2 deletions src/DiffEngine/DiffTools_Add.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ static void InitTools(bool throwForNoTool, IEnumerable<DiffTool> order)
firstTextTool = null;
resolved.Clear();

foreach (var definition in ToolsOrder.Sort(throwForNoTool, order).Reverse())
foreach (var (definition, requested) in ToolsOrder.Sort(order).Reverse())
{
var tool = definition.Tool;
AddTool(
var added = AddTool(
tool.ToString(),
tool,
definition.AutoRefresh,
Expand All @@ -133,6 +133,16 @@ static void InitTools(bool throwForNoTool, IEnumerable<DiffTool> order)
definition.UseShellExecute,
definition.CreateNoWindow,
definition.KillLockingProcess);

// Here rather than in Sort, because this is where being installed is decided: Sort
// works from Definitions, which holds every tool whether it is on the machine or not,
// so it could never answer this question
if (added == null &&
requested &&
throwForNoTool)
{
throw new($"`DiffEngine_ToolOrder` is configured to use '{tool}' but it is not installed.");
}
}

custom.Reverse();
Expand Down
27 changes: 16 additions & 11 deletions src/DiffEngine/ToolsOrder.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
static class ToolsOrder
{
public static IEnumerable<Definition> Sort(bool throwForNoTool, IEnumerable<DiffTool> order)
/// <summary>
/// The requested tools first, in the order asked for, then everything else. Each is flagged
/// with whether it was asked for, so the caller can tell a tool the user named and could not
/// have from one that simply is not installed on this machine and was never mentioned.
/// </summary>
public static IEnumerable<(Definition Definition, bool Requested)> Sort(IEnumerable<DiffTool> order)
{
var allTools = Definitions.Tools.ToList();
foreach (var diffTool in order)
// Distinct, because a repeated name is a typo rather than a request for two of something.
// Without it the second occurrence found nothing - the first had already removed it - and
// that was reported as "is not installed", which was both untrue and, from a static
// constructor, permanent: DiffEngine_ToolOrder=VisualStudio,VisualStudio turned every
// later use of DiffTools into a TypeInitializationException
foreach (var diffTool in order.Distinct())
{
var definition = allTools.SingleOrDefault(_ => _.Tool == diffTool);
if (definition == null)
{
if (!throwForNoTool)
{
continue;
}

throw new($"`DiffEngine_ToolOrder` is configured to use '{diffTool}' but it is not installed.");
continue;
}

yield return definition;
yield return (definition, true);
allTools.Remove(definition);
}

foreach (var definition in allTools)
{
yield return definition;
yield return (definition, false);
}
}
}
}
Loading