├── .gitignore ├── libs ├── Mono.Cecil.dll ├── Mono.Cecil.Mdb.dll ├── Mono.Cecil.Pdb.dll ├── Mono.Cecil.Rocks.dll ├── Newtonsoft.Json.dll └── nunit.framework.dll ├── .travis.yml ├── travisCoverageConfig.json ├── Gaillard.SharpCover.Tests ├── TestTarget.csproj ├── ProgramTests.csproj ├── TestTarget.cs └── ProgramTests.cs ├── Gaillard.SharpCover ├── Counter.csproj ├── Counter.cs ├── Program.csproj └── Program.cs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | TestResult.xml 4 | *~ 5 | coverageResults.txt 6 | -------------------------------------------------------------------------------- /libs/Mono.Cecil.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonderblue/SharpCover/HEAD/libs/Mono.Cecil.dll -------------------------------------------------------------------------------- /libs/Mono.Cecil.Mdb.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonderblue/SharpCover/HEAD/libs/Mono.Cecil.Mdb.dll -------------------------------------------------------------------------------- /libs/Mono.Cecil.Pdb.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonderblue/SharpCover/HEAD/libs/Mono.Cecil.Pdb.dll -------------------------------------------------------------------------------- /libs/Mono.Cecil.Rocks.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonderblue/SharpCover/HEAD/libs/Mono.Cecil.Rocks.dll -------------------------------------------------------------------------------- /libs/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonderblue/SharpCover/HEAD/libs/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /libs/nunit.framework.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonderblue/SharpCover/HEAD/libs/nunit.framework.dll -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | services: mongodb 3 | install: 4 | - sudo apt-get install mono-devel nunit-console 5 | script: sh build.sh 6 | branches: 7 | only: 8 | - master 9 | -------------------------------------------------------------------------------- /travisCoverageConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "assemblies": [ 3 | "Gaillard.SharpCover.Tests/bin/Debug/SharpCover.exe" 4 | ], 5 | "methodBodyExcludes": [ 6 | { 7 | "method": "System.Void Gaillard.SharpCover.Program::Instrument(Mono.Cecil.Cil.Instruction,Mono.Cecil.MethodReference,Mono.Cecil.MethodDefinition,Mono.Cecil.Cil.ILProcessor,System.String,Gaillard.SharpCover.Program/InstrumentConfig,System.IO.TextWriter,System.Int32&)", 8 | "lines": ["handler.FilterStart = pathParamLoadInstruction;"] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Gaillard.SharpCover.Tests/TestTarget.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | TestTarget 5 | 4 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Gaillard.SharpCover/Counter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | Counter 6 | 4 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Gaillard.SharpCover/Counter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Threading; 6 | using System.Diagnostics; 7 | 8 | [assembly: AssemblyVersion("1.0.2.*")] 9 | 10 | namespace Gaillard.SharpCover 11 | { 12 | public static class Counter 13 | { 14 | [ThreadStatic] 15 | private static HashSet indexes; 16 | 17 | [ThreadStatic] 18 | private static BinaryWriter writer; 19 | 20 | [ThreadStatic] 21 | private static string path; 22 | 23 | public static void Count(string pathPrefix, int index) 24 | { 25 | if (path == null) { 26 | path = pathPrefix + "|" + Process.GetCurrentProcess().Id + "|" + Thread.CurrentThread.ManagedThreadId; 27 | indexes = new HashSet(); 28 | writer = new BinaryWriter(File.Open(path, FileMode.CreateNew)); 29 | } 30 | 31 | if (indexes.Add(index)) 32 | writer.Write(index); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Gaillard.SharpCover.Tests/ProgramTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Library 4 | ProgramTests 5 | 4 6 | true 7 | 8 | 9 | 10 | 11 | ../libs/nunit.framework.dll 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Dominion Enterprises 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /Gaillard.SharpCover/Program.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | SharpCover 6 | 4 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ../libs/Newtonsoft.Json.dll 16 | 17 | 18 | ../libs/Mono.Cecil.dll 19 | 20 | 21 | ../libs/Mono.Cecil.Rocks.dll 22 | 23 | 24 | 25 | 26 | Mono.Cecil.Pdb.dll 27 | PreserveNewest 28 | 29 | 30 | Mono.Cecil.Mdb.dll 31 | PreserveNewest 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Gaillard.SharpCover.Tests/TestTarget.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Gaillard.SharpCover.Tests 4 | { 5 | public interface IEvent 6 | { 7 | event EventHandler TheEvent; 8 | } 9 | 10 | public class EventUsage : IEvent 11 | { 12 | public event EventHandler TheEvent; 13 | 14 | public void EventMethod(object sender, EventArgs e) 15 | { 16 | var i = 0; 17 | i += 1; 18 | } 19 | 20 | public void RaiseEvent() 21 | { 22 | TheEvent(null, null); 23 | } 24 | } 25 | 26 | public sealed class TestTarget 27 | { 28 | public sealed class Nested 29 | { 30 | public void Covered() 31 | { 32 | var i = 0; 33 | ++i; 34 | } 35 | } 36 | 37 | public static void UncoveredIf() 38 | { 39 | var i = 0; 40 | if (i == 0) 41 | ++i; 42 | else 43 | --i; 44 | } 45 | 46 | public static void UncoveredLeave() 47 | { 48 | var i = 0; 49 | ++i; 50 | try { 51 | if (i == 1) 52 | throw new Exception(); 53 | //miss a leave from this try here. 54 | } catch (Exception) { 55 | var j = 1; 56 | --j; 57 | } 58 | } 59 | 60 | public static void OffsetExcludes() 61 | { 62 | var i = 0; 63 | if (i == 1) 64 | ++i; 65 | } 66 | 67 | public static void LineExcludes() 68 | { 69 | var i = 0; 70 | if (i == 1) 71 | ++i; 72 | 73 | try { 74 | --i; 75 | } catch (Exception) { 76 | var b = false; b = !b;//will never get here 77 | } 78 | } 79 | 80 | //different bits c# syntax to exercise different instructions and jumps etc 81 | public void Covered() 82 | { 83 | var i = 0; 84 | ++i; 85 | 86 | try { 87 | --i; 88 | } finally { 89 | i += 2; 90 | } 91 | 92 | int j; 93 | goto There; 94 | There: 95 | j = 1234; 96 | ++j; 97 | 98 | foreach (var k in new [] { "boo", "foo" }) { 99 | k.EndsWith("oo"); 100 | } 101 | 102 | for (var k = 0; k < 2; ++k) 103 | k += 2; 104 | 105 | { 106 | var k = 2; 107 | while (k != 0) 108 | --k; 109 | } 110 | 111 | for (var k = 0; k < 2; ++k) { 112 | var b = k % 2 == 0 ? true : false; 113 | b &= b; 114 | } 115 | 116 | for (var k = 0; k < 4; ++k) { 117 | switch (k) { 118 | case 0: 119 | break; 120 | case 1: 121 | case 2: 122 | break; 123 | } 124 | } 125 | 126 | Func func = () => true; 127 | 128 | func(); 129 | 130 | { 131 | var b = (object)false; 132 | b = (bool)b; 133 | } 134 | 135 | using (var disposable = new Disposable()) { 136 | var b = true; 137 | b = !b; 138 | } 139 | } 140 | 141 | private sealed class Disposable : IDisposable 142 | { 143 | public void Dispose() { } 144 | } 145 | 146 | private sealed class Constrained 147 | { 148 | public string ToString(T value) where T : struct 149 | { 150 | return value.ToString(); 151 | } 152 | } 153 | 154 | public static void Main(string[] args) 155 | { 156 | new TestTarget().Covered(); 157 | new Nested().Covered(); 158 | 159 | UncoveredIf(); 160 | UncoveredLeave(); 161 | OffsetExcludes(); 162 | LineExcludes(); 163 | 164 | var eventUsage = new Gaillard.SharpCover.Tests.EventUsage(); 165 | eventUsage.TheEvent += eventUsage.EventMethod; 166 | eventUsage.RaiseEvent(); 167 | eventUsage.TheEvent -= eventUsage.EventMethod; 168 | 169 | new Constrained().ToString(5); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #SharpCover 2 | [![Build Status](https://travis-ci.org/gaillard/SharpCover.png)](https://travis-ci.org/gaillard/SharpCover) 3 | 4 | C# code coverage tool with Linux ([Mono](https://github.com/mono/mono)) and Windows ([.NET 4.0](http://www.microsoft.com/en-us/download/details.aspx?id=17851)) support. 5 | 6 | ##Features 7 | 8 | * [CIL](http://www.ecma-international.org/publications/standards/Ecma-335.htm) instruction coverage 9 | * Namespace, class, method, line and instruction inclusions/exclusions 10 | * Inclusions/Exclusions specifications are outside of code. 11 | * Cross platform Linux/Windows by way of [Cecil](http://www.mono-project.com/Cecil) 12 | * Easy integration into builds (target user program is invoked seperately) 13 | 14 | ##Usage 15 | 16 | * After [building](#tool-build) run `SharpCover.exe instrument json` where `json` is a string or file with contents that reflects the following format, most options 17 | are optional: 18 | 19 | ```json 20 | { 21 | "assemblies": ["../anAssembly.dll", "/someplace/anotherAssembly.dll"], 22 | "typeInclude": ".*SomePartOfAQualifiedTypeName.*", 23 | "typeExclude": ".*obviouslyARegex.*", 24 | "methodInclude": ".*SomePartOfAQualifiedMethodName.*", 25 | "methodExclude": ".*obviouslyARegex.*", 26 | "methodBodyExcludes": [ 27 | { 28 | "method": "System.Void Type::Method()", 29 | "offsets": [4, 8], 30 | "lines": ["line content", "++i;"] 31 | } 32 | ] 33 | } 34 | ``` 35 | 36 | The exit code will be zero on instrument success. 37 | 38 | * Excercise the assemblies you listed in the config. 39 | 40 | * Afterwards run `SharpCover.exe check` in the same directory you ran `instrument`. 41 | The results will be in `coverageResults.txt`, with missed instructions prefixed with `MISS !`. 42 | The exit code will be zero for success, and total coverage percentage is printed. 43 | 44 | ###Notes 45 | Full `method` names for `methodBodyExcludes` can be found in the output, as well as offsets. 46 | 47 | The `methodBodyExcludes` by `lines` are line content matches ignoring leading/trailing whitespace. 48 | This keeps coverage exclusions outside the code while not relying on offsets which can easily change if new code is added to the method. 49 | For excluding instructions by line that have no source, the last instruction to have a sequence point is used as that instructions "line". 50 | 51 | Remember to rebuild your assemblies before you instrument again ! 52 | 53 | It is highly recommended to use the includes/excludes to achieve a zero exit from `check`, otherwise you are cheating yourself ! 54 | 55 | ##Tool Build 56 | 57 | Make sure you are in the repository root. 58 | 59 | ###Linux 60 | 61 | Make sure [Mono](https://github.com/mono/mono) which comes with [xbuild](http://www.mono-project.com/Microsoft.Build) is installed. 62 | 63 | ```bash 64 | xbuild Gaillard.SharpCover/Program.csproj 65 | ``` 66 | 67 | ###Windows 68 | 69 | Make sure [.NET SDK](http://www.microsoft.com/en-us/download/details.aspx?id=8279) which comes with [MSBuild](http://msdn.microsoft.com/en-us/library/dd393574.aspx) is installed. 70 | 71 | ```dos 72 | C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe Gaillard.SharpCover\Program.csproj 73 | ``` 74 | 75 | Navigate to the `Gaillard.SharpCover/bin/Debug` directory where the `SharpCover.exe` executable can be used. 76 | 77 | ##Contact 78 | 79 | Developers may be contacted at: 80 | 81 | * [Pull Requests](https://github.com/gaillard/SharpCover/pulls) 82 | * [Issues](https://github.com/gaillard/SharpCover/issues) 83 | 84 | Questions / Feedback / Feature requests are welcome !! 85 | 86 | ##Project Build 87 | 88 | Make sure you are in the repository root. 89 | Make sure [nunit-console](http://www.nunit.org/index.php?p=nunit-console&r=2.2.10) is installed. 90 | 91 | ###Linux 92 | 93 | Make sure [Mono](https://github.com/mono/mono) which comes with [xbuild](http://www.mono-project.com/Microsoft.Build) is installed. 94 | 95 | 96 | ```bash 97 | sh build.sh 98 | ``` 99 | 100 | ###Windows 101 | 102 | Make sure [.NET SDK](http://www.microsoft.com/en-us/download/details.aspx?id=8279) which comes with [MSBuild](http://msdn.microsoft.com/en-us/library/dd393574.aspx) is installed. 103 | 104 | ```dos 105 | build.bat 106 | ``` 107 | 108 | #####Notes 109 | 110 | Some paths might need changing depending on your environment. 111 | 112 | ##Enhancements 113 | 114 | A standard output format that can be used with available visualizers would be very useful. 115 | 116 | A more complete test suite. 117 | 118 | Contributions welcome ! 119 | -------------------------------------------------------------------------------- /Gaillard.SharpCover.Tests/ProgramTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using NUnit.Framework; 4 | using System.Linq; 5 | using System.Diagnostics; 6 | 7 | namespace Gaillard.SharpCover.Tests 8 | { 9 | [TestFixture] 10 | public sealed class ProgramTests 11 | { 12 | private string testTargetExePath; 13 | private bool onDotNet; 14 | 15 | [SetUp] 16 | public void TestSetup() 17 | { 18 | onDotNet = Type.GetType("Mono.Runtime") == null; 19 | 20 | string buildCommand; 21 | if (onDotNet) { 22 | buildCommand = @"C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe"; 23 | testTargetExePath = @"bin\Debug\TestTarget.exe"; 24 | } else { 25 | buildCommand = "xbuild"; 26 | testTargetExePath = "bin/Debug/TestTarget.exe"; 27 | } 28 | 29 | var process = Process.Start(buildCommand, "TestTarget.csproj"); 30 | process.WaitForExit(); 31 | Assert.AreEqual(0, process.ExitCode); 32 | } 33 | 34 | [Test] 35 | public void NoBody() 36 | { 37 | var config = 38 | @"{""assemblies"": [""bin/Debug/TestTarget.exe""], ""typeInclude"": "".*Tests.*Event.*""}"; 39 | 40 | File.WriteAllText("testConfig.json", config); 41 | 42 | Assert.AreEqual(0, Program.Main(new []{ "instrument", "testConfig.json" })); 43 | 44 | Process.Start(testTargetExePath).WaitForExit(); 45 | 46 | Assert.AreEqual(0, Program.Main(new []{ "check" })); 47 | 48 | Assert.IsTrue(File.ReadLines(Program.RESULTS_FILENAME).Any()); 49 | } 50 | 51 | [Test] 52 | public void Covered() 53 | { 54 | var config = 55 | @"{""assemblies"": [""bin/Debug/TestTarget.exe""], ""typeInclude"": "".*TestTarget"", ""methodInclude"": "".*Covered.*""}"; 56 | 57 | File.WriteAllText("testConfig.json", config); 58 | 59 | //write some extraneous hit files to make sure they dont affect run 60 | File.WriteAllText(Program.HITS_FILENAME_PREFIX, "doesnt matter"); 61 | 62 | Assert.AreEqual(0, Program.Main(new []{ "instrument", "testConfig.json" })); 63 | 64 | Process.Start(testTargetExePath).WaitForExit(); 65 | 66 | Assert.AreEqual(0, Program.Main(new []{ "check" })); 67 | 68 | Assert.IsTrue(File.ReadLines(Program.RESULTS_FILENAME).Any()); 69 | } 70 | 71 | [Test] 72 | public void UncoveredIf() 73 | { 74 | var config = @"{""assemblies"": [""bin/Debug/TestTarget.exe""], ""methodInclude"": "".*UncoveredIf.*""}"; 75 | 76 | Assert.AreEqual(0, Program.Main(new []{ "instrument", config })); 77 | 78 | Process.Start(testTargetExePath).WaitForExit(); 79 | 80 | Assert.AreEqual(1, Program.Main(new []{ "check" })); 81 | 82 | var missCount = File.ReadLines(Program.RESULTS_FILENAME).Where(l => l.StartsWith(Program.MISS_PREFIX)).Count(); 83 | var knownCount = File.ReadLines(Program.RESULTS_FILENAME).Count(); 84 | 85 | Assert.IsTrue(knownCount > 0); 86 | Assert.IsTrue(missCount > 0); 87 | Assert.IsTrue(knownCount > missCount); 88 | } 89 | 90 | [Test] 91 | public void UncoveredLeave() 92 | { 93 | var config = @"{""assemblies"": [""bin/Debug/TestTarget.exe""], ""methodInclude"": "".*UncoveredLeave.*""}"; 94 | 95 | Assert.AreEqual(0, Program.Main(new []{ "instrument", config })); 96 | 97 | Process.Start(testTargetExePath).WaitForExit(); 98 | 99 | Assert.AreEqual(1, Program.Main(new []{ "check" })); 100 | 101 | var missCount = File.ReadLines(Program.RESULTS_FILENAME).Where(l => l.StartsWith(Program.MISS_PREFIX)).Count(); 102 | var knownCount = File.ReadLines(Program.RESULTS_FILENAME).Count(); 103 | 104 | Assert.IsTrue(knownCount > 0); 105 | Assert.IsTrue(missCount > 0); 106 | Assert.IsTrue(knownCount > missCount); 107 | } 108 | 109 | [Test] 110 | public void Nested() 111 | { 112 | var config = @"{""assemblies"": [""bin/Debug/TestTarget.exe""], ""typeInclude"": "".*Nested""}"; 113 | 114 | Assert.AreEqual(0, Program.Main(new []{ "instrument", config })); 115 | 116 | Process.Start(testTargetExePath).WaitForExit(); 117 | 118 | Assert.AreEqual(0, Program.Main(new []{ "check" })); 119 | 120 | Assert.IsTrue(File.ReadLines(Program.RESULTS_FILENAME).Any()); 121 | } 122 | 123 | [Test] 124 | public void LineExcludes() 125 | { 126 | var config = 127 | @"{ 128 | ""assemblies"": [""bin/Debug/TestTarget.exe""], 129 | ""typeInclude"": "".*TestTarget"", 130 | ""methodInclude"": "".*LineExcludes.*"", 131 | ""methodBodyExcludes"": [ 132 | { 133 | ""method"": ""System.Void Gaillard.SharpCover.Tests.TestTarget::LineExcludes()"", 134 | ""lines"": [""++i;"", ""} catch (Exception) {"", ""var b = false; b = !b;//will never get here"", ""}""] 135 | } 136 | ] 137 | }"; 138 | 139 | Assert.AreEqual(0, Program.Main(new []{ "instrument", config })); 140 | 141 | Process.Start(testTargetExePath).WaitForExit(); 142 | 143 | Assert.AreEqual(0, Program.Main(new []{ "check" })); 144 | 145 | Assert.IsTrue(File.ReadLines(Program.RESULTS_FILENAME).Any()); 146 | } 147 | 148 | [Test] 149 | public void OffsetExcludes() 150 | { 151 | string offsets; 152 | if (onDotNet) 153 | offsets = "14, 15, 16, 17"; 154 | else 155 | offsets = "9, 10, 11, 12, 13"; 156 | 157 | var config = 158 | string.Format(@"{{ 159 | ""assemblies"": [""bin/Debug/TestTarget.exe""], 160 | ""typeInclude"": "".*TestTarget"", 161 | ""methodInclude"": "".*OffsetExcludes.*"", 162 | ""methodBodyExcludes"": [ 163 | {{ 164 | ""method"": ""System.Void Gaillard.SharpCover.Tests.TestTarget::OffsetExcludes()"", 165 | ""offsets"": [{0}] 166 | }} 167 | ] 168 | }}", offsets); 169 | 170 | Assert.AreEqual(0, Program.Main(new []{ "instrument", config })); 171 | 172 | Process.Start(testTargetExePath).WaitForExit(); 173 | 174 | Assert.AreEqual(0, Program.Main(new []{ "check" })); 175 | 176 | Assert.IsTrue(File.ReadLines(Program.RESULTS_FILENAME).Any()); 177 | } 178 | 179 | //to get an IL instruction that uses a prefix instruction like constrained 180 | [Test] 181 | public void Constrained() 182 | { 183 | var config = @"{""assemblies"": [""bin/Debug/TestTarget.exe""], ""typeInclude"": "".*Constrained""}"; 184 | 185 | Assert.AreEqual(0, Program.Main(new []{ "instrument", config })); 186 | 187 | Process.Start(testTargetExePath).WaitForExit(); 188 | 189 | Assert.AreEqual(0, Program.Main(new []{ "check" })); 190 | 191 | Assert.IsTrue(File.ReadLines(Program.RESULTS_FILENAME).Any()); 192 | } 193 | 194 | [Test] 195 | public void MissingCommand() 196 | { 197 | Assert.AreEqual(2, Program.Main(new string[0])); 198 | } 199 | 200 | [Test] 201 | public void BadCommand() 202 | { 203 | Assert.AreEqual(2, Program.Main(new []{ "BAD_COMMAND" })); 204 | } 205 | 206 | [Test] 207 | public void MissingConfig() 208 | { 209 | Assert.AreEqual(2, Program.Main(new []{ "instrument" })); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Gaillard.SharpCover/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Mono.Cecil; 3 | using Mono.Cecil.Cil; 4 | using Mono.Cecil.Rocks; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text.RegularExpressions; 10 | using Newtonsoft.Json.Linq; 11 | 12 | [assembly: AssemblyVersion("1.0.2.*")] 13 | 14 | namespace Gaillard.SharpCover 15 | { 16 | public static class Program 17 | { 18 | public const string RESULTS_FILENAME = "coverageResults.txt", MISS_PREFIX = "MISS ! ", HITS_FILENAME_PREFIX = "coverageHits"; 19 | private const string KNOWNS_FILENAME = "coverageKnowns"; 20 | private static readonly MethodInfo countMethodInfo = typeof(Counter).GetMethod("Count"); 21 | 22 | //immutable 23 | private sealed class InstrumentConfig 24 | { 25 | public readonly IEnumerable AssemblyPaths; 26 | public readonly string TypeInclude, 27 | TypeExclude, 28 | MethodInclude, 29 | MethodExclude, 30 | HitsPathPrefix = Path.Combine(Directory.GetCurrentDirectory(), HITS_FILENAME_PREFIX); 31 | private readonly IDictionary> methodOffsetExcludes = new Dictionary>(); 32 | private readonly IDictionary> methodLineExcludes = new Dictionary>(); 33 | 34 | public InstrumentConfig(string json) 35 | { 36 | if (File.Exists(json)) 37 | json = File.ReadAllText(json); 38 | 39 | var config = JObject.Parse(json); 40 | 41 | AssemblyPaths = config.SelectToken("assemblies", true).Values(); 42 | TypeInclude = ((string)config.SelectToken("typeInclude")) ?? ".*"; 43 | TypeExclude = ((string)config.SelectToken("typeExclude")) ?? ".^";//any char and THEN start of line matches nothing 44 | MethodInclude = ((string)config.SelectToken("methodInclude")) ?? ".*"; 45 | MethodExclude = ((string)config.SelectToken("methodExclude")) ?? ".^";//any char and THEN start of line matches nothing 46 | 47 | 48 | foreach (var methodBodyExclude in config.SelectToken("methodBodyExcludes") ?? new JArray()) { 49 | var method = (string)methodBodyExclude.SelectToken("method", true); 50 | var offsets = (methodBodyExclude.SelectToken("offsets") ?? new JArray()).Values(); 51 | var lines = (methodBodyExclude.SelectToken("lines") ?? new JArray()).Values(); 52 | methodOffsetExcludes.Add(method, offsets); 53 | methodLineExcludes.Add(method, lines); 54 | } 55 | } 56 | 57 | public bool HasOffset(string method, int offset) 58 | { 59 | IEnumerable offsets; 60 | return methodOffsetExcludes.TryGetValue(method, out offsets) && offsets.Contains(offset); 61 | } 62 | 63 | public bool HasLine(string method, string line) 64 | { 65 | IEnumerable lines; 66 | return methodLineExcludes.TryGetValue(method, out lines) && lines.Select(l => l.Trim()).Contains(line.Trim()); 67 | } 68 | } 69 | 70 | private static void Instrument(Instruction instruction, 71 | MethodReference countReference, 72 | MethodDefinition method, 73 | ILProcessor worker, 74 | string lastLine, 75 | InstrumentConfig config, 76 | TextWriter writer, 77 | ref int instrumentIndex) 78 | { 79 | //if the previous instruction is a Prefix instruction then this instruction MUST go with it. 80 | //we cannot put an instruction between the two. 81 | if (instruction.Previous != null && instruction.Previous.OpCode.OpCodeType == OpCodeType.Prefix) 82 | return; 83 | 84 | if (config.HasOffset(method.FullName, instruction.Offset)) 85 | return; 86 | 87 | if (lastLine != null && config.HasLine(method.FullName, lastLine)) { 88 | return; 89 | } 90 | 91 | var lineNum = -1; 92 | if (instruction.SequencePoint != null) 93 | lineNum = instruction.SequencePoint.StartLine; 94 | 95 | var line = string.Join(", ", 96 | "Method: " + method.FullName, 97 | "Line: " + lineNum, 98 | "Offset: " + instruction.Offset, 99 | "Instruction: " + instruction); 100 | 101 | writer.WriteLine(line); 102 | 103 | var pathParamLoadInstruction = worker.Create(OpCodes.Ldstr, config.HitsPathPrefix); 104 | var lineParamLoadInstruction = worker.Create(OpCodes.Ldc_I4, instrumentIndex); 105 | var registerInstruction = worker.Create(OpCodes.Call, countReference); 106 | 107 | //inserting method before instruction because after will not happen after a method Ret instruction 108 | worker.InsertBefore(instruction, pathParamLoadInstruction); 109 | worker.InsertAfter(pathParamLoadInstruction, lineParamLoadInstruction); 110 | worker.InsertAfter(lineParamLoadInstruction, registerInstruction); 111 | 112 | ++instrumentIndex; 113 | 114 | //change try/finally etc to point to our first instruction if they referenced the one we inserted before 115 | foreach (var handler in method.Body.ExceptionHandlers) { 116 | if (handler.FilterStart == instruction) 117 | handler.FilterStart = pathParamLoadInstruction; 118 | 119 | if (handler.TryStart == instruction) 120 | handler.TryStart = pathParamLoadInstruction; 121 | if (handler.TryEnd == instruction) 122 | handler.TryEnd = pathParamLoadInstruction; 123 | 124 | if (handler.HandlerStart == instruction) 125 | handler.HandlerStart = pathParamLoadInstruction; 126 | if (handler.HandlerEnd == instruction) 127 | handler.HandlerEnd = pathParamLoadInstruction; 128 | } 129 | 130 | //change instructions with a target instruction if they referenced the one we inserted before to be our first instruction 131 | foreach (var iteratedInstruction in method.Body.Instructions) { 132 | var operand = iteratedInstruction.Operand; 133 | if (operand == instruction) { 134 | iteratedInstruction.Operand = pathParamLoadInstruction; 135 | continue; 136 | } 137 | 138 | if (!(operand is Instruction[])) 139 | continue; 140 | 141 | var operands = (Instruction[])operand; 142 | for (var i = 0; i < operands.Length; ++i) { 143 | if (operands[i] == instruction) 144 | operands[i] = pathParamLoadInstruction; 145 | } 146 | } 147 | } 148 | 149 | private static void Instrument( 150 | MethodDefinition method, 151 | MethodReference countReference, 152 | InstrumentConfig config, 153 | TextWriter writer, 154 | ref int instrumentIndex) 155 | { 156 | if (!Regex.IsMatch(method.FullName, config.MethodInclude) || Regex.IsMatch(method.FullName, config.MethodExclude)) 157 | return; 158 | 159 | var worker = method.Body.GetILProcessor(); 160 | 161 | method.Body.SimplifyMacros(); 162 | 163 | string lastLine = null;//the sequence point for instructions that dont have one is the last set (if one exists) 164 | //need to copy instruction list since we modify using worker inserts 165 | foreach (var instruction in new List(method.Body.Instructions).OrderBy(i => i.Offset)) { 166 | var sequencePoint = instruction.SequencePoint; 167 | if (sequencePoint != null) { 168 | var line = File.ReadLines(sequencePoint.Document.Url).ElementAtOrDefault(sequencePoint.StartLine - 1); 169 | if (line != null) 170 | lastLine = line; 171 | } 172 | 173 | Instrument(instruction, countReference, method, worker, lastLine, config, writer, ref instrumentIndex); 174 | } 175 | 176 | method.Body.OptimizeMacros(); 177 | } 178 | 179 | private static void Instrument( 180 | TypeDefinition type, 181 | MethodReference countReference, 182 | InstrumentConfig config, 183 | TextWriter writer, 184 | ref int instrumentIndex) 185 | { 186 | if (type.FullName == "") 187 | return; 188 | 189 | if (!Regex.IsMatch(type.FullName, config.TypeInclude) || Regex.IsMatch(type.FullName, config.TypeExclude)) 190 | return; 191 | 192 | foreach (var method in type.Methods.Where(m => m.HasBody)) 193 | Instrument(method, countReference, config, writer, ref instrumentIndex); 194 | } 195 | 196 | private static void Instrument(string assemblyPath, InstrumentConfig config, TextWriter writer, ref int instrumentIndex) 197 | { 198 | //Mono.Cecil.[Mdb|Pdb].dll must be alongsize this exe to include sequence points from ReadSymbols 199 | var assembly = AssemblyDefinition.ReadAssembly(assemblyPath, new ReaderParameters { ReadSymbols = true }); 200 | var countReference = assembly.MainModule.Import(countMethodInfo); 201 | 202 | foreach (var type in assembly.MainModule.GetTypes())//.Types doesnt include nested types 203 | Instrument(type, countReference, config, writer, ref instrumentIndex); 204 | 205 | assembly.Write(assemblyPath, new WriterParameters { WriteSymbols = true }); 206 | 207 | var counterPath = typeof(Counter).Assembly.Location; 208 | 209 | File.Copy(counterPath, Path.Combine(Path.GetDirectoryName(assemblyPath), Path.GetFileName(counterPath)), true); 210 | } 211 | 212 | private static int Check() 213 | { 214 | var currentDirectory = Directory.GetCurrentDirectory(); 215 | 216 | var hits = new HashSet(); 217 | foreach (var hitsPath in Directory.GetFiles(currentDirectory, HITS_FILENAME_PREFIX + "*")) { 218 | using (var hitsStream = File.OpenRead(hitsPath)) 219 | using (var hitsReader = new BinaryReader(hitsStream)) { 220 | while (hitsStream.Position < hitsStream.Length) 221 | hits.Add (hitsReader.ReadInt32()); 222 | } 223 | } 224 | 225 | var missCount = 0; 226 | var knownIndex = 0; 227 | 228 | using (var resultsWriter = new StreamWriter(RESULTS_FILENAME)) {//overwrites 229 | foreach (var knownLine in File.ReadLines(KNOWNS_FILENAME)) { 230 | if (hits.Contains(knownIndex)) 231 | resultsWriter.WriteLine(knownLine); 232 | else { 233 | resultsWriter.WriteLine(MISS_PREFIX + knownLine); 234 | ++missCount; 235 | } 236 | 237 | ++knownIndex; 238 | } 239 | } 240 | 241 | //cleanup to leave only results file 242 | foreach (var hitsPath in Directory.GetFiles(currentDirectory, HITS_FILENAME_PREFIX + "*")) 243 | File.Delete(hitsPath); 244 | File.Delete(KNOWNS_FILENAME); 245 | 246 | var missRatio = (double)missCount / (double)knownIndex; 247 | var coverage = Math.Round((1.0 - missRatio) * 100.0, 2); 248 | 249 | Console.WriteLine(string.Format("Overall coverage was {0}%.", coverage)); 250 | 251 | return missCount == 0 ? 0 : 1; 252 | } 253 | 254 | public static int Main(string[] args) 255 | { 256 | try { 257 | if (args[0] == "instrument") { 258 | var config = new InstrumentConfig(args[1]); 259 | 260 | //delete existing hit files generatig during program exercising 261 | foreach (var hitsPath in Directory.GetFiles(Directory.GetCurrentDirectory(), HITS_FILENAME_PREFIX + "*")) 262 | File.Delete(hitsPath); 263 | 264 | //used to track the line index of the instrumented instruction in the knowns file 265 | var instrumentIndex = 0; 266 | 267 | using (var writer = new StreamWriter(KNOWNS_FILENAME)) {//overwrites 268 | foreach (var assemblyPath in config.AssemblyPaths) 269 | Instrument(assemblyPath, config, writer, ref instrumentIndex); 270 | } 271 | 272 | return 0; 273 | } else if (args[0] == "check") 274 | return Check(); 275 | 276 | Console.Error.WriteLine("need 'instrument' or 'check' command"); 277 | } catch (Exception e) { 278 | Console.Error.WriteLine(e); 279 | } 280 | 281 | return 2; 282 | } 283 | } 284 | } 285 | --------------------------------------------------------------------------------