Create unit tests for Calculator plugin (#6356)

* Refactored logic and made it unit testable

* Changes after code review

* Added to build steps, and modified bracket to new class with unittest. Validates complexer cases now.

Co-authored-by: p-storm <paul.de.man@gmail.com>
This commit is contained in:
P-Storm
2020-09-10 05:01:30 +02:00
committed by GitHub
parent cfda69a120
commit 3137aaa660
11 changed files with 519 additions and 85 deletions

View File

@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Plugin.Calculator
{
public static class BracketHelper
{
public static bool IsBracketComplete(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
return true;
}
var valueTuples = query
.Select(BracketTrail)
.Where(r => r != default);
var trailTest = new Stack<TrailType>();
foreach (var (direction, type) in valueTuples)
{
switch (direction)
{
case TrailDirection.Open:
trailTest.Push(type);
break;
case TrailDirection.Close:
// Try to get item out of stack
if (!trailTest.TryPop(out var popped))
{
return false;
}
if (type != popped)
{
return false;
}
continue;
default:
{
throw new ArgumentOutOfRangeException(nameof(direction), direction, "Can't process value");
}
}
}
return !trailTest.Any();
}
private static (TrailDirection direction, TrailType type) BracketTrail(char @char)
{
switch (@char)
{
case '(':
return (TrailDirection.Open, TrailType.Round);
case ')':
return (TrailDirection.Close, TrailType.Round);
case '[':
return (TrailDirection.Open, TrailType.Bracket);
case ']':
return (TrailDirection.Close, TrailType.Bracket);
default:
return default;
}
}
private enum TrailDirection
{
None,
Open,
Close,
}
private enum TrailType
{
None,
Bracket,
Round,
}
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Mages.Core;
namespace Microsoft.Plugin.Calculator
{
public class CalculateEngine
{
private readonly Engine _magesEngine = new Engine();
public const int RoundingDigits = 10;
public CalculateResult Interpret(string input)
{
return Interpret(input, CultureInfo.CurrentCulture);
}
public CalculateResult Interpret(string input, CultureInfo cultureInfo)
{
if (!CalculateHelper.InputValid(input))
{
return default;
}
var result = _magesEngine.Interpret(input);
// This could happen for some incorrect queries, like pi(2)
if (result == null)
{
return default;
}
result = TransformResult(result);
if (string.IsNullOrEmpty(result?.ToString()))
{
return default;
}
var decimalResult = Convert.ToDecimal(result, cultureInfo);
var roundedResult = Math.Round(decimalResult, RoundingDigits, MidpointRounding.AwayFromZero);
return new CalculateResult()
{
Result = decimalResult,
RoundedResult = roundedResult,
};
}
private static object TransformResult(object result)
{
if (result.ToString() == "NaN")
{
return Properties.Resources.wox_plugin_calculator_not_a_number;
}
if (result is Function)
{
return Properties.Resources.wox_plugin_calculator_expression_not_complete;
}
return result;
}
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text.RegularExpressions;
namespace Microsoft.Plugin.Calculator
{
public static class CalculateHelper
{
private static readonly Regex RegValidExpressChar = new Regex(
@"^(" +
@"ceil|floor|exp|pi|e|max|min|det|abs|log|ln|sqrt|" +
@"sin|cos|tan|arcsin|arccos|arctan|" +
@"eigval|eigvec|eig|sum|polar|plot|round|sort|real|zeta|" +
@"bin2dec|hex2dec|oct2dec|" +
@"==|~=|&&|\|\||" +
@"[ei]|[0-9]|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
@")+$", RegexOptions.Compiled);
public static bool InputValid(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentNullException(paramName: nameof(input));
}
if (input.Length <= 2)
{
return false;
}
if (!RegValidExpressChar.IsMatch(input))
{
return false;
}
if (!BracketHelper.IsBracketComplete(input))
{
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
namespace Microsoft.Plugin.Calculator
{
public struct CalculateResult : IEquatable<CalculateResult>
{
public decimal Result { get; set; }
public decimal RoundedResult { get; set; }
public bool Equals(CalculateResult other)
{
return Result == other.Result && RoundedResult == other.RoundedResult;
}
public override bool Equals(object obj)
{
return obj is CalculateResult other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Result, RoundedResult);
}
public static bool operator ==(CalculateResult left, CalculateResult right)
{
return left.Equals(right);
}
public static bool operator !=(CalculateResult left, CalculateResult right)
{
return !(left == right);
}
}
}

View File

@@ -5,11 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using Mages.Core;
using Wox.Infrastructure.Logger;
using Wox.Plugin;
@@ -17,18 +12,7 @@ namespace Microsoft.Plugin.Calculator
{
public class Main : IPlugin, IPluginI18n, IDisposable
{
private static readonly Regex RegValidExpressChar = new Regex(
@"^(" +
@"ceil|floor|exp|pi|e|max|min|det|abs|log|ln|sqrt|" +
@"sin|cos|tan|arcsin|arccos|arctan|" +
@"eigval|eigvec|eig|sum|polar|plot|round|sort|real|zeta|" +
@"bin2dec|hex2dec|oct2dec|" +
@"==|~=|&&|\|\||" +
@"[ei]|[0-9]|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
@")+$", RegexOptions.Compiled);
private static readonly Regex RegBrackets = new Regex(@"[\(\)\[\]]", RegexOptions.Compiled);
private static readonly Engine MagesEngine = new Engine();
private static readonly CalculateEngine CalculateEngine = new CalculateEngine();
private PluginInitContext Context { get; set; }
@@ -43,68 +27,25 @@ namespace Microsoft.Plugin.Calculator
throw new ArgumentNullException(paramName: nameof(query));
}
if (query.Search.Length <= 2 // don't affect when user only input "e" or "i" keyword
|| !RegValidExpressChar.IsMatch(query.Search)
|| !IsBracketComplete(query.Search))
if (!CalculateHelper.InputValid(query.Search))
{
return new List<Result>();
}
try
{
var result = MagesEngine.Interpret(query.Search);
var result = CalculateEngine.Interpret(query.Search, CultureInfo.CurrentUICulture);
// This could happen for some incorrect queries, like pi(2)
if (result == null)
if (result.Equals(default(CalculateResult)))
{
return new List<Result>();
}
if (result.ToString() == "NaN")
return new List<Result>
{
result = Properties.Resources.wox_plugin_calculator_not_a_number;
}
if (result is Function)
{
result = Properties.Resources.wox_plugin_calculator_expression_not_complete;
}
if (!string.IsNullOrEmpty(result?.ToString()))
{
var roundedResult = Math.Round(Convert.ToDecimal(result, CultureInfo.CurrentCulture), 10, MidpointRounding.AwayFromZero);
return new List<Result>
{
new Result
{
Title = roundedResult.ToString(CultureInfo.CurrentCulture),
IcoPath = IconPath,
Score = 300,
SubTitle = Properties.Resources.wox_plugin_calculator_copy_number_to_clipboard,
Action = c =>
{
var ret = false;
var thread = new Thread(() =>
{
try
{
Clipboard.SetText(result.ToString());
ret = true;
}
catch (ExternalException)
{
MessageBox.Show(Properties.Resources.wox_plugin_calculator_copy_failed);
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return ret;
},
},
};
}
ResultHelper.CreateResult(result.Result, result.RoundedResult, IconPath),
};
} // We want to keep the process alive if any the mages library throws any exceptions.
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception e)
@@ -116,25 +57,6 @@ namespace Microsoft.Plugin.Calculator
return new List<Result>();
}
private static bool IsBracketComplete(string query)
{
var matchs = RegBrackets.Matches(query);
var leftBracketCount = 0;
foreach (Match match in matchs)
{
if (match.Value == "(" || match.Value == "[")
{
leftBracketCount++;
}
else
{
leftBracketCount--;
}
}
return leftBracketCount == 0;
}
public void Init(PluginInitContext context)
{
Context = context ?? throw new ArgumentNullException(paramName: nameof(context));

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using Wox.Plugin;
namespace Microsoft.Plugin.Calculator
{
public static class ResultHelper
{
public static Result CreateResult(CalculateResult result, string iconPath)
{
return CreateResult(result.Result, result.RoundedResult, iconPath);
}
public static Result CreateResult(decimal result, decimal roundedResult, string iconPath)
{
return new Result
{
Title = roundedResult.ToString(CultureInfo.CurrentCulture),
IcoPath = iconPath,
Score = 300,
SubTitle = Properties.Resources.wox_plugin_calculator_copy_number_to_clipboard,
Action = c => Action(result),
};
}
public static bool Action(decimal result)
{
var ret = false;
var thread = new Thread(() =>
{
try
{
Clipboard.SetText(result.ToString(CultureInfo.CurrentUICulture.NumberFormat));
ret = true;
}
catch (ExternalException)
{
MessageBox.Show(Properties.Resources.wox_plugin_calculator_copy_failed);
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return ret;
}
}
}