diff --git a/.gitignore b/.gitignore index 940794e..a1b7e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ +BenchmarkDotNet.Artifacts/ # Visual Studio 2015 cache/options directory .vs/ diff --git a/Ben.Demystifier.sln b/Ben.Demystifier.sln index dbecaf3..5945254 100644 --- a/Ben.Demystifier.sln +++ b/Ben.Demystifier.sln @@ -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} diff --git a/README.md b/README.md index f2f2946..944d392 100644 --- a/README.md +++ b/README.md @@ -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 +``` +Note: we're only kicking off via `netcoreapp2.0`, benchmarks will run for all configured platforms like `net462`. diff --git a/appveyor.yml b/appveyor.yml index 75a183e..9f08f22 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -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 @@ -31,4 +29,4 @@ deploy: off artifacts: - path: artifacts/build - path: artifacts/packages -- path: artifacts/test \ No newline at end of file +- path: artifacts/test diff --git a/build.ps1 b/build.ps1 index d0bacc3..7b5ffcc 100644 --- a/build.ps1 +++ b/build.ps1 @@ -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 } \ No newline at end of file +exec { & dotnet pack .\src\Ben.Demystifier -c Release -o .\artifacts } diff --git a/directory.build.props b/directory.build.props new file mode 100644 index 0000000..44c7d4e --- /dev/null +++ b/directory.build.props @@ -0,0 +1,5 @@ + + + 7.2 + + \ No newline at end of file diff --git a/sample/StackTrace/StackTrace.csproj b/sample/StackTrace/StackTrace.csproj index c74a7c5..3e72966 100644 --- a/sample/StackTrace/StackTrace.csproj +++ b/sample/StackTrace/StackTrace.csproj @@ -5,14 +5,6 @@ netcoreapp2.0 - - 7.2 - - - - 7.2 - - diff --git a/src/Ben.Demystifier/Ben.Demystifier.csproj b/src/Ben.Demystifier/Ben.Demystifier.csproj index e43a689..c1cd196 100644 --- a/src/Ben.Demystifier/Ben.Demystifier.csproj +++ b/src/Ben.Demystifier/Ben.Demystifier.csproj @@ -9,17 +9,19 @@ https://github.com/benaadams/Ben.Demystifier https://github.com/benaadams/Ben.Demystifier/blob/master/LICENSE git - true true - 0.0.7 + embedded netstandard2.0;net45 - 7.1 + true + key.snk + + 1.5.0 diff --git a/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs b/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs index 5b6e1d7..d2acf07 100644 --- a/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs +++ b/src/Ben.Demystifier/EnhancedStackTrace.Frames.cs @@ -19,7 +19,6 @@ namespace System.Diagnostics { public partial class EnhancedStackTrace { - private static List 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 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 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) diff --git a/src/Ben.Demystifier/EnhancedStackTrace.cs b/src/Ben.Demystifier/EnhancedStackTrace.cs index 181bfef..d93b014 100644 --- a/src/Ben.Demystifier/EnhancedStackTrace.cs +++ b/src/Ben.Demystifier/EnhancedStackTrace.cs @@ -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 GetEnumerator() => EnumerableIList.Create(_frames); IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); + + /// + /// Tries to convert a given to a full path. + /// Returns original value if the conversion isn't possible or a given path is relative. + /// + 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; + } } } diff --git a/src/Ben.Demystifier/ExceptionExtentions.cs b/src/Ben.Demystifier/ExceptionExtentions.cs index b4ab1b9..23203d1 100644 --- a/src/Ben.Demystifier/ExceptionExtentions.cs +++ b/src/Ben.Demystifier/ExceptionExtentions.cs @@ -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 { + /// public static class ExceptionExtentions { private static readonly FieldInfo stackTraceString = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic); public static T Demystify(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); + + /// + /// Demystifies the given and tracks the original stack traces for the whole exception tree. + /// + private static T Demystify(this T exception, Dictionary 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; } + + /// + /// Gets demystified string representation of the . + /// + /// + /// method mutates the exception instance that can cause + /// issues if a system relies on the stack trace be in the specific form. + /// Unlike this method is pure. It calls first, + /// computes a demystified string representation and then restores the original state of the exception back. + /// + [Contracts.Pure] + public static string ToStringDemystified(this Exception exception) + { + try + { + var originalStacks = new Dictionary(); + 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(); + } } } diff --git a/src/Ben.Demystifier/Internal/ReflectionHelper.cs b/src/Ben.Demystifier/Internal/ReflectionHelper.cs new file mode 100644 index 0000000..9306532 --- /dev/null +++ b/src/Ben.Demystifier/Internal/ReflectionHelper.cs @@ -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 +{ + /// + /// A helper class that contains utilities methods for dealing with reflection. + /// + public static class ReflectionHelper + { + /// + /// Returns true if is System.Runtime.CompilerServices.IsReadOnlyAttribute. + /// + public static bool IsReadOnlyAttribute(this Type type) + { + return type.Namespace == "System.Runtime.CompilerServices" && type.Name == "IsReadOnlyAttribute"; + } + + /// + /// Returns true if the is a value tuple type. + /// + public static bool IsValueTuple(this Type type) + { + return type.Namespace == "System" && type.Name.Contains("ValueTuple`"); + } + } +} diff --git a/src/Ben.Demystifier/TypeNameHelper.cs b/src/Ben.Demystifier/TypeNameHelper.cs index 67c98f5..a4ca3a8 100644 --- a/src/Ben.Demystifier/TypeNameHelper.cs +++ b/src/Ben.Demystifier/TypeNameHelper.cs @@ -43,6 +43,22 @@ namespace System.Diagnostics return builder.ToString(); } + /// + /// Returns a name of given generic type without '`'. + /// + 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) diff --git a/src/Ben.Demystifier/key.snk b/src/Ben.Demystifier/key.snk new file mode 100644 index 0000000..9367ae1 Binary files /dev/null and b/src/Ben.Demystifier/key.snk differ diff --git a/test/Ben.Demystifier.Benchmarks/Ben.Demystifier.Benchmarks.csproj b/test/Ben.Demystifier.Benchmarks/Ben.Demystifier.Benchmarks.csproj new file mode 100644 index 0000000..75d99f0 --- /dev/null +++ b/test/Ben.Demystifier.Benchmarks/Ben.Demystifier.Benchmarks.csproj @@ -0,0 +1,11 @@ + + + netcoreapp2.0;net462 + Release + Exe + + + + + + \ No newline at end of file diff --git a/test/Ben.Demystifier.Benchmarks/Exceptions.cs b/test/Ben.Demystifier.Benchmarks/Exceptions.cs new file mode 100644 index 0000000..0bc8881 --- /dev/null +++ b/test/Ben.Demystifier.Benchmarks/Exceptions.cs @@ -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(); + } +} diff --git a/test/Ben.Demystifier.Benchmarks/Program.cs b/test/Ben.Demystifier.Benchmarks/Program.cs new file mode 100644 index 0000000..4b8a7e4 --- /dev/null +++ b/test/Ben.Demystifier.Benchmarks/Program.cs @@ -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()); + } +} diff --git a/test/Ben.Demystifier.Test/AggregateException.cs b/test/Ben.Demystifier.Test/AggregateException.cs index b2a8667..3c0034a 100644 --- a/test/Ben.Demystifier.Test/AggregateException.cs +++ b/test/Ben.Demystifier.Test/AggregateException.cs @@ -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]+"); } } diff --git a/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj b/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj index 9b4760b..9400fa6 100644 --- a/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj +++ b/test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj @@ -1,5 +1,5 @@ - + netcoreapp2.0;net46 @@ -7,9 +7,8 @@ - - - + + diff --git a/test/Ben.Demystifier.Test/DynamicCompilation.cs b/test/Ben.Demystifier.Test/DynamicCompilation.cs index d05f3d6..3731011 100644 --- a/test/Ben.Demystifier.Test/DynamicCompilation.cs +++ b/test/Ben.Demystifier.Test/DynamicCompilation.cs @@ -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]+"); } } diff --git a/test/Ben.Demystifier.Test/EnhancedStackTraceTests.cs b/test/Ben.Demystifier.Test/EnhancedStackTraceTests.cs new file mode 100644 index 0000000..c00acc9 --- /dev/null +++ b/test/Ben.Demystifier.Test/EnhancedStackTraceTests.cs @@ -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("\\", "/"); + } +} diff --git a/test/Ben.Demystifier.Test/LineEndingsHelper.cs b/test/Ben.Demystifier.Test/LineEndingsHelper.cs new file mode 100644 index 0000000..8a475f2 --- /dev/null +++ b/test/Ben.Demystifier.Test/LineEndingsHelper.cs @@ -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, ""); + } + } +} diff --git a/test/Ben.Demystifier.Test/MixedStack.cs b/test/Ben.Demystifier.Test/MixedStack.cs index c98db5d..0fde176 100644 --- a/test/Ben.Demystifier.Test/MixedStack.cs +++ b/test/Ben.Demystifier.Test/MixedStack.cs @@ -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 ExpectedCallStack = new List() { - "IEnumerable Demystify.MixedStack.Iterator()+MoveNext()", + "IEnumerable Ben.Demystifier.Test.MixedStack.Iterator()+MoveNext()", "string string.Join(string separator, IEnumerable values)", - "string Demystify.MixedStack+GenericClass.GenericMethod(ref V value)", - "async Task Demystify.MixedStack.MethodAsync(int value)", - "async Task Demystify.MixedStack.MethodAsync(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.GenericMethod(ref V value)", + "async Task Ben.Demystifier.Test.MixedStack.MethodAsync(int value)", + "async Task Ben.Demystifier.Test.MixedStack.MethodAsync(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)" }; diff --git a/test/Ben.Demystifier.Test/NonThrownException.cs b/test/Ben.Demystifier.Test/NonThrownException.cs index 643ff88..ef5abf2 100644 --- a/test/Ben.Demystifier.Test/NonThrownException.cs +++ b/test/Ben.Demystifier.Test/NonThrownException.cs @@ -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]+"); } } diff --git a/test/Ben.Demystifier.Test/ReflectionHelperTests.cs b/test/Ben.Demystifier.Test/ReflectionHelperTests.cs new file mode 100644 index 0000000..5773e6b --- /dev/null +++ b/test/Ben.Demystifier.Test/ReflectionHelperTests.cs @@ -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).IsValueTuple()); + } + + [Fact] + public void IsValueTupleReturnsTrueForTupleWith1ElementWithOpenedType() + { + Assert.True(typeof(ValueTuple<>).IsValueTuple()); + } + + [Fact] + public void IsValueTupleReturnsTrueForTupleWith6ElementsWithOpenedType() + { + Assert.True(typeof(ValueTuple<,,,,,>).IsValueTuple()); + } + } +} diff --git a/test/Ben.Demystifier.Test/ToDemystifiedStringTests.cs b/test/Ben.Demystifier.Test/ToDemystifiedStringTests.cs new file mode 100644 index 0000000..47ff7e2 --- /dev/null +++ b/test/Ben.Demystifier.Test/ToDemystifiedStringTests.cs @@ -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"); + } + } + } +} diff --git a/test/Ben.Demystifier.Test/TuplesTests.cs b/test/Ben.Demystifier.Test/TuplesTests.cs new file mode 100644 index 0000000..698336e --- /dev/null +++ b/test/Ben.Demystifier.Test/TuplesTests.cs @@ -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(); + } + } +} diff --git a/version.json b/version.json new file mode 100644 index 0000000..2a116e5 --- /dev/null +++ b/version.json @@ -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 + } + } +}