monocov/CoverageModel.cs

600 строки
16 KiB
C#

using System;
using System.Collections;
using System.Xml;
using System.IO;
using System.Text;
using System.Reflection;
using System.Text.RegularExpressions;
using Mono.CompilerServices.SymbolWriter;
using Mono.Cecil;
using Mono.Cecil.Metadata;
namespace MonoCov
{
public delegate void CoverageProgress (string item, double percent);
public class CoverageModel : CoverageItem {
private string dataFileName;
private Hashtable namespaces;
private Hashtable classes;
private Hashtable sources;
private Hashtable loadedAssemblies;
private Hashtable symbolFiles;
public event CoverageProgress Progress;
/**
* List of filters, which are strings
*/
private ArrayList filters;
public CoverageModel ()
{
dataFileName = string.Empty;
namespaces = new Hashtable ();
classes = new Hashtable ();
sources = new Hashtable ();
filters = new ArrayList ();
Progress += delegate {}; // better than having to check every time...
}
public Hashtable Classes {
get {
return classes;
}
}
public Hashtable Namespaces {
get {
return namespaces;
}
}
public void AddFilter (String pattern) {
filters.Add (pattern);
}
private bool IsFiltered (string name)
{
// Check positive filters first
bool hasPositive = false;
bool found = false;
foreach (String pattern in filters) {
if (pattern [0] == '+') {
string p = pattern.Substring (1);
if (name.IndexOf (p) != -1) {
//Console.WriteLine ("FILTERED: " + pattern + " -> " + name);
found = true;
}
hasPositive = true;
}
}
if (hasPositive && !found)
return true;
foreach (String pattern in filters) {
if (pattern [0] == '-') {
string p = pattern.Substring (1);
if (name.IndexOf (p) != -1) {
//Console.WriteLine ("FILTERED: " + pattern + " -> " + name);
return true;
}
}
}
return false;
}
private void LoadAssemblies (XmlDocument dom)
{
foreach (XmlNode n in dom.GetElementsByTagName ("assembly")) {
string assemblyName = n.Attributes ["name"].Value;
string guid = n.Attributes ["guid"].Value;
string filename = n.Attributes ["filename"].Value;
MonoSymbolFile symbolFile;
if (!File.Exists (filename)) {
string newFilename = Path.Combine(Path.GetDirectoryName (dataFileName), Path.GetFileName (filename));
if (File.Exists (newFilename))
filename = newFilename;
}
#if USE_REFLECTION
Assembly assembly = Assembly.Load (assemblyName);
MethodInfo getguid = typeof (Module).GetMethod (
"Mono_GetGuid", BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic,
null, CallingConventions.Any, new Type [0], null);
if (getguid != null) {
Guid assembly_guid = (Guid)getguid.Invoke (assembly.GetLoadedModules ()[0], new object [0]);
Console.WriteLine (assembly_guid);
if (assembly_guid != new Guid (guid)) {
Console.WriteLine ("WARNING: Loaded version of assembly " + assembly + " is different from the version used to collect coverage data.");
}
} else {
Console.WriteLine ("WARNING: Can't verify the guid of " + assembly);
}
loadedAssemblies [assemblyName] = assembly;
Console.Write ("Reading symbols for " + assembly + " ...");
symbolFile = MonoSymbolFile.ReadSymbolFile (assembly);
if (symbolFile == null)
Console.WriteLine (" (No symbols found)");
else {
symbolFiles [assembly] = symbolFile;
Console.WriteLine (" (" + symbolFile.SourceCount + " files, " + symbolFile.MethodCount + " methods)");
}
#else
AssemblyDefinition assembly = AssemblyFactory.GetAssembly (filename);
ModuleDefinition module = assembly.MainModule;
if (module.Mvid != new Guid (guid)) {
Console.WriteLine ("WARNING: Loaded version of assembly " + assembly + " is different from the version used to collect coverage data.");
}
loadedAssemblies [assemblyName] = assembly;
Console.Write ("Reading symbols for " + assemblyName + " ...");
symbolFile = MonoSymbolFile.ReadSymbolFile (filename + ".mdb");
if (symbolFile == null)
Console.WriteLine (" (No symbols found)");
else {
symbolFiles [assembly] = symbolFile;
Console.WriteLine (" (" + symbolFile.SourceCount + " files, " + symbolFile.MethodCount + " methods)");
}
#endif
}
}
private void LoadFilters (XmlDocument dom)
{
foreach (XmlNode n in dom.GetElementsByTagName ("filter")) {
AddFilter (n.Attributes ["pattern"].Value);
}
}
#if USE_REFLECTION
static Type LoadType (Assembly assembly, string name) {
Type type = assembly.GetType (name);
if (type != null)
return type;
int last_dot = name.LastIndexOf ('.');
// covert names from IL to reflection naming
// needed to deal with nested types
while (last_dot >= 0) {
StringBuilder sb = new StringBuilder (name);
sb [last_dot] = '/';
name = sb.ToString ();
type = assembly.GetType (name);
if (type != null)
return type;
last_dot = name.LastIndexOf ('.');
}
return null;
}
#else
static TypeDefinition LoadType (AssemblyDefinition assembly, string name) {
TypeDefinition type = assembly.MainModule.Types [name];
if (type != null)
return type;
int last_dot = name.LastIndexOf ('.');
// covert names from IL to reflection naming
// needed to deal with nested types
while (last_dot >= 0) {
StringBuilder sb = new StringBuilder (name);
sb [last_dot] = '/';
name = sb.ToString ();
type = assembly.MainModule.Types [name];
if (type != null)
return type;
last_dot = name.LastIndexOf ('.');
}
return null;
}
#endif
public void ReadFromFile (string fileName)
{
dataFileName = fileName;
namespaces = new Hashtable ();
classes = new Hashtable ();
long begin = DateTime.Now.Ticks / 10000;
long msec = DateTime.Now.Ticks / 10000;
long msec2;
loadedAssemblies = new Hashtable ();
symbolFiles = new Hashtable ();
XmlDocument dom = new XmlDocument ();
Progress ("XML reading", 0);
Console.Write ("Loading " + fileName + "...");
dom.Load (new XmlTextReader (new FileStream (fileName, FileMode.Open)));
Console.WriteLine (" Done.");
msec2 = DateTime.Now.Ticks / 10000;
Console.WriteLine ("XML Reading: " + (msec2 - msec) + " msec");
msec = msec2;
Progress ("Load assemblies", 0.2);
LoadAssemblies (dom);
LoadFilters (dom);
msec2 = DateTime.Now.Ticks / 10000;
Console.WriteLine ("Load assemblies: " + (msec2 - msec) + " msec");
msec = msec2;
Progress ("Load methods", 0.4);
foreach (XmlNode n in dom.GetElementsByTagName ("method")) {
string assemblyName = n.Attributes ["assembly"].Value;
string className = n.Attributes ["class"].Value;
string methodName = n.Attributes ["name"].Value;
string token = n.Attributes ["token"].Value;
string cov_info = n.FirstChild.Value;
int itok = int.Parse (token);
#if USE_REFLECTION
Assembly assembly = (Assembly)loadedAssemblies [assemblyName];
MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [assembly];
if (symbolFile == null)
continue;
Type t = LoadType (assembly, className);
if (t == null) {
Console.WriteLine ("ERROR: Unable to resolve type " + className + " in " + assembly);
continue;
}
ClassCoverageItem klass = ProcessClass (t);
MethodEntry entry = symbolFile.GetMethodByToken (Int32.Parse (token));
Module[] modules = assembly.GetModules();
if (modules.Length > 1)
Console.WriteLine("WARNING: Assembly had more than one module. Using the first.");
Module module = modules[0];
MethodBase monoMethod = module.ResolveMethod(Int32.Parse(token));
ProcessMethod (monoMethod, entry, klass, methodName, cov_info);
#else
if ((TokenType)(itok & 0xff000000) != TokenType.Method)
continue;
AssemblyDefinition assembly = (AssemblyDefinition)loadedAssemblies [assemblyName];
MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [assembly];
if (symbolFile == null)
continue;
TypeDefinition t = LoadType (assembly, className);
if (t == null) {
Console.WriteLine ("ERROR: Unable to resolve type " + className + " in " + assembly);
continue;
}
ClassCoverageItem klass = ProcessClass (t);
MethodEntry entry = symbolFile.GetMethodByToken (itok);
MethodDefinition monoMethod = assembly.MainModule.LookupByToken (
new MetadataToken ((TokenType)(itok & 0xff000000), (uint)(itok & 0xffffff)))
as MethodDefinition;
//Console.WriteLine (monoMethod);
ProcessMethod (monoMethod, entry, klass, methodName, cov_info);
#endif
}
msec2 = DateTime.Now.Ticks / 10000;
Console.WriteLine ("Process methods: " + (msec2 - msec) + " msec");
msec = msec2;
// Add info for klasses for which we have no coverage
#if USE_REFLECTION
foreach (Assembly assembly in loadedAssemblies.Values) {
foreach (Type t in assembly.GetTypes ()) {
ProcessClass (t);
}
}
// Add info for methods for which we have no coverage
foreach (ClassCoverageItem klass in classes.Values) {
foreach (MethodInfo mb in klass.type.GetMethods (BindingFlags.NonPublic|BindingFlags.Public|BindingFlags.Static|BindingFlags.Instance | BindingFlags.DeclaredOnly)) {
MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [klass.type.Assembly];
if (symbolFile == null)
continue;
if (! klass.methodsByMethod.ContainsKey (mb)) {
MethodEntry entry = symbolFile.GetMethod (mb);
ProcessMethod (mb, entry, klass, mb.Name, null);
}
}
}
#else
Progress ("Not covered classes", 0.6);
foreach (AssemblyDefinition assembly in loadedAssemblies.Values) {
foreach (TypeDefinition t in assembly.MainModule.Types) {
ProcessClass (t);
}
}
Progress ("Not covered methods", 0.7);
// Add info for methods for which we have no coverage
foreach (ClassCoverageItem klass in classes.Values) {
foreach (MethodDefinition mb in klass.type.Methods) {
MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [klass.type.Module.Assembly];
if (symbolFile == null)
continue;
if (! klass.methodsByMethod.ContainsKey (mb)) {
MethodEntry entry = symbolFile.GetMethodByToken ((int)mb.MetadataToken.ToUInt());
ProcessMethod (mb, entry, klass, mb.Name, null);
}
}
}
#endif
msec2 = DateTime.Now.Ticks / 10000;
Console.WriteLine ("Additional classes: " + (msec2 - msec) + " msec");
msec = msec2;
Progress ("Compute coverage", 0.9);
// Compute coverage for all items
computeCoverage (true);
msec2 = DateTime.Now.Ticks / 10000;
Console.WriteLine ("Compute coverage: " + (msec2 - msec) + " msec");
msec = msec2;
Console.WriteLine ("All: " + (msec2 - begin) + " msec");
Progress ("Done loading", 0.9);
// Free memory
symbolFiles = null;
}
//
// Computes the coverage of METHOD
//
private char[] digits = "0123456789".ToCharArray ();
private char[] ws = "\t\n ".ToCharArray ();
private int parsePositiveInteger (string s, int pos) {
int n = 0;
while (s [pos] >= '0' && s [pos] <= '9'){
n = n * 10 + (s [pos] - '0');
pos ++;
}
return n;
}
private void computeMethodCoverage (MethodCoverageItem method, LineNumberEntry[] lines, string cov_info)
{
ClassCoverageItem klass = method.Class;
SourceFileCoverageData source = klass.sourceFile;
source.AddMethod (method);
int nlines = method.endLine - method.startLine + 1;
int[] coverage = new int [nlines];
if (cov_info == null) {
for (int i = 0; i < nlines; ++i)
coverage [i] = 0;
}
else {
for (int i = 0; i < nlines; ++i)
coverage [i] = -1;
// Hand crafted parsing code since this is performance critical
int pos = 0;
int prev_offset = 0;
while (pos < cov_info.Length) {
int pos2 = cov_info.IndexOfAny (digits, pos);
if (pos2 == -1)
break;
pos = cov_info.IndexOfAny (ws, pos2);
if (pos == -1)
break;
int offset = parsePositiveInteger (cov_info, pos2);
pos2 = cov_info.IndexOfAny (digits, pos);
if (pos2 == -1)
break;
pos = cov_info.IndexOfAny (ws, pos2);
int count = parsePositiveInteger (cov_info, pos2);
offset += prev_offset;
prev_offset = offset;
int line1 = 0;
int line2 = 0;
bool found = GetSourceRangeFor (offset, method, lines, ref line1, ref line2);
/*
if (found && (entry.Name.IndexOf ("Find") != -1)) {
Console.WriteLine ("OFFSET: " + offset + " " + line1 + ":" + line2);
}
*/
if (found) {
for (int i = line1; i < line2 + 1; ++i)
if ((i >= method.startLine) && (i <= method.endLine))
if (coverage [i - method.startLine] < count)
coverage [i - method.startLine] = count;
}
}
}
int hit = 0;
int missed = 0;
for (int i = 0; i < nlines; ++i) {
int count = coverage [i];
if (count > 0)
hit ++;
else if (count == 0)
missed ++;
}
method.setCoverage (hit, missed);
method.lineCoverage = coverage;
}
//
// Return a range of source lines which have something to do with OFFSET.
//
private bool GetSourceRangeFor (int offset, MethodCoverageItem method,
LineNumberEntry[] lines,
ref int startLine, ref int endLine)
{
for (int i = 0; i < lines.Length; ++i) {
if (offset >= lines [i].Offset)
if (i == lines.Length - 1) {
startLine = lines [i].Row;
endLine = lines [i].Row;
return true;
}
else if (offset < lines [i + 1].Offset) {
startLine = lines [i].Row;
endLine = lines [i + 1].Row - 1;
return true;
}
}
if (offset <= lines [0].Offset) {
return false;
}
else {
for (int i = 0; i < lines.Length; ++i)
Console.WriteLine (lines [i]);
throw new Exception ("Unable to determine source range for offset " + offset + " in " + method.name);
}
}
#if USE_REFLECTION
private ClassCoverageItem ProcessClass (Type t)
#else
private ClassCoverageItem ProcessClass (TypeDefinition t)
#endif
{
string className = t.FullName;
int nsindex = className.LastIndexOf (".");
string namespace2;
string scopedName;
if (nsindex == -1) {
namespace2 = "<GLOBAL>";
scopedName = className;
} else if (nsindex == 0) {
namespace2 = "<GLOBAL>";
scopedName = className.Substring (1);
}
else {
namespace2 = className.Substring (0, nsindex);
scopedName = className.Substring (nsindex + 1);
}
// Create namespaces
NamespaceCoverageItem ns = (NamespaceCoverageItem)namespaces [namespace2];
if (ns == null) {
string nsPrefix = "";
foreach (String nsPart in namespace2.Split ('.')) {
if (nsPrefix == "")
nsPrefix = nsPart;
else
nsPrefix = nsPrefix + "." + nsPart;
NamespaceCoverageItem ns2 = (NamespaceCoverageItem)namespaces [nsPrefix];
if (ns2 == null) {
if (ns == null)
ns2 = new NamespaceCoverageItem (this, nsPrefix);
else
ns2 = new NamespaceCoverageItem (ns, nsPrefix);
namespaces [nsPrefix] = ns2;
}
ns = ns2;
}
}
ClassCoverageItem klass = (ClassCoverageItem)classes [className];
if (klass == null) {
klass = new ClassCoverageItem (ns);
klass.name_space = namespace2;
klass.name = scopedName;
klass.type = t;
klass.parent = ns;
#if USE_REFLECTION
klass.filtered = IsFiltered ("[" + t.Assembly + "]" + className);
#else
klass.filtered = IsFiltered ("[" + t.Module.Name + "]" + className);
#endif
classes [className] = klass;
}
return klass;
}
#if USE_REFLECTION
private void ProcessMethod (MethodBase monoMethod, MethodEntry entry, ClassCoverageItem klass, string methodName, string cov_info)
#else
private void ProcessMethod (MethodDefinition monoMethod, MethodEntry entry, ClassCoverageItem klass, string methodName, string cov_info)
#endif
{
if (entry == null)
// Compiler generated, abstract method etc.
return;
LineNumberEntry[] lines = entry.GetLineNumberTable ().LineNumbers;
if (lines.Length == 0)
return;
int start_line = lines [0].Row;
int end_line = lines [lines.Length - 1].Row;
MethodCoverageItem method
= new MethodCoverageItem (klass, methodName);
method.startLine = start_line;
method.endLine = end_line;
#if USE_REFLECTION
method.filtered = IsFiltered ("[" + monoMethod.DeclaringType.Assembly + "]" + monoMethod.DeclaringType + "::" + monoMethod.Name);
#else
method.filtered = IsFiltered ("[" + monoMethod.DeclaringType.Module.Name + "]" + monoMethod.DeclaringType + "::" + monoMethod.Name);
#endif
klass.methodsByMethod [monoMethod] = method;
if (klass.sourceFile == null) {
string sourceFile = entry.CompileUnit.SourceFile.FileName;
SourceFileCoverageData source = (SourceFileCoverageData)sources [sourceFile];
if (source == null) {
source = new SourceFileCoverageData (sourceFile);
sources [sourceFile] = source;
}
klass.sourceFile = source;
}
computeMethodCoverage (method, lines, cov_info);
}
}
}