├── .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 | [](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 |
--------------------------------------------------------------------------------