This commit is contained in:
George Duckett 2018-02-22 09:43:00 +00:00
commit 5fbf64f31b
28 changed files with 525 additions and 99 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@ bld/
[Bb]in/
[Oo]bj/
[Ll]og/
BenchmarkDotNet.Artifacts/
# Visual Studio 2015 cache/options directory
.vs/

View File

@ -13,7 +13,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier.Test", "tes
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{455921D3-DD54-4355-85CF-F4009DF2AB70}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackTrace", "sample\StackTrace\StackTrace.csproj", "{E161FC12-53C2-47CD-A5FC-3684B86723A9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackTrace", "sample\StackTrace\StackTrace.csproj", "{E161FC12-53C2-47CD-A5FC-3684B86723A9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5937ACDF-0059-488E-9604-D84689C72933}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
build.ps1 = build.ps1
directory.build.props = directory.build.props
README.md = README.md
version.json = version.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier.Benchmarks", "test\Ben.Demystifier.Benchmarks\Ben.Demystifier.Benchmarks.csproj", "{EF5557DF-C48E-4999-846C-D99A92E86373}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -33,6 +44,10 @@ Global
{E161FC12-53C2-47CD-A5FC-3684B86723A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.Build.0 = Release|Any CPU
{EF5557DF-C48E-4999-846C-D99A92E86373}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{EF5557DF-C48E-4999-846C-D99A92E86373}.Debug|Any CPU.Build.0 = Release|Any CPU
{EF5557DF-C48E-4999-846C-D99A92E86373}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF5557DF-C48E-4999-846C-D99A92E86373}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -41,6 +56,7 @@ Global
{5410A056-89AB-4912-BD1E-A63616AD91D0} = {A2FCCAAC-BE90-4F7E-B95F-A72D46DDD6B3}
{B9E150B0-AEEB-4D98-8BE1-92C1296699A2} = {59CA6310-4AA5-4093-95D4-472B94DC0CD4}
{E161FC12-53C2-47CD-A5FC-3684B86723A9} = {455921D3-DD54-4355-85CF-F4009DF2AB70}
{EF5557DF-C48E-4999-846C-D99A92E86373} = {59CA6310-4AA5-4093-95D4-472B94DC0CD4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {841B7D5F-E810-4F94-A529-002C7E075216}

View File

@ -158,3 +158,11 @@ Which is far less helpful, and close to jibberish in places
* **return types**
Skipped entirely from method signature
### Benchmarks
To run benchmarks from the repository root:
```
dotnet run -p .\test\Ben.Demystifier.Benchmarks\ -c Release -f netcoreapp2.0 All
```
<sub>Note: we're only kicking off via `netcoreapp2.0`, benchmarks will run for all configured platforms like `net462`.</sub>

View File

@ -1,7 +1,5 @@
image: Visual Studio 2017
shallow_clone: true
branches:
only:
- master
@ -22,7 +20,7 @@ nuget:
disable_publish_on_pr: true
build_script:
- ps: .\build.ps1 -target appveyor -buildAssemblyVersion ($env:BuildVersion + $env:APPVEYOR_BUILD_NUMBER) -buildSemanticVersion ($env:BuildSemanticVersion + $env:APPVEYOR_BUILD_NUMBER)
- ps: .\build.ps1 -target appveyor
test: off

View File

@ -22,11 +22,6 @@ function Exec
if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse }
exec { & dotnet restore }
$revision = @{ $true = $env:APPVEYOR_BUILD_NUMBER; $false = 1 }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
$revision = "{0:D4}" -f [convert]::ToInt32($revision, 10)
exec { & dotnet test .\test\Ben.Demystifier.Test -c Release }
exec { & dotnet pack .\src\Ben.Demystifier -c Release -o .\artifacts --version-suffix=$revision }
exec { & dotnet pack .\src\Ben.Demystifier -c Release -o .\artifacts }

5
directory.build.props Normal file
View File

@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
</Project>

View File

@ -5,14 +5,6 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Ben.Demystifier\Ben.Demystifier.csproj" />
</ItemGroup>

View File

@ -9,17 +9,19 @@
<PackageProjectUrl>https://github.com/benaadams/Ben.Demystifier</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/benaadams/Ben.Demystifier/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryType>git</RepositoryType>
<IncludeSymbols>true</IncludeSymbols>
<IncludeSource>true</IncludeSource>
<Version>0.0.7</Version>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45</TargetFrameworks>
<LangVersion>7.1</LangVersion>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="2.1.17" PrivateAssets="all" />
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.7.6" PrivateAssets="all" />
<PackageReference Include="System.Reflection.Metadata">
<Version>1.5.0</Version>
</PackageReference>

View File

@ -19,7 +19,6 @@ namespace System.Diagnostics
{
public partial class EnhancedStackTrace
{
private static List<EnhancedStackFrame> GetFrames(Exception exception)
{
if (exception == null)
@ -516,7 +515,7 @@ namespace System.Diagnostics
{
foreach (var attrib in attribs)
{
if (attrib is Attribute att && att.GetType().Namespace == "System.Runtime.CompilerServices" && att.GetType().Name == "IsReadOnlyAttribute")
if (attrib is Attribute att && att.GetType().IsReadOnlyAttribute())
{
return "in";
}
@ -579,6 +578,32 @@ namespace System.Diagnostics
}
private static ResolvedParameter GetValueTupleParameter(IList<string> tupleNames, string prefix, string name, Type parameterType)
{
string typeName;
if (parameterType.IsValueTuple())
{
typeName = GetValueTupleParameterName(tupleNames, parameterType);
}
else
{
// Need to unwrap the first generic argument first.
var genericTypeName = TypeNameHelper.GetTypeNameForGenericType(parameterType);
var valueTupleFullName = GetValueTupleParameterName(tupleNames, parameterType.GetGenericArguments()[0]);
typeName = $"{genericTypeName}<{valueTupleFullName}>";
}
return new ResolvedParameter
{
Prefix = prefix,
Name = name,
Type = typeName,
ResolvedType = parameterType,
};
}
private static string GetValueTupleParameterName(IList<string> tupleNames, Type parameterType)
{
var sb = new StringBuilder();
sb.Append("(");
@ -608,14 +633,7 @@ namespace System.Diagnostics
}
sb.Append(")");
return new ResolvedParameter
{
Prefix = prefix,
Name = name,
Type = sb.ToString(),
ResolvedType = parameterType,
};
return sb.ToString();
}
private static bool ShowInStackTrace(MethodBase method)

View File

@ -1,9 +1,10 @@
// Copyright (c) Ben A Adams. All rights reserved.
// Copyright (c) Ben A Adams. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Generic;
using System.Collections.Generic.Enumerable;
using System.IO;
using System.Text;
namespace System.Diagnostics
@ -104,21 +105,9 @@ namespace System.Diagnostics
var filePath = frame.GetFileName();
if (!string.IsNullOrEmpty(filePath))
{
try
{
sb.Append(" in ");
var uri = new Uri(filePath);
if (uri.IsFile)
{
sb.Append(System.IO.Path.GetFullPath(filePath));
}
else
{
sb.Append(uri);
}
}
catch
{ }
sb.Append(" in ");
sb.Append(TryGetFullPath(filePath));
}
var lineNo = frame.GetFileLineNumber();
@ -133,5 +122,27 @@ namespace System.Diagnostics
EnumerableIList<EnhancedStackFrame> GetEnumerator() => EnumerableIList.Create(_frames);
IEnumerator<EnhancedStackFrame> IEnumerable<EnhancedStackFrame>.GetEnumerator() => _frames.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator();
/// <summary>
/// Tries to convert a given <paramref name="filePath"/> to a full path.
/// Returns original value if the conversion isn't possible or a given path is relative.
/// </summary>
public static string TryGetFullPath(string filePath)
{
try
{
var uri = new Uri(filePath);
if (uri.IsFile)
{
return uri.AbsolutePath;
}
return uri.ToString();
}
catch (ArgumentException) { }
catch (UriFormatException) { }
return filePath;
}
}
}

View File

@ -1,35 +1,54 @@
// Copyright (c) Ben A Adams. All rights reserved.
// Copyright (c) Ben A Adams. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Generic.Enumerable;
using System.Reflection;
namespace System.Diagnostics
{
/// <nodoc />
public static class ExceptionExtentions
{
private static readonly FieldInfo stackTraceString = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);
public static T Demystify<T>(this T exception) where T : Exception
=> Demystify(exception, originalStacksTracker: null);
private static string GetStackTracesString(this Exception exception)
=> (string)stackTraceString.GetValue(exception);
private static void SetStackTracesString(this Exception exception, string value)
=> stackTraceString.SetValue(exception, value);
/// <summary>
/// Demystifies the given <paramref name="exception"/> and tracks the original stack traces for the whole exception tree.
/// </summary>
private static T Demystify<T>(this T exception, Dictionary<Exception, string> originalStacksTracker) where T : Exception
{
try
{
if (originalStacksTracker?.ContainsKey(exception) == false)
{
originalStacksTracker[exception] = exception.GetStackTracesString();
}
var stackTrace = new EnhancedStackTrace(exception);
if (stackTrace.FrameCount > 0)
{
stackTraceString.SetValue(exception, stackTrace.ToString());
exception.SetStackTracesString(stackTrace.ToString());
}
if (exception is AggregateException aggEx)
{
foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions))
{
ex.Demystify();
ex.Demystify(originalStacksTracker);
}
}
exception.InnerException?.Demystify();
exception.InnerException?.Demystify(originalStacksTracker);
}
catch
{
@ -38,5 +57,39 @@ namespace System.Diagnostics
return exception;
}
/// <summary>
/// Gets demystified string representation of the <paramref name="exception"/>.
/// </summary>
/// <remarks>
/// <see cref="Demystify{T}"/> method mutates the exception instance that can cause
/// issues if a system relies on the stack trace be in the specific form.
/// Unlike <see cref="Demystify{T}"/> this method is pure. It calls <see cref="Demystify{T}"/> first,
/// computes a demystified string representation and then restores the original state of the exception back.
/// </remarks>
[Contracts.Pure]
public static string ToStringDemystified(this Exception exception)
{
try
{
var originalStacks = new Dictionary<Exception, string>();
exception.Demystify(originalStacks);
var result = exception.ToString();
foreach (var kvp in originalStacks)
{
kvp.Key.SetStackTracesString(kvp.Value);
}
return result;
}
catch
{
// Processing exceptions shouldn't throw exceptions; if it fails
}
return exception.ToString();
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) Ben A Adams. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection;
using System.Reflection.Emit;
namespace System.Diagnostics.Internal
{
/// <summary>
/// A helper class that contains utilities methods for dealing with reflection.
/// </summary>
public static class ReflectionHelper
{
/// <summary>
/// Returns true if <paramref name="type"/> is <code>System.Runtime.CompilerServices.IsReadOnlyAttribute</code>.
/// </summary>
public static bool IsReadOnlyAttribute(this Type type)
{
return type.Namespace == "System.Runtime.CompilerServices" && type.Name == "IsReadOnlyAttribute";
}
/// <summary>
/// Returns true if the <paramref name="type"/> is a value tuple type.
/// </summary>
public static bool IsValueTuple(this Type type)
{
return type.Namespace == "System" && type.Name.Contains("ValueTuple`");
}
}
}

View File

@ -43,6 +43,22 @@ namespace System.Diagnostics
return builder.ToString();
}
/// <summary>
/// Returns a name of given generic type without '`'.
/// </summary>
public static string GetTypeNameForGenericType(Type type)
{
if (!type.IsGenericType)
{
throw new ArgumentException("The given type should be generic", nameof(type));
}
var genericPartIndex = type.Name.IndexOf('`');
Debug.Assert(genericPartIndex >= 0);
return type.Name.Substring(0, genericPartIndex);
}
private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options)
{
if (type.IsGenericType)

BIN
src/Ben.Demystifier/key.snk Normal file

Binary file not shown.

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net462</TargetFrameworks>
<Configuration>Release</Configuration>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Ben.Demystifier\Ben.Demystifier.csproj" />
<PackageReference Include="BenchmarkDotNet" Version="0.10.12" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,18 @@
using System;
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Jobs;
namespace Ben.Demystifier.Benchmarks
{
[ClrJob, CoreJob]
[Config(typeof(Config))]
public class ExceptionTests
{
[Benchmark(Baseline = true, Description = ".ToString()")]
public string Baseline() => new Exception().ToString();
[Benchmark(Description = "Demystify().ToString()")]
public string Demystify() => new Exception().Demystify().ToString();
}
}

View File

@ -0,0 +1,48 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Running;
using System;
using System.Linq;
using System.Reflection;
namespace Ben.Demystifier.Benchmarks
{
public static class Program
{
private const string BenchmarkSuffix = "Tests";
public static void Main(string[] args)
{
var benchmarks = Assembly.GetEntryAssembly()
.DefinedTypes.Where(t => t.Name.EndsWith(BenchmarkSuffix))
.ToDictionary(t => t.Name.Substring(0, t.Name.Length - BenchmarkSuffix.Length), t => t, StringComparer.OrdinalIgnoreCase);
if (args.Length > 0 && args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Running full benchmarks suite");
benchmarks.Select(pair => pair.Value).ToList().ForEach(action => BenchmarkRunner.Run(action));
return;
}
if (args.Length == 0 || !benchmarks.ContainsKey(args[0]))
{
Console.WriteLine("Please, select benchmark, list of available:");
benchmarks
.Select(pair => pair.Key)
.ToList()
.ForEach(Console.WriteLine);
Console.WriteLine("All");
return;
}
BenchmarkRunner.Run(benchmarks[args[0]]);
Console.Read();
}
}
internal class Config : ManualConfig
{
public Config() => Add(new MemoryDiagnoser());
}
}

View File

@ -2,12 +2,11 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;
namespace Demystify
namespace Ben.Demystifier.Test
{
public class AggregateException
{
@ -34,11 +33,11 @@ namespace Demystify
// Assert
var stackTrace = demystifiedException.ToString();
stackTrace = ReplaceLineEndings.Replace(stackTrace, "");
stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace);
var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
// Remove items that vary between test runners
.Where(s =>
s != " at Task Demystify.DynamicCompilation.DoesNotPreventStackTrace()+() => { }" &&
s != " at Task Ben.Demystifier.Test.DynamicCompilation.DoesNotPreventStackTrace()+() => { }" &&
!s.Contains("System.Threading.Tasks.Task.WaitAll")
)
.Skip(1)
@ -47,19 +46,19 @@ namespace Demystify
.Replace("<---", "");
var expected = string.Join("", new[] {
" at async Task Demystify.AggregateException.Throw1()",
" at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?) => { }",
" at async Task Ben.Demystifier.Test.AggregateException.Throw1()",
" at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }",
" --- End of inner exception stack trace ---",
" at void Demystify.AggregateException.DemystifiesAggregateExceptions()",
" at void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()",
"---> (Inner Exception #0) System.ArgumentException: Value does not fall within the expected range.",
" at async Task Demystify.AggregateException.Throw1()",
" at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?) => { }",
" at async Task Ben.Demystifier.Test.AggregateException.Throw1()",
" at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }",
"---> (Inner Exception #1) System.NullReferenceException: Object reference not set to an instance of an object.",
" at async Task Demystify.AggregateException.Throw2()",
" at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?) => { }",
" at async Task Ben.Demystifier.Test.AggregateException.Throw2()",
" at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }",
"---> (Inner Exception #2) System.InvalidOperationException: Operation is not valid due to the current state of the object.",
" at async Task Demystify.AggregateException.Throw3()",
" at async void Demystify.AggregateException.DemystifiesAggregateExceptions()+(?) => { }"});
" at async Task Ben.Demystifier.Test.AggregateException.Throw3()",
" at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }"});
Assert.Equal(expected, trace);
}
@ -81,8 +80,5 @@ namespace Demystify
await Task.Delay(1).ConfigureAwait(false);
throw new InvalidOperationException();
}
private Regex ReplaceLineEndings = new Regex(" in [^\n\r]+");
}
}

