traceabilitytool/reportgenerator.cs (252 lines of code) (raw):
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Generic; // Used for List class.
using System.Linq; // Used for Cast<OpenXmlElement>
using System.Text.RegularExpressions; // Used for Regex class.
using DocumentFormat.OpenXml.Wordprocessing; // Used for Body class.
using DocumentFormat.OpenXml.Packaging; // Used for WordprocessingDocument class.
using DocumentFormat.OpenXml; // Used for OpenXmlElement class
using System.Windows.Forms; // Used for MessageBox class
using System; // Used for Exception and Console classes
namespace TraceabilityTool
{
// This class is responsible for extracting requirement IDs from requirement documents in Word OpenXML format (.docm files only),
// extracting requirement IDs from source code files (.c, .cpp, .h) and generating data structures making it easy to create
// requirement traceability reports and reports showing errors and mismatches between requirements and implementation/tests.
static class ReportGenerator
{
// Lists of files found in the root directory
public static List<string> requirementDocuments = new List<string>();
public static List<string> sourceCodeFiles = new List<string>();
// A list of requirement IDs and corresponding document file paths
public static Dictionary<string, string> reqDocLookup = new Dictionary<string, string>();
// A count of requirements per document.
public static Dictionary<string, int> reqDocCount = new Dictionary<string, int>();
// A list of requirement IDs and corresponding source code file paths
public static ReqPathMatrix reqCodeLookup = new ReqPathMatrix();
// A list of requirement IDs found in requirements documents and corresponding source code file paths (only contains traceable requirements).
public static ReqPathMatrix reqCodeMatrix = new ReqPathMatrix();
// A list of requirement IDs found in requirements documents and corresponding test code file paths (only contains traceable requirements).
public static ReqPathMatrix reqTestMatrix = new ReqPathMatrix();
// Requirements that were found in the source code files, but were not found in the requirement documents.
public static InvalidReqDictionary invalidRequirements = new InvalidReqDictionary();
// Requirements that were found in documents, but couldn't be found in the code.
public static Dictionary<string, string> missingCodeCoverage = new Dictionary<string, string>();
public static int missingCodeCoverageKeyWidth = 0;
// Requirements that were found in documents, but couldn't be found in the tests.
public static Dictionary<string, string> missingTestCoverage = new Dictionary<string, string>();
public static int missingTestCoverageKeyWidth = 0;
// Duplicate requirement IDs found in requirements documents.
public static Dictionary<string, List<string>> repeatingRequirements = new Dictionary<string, List<string>>();
public static int repeatingRequirementsKeyWidth = 0;
public static bool useGUI = true;
public static int GenerateReport(string rootFolderPath, string outputFolderPath, string[] exclusionDirs, MainForm mainForm)
{
// Clear the lists/dictionaries of files and requirement IDs in case we need to generate the reports again.
requirementDocuments.Clear();
reqCodeLookup.Clear();
reqDocCount.Clear();
sourceCodeFiles.Clear();
repeatingRequirements.Clear();
invalidRequirements.Clear();
reqDocLookup.Clear();
reqCodeMatrix.Clear();
reqTestMatrix.Clear();
missingCodeCoverage.Clear();
missingTestCoverage.Clear();
int result = 0;
// disable GUI elements if the program was called from command line interface.
if (mainForm == null)
useGUI = false;
// output dir is an empty string if not valid.
bool useOutputDir = (outputFolderPath.Length != 0);
// Read requirement identifiers and file paths from word documents and source code files.
GetRequirementsFromDocuments(rootFolderPath, exclusionDirs);
// Update status on the progress bar
if (mainForm != null)
{
mainForm.UpdateStatus(25);
}
GetRequirementsFromSource(rootFolderPath, exclusionDirs);
// Update status on the progress bar
if (mainForm != null)
{
mainForm.UpdateStatus(50);
}
GenerateTraceabilityMatrix();
// Update status on the progress bar
if (mainForm != null)
{
mainForm.UpdateStatus(75);
}
FindUncoveredRequirements();
// Write all reports to plain text files
if (MainForm.outputText && useOutputDir)
{
ReportWriter.WriteTraceabilityReport(outputFolderPath);
ReportWriter.WriteInvalidReqReport(outputFolderPath);
ReportWriter.WriteMissingCodeCoverageReport(outputFolderPath);
ReportWriter.WriteMissingTestCoverageReport(outputFolderPath);
ReportWriter.WriteMissingCodeAndTestCoverageReport(outputFolderPath);
ReportWriter.WriteRepeatingReqReport(outputFolderPath);
}
// Write all reports to CSV files
if (MainForm.outputCSV && useOutputDir)
{
CSVReportWriter.WriteTraceabilityReport(outputFolderPath);
CSVReportWriter.WriteMissingReqReport(outputFolderPath);
CSVReportWriter.WriteRepeatingReqReport(outputFolderPath);
}
if (MainForm.buildCheck)
{
result = ConsoleReportWriter.WriteMissingReqReport();
}
// Update status on the progress bar
if (mainForm != null)
{
mainForm.UpdateStatus(100);
}
return result;
}
private static void GetRequirementsFromDocuments(string rootFolderPath, string[] exclusionDirs)
{
// Generate a list of Word document files under the root folder.
List<string> reqDocFileFilter = new List<string>();
reqDocFileFilter.Add("*.docm");
reqDocFileFilter.Add("*.md");
FileFinder.SetFileFilters(reqDocFileFilter);
FileFinder.GetFileList(rootFolderPath, exclusionDirs, ref requirementDocuments);
// Read requirement identifiers from the Word documents.
foreach (string requirementDoc in requirementDocuments)
{
try
{
if (requirementDoc.EndsWith(".docm" ))
{
ReadWordDocRequirements(requirementDoc, ref reqDocLookup);
}
else if (requirementDoc.EndsWith(".md"))
{
ReadMarkdownRequirements(requirementDoc, ref reqDocLookup);
}
}
catch (Exception exception)
{
string message = "An error occurred while attempting to access the file " + requirementDoc + System.Environment.NewLine +
"The error is: " + exception.Message + System.Environment.NewLine;
if (ReportGenerator.useGUI)
MessageBox.Show(message, "Error");
else
Console.WriteLine(message);
Program.exitCode = 1;
}
}
}
private static void GetRequirementsFromSource(string rootFolderPath, string[] exclusionDirs)
{
// Generate a list of source code files under the root folder.
List<string> sourceCodeFilterList = new List<string>();
sourceCodeFilterList.Add("*.c");
sourceCodeFilterList.Add("*.cpp");
sourceCodeFilterList.Add("*.h");
sourceCodeFilterList.Add("*.cs");
sourceCodeFilterList.Add("*.java");
FileFinder.SetFileFilters(sourceCodeFilterList);
FileFinder.GetFileList(rootFolderPath, exclusionDirs, ref sourceCodeFiles);
// Read requirement identifiers from the source code files.
foreach (string sourceFile in sourceCodeFiles)
{
try
{
ReadSourceCodeRequirements(sourceFile, ref reqCodeLookup);
}
catch (Exception exception)
{
string message = "An error occurred while attempting to access the file " + sourceFile + System.Environment.NewLine +
"The error is: " + exception.Message + System.Environment.NewLine;
if (ReportGenerator.useGUI)
MessageBox.Show(message, "Error");
else
Console.WriteLine(message);
Program.exitCode = 1;
}
}
}
private static void GenerateTraceabilityMatrix()
{
// Generate traceability matrix by checking if every requirement found in the source code
// can be found in requirements documents.
int srsStartPos;
string reqID;
string reqPrefix;
// Loop through each requirement reference (Codes_SRS_* or Tests_SRS_*) found in the code
foreach (string codeReq in reqCodeLookup.Keys)
{
// Find the prefix of the requirement reference in the code
srsStartPos = codeReq.IndexOf("SRS");
reqPrefix = codeReq.Substring(0, srsStartPos);
// Find the requirement identifier (starting with "SRS_") in the key.
reqID = codeReq.Substring(srsStartPos);
// Check if the requirement reference prefix is correct and the requirement is found in requirement documents.
if (reqPrefix.Equals("Codes_") &&
reqDocLookup.ContainsKey(reqID))
{
// Update requirement-to-code matrix (req. ID and source code file paths).
reqCodeMatrix.Add(reqID, reqCodeLookup[codeReq]);
}
else if (reqPrefix.Equals("Tests_") &&
reqDocLookup.ContainsKey(reqID))
{
// Update requirement-to-test matrix (req. ID and test code file paths).
reqTestMatrix.Add(reqID, reqCodeLookup[codeReq]);
}
else if (reqPrefix.Equals("Codes_") ||
reqPrefix.Equals("Tests_"))
{
// Add requirement to the list of invalid requirements - requirement has no defition in .docm files.
invalidRequirements.Add(reqID, reqCodeLookup[codeReq], "Requirement definition not found");
}
else
{
// Add requirement to the list of invalid requirements - requirement reference is missing "Codes_" or "Tests_" prefix.
invalidRequirements.Add(reqID, reqCodeLookup[codeReq], "Requirement prefix not valid");
}
}
}
public static void ReadWordDocRequirements(string filePath, ref Dictionary<string, string> reqLookup)
{
WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(filePath, false);
Body body = wordprocessingDocument.MainDocumentPart.Document.Body;
// Accept deletions in tracked changes (revisions)
List<OpenXmlElement> deletions = body.Descendants<Deleted>().Cast<OpenXmlElement>().ToList();
deletions.AddRange(body.Descendants<DeletedRun>().Cast<OpenXmlElement>().ToList());
deletions.AddRange(body.Descendants<DeletedMathControl>().Cast<OpenXmlElement>().ToList());
foreach (OpenXmlElement deletion in deletions)
{
deletion.Remove();
}
string text = body.InnerText;
wordprocessingDocument.Close();
string pattern = @"SRS_[A-Z_\d]+_\d{2}_\d{3}";
ExtractRequirements(filePath, pattern, text, ref reqLookup);
}
public static void ReadMarkdownRequirements(string filePath, ref Dictionary<string, string> reqLookup)
{
System.IO.StreamReader fileAsStream = new System.IO.StreamReader(filePath);
string fileAsString = fileAsStream.ReadToEnd();
fileAsStream.Close();
string pattern = @"SRS_[A-Z_\d]+_\d{2}_\d{3}";
ExtractRequirements(filePath, pattern, fileAsString, ref reqLookup);
}
public static void ExtractRequirements(string filePath, string pattern, string text, ref Dictionary<string, string> reqLookup)
{
int count = 0;
foreach (Match m in Regex.Matches(text, pattern))
{
count++;
// Add each requirement from Word documents to the lookup dictionary
// unless this requirement already exists.
if (reqLookup.ContainsKey(m.Value))
{
// Requirement already exists. Put it in the dictionary of repeating requirements.
List<string> filePaths;
if (repeatingRequirements.ContainsKey(m.Value))
{
// Update the existing list of file paths.
filePaths = repeatingRequirements[m.Value];
}
else
{
// Create a new list of file paths.
filePaths = new List<string>();
// Add the file path from the dictionary first (original document path), then add the current path.
filePaths.Add(reqLookup[m.Value]);
}
// Add the new path for the repeating requirement.
filePaths.Add(filePath);
// Add all paths to the dictionary.
repeatingRequirements.Add(m.Value, filePaths);
if (m.Value.Length > repeatingRequirementsKeyWidth)
repeatingRequirementsKeyWidth = m.Value.Length;
}
else
{
// Create a new entry with requirement ID as the key and the path as the value to the dictionary.
reqLookup.Add(m.Value, filePath);
}
}
if (count > 0)
{
reqDocCount.Add(filePath, count);
}
}
public static void ReadSourceCodeRequirements(string filePath, ref ReqPathMatrix codeReqLookup)
{
// Read the file as one string.
System.IO.StreamReader fileAsStream = new System.IO.StreamReader(filePath);
string fileAsString = fileAsStream.ReadToEnd();
fileAsStream.Close();
// Look for requirement references in the format something_SRS_something_123 or SRS_something_123
// or _something_SRS_something_123 and _SRS_something_123
string pattern = @"\b[A-Za-z_\d]*_?SRS_\w+_\d{2}_\d{3}";
int lineNum = 1; // Start counting lines from 1.
int pos = 0; // First character index for a regular expression match is 0.
foreach (Match m in Regex.Matches(fileAsString, pattern))
{
while (pos < fileAsString.Length && pos < m.Index)
{
if (fileAsString[pos] == '\n')
lineNum++;
pos++;
}
codeReqLookup.Add(m.Value, new FilePathLineNum(filePath, lineNum));
}
}
private static void FindUncoveredRequirements()
{
// Find all requirements from Word documents that don't exist in either code or test traceability matrix.
// Loop through each requirement from Word documents and check it against the requirements found in the source code.
foreach (string req in reqDocLookup.Keys)
{
if (!reqCodeMatrix.ContainsKey(req))
{
missingCodeCoverage.Add(req, reqDocLookup[req]);
if (req.Length > missingCodeCoverageKeyWidth)
missingCodeCoverageKeyWidth = req.Length;
}
if (!reqTestMatrix.ContainsKey(req))
{
missingTestCoverage.Add(req, reqDocLookup[req]);
if (req.Length > missingTestCoverageKeyWidth)
missingTestCoverageKeyWidth = req.Length;
}
}
}
}
}