View File

@ -7,9 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0-preview-20170810-02" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
<ItemGroup>

View File

@ -6,7 +6,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;
namespace Demystify
namespace Ben.Demystifier.Test
{
public class DynamicCompilation
{
@ -37,12 +37,12 @@ namespace Demystify
// Assert
var stackTrace = demystifiedException.ToString();
stackTrace = ReplaceLineEndings.Replace(stackTrace, "");
stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace);
var trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
// Remove items that vary between test runners
.Where(s =>
s != " at void System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object state)" &&
s != " at Task Demystify.DynamicCompilation.DoesNotPreventStackTrace()+() => { }"
s != " at Task Ben.Demystifier.Test.DynamicCompilation.DoesNotPreventStackTrace()+() => { }"
)
.ToArray();
@ -50,10 +50,8 @@ namespace Demystify
new[] {
"System.ArgumentException: Message",
" at void lambda_method(Closure)",
" at async Task Demystify.DynamicCompilation.DoesNotPreventStackTrace()"},
" at async Task Ben.Demystifier.Test.DynamicCompilation.DoesNotPreventStackTrace()"},
trace);
}
private Regex ReplaceLineEndings = new Regex(" in [^\n\r]+");
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Xunit;
namespace Ben.Demystifier.Test
{
public class EnhancedStackTraceTests
{
[Theory]
[InlineData(@"file://Sources\MySolution\Foo.cs", @"/MySolution/Foo.cs")]
[InlineData(@"d:\Public\Src\Foo.cs", @"d:/Public/Src/Foo.cs")]
// To be deterministic, the C# compiler can take a /pathmap command line option.
// This option force the compiler to emit the same bits even when their built from the
// differrent locations.
// The binaries built with the pathmap usually don't have an absolute path,
// but have some prefix like \.\.
// This test case makes sure that EhancedStackTrace can deal with such kind of paths.
[InlineData(@"\.\Public\Src\Foo.cs", @"/./Public/Src/Foo.cs")]
public void RelativePathIsConvertedToAnAbsolutePath(string original, string expected)
{
var converted = EnhancedStackTrace.TryGetFullPath(original);
Assert.Equal(expected, NormalizePath(converted));
}
// Used in tests to avoid platform-specific issues.
private static string NormalizePath(string path)
=> path.Replace("\\", "/");
}
}

View File

@ -0,0 +1,14 @@
using System.Text.RegularExpressions;
namespace Ben.Demystifier.Test
{
internal static class LineEndingsHelper
{
private static readonly Regex ReplaceLineEndings = new Regex(" in [^\n\r]+");
public static string RemoveLineEndings(string original)
{
return ReplaceLineEndings.Replace(original, "");
}
}
}

View File

@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xunit;
namespace Demystify
namespace Ben.Demystifier.Test
{
public class MixedStack
{
@ -57,16 +57,16 @@ namespace Demystify
static List<string> ExpectedCallStack = new List<string>()
{
"IEnumerable<string> Demystify.MixedStack.Iterator()+MoveNext()",
"IEnumerable<string> Ben.Demystifier.Test.MixedStack.Iterator()+MoveNext()",
"string string.Join(string separator, IEnumerable<string> values)",
"string Demystify.MixedStack+GenericClass<T>.GenericMethod<V>(ref V value)",
"async Task<string> Demystify.MixedStack.MethodAsync(int value)",
"async Task<string> Demystify.MixedStack.MethodAsync<TValue>(TValue value)",
"(string val, bool) Demystify.MixedStack.Method(string value)",
"ref string Demystify.MixedStack.RefMethod(string value)",
"(string val, bool) Demystify.MixedStack.s_func(string s, bool b)",
"void Demystify.MixedStack.s_action(string s, bool b)",
"void Demystify.MixedStack.Start((string val, bool) param)"
"string Ben.Demystifier.Test.MixedStack+GenericClass<T>.GenericMethod<V>(ref V value)",
"async Task<string> Ben.Demystifier.Test.MixedStack.MethodAsync(int value)",
"async Task<string> Ben.Demystifier.Test.MixedStack.MethodAsync<TValue>(TValue value)",
"(string val, bool) Ben.Demystifier.Test.MixedStack.Method(string value)",
"ref string Ben.Demystifier.Test.MixedStack.RefMethod(string value)",
"(string val, bool) Ben.Demystifier.Test.MixedStack.s_func(string s, bool b)",
"void Ben.Demystifier.Test.MixedStack.s_action(string s, bool b)",
"void Ben.Demystifier.Test.MixedStack.Start((string val, bool) param)"
};

View File

@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;
namespace Demystify
namespace Ben.Demystifier.Test
{
public class NonThrownException
{
@ -28,14 +28,14 @@ namespace Demystify
// Assert
var stackTrace = demystifiedException.ToString();
stackTrace = ReplaceLineEndings.Replace(stackTrace, "");
stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace);
var trace = stackTrace.Split(new[]{Environment.NewLine}, StringSplitOptions.None);
Assert.Equal(
new[] {
"System.Exception: Exception of type 'System.Exception' was thrown. ---> System.Exception: Exception of type 'System.Exception' was thrown.",
" at Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }",
" at async Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()",
" at Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }",
" at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()",
" --- End of inner exception stack trace ---"},
trace);
@ -51,16 +51,16 @@ namespace Demystify
// Assert
stackTrace = demystifiedException.ToString();
stackTrace = ReplaceLineEndings.Replace(stackTrace, "");
stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace);
trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
Assert.Equal(
new[] {
"System.Exception: Exception of type 'System.Exception' was thrown. ---> System.Exception: Exception of type 'System.Exception' was thrown.",
" at Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }",
" at async Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()",
" at Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }",
" at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()",
" --- End of inner exception stack trace ---",
" at async Task Demystify.NonThrownException.DoesNotPreventThrowStackTrace()"
" at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()"
},
trace);
}
@ -76,7 +76,7 @@ namespace Demystify
// Assert
var stackTrace = est.ToString();
stackTrace = ReplaceLineEndings.Replace(stackTrace, "");
stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace);
var trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
// Remove Full framework entries
.Where(s => !s.StartsWith(" at bool System.Threading._ThreadPoolWaitCallbac") &&
@ -88,7 +88,5 @@ namespace Demystify
" at bool System.Threading.ThreadPoolWorkQueue.Dispatch()"},
trace);
}
private Regex ReplaceLineEndings = new Regex(" in [^\n\r]+");
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Diagnostics.Internal;
using Xunit;
namespace Ben.Demystifier.Test
{
public class ReflectionHelperTest
{
[Fact]
public void IsValueTupleReturnsTrueForTupleWith1Element()
{
Assert.True(typeof(ValueTuple<int>).IsValueTuple());
}
[Fact]
public void IsValueTupleReturnsTrueForTupleWith1ElementWithOpenedType()
{
Assert.True(typeof(ValueTuple<>).IsValueTuple());
}
[Fact]
public void IsValueTupleReturnsTrueForTupleWith6ElementsWithOpenedType()
{
Assert.True(typeof(ValueTuple<,,,,,>).IsValueTuple());
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
namespace Ben.Demystifier.Test
{
public sealed class ToDemystifiedStringTests
{
private readonly ITestOutputHelper _output;
public ToDemystifiedStringTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void DemystifyShouldNotAffectTheOriginalStackTrace()
{
try
{
SimpleMethodThatThrows().Wait();
}
catch (Exception e)
{
var original = e.ToString();
var stringDemystified = e.ToStringDemystified();
_output.WriteLine("Demystified: ");
_output.WriteLine(stringDemystified);
_output.WriteLine("Original: ");
var afterDemystified = e.ToString();
_output.WriteLine(afterDemystified);
Assert.Equal(original, afterDemystified);
}
async Task SimpleMethodThatThrows()
{
throw new InvalidOperationException("message");
}
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Xunit;
namespace Ben.Demystifier.Test
{
public class TuplesTests
{
[Fact]
public void DemistifiesAsyncMethodWithTuples()
{
Exception demystifiedException = null;
try
{
AsyncThatReturnsTuple().GetAwaiter().GetResult();
}
catch (Exception ex)
{
demystifiedException = ex.Demystify();
}
// Assert
var stackTrace = demystifiedException.ToString();
stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace);
var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries));
var expected = string.Join("", new[] {
"System.ArgumentException: Value does not fall within the expected range.",
" at async Task<(int left, int right)> Ben.Demystifier.Test.TuplesTests.AsyncThatReturnsTuple()",
" at void Ben.Demystifier.Test.TuplesTests.DemistifiesAsyncMethodWithTuples()"});
Assert.Equal(expected, trace);
}
[Fact]
public void DemistifiesListOfTuples()
{
Exception demystifiedException = null;
try
{
ListOfTuples();
}
catch (Exception ex)
{
demystifiedException = ex.Demystify();
}
// Assert
var stackTrace = demystifiedException.ToString();
stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace);
var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries));
var expected = string.Join("", new[] {
"System.ArgumentException: Value does not fall within the expected range.",
" at List<(int left, int right)> Ben.Demystifier.Test.TuplesTests.ListOfTuples()",
" at void Ben.Demystifier.Test.TuplesTests.DemistifiesListOfTuples()"});
Assert.Equal(expected, trace);
}
async Task<(int left, int right)> AsyncThatReturnsTuple()
{
await Task.Delay(1).ConfigureAwait(false);
throw new ArgumentException();
}
List<(int left, int right)> ListOfTuples()
{
throw new ArgumentException();
}
}
}

17
version.json Normal file
View File

@ -0,0 +1,17 @@
{
"version": "0.1",
"publicReleaseRefSpec": [
"^refs/heads/master$", // we release out of master
"^refs/heads/dev$", // we release out of develop
"^refs/tags/v\\d+\\.\\d+" // we also release tags starting with vN.N
],
"nugetPackageVersion":{
"semVer": 2
},
"cloudBuild": {
"buildNumber": {
"enabled": true,
"setVersionVariables": true
}
}
}