diff --git a/RoboClerk.AnnotatedUnitTests/AnnotatedUnitTestsPlugin.cs b/RoboClerk.AnnotatedUnitTests/AnnotatedUnitTestsPlugin.cs index 3689d7f..c9b4ee7 100644 --- a/RoboClerk.AnnotatedUnitTests/AnnotatedUnitTestsPlugin.cs +++ b/RoboClerk.AnnotatedUnitTests/AnnotatedUnitTestsPlugin.cs @@ -1,4 +1,5 @@ -using RoboClerk.Configuration; +using DocumentFormat.OpenXml.Wordprocessing; +using RoboClerk.Configuration; using System; using System.Collections.Generic; using System.IO; @@ -15,6 +16,8 @@ public class AnnotatedUnitTestsPlugin : SourceCodeAnalysisPluginBase private string parameterStartDelimiter = string.Empty; private string parameterEndDelimiter = string.Empty; private string parameterSeparator = string.Empty; + private string functionNameStartSeq = string.Empty; + private string functionNameEndSeq = string.Empty; private Dictionary information = new Dictionary(); @@ -52,16 +55,26 @@ public override void Initialize(IConfiguration configuration) base.Initialize(configuration); var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); - decorationMarker = configuration.CommandLineOptionOrDefault("DecorationMarker", GetStringForKey(config, "DecorationMarker", true)); - parameterStartDelimiter = configuration.CommandLineOptionOrDefault("ParameterStartDelimiter", GetStringForKey(config, "ParameterStartDelimiter", true)); - parameterEndDelimiter = configuration.CommandLineOptionOrDefault("ParameterEndDelimiter", GetStringForKey(config, "ParameterEndDelimiter", true)); - parameterSeparator = configuration.CommandLineOptionOrDefault("ParameterSeparator", GetStringForKey(config, "ParameterSeparator", true)); + decorationMarker = configuration.CommandLineOptionOrDefault("DecorationMarker", GetObjectForKey(config, "DecorationMarker", true)); + parameterStartDelimiter = configuration.CommandLineOptionOrDefault("ParameterStartDelimiter", GetObjectForKey(config, "ParameterStartDelimiter", true)); + parameterEndDelimiter = configuration.CommandLineOptionOrDefault("ParameterEndDelimiter", GetObjectForKey(config, "ParameterEndDelimiter", true)); + parameterSeparator = configuration.CommandLineOptionOrDefault("ParameterSeparator", GetObjectForKey(config, "ParameterSeparator", true)); PopulateUTInfo("Purpose", config); PopulateUTInfo("PostCondition", config); PopulateUTInfo("Identifier", config); PopulateUTInfo("TraceID", config); + if(config.ContainsKey("FunctionName")) + { + var tomlTable = (TomlTable)config["FunctionName"]; + functionNameStartSeq = tomlTable["StartString"].ToString(); + functionNameEndSeq = tomlTable["EndString"].ToString(); + } + else + { + throw new Exception($"Table \"FunctionName\" missing from configuration file: \"{name}.toml\"."); + } } catch (Exception e) { @@ -78,20 +91,28 @@ private int ParameterEnd(string input) int closers = 0; bool insideStringLiteral = false; bool ignoreStringDelim = false; - + bool insideStringBlock = false; for (int i = 0; i < input.Length; i++) { var temp = input.Substring(i); - if (temp.StartsWith("\"") && !ignoreStringDelim) + if (temp.StartsWith("\"\"\"") && !ignoreStringDelim) + { + if (insideStringBlock && temp.Length >= 4 && temp[3] == '"') + continue; + insideStringBlock = !insideStringBlock; + i += 2; + continue; + } + if (temp.StartsWith("\"") && !ignoreStringDelim && !insideStringBlock) { insideStringLiteral = !insideStringLiteral; } ignoreStringDelim = temp.StartsWith("\\\""); - if (!insideStringLiteral && temp.StartsWith(parameterStartDelimiter)) + if ( (!insideStringLiteral && !insideStringBlock) && temp.StartsWith(parameterStartDelimiter)) { openers++; } - if (!insideStringLiteral && temp.StartsWith(parameterEndDelimiter)) + if ( (!insideStringLiteral && !insideStringBlock) && temp.StartsWith(parameterEndDelimiter)) { closers++; } @@ -110,17 +131,32 @@ private Dictionary ParseParameterString(string pms, int startLin //to be used in practice StringBuilder pmsSb = new StringBuilder(pms); bool insideString = false; + bool insideTextBlock = false; for (int i = 0; i < pms.Length; i++) { if (pms[i] == '"') { - insideString = !insideString; + if (i + 2 < pms.Length && pms[i + 1] == '"' && pms[i + 2] == '"') + { + int index = i + 2; + while (index < pms.Length && pms[index] == '"') + { + index++; + } + insideTextBlock = !insideTextBlock; + i = index; + continue; + } + else if(!insideTextBlock) + { + insideString = !insideString; + } } - if (pms[i] == '=' && insideString) + if (pms[i] == '=' && (insideString || insideTextBlock)) { pmsSb[i] = '\a'; } - if (pms[i] == ',' && insideString) + if (pms[i] == ',' && (insideString || insideTextBlock)) { pmsSb[i] = '\f'; } @@ -177,6 +213,7 @@ private void FindAndProcessAnnotations(string[] lines, string filename) paramEndIndex = ParameterEnd(foundAnnotation.ToString()); if (paramEndIndex >= 0) { + i = j; break; } else @@ -195,24 +232,48 @@ private void FindAndProcessAnnotations(string[] lines, string filename) throw new Exception($"Required parameter {info.Key} missing from unit test anotation starting on {startLine} of \"{filename}\"."); } } - AddUnitTest(filename, startLine, foundParameters); + // extract the function name + int startI = i; + string functionName = string.Empty; + for (int j = i ; j < lines.Length && j-startI<3 ; j++) + { + int startIndex = lines[j].IndexOf(functionNameStartSeq); + if( startIndex>=0 ) + { + int endIndex = lines[j].IndexOf(functionNameEndSeq); + if( endIndex>=0 ) + { + functionName = lines[j].Substring(startIndex+functionNameStartSeq.Length, endIndex-(startIndex+functionNameStartSeq.Length)); + functionName = functionName.Trim(); + i = j; + break; + } + } + } + AddUnitTest(filename, startLine, foundParameters, functionName); } } } - private void AddUnitTest(string fileName, int lineNumber, Dictionary parameterValues) + private void AddUnitTest(string fileName, int lineNumber, Dictionary parameterValues, string functionName) { var unitTest = new UnitTestItem(); + unitTest.UnitTestFunctionName = functionName; bool identified = false; string shortFileName = Path.GetFileName(fileName); + unitTest.UnitTestFileName = shortFileName; foreach (var info in information) { if (parameterValues.ContainsKey(info.Key)) { var value = parameterValues[info.Key]; - //all strings are assumed to start and end with a string delimiter for all supported languages - value = value.Substring(1, value.Length - 2).Replace("\\\"","\""); + //all strings are assumed to start and end with a string delimiter for all supported languages, + //note that for some languages the string delimiter can be """ + if (value.StartsWith("\"\"\"")) + value = value.Substring(3, value.Length - 6).Replace("\\\"", "\""); + else + value = value.Substring(1, value.Length - 2).Replace("\\\"", "\""); switch (info.Key) { case "Purpose": unitTest.UnitTestPurpose = value; break; diff --git a/RoboClerk.AnnotatedUnitTests/Configuration/AnnotatedUnitTestPlugin.toml b/RoboClerk.AnnotatedUnitTests/Configuration/AnnotatedUnitTestPlugin.toml index a67da89..1d48862 100644 --- a/RoboClerk.AnnotatedUnitTests/Configuration/AnnotatedUnitTestPlugin.toml +++ b/RoboClerk.AnnotatedUnitTests/Configuration/AnnotatedUnitTestPlugin.toml @@ -66,4 +66,18 @@ ParameterSeparator = "," Keyword = "TraceID" Optional = true +# RoboClerk assumes that after the annotation, the test function name is specified. +# In order for RoboClerk to be able to extract the function name, you need to specify +# the starting and ending elements of the function name. RoboClerk will match the first +# instance with the appropriate startstring and endstring within three lines after +# the annotation end. +# It is assumed that whatever is between the StartString and EndString is the unit +# test function name. Note that the StartString, the function name +# and the EndString are all on a single line. Otherwise, no function name will be +# detected. + +[FunctionName] + StartString = "public void " + EndString = "(" + diff --git a/RoboClerk.AzureDevOps/AzureDevOpsSLMSPlugin.cs b/RoboClerk.AzureDevOps/AzureDevOpsSLMSPlugin.cs index 4ac68b9..bcc274d 100644 --- a/RoboClerk.AzureDevOps/AzureDevOpsSLMSPlugin.cs +++ b/RoboClerk.AzureDevOps/AzureDevOpsSLMSPlugin.cs @@ -67,7 +67,7 @@ public override void RefreshItems() List retrievedIDs = new List(); logger.Info("Retrieving and processing system level requirements."); - foreach (var workitem in GetWorkItems(prsName)) + foreach (var workitem in GetWorkItems(PrsConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); @@ -76,7 +76,7 @@ public override void RefreshItems() } logger.Info("Retrieving and processing software level requirements."); - foreach (var workitem in GetWorkItems(srsName)) + foreach (var workitem in GetWorkItems(SrsConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); @@ -85,7 +85,7 @@ public override void RefreshItems() } logger.Info("Retrieving and processing documentation requirements."); - foreach (var workitem in GetWorkItems(docName)) + foreach (var workitem in GetWorkItems(DocConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); @@ -94,7 +94,7 @@ public override void RefreshItems() } logger.Info("Retrieving and processing docContent requirements."); - foreach (var workitem in GetWorkItems(cntName)) + foreach (var workitem in GetWorkItems(CntConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); @@ -103,7 +103,7 @@ public override void RefreshItems() } logger.Info("Retrieving and SOUP items."); - foreach (var workitem in GetWorkItems(soupName)) + foreach (var workitem in GetWorkItems(SoupConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); @@ -112,7 +112,7 @@ public override void RefreshItems() } logger.Info("Retrieving test cases."); - foreach (var workitem in GetWorkItems(tcName)) + foreach (var workitem in GetWorkItems(TcConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); @@ -122,7 +122,7 @@ public override void RefreshItems() logger.Info("Retrieving and processing bugs."); - foreach (var workitem in GetWorkItems(bugName)) + foreach (var workitem in GetWorkItems(BugConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); @@ -133,7 +133,7 @@ public override void RefreshItems() logger.Info("Retrieving and processing risks."); //Note that to gather all information about the risk item, this code relies on the //system level requirements having been retrieved already. - foreach (var workitem in GetWorkItems(riskName)) + foreach (var workitem in GetWorkItems(RiskConfig.Name)) { if (IgnoreItem(workitem)) continue; retrievedIDs.Add(workitem.Id.ToString()); diff --git a/RoboClerk.AzureDevOps/Configuration/AzureDevOpsSLMSPlugin.toml b/RoboClerk.AzureDevOps/Configuration/AzureDevOpsSLMSPlugin.toml index 0973168..b27244d 100644 --- a/RoboClerk.AzureDevOps/Configuration/AzureDevOpsSLMSPlugin.toml +++ b/RoboClerk.AzureDevOps/Configuration/AzureDevOpsSLMSPlugin.toml @@ -4,18 +4,70 @@ AccessToken = "" OrganizationName = "" ProjectName = "RoboClerk" -# The following allows you to indicate the work item names that map -# to various entities in the RoboClerk software -SystemRequirement = "Epic" -SoftwareRequirement = "User Story" -DocumentationRequirement = "Documentation" -DocContent = "DocContent" -SoftwareSystemTest = "Test Case" -Risk = "Risk" -Anomaly = "Bug" -SOUP = "SOUP" - # certain items in azure devops should be ignored because they are no longer # relevant. Indicate which item statuses should be ignored. -Ignore = [ "Removed" ] \ No newline at end of file +Ignore = [ "Removed" ] + +# The following allows you to indicate the redmine trackers that map +# to various entities in the RoboClerk software. Set the name to the +# redmine ticket type. You can also indicate if the items are subject +# are subject to the inclusion and exclusion filters defined further +# on in this file. + +[SystemRequirement] + name = "Epic" + filter = true + +[SoftwareRequirement] + name = "User Story" + filter = true + +[DocumentationRequirement] + name = "Documentation" + filter = true + +[DocContent] + name = "DocContent" + filter = false + +[SoftwareSystemTest] + name = "Test Case" + filter = false + +[Risk] + name = "Risk" + filter = true + +[Anomaly] + name = "Bug" + filter = false + +[SOUP] + name = "SOUP" + filter = true + +# To support the use case of documenting different versions of the software +# for example an RUO vs IVD version with different features or a US vs an EU +# version of the software, the RoboClerk Redmine Plugin supports providing +# fields here that will cause it to either include or exclude items and all +# attached items as well. As an example, by providing the value +# [ExcludedItemFilter] +# ReleaseType = ["IVD"] +# Roboclerk will look for a field named "ReleaseType" in the item tickets +# and if the field value is in the list (e.g. "IVD") it will ignore that ticket +# and all attached items. +# Another use of this feature is to include only those +# tickets associated with a particular release. +# [IncludedItemFilter] +# MileStone = ["1.0.0","1.0.1"] +# this will ensure that only items that have a field named MileStone with the values +# 1.0.0 or 1.0.1 will be included +# Note that we only include those items types that have the filter property set +# to true (see above) + +[ExcludedItemFilter] + ReleaseType = [ "IVD" ] + +[IncludedItemFilter] + MileStone = ["1.0.0","1.0.1"] \ No newline at end of file diff --git a/RoboClerk.OpenAI/OpenAIPlugin.cs b/RoboClerk.OpenAI/OpenAIPlugin.cs index cf08ae2..a8e0cb5 100644 --- a/RoboClerk.OpenAI/OpenAIPlugin.cs +++ b/RoboClerk.OpenAI/OpenAIPlugin.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO.Abstractions; +using RoboClerk.AISystem; namespace RoboClerk.OpenAI { @@ -23,16 +24,16 @@ public override void Initialize(IConfiguration configuration) { base.Initialize(configuration); var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); - bool useAzureOpenAI = configuration.CommandLineOptionOrDefault("UseAzureOpenAI", GetStringForKey(config, "UseAzureOpenAI", true)).ToUpper() == "TRUE"; + bool useAzureOpenAI = configuration.CommandLineOptionOrDefault("UseAzureOpenAI", GetObjectForKey(config, "UseAzureOpenAI", true)).ToUpper() == "TRUE"; if (useAzureOpenAI) { - string azureOpenAIUri = configuration.CommandLineOptionOrDefault("AzureOpenAIUri", GetStringForKey(config, "AzureOpenAIUri", true)); - string azureOpenAIResourceKey = configuration.CommandLineOptionOrDefault("AzureOpenAIResourceKey", GetStringForKey(config, "AzureOpenAIResourceKey", true)); + string azureOpenAIUri = configuration.CommandLineOptionOrDefault("AzureOpenAIUri", GetObjectForKey(config, "AzureOpenAIUri", true)); + string azureOpenAIResourceKey = configuration.CommandLineOptionOrDefault("AzureOpenAIResourceKey", GetObjectForKey(config, "AzureOpenAIResourceKey", true)); openAIClient = new OpenAIClient(new Uri(azureOpenAIUri),new Azure.AzureKeyCredential(azureOpenAIResourceKey)); } else { - string openAIKey = configuration.CommandLineOptionOrDefault("OpenAIKey", GetStringForKey(config, "OpenAIKey", true)); + string openAIKey = configuration.CommandLineOptionOrDefault("OpenAIKey", GetObjectForKey(config, "OpenAIKey", true)); openAIClient = new OpenAIClient(openAIKey); } diff --git a/RoboClerk.OpenAI/OpenAIPromptTemplate.cs b/RoboClerk.OpenAI/OpenAIPromptTemplate.cs index f95e6f8..ce45f69 100644 --- a/RoboClerk.OpenAI/OpenAIPromptTemplate.cs +++ b/RoboClerk.OpenAI/OpenAIPromptTemplate.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using RoboClerk.AISystem; using Tomlet; namespace RoboClerk diff --git a/RoboClerk.Redmine/Configuration/RedmineSLMSPlugin.toml b/RoboClerk.Redmine/Configuration/RedmineSLMSPlugin.toml index daa677c..2719a76 100644 --- a/RoboClerk.Redmine/Configuration/RedmineSLMSPlugin.toml +++ b/RoboClerk.Redmine/Configuration/RedmineSLMSPlugin.toml @@ -1,32 +1,112 @@ -# This is the RoboClerk Redmine plugin configuration - -RedmineAPIEndpoint = "http://localhost:3001/" -RedmineAPIKey = "014a75c443c18891c3baf8f7f2d2ce37aa27b94c" - -# The name of the project that we will be pulling from Redmine - -RedmineProject = "TestProject" - -# The following allows you to indicate the redmine trackers that map -# to various entities in the RoboClerk software - -SystemRequirement = "SystemRequirement" -SoftwareRequirement = "SoftwareRequirement" -DocumentationRequirement = "Documentation" -DocContent = "DocContent" -SoftwareSystemTest = "SoftwareSystemTest" -Risk = "Risk" -Anomaly = "Bug" -SOUP = "SOUP" - -# certain items in redmine should be ignored because they are no longer -# relevant. Indicate which item statuses should be ignored. - -Ignore = [ "Rejected" ] - -# In order to provide hyperlinks in the documents provide the redmine -# base URL here. NOTE: Ensure the URL ends with a single / -# If you do not wish the software to include hyperlinks, just set this -# variable to an empty string "". - -RedmineBaseURL = "http://localhost:3001/issues/" \ No newline at end of file +# This is the RoboClerk Redmine plugin configuration + +RedmineAPIEndpoint = "http://localhost:3001/" +RedmineAPIKey = "014a75c443c18891c3baf8f7f2d2ce37aa27b94c" + +# The name of the project that we will be pulling from Redmine + +RedmineProject = "TestProject" + +# certain items in redmine should be ignored because they are no longer +# relevant. Indicate which item statuses should be ignored. +Ignore = [ "Rejected" ] + +# In order to provide hyperlinks in the documents provide the redmine +# base URL here. NOTE: Ensure the URL ends with a single / +# If you do not wish the software to include hyperlinks, just set this +# variable to an empty string "". +RedmineBaseURL = "http://localhost:3001/issues/" + +# The following allows you to indicate the redmine trackers that map +# to various entities in the RoboClerk software. Set the name to the +# redmine ticket type. You can also indicate if the items are subject +# are subject to the inclusion and exclusion filters defined further +# on in this file. + +[SystemRequirement] + name = "SystemRequirement" + filter = true + +[SoftwareRequirement] + name = "SoftwareRequirement" + filter = true + +[DocumentationRequirement] + name = "Documentation" + filter = true + +[DocContent] + name = "DocContent" + filter = false + +[SoftwareSystemTest] + name = "SoftwareSystemTest" + filter = false + +[Risk] + name = "Risk" + filter = true + +[Anomaly] + name = "Bug" + filter = false + +[SOUP] + name = "SOUP" + filter = true + +# To support the use case of documenting different versions of the software +# for example an RUO vs IVD version with different features or a US vs an EU +# version of the software, the RoboClerk Redmine Plugin supports providing +# fields here that will cause it to either include or exclude items and all +# attached items as well. As an example, by providing the value +# [ExcludedItemFilter] +# ReleaseType = ["IVD"] +# Roboclerk will look for a field named "ReleaseType" in the item tickets +# and if the field value is in the list (e.g. "IVD") it will ignore that ticket +# and all attached items. +# Another use of this feature is to include only those +# tickets associated with a particular release. +# [IncludedItemFilter] +# MileStone = ["1.0.0","1.0.1"] +# this will ensure that only items that have a field named MileStone with the values +# 1.0.0 or 1.0.1 will be included +# Note that we only include those items types that have the filter property set +# to true (see above) +# +# If you want to filter on one of the default Redmine fields then use the following +# names to identify them: +# Id = the issue number field +# Project = name of the project this issue is associated with +# Tracker = name of the tracker this issue is associated with +# Status = the status of the issue +# Priority = the priority of the issue +# Author = the name of the author of the issue +# AssignedTo = the name of the person the issue is assigned to +# FixedVersion = the name of the version in which the issue has been fixed +# Subject = The subject of the issue +# Description = The description of the issue +# StartDate = The start date (MM-dd-yyyy) +# DueDate = The due date (MM-dd-yyyy) +# DoneRatio = The percentage (integer) that is done +# IsPrivate = True/False +# CreatedOn = Creation date (MM-dd-yyyy) +# UpdatedOn = Date updated (MM-dd-yyyy) +# ClosedOn = Date closed (MM-dd-yyyy) + +# Provide a list of custom fields of type version as these need special handeling +[VersionCustomFields] + FieldNames = ["TestVersion"] + +[ExcludedItemFilter] + #Superseded = ["0.5.0","1.0.0","1.1.0"] + TestVersion = ["0.5.0"] + +[IncludedItemFilter] + ReleaseRegion = ["EU","US"] + #Milestone = ["0.5.0","1.0.0","1.1.0"] + + + + + \ No newline at end of file diff --git a/RoboClerk.Redmine/CustomFieldsJSONObjects.cs b/RoboClerk.Redmine/CustomFieldsJSONObjects.cs new file mode 100644 index 0000000..df3bac1 --- /dev/null +++ b/RoboClerk.Redmine/CustomFieldsJSONObjects.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace RoboClerk.Redmine +{ + public class Tracker + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public class PossibleValue + { + [JsonPropertyName("value")] + public string Value { get; set; } + + [JsonPropertyName("label")] + public string Label { get; set; } + } + + public class CustomRedmineField + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("customized_type")] + public string CustomizedType { get; set; } + + [JsonPropertyName("field_format")] + public string FieldFormat { get; set; } + + [JsonPropertyName("regexp")] + public string Regexp { get; set; } + + [JsonPropertyName("min_length")] + public int? MinLength { get; set; } + + [JsonPropertyName("max_length")] + public int? MaxLength { get; set; } + + [JsonPropertyName("is_required")] + public bool IsRequired { get; set; } + + [JsonPropertyName("is_filter")] + public bool IsFilter { get; set; } + + [JsonPropertyName("searchable")] + public bool Searchable { get; set; } + + [JsonPropertyName("multiple")] + public bool Multiple { get; set; } + + [JsonPropertyName("default_value")] + public string DefaultValue { get; set; } + + [JsonPropertyName("visible")] + public bool Visible { get; set; } + + [JsonPropertyName("possible_values")] + public List PossibleValues { get; set; } + + [JsonPropertyName("trackers")] + public List Trackers { get; set; } + + [JsonPropertyName("roles")] + public List Roles { get; set; } // Assuming roles are not detailed in the provided JSON + } + + public class CustomFieldList + { + [JsonPropertyName("custom_fields")] + public List CustomFields { get; set; } + } +} \ No newline at end of file diff --git a/RoboClerk.Redmine/ProjectVersionsJSONObjects.cs b/RoboClerk.Redmine/ProjectVersionsJSONObjects.cs new file mode 100644 index 0000000..68cf3b7 --- /dev/null +++ b/RoboClerk.Redmine/ProjectVersionsJSONObjects.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace RoboClerk.Redmine +{ + public class Project + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + } + + public class Version + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("project")] + public Project Project { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("due_date")] + public string DueDate { get; set; } + + [JsonPropertyName("sharing")] + public string Sharing { get; set; } + + [JsonPropertyName("wiki_page_title")] + public string WikiPageTitle { get; set; } + + [JsonPropertyName("created_on")] + public DateTime CreatedOn { get; set; } + + [JsonPropertyName("updated_on")] + public DateTime UpdatedOn { get; set; } + } + + public class VersionList + { + [JsonPropertyName("versions")] + public List Versions { get; set; } + + [JsonPropertyName("total_count")] + public int TotalCount { get; set; } + } +} \ No newline at end of file diff --git a/RoboClerk.Redmine/RedmineJSONObjects.cs b/RoboClerk.Redmine/RedmineJSONObjects.cs index 7bb87c3..5c82ec0 100644 --- a/RoboClerk.Redmine/RedmineJSONObjects.cs +++ b/RoboClerk.Redmine/RedmineJSONObjects.cs @@ -1,6 +1,4 @@ -// Root myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse); -//using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/RoboClerk.Redmine/RedmineSLMSPlugin.cs b/RoboClerk.Redmine/RedmineSLMSPlugin.cs index 5a83515..9797e5c 100644 --- a/RoboClerk.Redmine/RedmineSLMSPlugin.cs +++ b/RoboClerk.Redmine/RedmineSLMSPlugin.cs @@ -1,9 +1,11 @@ -using RestSharp; +using DocumentFormat.OpenXml.Bibliography; +using RestSharp; using RoboClerk.Configuration; using System; using System.Collections.Generic; using System.IO.Abstractions; using System.Linq; +using System.Reflection; using Tomlyn.Model; namespace RoboClerk.Redmine @@ -14,7 +16,9 @@ public class RedmineSLMSPlugin : SLMSPluginBase private string apiEndpoint = string.Empty; private string apiKey = string.Empty; private string projectName = string.Empty; + private List redmineVersionFields = new List(); private RestClient client = null; + private List versions = null; public RedmineSLMSPlugin(IFileSystem fileSystem) : base(fileSystem) @@ -30,12 +34,25 @@ public override void Initialize(IConfiguration configuration) base.Initialize(configuration); try { - var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); - apiEndpoint = configuration.CommandLineOptionOrDefault("RedmineAPIEndpoint", GetStringForKey(config, "RedmineAPIEndpoint", true)); + TomlTable config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); + apiEndpoint = configuration.CommandLineOptionOrDefault("RedmineAPIEndpoint", GetObjectForKey(config, "RedmineAPIEndpoint", true)); client = new RestClient(apiEndpoint); - apiKey = configuration.CommandLineOptionOrDefault("RedmineAPIKey", GetStringForKey(config, "RedmineAPIKey", true)); - projectName = configuration.CommandLineOptionOrDefault("RedmineProject", GetStringForKey(config, "RedmineProject", true)); - baseURL = configuration.CommandLineOptionOrDefault("RedmineBaseURL", GetStringForKey(config, "RedmineBaseURL", false)); + apiKey = configuration.CommandLineOptionOrDefault("RedmineAPIKey", GetObjectForKey(config, "RedmineAPIKey", true)); + projectName = configuration.CommandLineOptionOrDefault("RedmineProject", GetObjectForKey(config, "RedmineProject", true)); + baseURL = configuration.CommandLineOptionOrDefault("RedmineBaseURL", GetObjectForKey(config, "RedmineBaseURL", false)); + if(config.ContainsKey("VersionCustomFields")) + { + //this is needed specifically for Redmine because we cannot via the API figure out if a custom field is of type "version" + //without having admin rights. + TomlTable versionCustomFields = (TomlTable)config["VersionCustomFields"]; + foreach (var field in versionCustomFields) + { + foreach (var fieldValue in (TomlArray)field.Value) + { + redmineVersionFields.Add((string)fieldValue); + } + } + } } catch (Exception e) { @@ -47,9 +64,9 @@ public override void Initialize(IConfiguration configuration) private List GetTrackerList() { - var result = new List { prsName, srsName, tcName, - bugName, riskName, soupName, - docName, cntName }; + var result = new List { PrsConfig.Name, SrsConfig.Name, TcConfig.Name, + BugConfig.Name, RiskConfig.Name, SoupConfig.Name, + DocConfig.Name, CntConfig.Name }; result.RemoveAll(x => x == string.Empty); return result; } @@ -74,51 +91,153 @@ public override void RefreshItems() continue; } retrievedIDs.Add(redmineIssue.Id.ToString()); - if (redmineIssue.Tracker.Name == prsName) + if (redmineIssue.Tracker.Name == PrsConfig.Name) { logger.Debug($"System level requirement found: {redmineIssue.Id}"); - systemRequirements.Add(CreateRequirement(redmineIssues, redmineIssue, RequirementType.SystemRequirement)); + if (!ShouldIgnoreIssue(redmineIssue,PrsConfig)) + { + systemRequirements.Add(CreateRequirement(redmineIssues, redmineIssue, RequirementType.SystemRequirement)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } - else if (redmineIssue.Tracker.Name == srsName) + else if (redmineIssue.Tracker.Name == SrsConfig.Name) { logger.Debug($"Software level requirement found: {redmineIssue.Id}"); - softwareRequirements.Add(CreateRequirement(redmineIssues, redmineIssue, RequirementType.SoftwareRequirement)); + if (!ShouldIgnoreIssue(redmineIssue, SrsConfig)) + { + softwareRequirements.Add(CreateRequirement(redmineIssues, redmineIssue, RequirementType.SoftwareRequirement)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } - else if (redmineIssue.Tracker.Name == tcName) + else if (redmineIssue.Tracker.Name == TcConfig.Name) { logger.Debug($"Testcase found: {redmineIssue.Id}"); - testCases.Add(CreateTestCase(redmineIssues, redmineIssue)); + if (!ShouldIgnoreIssue(redmineIssue, TcConfig)) + { + testCases.Add(CreateTestCase(redmineIssues, redmineIssue)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } - else if (redmineIssue.Tracker.Name == bugName) + else if (redmineIssue.Tracker.Name == BugConfig.Name) { logger.Debug($"Bug item found: {redmineIssue.Id}"); - anomalies.Add(CreateBug(redmineIssue)); + if (!ShouldIgnoreIssue(redmineIssue, BugConfig)) + { + anomalies.Add(CreateBug(redmineIssue)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } - else if (redmineIssue.Tracker.Name == riskName) + else if (redmineIssue.Tracker.Name == RiskConfig.Name) { logger.Debug($"Risk item found: {redmineIssue.Id}"); - risks.Add(CreateRisk(redmineIssues, redmineIssue)); + if (!ShouldIgnoreIssue(redmineIssue, RiskConfig)) + { + risks.Add(CreateRisk(redmineIssues, redmineIssue)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } - else if (redmineIssue.Tracker.Name == soupName) + else if (redmineIssue.Tracker.Name == SoupConfig.Name) { logger.Debug($"SOUP item found: {redmineIssue.Id}"); - soup.Add(CreateSOUP(redmineIssue)); + if (!ShouldIgnoreIssue(redmineIssue, SoupConfig)) + { + soup.Add(CreateSOUP(redmineIssue)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } - else if (redmineIssue.Tracker.Name == docName) + else if (redmineIssue.Tracker.Name == DocConfig.Name) { logger.Debug($"Documentation item found: {redmineIssue.Id}"); - documentationRequirements.Add(CreateRequirement(redmineIssues, redmineIssue, RequirementType.DocumentationRequirement)); + if (!ShouldIgnoreIssue(redmineIssue, DocConfig)) + { + documentationRequirements.Add(CreateRequirement(redmineIssues, redmineIssue, RequirementType.DocumentationRequirement)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } - else if (redmineIssue.Tracker.Name == cntName) + else if (redmineIssue.Tracker.Name == CntConfig.Name) { logger.Debug($"DocContent item found: {redmineIssue.Id}"); - docContents.Add(CreateDocContent(redmineIssue)); + if (!ShouldIgnoreIssue(redmineIssue, CntConfig)) + { + docContents.Add(CreateDocContent(redmineIssue)); + } + else + { + retrievedIDs.Remove(redmineIssue.Id.ToString()); + } } } + RemoveAllItemsNotLinked(retrievedIDs); RemoveIgnoredLinks(retrievedIDs); //go over all items and remove any links to ignored items ScrubItemContents(); //go over all relevant items and escape any | characters } + private List CheckForLinkedItem(List retrievedIDs, List inputItems, List lt) where T : LinkedItem + { + List items = new List(); + string removeItem = string.Empty; + foreach (var item in inputItems) + { + if(item.LinkedItems.Count() > 0) //orphan items are always included so they don't get lost or ingnored. + removeItem = item.ItemID; + foreach (var link in item.LinkedItems) + { + + if (link != null && lt.Contains(link.LinkType) && + retrievedIDs.Contains(link.TargetID) && + (systemRequirements.Any(obj => obj.ItemID == link.TargetID) || + softwareRequirements.Any(obj => obj.ItemID == link.TargetID) || + documentationRequirements.Any(obj => obj.ItemID == link.TargetID)) ) + { + removeItem = string.Empty; + break; + } + } + if (removeItem == string.Empty) + { + items.Add(item); + } + else + { + logger.Info($"Removing item because it is not linked to a valid item: {item.ItemID}"); + retrievedIDs.Remove(removeItem); + removeItem = string.Empty; + } + } + return items; + } + + private void RemoveAllItemsNotLinked(List retrievedIDs) + { + softwareRequirements = CheckForLinkedItem(retrievedIDs, softwareRequirements, new List { ItemLinkType.Parent }); + testCases = CheckForLinkedItem(retrievedIDs, testCases, new List { ItemLinkType.Parent, ItemLinkType.Related }); + docContents = CheckForLinkedItem(retrievedIDs, docContents, new List { ItemLinkType.Parent }); + risks = CheckForLinkedItem(retrievedIDs, risks, new List { ItemLinkType.RiskControl }); + //need to remove any bugs connected to items that were removed. + anomalies = CheckForLinkedItem(retrievedIDs, anomalies, new List { ItemLinkType.Related } ); + } + private void RemoveIgnoredLinks(List retrievedIDs) { TrimLinkedItems(systemRequirements, retrievedIDs); @@ -219,7 +338,7 @@ private SoftwareSystemTestItem CreateTestCase(List issues, Redmine { foreach (var issue in issues) { - if (issue.Id.ToString() == link.TargetID && issue.Tracker.Name == srsName) + if (issue.Id.ToString() == link.TargetID && issue.Tracker.Name == SrsConfig.Name) { link.LinkType = ItemLinkType.Parent; } @@ -430,7 +549,8 @@ private RiskItem CreateRisk(List issues, RedmineIssue redmineItem) int nrOfResults = 0; if ((nrOfResults = resultItem.LinkedItems.Where(x => x.LinkType == ItemLinkType.Related).Count()) > 1) { - logger.Warn($"Expected 1 related link for risk item \"{resultItem.ItemID}\". Multiple related items linked. Please check the item in Redmine."); + logger.Error($"Expected 1 related link for risk item \"{resultItem.ItemID}\". Multiple related items linked. Please check the item in Redmine."); + throw new Exception($"Only a single related link to risk item \"{resultItem.ItemID}\" allowed. Cannot determine if traceability to risk item is correct."); } else if (nrOfResults == 1) { @@ -498,6 +618,90 @@ private void AddLinksToItem(RedmineIssue redmineItem, LinkedItem resultItem) } } + private string ConvertValue(bool version, string value) + { + if (!version) + return value; + foreach( var rmVersion in versions ) + { + if (rmVersion.Id.ToString() == value) + { + return rmVersion.Name; + } + } + return value; + } + + private bool ShouldIgnoreIssue(RedmineIssue redmineItem, TruthItemConfig config) + { + if (!config.Filtered) + return false; + + var properties = redmineItem.GetType().GetProperties(); + foreach (var property in properties) + { + if (property.Name != "CustomFields" && + property.Name != "EstimatedHours" && + property.Name != "Relations") + { + HashSet values = new HashSet(); + var value = property.GetValue(redmineItem); + if (value != null) + { + if (value is string || value is int || value is bool) + { + values.Add(value.ToString()); + } + else if (value is DateTime date) + { + values.Add(date.ToString("MM-dd-yyyy")); + } + else + { + PropertyInfo nameProperty = value.GetType().GetProperty("Name"); + if (nameProperty != null && nameProperty.PropertyType == typeof(string)) + { + values.Add(nameProperty.GetValue(value) as string); + } + } + if (!IncludeItem(property.Name, values) || ExcludeItem(property.Name, values)) + { + logger.Debug($"Ignoring requirement item {redmineItem.Id} due to \"{property.Name}\" being equal to \"{String.Join(", ", values)}\"."); + return true; + } + } + } + } + if (redmineItem.CustomFields.Count != 0) + { + foreach (var field in redmineItem.CustomFields) + { + if (field.Value != null) + { + bool versionField = redmineVersionFields.Contains(field.Name); + HashSet values = new HashSet(); + if (field.Multiple) + { + foreach(var value in (System.Text.Json.JsonElement.ArrayEnumerator)field.Value) + { + values.Add(ConvertValue(versionField,value.ToString())); + } + } + else + { + values.Add(ConvertValue(versionField,((System.Text.Json.JsonElement)field.Value).GetString())); + } + if (!IncludeItem(field.Name, values) || ExcludeItem(field.Name,values)) + { + logger.Debug($"Ignoring requirement item {redmineItem.Id} due to \"{field.Name}\" being equal to \"{String.Join(", ", values)}\"."); + return true; + } + } + } + } + return false; + } + private RequirementItem CreateRequirement(List issues, RedmineIssue redmineItem, RequirementType requirementType) { logger.Debug($"Creating requirement item: {redmineItem.Id}"); @@ -510,7 +714,7 @@ private RequirementItem CreateRequirement(List issues, RedmineIssu if (field.Name == "Functional Area" && field.Value != null) { resultItem.ItemCategory = ((System.Text.Json.JsonElement)field.Value).GetString(); - } + } } } @@ -635,6 +839,7 @@ private List GetIssues(int projectID, int trackerID) private List PullAllIssuesFromServer(List queryTrackers) { int projectID = GetProjectID(projectName); + versions = PullAllVersionsFromServer(projectID); var trackers = GetTrackers(); List issueList = new List(); foreach (var queryTracker in queryTrackers) @@ -647,5 +852,20 @@ private List PullAllIssuesFromServer(List queryTrackers) } return issueList; } + + private List PullAllVersionsFromServer(int projectID) + { + var request = new RestRequest($"projects/{projectID}/versions.json", Method.Get) + .AddParameter("key", apiKey); + var response = client.GetAsync(request).GetAwaiter().GetResult(); + if (response.TotalCount == 0) + { + return null; + } + else + { + return response.Versions; + } + } } } diff --git a/RoboClerk.Tests/RoboClerk.Tests.csproj b/RoboClerk.Tests/RoboClerk.Tests.csproj index aaaa377..8c766ef 100644 --- a/RoboClerk.Tests/RoboClerk.Tests.csproj +++ b/RoboClerk.Tests/RoboClerk.Tests.csproj @@ -5,6 +5,10 @@ Debug;Release;ReleasePublish + + + + @@ -19,6 +23,7 @@ + \ No newline at end of file diff --git a/RoboClerk.Tests/TestAnnotatedUnitTestPlugin.cs b/RoboClerk.Tests/TestAnnotatedUnitTestPlugin.cs new file mode 100644 index 0000000..3e019d5 --- /dev/null +++ b/RoboClerk.Tests/TestAnnotatedUnitTestPlugin.cs @@ -0,0 +1,122 @@ +using NSubstitute; +using NUnit.Framework; +using RoboClerk.AnnotatedUnitTests; +using RoboClerk.Configuration; +using RoboClerk.ContentCreators; +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace RoboClerk.Tests +{ + [TestFixture] + [Description("These tests test the annotated unit test plugin")] + internal class TestAnnotatedUnitTestPlugin + { + private IFileSystem fileSystem = null; + private IConfiguration configuration = null; + + [SetUp] + public void TestSetup() + { + StringBuilder configFile = new StringBuilder(); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + configFile.Append(@"TestDirectories = [""/c/temp""]"); + } + else + { + configFile.Append(@"TestDirectories = [""c:/temp""]"); + } + configFile.Append(@" +SubDirs = true +FileMasks = [""Test*.cs""] +UseGit = false +DecorationMarker = ""[UnitTestAttribut"" +ParameterStartDelimiter = ""("" +ParameterEndDelimiter = "")"" +ParameterSeparator = "","" +[Purpose] + Keyword = ""Purpose"" + Optional = false +[PostCondition] + Keyword = ""PostCondition"" + Optional = false +[Identifier] + Keyword = ""Identifier"" + Optional = true +[TraceID] + Keyword = ""TraceID"" + Optional = true +[FunctionName] + StartString = ""public void "" + EndString = ""("""); + string testFile = @" + [UnitTestAttribut( + Identifier = ""9A3258CF-F9EE-4A1A-95E6-B49EF25FB200"", + Purpose = """"""RoboClerk Processes the media directory including subdirs, +output media directory exists including subdirs"" +"""""", + PostCondition = ""Media directory is deleted, recreated and files are copied (except .gitignore)"")] + [Test] + public void CheckMediaDirectory() + { + } + +[UnitTestAttribut( + Identifier = ""5D8F1310-1D33-49C1-93C9-0072428EF215"", + Purpose = ""SourceCodeAnalysisPlugin is created and initialized, subdirs set to false"", + PostCondition = ""Expected files =found"")] + [Test] + public void TestSourceCodeAnalysisPlugin3() + { + }"; + fileSystem = new MockFileSystem(new Dictionary + { + { TestingHelpers.ConvertFileName(@"c:\test\AnnotatedUnitTestPlugin.toml"), new MockFileData(configFile.ToString()) }, + { TestingHelpers.ConvertFileName(@"c:\temp\TestDummy.cs"), new MockFileData(testFile) }, + }); + configuration = Substitute.For(); + configuration.PluginConfigDir.Returns(TestingHelpers.ConvertFileName(@"c:/test/")); + configuration.ProjectRoot.Returns(TestingHelpers.ConvertFileName(@"c:/temp/")); + configuration.CommandLineOptionOrDefault(Arg.Any(), Arg.Any()) + .ReturnsForAnyArgs(callInfo => callInfo.ArgAt(1)); + } + + [UnitTestAttribute(Purpose = "AnnotatedUnitTestPlugin is created", + Identifier = "1C2B7995-DFDF-466B-96D8-B8165EDC28C8", + PostCondition = "No exception is thrown")] + [Test] + public void TestUnitTestPlugin1() + { + var temp = new AnnotatedUnitTestsPlugin(fileSystem); + temp.Initialize(configuration); + } + + [UnitTestAttribute(Purpose = "AnnotatedUnitTestPlugin is created, refresh is called", + Identifier = "1C2B7995-DFDF-466B-96D8-B8165VFC28C8", + PostCondition = "The appropriate unit test information is extracted")] + [Test] + public void TestUnitTestPlugin2() + { + var temp = new AnnotatedUnitTestsPlugin(fileSystem); + temp.Initialize(configuration); + temp.RefreshItems(); + + var tests = temp.GetUnitTests().ToArray(); + + Assert.That(tests.Length == 2); + Assert.That(tests[0].ItemID == "9A3258CF-F9EE-4A1A-95E6-B49EF25FB200"); + Assert.That(tests[1].ItemID == "5D8F1310-1D33-49C1-93C9-0072428EF215"); + Assert.That(tests[0].UnitTestPurpose == "RoboClerk Processes the media directory including subdirs, output media directory exists including subdirs\""); + Assert.That(tests[1].UnitTestPurpose == "SourceCodeAnalysisPlugin is created and initialized, subdirs set to false"); + Assert.That(tests[0].UnitTestAcceptanceCriteria == "Media directory is deleted, recreated and files are copied (except .gitignore)"); + Assert.That(tests[1].UnitTestAcceptanceCriteria == "Expected files =found"); + } + } +} diff --git a/RoboClerk.Tests/TestDocumentContentCreator.cs b/RoboClerk.Tests/TestDocumentContentCreator.cs new file mode 100644 index 0000000..1b36a00 --- /dev/null +++ b/RoboClerk.Tests/TestDocumentContentCreator.cs @@ -0,0 +1,154 @@ +using NUnit.Framework; +using NSubstitute; +using System.Collections.Generic; +using RoboClerk.Configuration; +using System.IO.Abstractions; +using RoboClerk.ContentCreators; +using System; +using System.Text.RegularExpressions; +using System.IO; + +namespace RoboClerk.Tests +{ + [TestFixture] + [Description("These tests test the RoboClerk Document content creator")] + internal class TestDocumentContentCreator + { + //private IConfiguration config = null; + //private IDataSources dataSources = null; + private ITraceabilityAnalysis traceAnalysis = null; + //private IFileSystem fs = null; + private DocumentConfig documentConfig = null; + //private List testcaseItems = new List(); + private TraceEntity te = null; + + [SetUp] + public void TestSetup() + { + te = new TraceEntity("TestCase", "Test Case", "TC", TraceEntityType.Truth); + documentConfig = new DocumentConfig("SystemLevelTestPlan", "docID", "docTitle", "docAbbr", @"c:\in\template.adoc"); + traceAnalysis = Substitute.For(); + traceAnalysis.GetTraceEntityForID("TestCase").Returns(te); + traceAnalysis.GetTraceEntityForAnyProperty("TC").Returns(te); + } + + [UnitTestAttribute( + Identifier = "72A3CD11-2E49-4B02-B71F-767CF3B1EA4F", + Purpose = "Document content creator is created", + PostCondition = "No exception is thrown")] + [Test] + public void CreateDocumentCC() + { + var sst = new ContentCreators.Document(traceAnalysis); + } + + [UnitTestAttribute( + Identifier = "CDA4BF30-FDEF-42FC-B82C-0DA644636DA9", + Purpose = "Document content creator is fed an appropriate tag and documentConfig.", + PostCondition = "The expected strings are returned")] + [Test] + public void TestDocumentCC1() + { + var sst = new ContentCreators.Document(traceAnalysis); + string tagString = "@@Document:title()@@"; + RoboClerkTag tag = new RoboClerkTag(0, tagString.Length, tagString, true); + string result = sst.GetContent(tag,documentConfig); + Assert.That(result, Is.EqualTo("docTitle")); + + tagString = "@@Document:aBbreviation()@@"; + tag = new RoboClerkTag(0, tagString.Length, tagString, true); + result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo("docAbbr")); + + tagString = "@@Document:IDENTIFIER()@@"; + tag = new RoboClerkTag(0, tagString.Length, tagString, true); + result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo("docID")); + + tagString = "@@Document:template()@@"; + tag = new RoboClerkTag(0, tagString.Length, tagString, true); + result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo(@"c:\in\template.adoc")); + + tagString = "@@Document:RoboClerkID()@@"; + tag = new RoboClerkTag(0, tagString.Length, tagString, true); + result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo("SystemLevelTestPlan")); + + tagString = "@@Document:GenDateTime()@@"; + tag = new RoboClerkTag(0, tagString.Length, tagString, true); + result = sst.GetContent(tag, documentConfig); + DateTime now = DateTime.Now; + DateTime dateTime = DateTime.Parse(result); + TimeSpan diff = now - dateTime; + Assert.That(diff.Minutes, Is.LessThanOrEqualTo(1)); + } + + [UnitTestAttribute( + Identifier = "991A810C-3C44-411F-9EAB-58D52E990240", + Purpose = "Document content creator is fed an appropriate tag and documentConfig that has a certain entity count.", + PostCondition = "The expected count is returned")] + [Test] + public void TestDocumentCC2() + { + documentConfig.AddEntityCount(te, 3); + var sst = new ContentCreators.Document(traceAnalysis); + string tagString = "@@Document:countentities(entity=TC)@@"; + RoboClerkTag tag = new RoboClerkTag(0, tagString.Length, tagString, true); + string result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo("3")); + + tagString = "@@Document:countentities(entity=TC, restart=true)@@"; + tag = new RoboClerkTag(0, tagString.Length, tagString, true); + result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo("")); + + documentConfig.AddEntityCount(te, 1); + documentConfig.AddEntityCount(te, 1); + tagString = "@@Document:countentities(entity=TC)@@"; + tag = new RoboClerkTag(0, tagString.Length, tagString, true); + result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo("2")); + } + + [UnitTestAttribute( + Identifier = "296BDCB4-F8B8-4F4C-8491-E848188B8F99", + Purpose = "Document content creator is fed an appropriate tag and documentConfig. A valid trace entity count is requested but trace entity not in document config.", + PostCondition = "The expected count (0) is returned")] + [Test] + public void TestDocumentCC3() + { + var sst = new ContentCreators.Document(traceAnalysis); + string tagString = "@@Document:countentities(entity=TC)@@"; + RoboClerkTag tag = new RoboClerkTag(0, tagString.Length, tagString, true); + string result = sst.GetContent(tag, documentConfig); + Assert.That(result, Is.EqualTo("0")); + } + + [UnitTestAttribute( + Identifier = "A8C2E267-7103-4273-A3BC-FF58FA3F5CBE", + Purpose = "Document content creator is fed an appropriate tag and documentConfig. An invalid trace entity count is requested.", + PostCondition = "The expected exception is thrown")] + [Test] + public void TestDocumentCC4() + { + var sst = new ContentCreators.Document(traceAnalysis); + string tagString = "@@Document:countentities(entity=TCC)@@"; + RoboClerkTag tag = new RoboClerkTag(0, tagString.Length, tagString, true); + Assert.Throws(() => sst.GetContent(tag, documentConfig)); + } + + [UnitTestAttribute( + Identifier = "53000310-1951-4809-89DA-769FCDFCBC06", + Purpose = "Document content creator is fed an inappropriate tag.", + PostCondition = "The expected exception is thrown")] + [Test] + public void TestDocumentCC5() + { + var sst = new ContentCreators.Document(traceAnalysis); + string tagString = "@@Document:nonsense(entity=TCC)@@"; + RoboClerkTag tag = new RoboClerkTag(0, tagString.Length, tagString, true); + Assert.Throws(() => sst.GetContent(tag, documentConfig)); + } + } +} diff --git a/RoboClerk.Tests/TestPromptTemplate.cs b/RoboClerk.Tests/TestPromptTemplate.cs index b73ea68..1496d39 100644 --- a/RoboClerk.Tests/TestPromptTemplate.cs +++ b/RoboClerk.Tests/TestPromptTemplate.cs @@ -1,6 +1,7 @@ using NSubstitute; using NUnit.Framework; using System.Collections.Generic; +using RoboClerk.AISystem; namespace RoboClerk.Tests { diff --git a/RoboClerk.Tests/TestRoboClerkMarkdown.cs b/RoboClerk.Tests/TestRoboClerkMarkdown.cs index 1e4982b..fdb9992 100644 --- a/RoboClerk.Tests/TestRoboClerkMarkdown.cs +++ b/RoboClerk.Tests/TestRoboClerkMarkdown.cs @@ -59,9 +59,9 @@ public void Mismatched_Tag_Detection_VERIFIES_Exception_Thrown_When_Initial_Cont public void Mismatched_Tag_Detection_VERIFIES_Exception_Thrown_When_Final_Container_Tags_Do_Not_Match() { StringBuilder sb = new StringBuilder(validText); - sb[65] = ' '; - sb[66] = ' '; sb[67] = ' '; + sb[68] = ' '; + sb[69] = ' '; Assert.Throws(() => RoboClerkAsciiDoc.ExtractRoboClerkTags(sb.ToString())); } @@ -145,10 +145,10 @@ public void Content_Fields_Are_Parsed_Correctly_VERIFIES_The_Correct_Content_Fie { var tags = RoboClerkAsciiDoc.ExtractRoboClerkTags(validText); Assert.AreEqual("This is the content\n", tags[0].Contents); - Assert.AreEqual("Source:testinfo()", tags[3].Contents); - Assert.AreEqual("Config:testinfo2()", tags[4].Contents); - Assert.AreEqual("", tags[1].Contents); - Assert.AreEqual("this is some contents\nit is even multiline\n# it contains a *header*\n", tags[2].Contents); + Assert.AreEqual("Source:testinfo()", tags[1].Contents); + Assert.AreEqual("Config:testinfo2()", tags[2].Contents); + Assert.AreEqual("", tags[3].Contents); + Assert.AreEqual("this is some contents\nit is even multiline\n# it contains a *header*\n", tags[4].Contents); Assert.AreEqual("Foo:inline()", tags[5].Contents); Assert.AreEqual("Trace:SWR(id=1234, name =test name )", tags[6].Contents); } @@ -158,10 +158,10 @@ public void Info_Fields_Are_Parsed_Correctly_VERIFIES_The_Correct_Info_Fields_Ar { var tags = RoboClerkAsciiDoc.ExtractRoboClerkTags(validText); Assert.AreEqual("TheFirstInfo", tags[0].ContentCreatorID); - Assert.AreEqual("testinfo", tags[3].ContentCreatorID); - Assert.AreEqual("testinfo2", tags[4].ContentCreatorID); - Assert.AreEqual("empty", tags[1].ContentCreatorID); - Assert.AreEqual("huff", tags[2].ContentCreatorID); + Assert.AreEqual("testinfo", tags[1].ContentCreatorID); + Assert.AreEqual("testinfo2", tags[2].ContentCreatorID); + Assert.AreEqual("empty", tags[3].ContentCreatorID); + Assert.AreEqual("huff", tags[4].ContentCreatorID); Assert.AreEqual("inline", tags[5].ContentCreatorID); Assert.AreEqual("SWR", tags[6].ContentCreatorID); } @@ -171,10 +171,10 @@ public void Source_Fields_Are_Parsed_Correctly_VERIFIES_The_Correct_Source_Field { var tags = RoboClerkAsciiDoc.ExtractRoboClerkTags(validText); Assert.AreEqual(DataSource.SLMS, tags[0].Source); - Assert.AreEqual(DataSource.Source, tags[3].Source); - Assert.AreEqual(DataSource.Config, tags[4].Source); - Assert.AreEqual(DataSource.OTS, tags[1].Source); - Assert.AreEqual(DataSource.Comment, tags[2].Source); + Assert.AreEqual(DataSource.Source, tags[1].Source); + Assert.AreEqual(DataSource.Config, tags[2].Source); + Assert.AreEqual(DataSource.OTS, tags[3].Source); + Assert.AreEqual(DataSource.Comment, tags[4].Source); Assert.AreEqual(DataSource.Unknown, tags[5].Source); Assert.AreEqual(DataSource.Trace, tags[6].Source); } @@ -184,10 +184,10 @@ public void Tag_Content_Replacement_Behavior_VERIFIES_Content_Is_Inserted_In_The { var tags = RoboClerkAsciiDoc.ExtractRoboClerkTags(validText); tags[0].Contents = "item1"; - tags[3].Contents = "item2"; - tags[4].Contents = ""; - tags[1].Contents = "item4"; + tags[1].Contents = "item2"; tags[2].Contents = ""; + tags[3].Contents = "item4"; + tags[4].Contents = ""; tags[5].Contents = "item6"; tags[6].Contents = "D"; diff --git a/RoboClerk.Tests/TestUnitTestContentCreator.cs b/RoboClerk.Tests/TestUnitTestContentCreator.cs index a4ef902..646fae6 100644 --- a/RoboClerk.Tests/TestUnitTestContentCreator.cs +++ b/RoboClerk.Tests/TestUnitTestContentCreator.cs @@ -47,6 +47,8 @@ public void TestSetup() unittestItem.ItemTitle = "title1"; unittestItem.UnitTestPurpose = "purpose1"; unittestItem.UnitTestAcceptanceCriteria = "accept1"; + unittestItem.UnitTestFileName = "filename"; + unittestItem.UnitTestFunctionName = "functionname"; unittestItems.Add(unittestItem); unittestItem = new UnitTestItem(); unittestItem.ItemID = "tcid2"; @@ -84,7 +86,7 @@ public void CreateUnitTestCC2() //make sure we can find the item linked to this test dataSources.GetItem("target1").Returns(new RequirementItem(RequirementType.SystemRequirement) { ItemID = "target1" }); string content = sst.GetContent(tag, documentConfig); - string expectedContent = "\n|====\n| *unittest ID:* | tcid1 \n\n| *Revision:* | tcrev1\n\n| *Last Updated:* | 1999/10/10 00:00:00\n| *Trace Link:* | target1\n\n| *Purpose:* | purpose1\n\n| *Acceptance Criteria:* | accept1\n\n|===="; + string expectedContent = "\n|====\n| *unittest ID:* | tcid1\n\n| *Function / File Name:* | functionname / filename\n\n| *Revision:* | tcrev1\n\n| *Last Updated:* | 1999/10/10 00:00:00\n| *Trace Link:* | target1\n\n| *Purpose:* | purpose1\n\n| *Acceptance Criteria:* | accept1\n\n|===="; Assert.That(Regex.Replace(content, @"\r\n", "\n"), Is.EqualTo(expectedContent)); //ensure that we're always comparing the correct string, regardless of newline character for a platform Assert.DoesNotThrow(() => traceAnalysis.Received().AddTrace(Arg.Any(), "tcid1", Arg.Any(), "tcid1")); @@ -113,7 +115,7 @@ public void CreateUnitTestCC3() var sst = new UnitTest(dataSources, traceAnalysis, config); var tag = new RoboClerkTag(0, 31, "@@SLMS:UnitTest(ItemID=tcid2)@@", true); string content = sst.GetContent(tag, documentConfig); - string expectedContent = "\n|====\n| *unittest ID:* | http://localhost/[tcid2] \n\n| *Revision:* | \n\n\n| *Trace Link:* | N/A\n\n| *Purpose:* | N/A\n\n| *Acceptance Criteria:* | N/A\n\n|===="; + string expectedContent = "\n|====\n| *unittest ID:* | http://localhost/[tcid2]\n\n| *Function / File Name:* | N/A / N/A\n\n| *Revision:* | \n\n\n| *Trace Link:* | N/A\n\n| *Purpose:* | N/A\n\n| *Acceptance Criteria:* | N/A\n\n|===="; Assert.That(Regex.Replace(content, @"\r\n", "\n"), Is.EqualTo(expectedContent)); //ensure that we're always comparing the correct string, regardless of newline character for a platform Assert.DoesNotThrow(() => traceAnalysis.Received().AddTrace(Arg.Any(), "tcid2", Arg.Any(), "tcid2")); @@ -144,7 +146,7 @@ public void CreateUnitTestCC6() var sst = new UnitTest(dataSources, traceAnalysis, config); var tag = new RoboClerkTag(0, 29, "@@SLMS:UnitTest(brief=true)@@", true); string content = sst.GetContent(tag, documentConfig); - string expectedContent = "|====\n| unittest ID | unittest Purpose | Acceptance Criteria\n\n| tcid1 | purpose1 | accept1 \n\n| http://localhost/[tcid2] | | \n\n|====\n"; + string expectedContent = "|====\n| unittest ID | Function / File Name | unittest Purpose | Acceptance Criteria\n\n| tcid1 | functionname / filename | purpose1 | accept1 \n\n| http://localhost/[tcid2] | / | | \n\n|====\n"; Assert.That(Regex.Replace(content, @"\r\n", "\n"), Is.EqualTo(expectedContent)); //ensure that we're always comparing the correct string, regardless of newline character for a platform Assert.DoesNotThrow(() => traceAnalysis.Received().AddTrace(Arg.Any(), "tcid1", Arg.Any(), "tcid1")); diff --git a/RoboClerk.UnitTestFN/UnitTestFNPlugin.cs b/RoboClerk.UnitTestFN/UnitTestFNPlugin.cs index d5a04d2..0f90c41 100644 --- a/RoboClerk.UnitTestFN/UnitTestFNPlugin.cs +++ b/RoboClerk.UnitTestFN/UnitTestFNPlugin.cs @@ -29,11 +29,11 @@ public override void Initialize(IConfiguration configuration) base.Initialize(configuration); var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); - testFunctionDecoration = configuration.CommandLineOptionOrDefault("TestFunctionDecoration", GetStringForKey(config, "TestFunctionDecoration", false)); - var functionMask = configuration.CommandLineOptionOrDefault("FunctionMask", GetStringForKey(config, "FunctionMask", true)); + testFunctionDecoration = configuration.CommandLineOptionOrDefault("TestFunctionDecoration", GetObjectForKey(config, "TestFunctionDecoration", false)); + var functionMask = configuration.CommandLineOptionOrDefault("FunctionMask", GetObjectForKey(config, "FunctionMask", true)); functionMaskElements = ParseFunctionMask(functionMask); ValidateFunctionMaskElements(functionMaskElements); - sectionSeparator = configuration.CommandLineOptionOrDefault("SectionSeparator", GetStringForKey(config, "SectionSeparator", true)); + sectionSeparator = configuration.CommandLineOptionOrDefault("SectionSeparator", GetObjectForKey(config, "SectionSeparator", true)); } catch (Exception e) { @@ -155,6 +155,7 @@ private string SeparateSection(string section) foundMatch = foundMatch && longestString.Contains(functionMaskElement); } } + StringBuilder functionName = new StringBuilder(); if (foundMatch) { string remainingLine = longestString; @@ -171,6 +172,8 @@ private string SeparateSection(string section) } var items = remainingLine.Split(functionMaskElements[i]); resultingElements.Add((functionMaskElements[i - 1], items[0])); + functionName.Append(items[0]); + functionName.Append(functionMaskElements[i]); if (items.Length - 1 != 0) { remainingLine = String.Join(functionMaskElements[i], items, 1, items.Length - 1); @@ -188,7 +191,7 @@ private string SeparateSection(string section) return resultingElements; } - private void AddUnitTest(List<(string, string)> els, string fileName, int lineNumber) + private void AddUnitTest(List<(string, string)> els, string fileName, int lineNumber, string functionName) { var unitTest = new UnitTestItem(); bool identified = false; @@ -205,6 +208,8 @@ private void AddUnitTest(List<(string, string)> els, string fileName, int lineNu default: throw new Exception($"Unknown element identifier in FunctionMask: {el.Item1.ToUpper()}"); } } + unitTest.UnitTestFileName = shortFileName; + unitTest.UnitTestFunctionName = functionName; if (!identified) { unitTest.ItemID = $"{shortFileName}:{lineNumber}"; @@ -228,6 +233,15 @@ private void AddUnitTest(List<(string, string)> els, string fileName, int lineNu unitTests.Add(unitTest); } + private string GetFunctionName(string line) + { + var strings = line.Trim().Split(' '); + var longestString = strings.OrderByDescending(s => s.Length).First(); //we assume that the function name is the longest element + //we also assume the function parameters start with ( + int index = longestString.IndexOf('('); + return longestString.Substring(0, index); + } + private void FindAndProcessFunctions(string[] lines, string fileName) { bool nextLineIsFunction = testFunctionDecoration == string.Empty; @@ -242,8 +256,9 @@ private void FindAndProcessFunctions(string[] lines, string fileName) var els = ApplyFunctionNameMask(line); if (els.Count > 0) { + //Create unit test - AddUnitTest(els, fileName, currentLineNumber); + AddUnitTest(els, fileName, currentLineNumber, GetFunctionName(line)); } nextLineIsFunction = false || testFunctionDecoration == string.Empty; } diff --git a/RoboClerk/AISystem/AISystemPluginBase.cs b/RoboClerk/AISystem/AISystemPluginBase.cs index a885eff..0c2a396 100644 --- a/RoboClerk/AISystem/AISystemPluginBase.cs +++ b/RoboClerk/AISystem/AISystemPluginBase.cs @@ -1,9 +1,8 @@ -using RoboClerk.AISystem; -using RoboClerk.Configuration; +using RoboClerk.Configuration; using System.Collections.Generic; using System.IO.Abstractions; -namespace RoboClerk +namespace RoboClerk.AISystem { public abstract class AISystemPluginBase : PluginBase, IAISystemPlugin { @@ -28,9 +27,9 @@ public IEnumerable GetAIPromptTemplates() public override void Initialize(IConfiguration configuration) { var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); - promptTemplateFiles["SystemRequirement"]=configuration.CommandLineOptionOrDefault("SystemRequirement", GetStringForKey(config, "SystemRequirement", true)); - promptTemplateFiles["SoftwareRequirement"]=configuration.CommandLineOptionOrDefault("SoftwareRequirement", GetStringForKey(config, "SoftwareRequirement", true)); - promptTemplateFiles["DocumentationRequirement"]=configuration.CommandLineOptionOrDefault("DocumentationRequirement", GetStringForKey(config, "DocumentationRequirement", true)); + promptTemplateFiles["SystemRequirement"]=configuration.CommandLineOptionOrDefault("SystemRequirement", GetObjectForKey(config, "SystemRequirement", true)); + promptTemplateFiles["SoftwareRequirement"]=configuration.CommandLineOptionOrDefault("SoftwareRequirement", GetObjectForKey(config, "SoftwareRequirement", true)); + promptTemplateFiles["DocumentationRequirement"]=configuration.CommandLineOptionOrDefault("DocumentationRequirement", GetObjectForKey(config, "DocumentationRequirement", true)); } public abstract string GetFeedback(TraceEntity et, Item item); diff --git a/RoboClerk/AISystem/PromptTemplate.cs b/RoboClerk/AISystem/PromptTemplate.cs index bbd52b0..ace62fd 100644 --- a/RoboClerk/AISystem/PromptTemplate.cs +++ b/RoboClerk/AISystem/PromptTemplate.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Reflection; -namespace RoboClerk +namespace RoboClerk.AISystem { public class PromptTemplate { diff --git a/RoboClerk/Configuration/DocumentConfig.cs b/RoboClerk/Configuration/DocumentConfig.cs index b496bd9..e6c52d7 100644 --- a/RoboClerk/Configuration/DocumentConfig.cs +++ b/RoboClerk/Configuration/DocumentConfig.cs @@ -1,4 +1,7 @@ -namespace RoboClerk.Configuration +using DocumentFormat.OpenXml.Packaging; +using System.Collections.Generic; + +namespace RoboClerk.Configuration { public class DocumentConfig { @@ -7,6 +10,7 @@ public class DocumentConfig private string documentTitle = string.Empty; private string documentAbbreviation = string.Empty; private string documentTemplate = string.Empty; + private Dictionary entityCounts = new Dictionary(); private Commands commands = null; public DocumentConfig(string roboClerkID, string documentID, string documentTitle, string documentAbbreviation, string documentTemplate) @@ -30,5 +34,34 @@ public void AddCommands(Commands commands) public string DocumentAbbreviation => documentAbbreviation; public string DocumentTemplate => documentTemplate; public Commands Commands => commands; + public void AddEntityCount(TraceEntity te, uint count) + { + if (te != null) + if (entityCounts.ContainsKey(te)) + { + entityCounts[te] += count; + } + else + { + entityCounts.Add(te, count); + } + } + + public void ResetEntityCount(TraceEntity te) + { + if (te != null && entityCounts.ContainsKey(te)) + { + entityCounts[te]=0; + } + } + + public uint GetEntityCount(TraceEntity te) + { + if (te != null && entityCounts.ContainsKey(te)) + { + return entityCounts[te]; + } + else return 0; + } } } diff --git a/RoboClerk/Configuration/Project/projectConfig.toml b/RoboClerk/Configuration/Project/projectConfig.toml index 17d3f93..bb9f822 100644 --- a/RoboClerk/Configuration/Project/projectConfig.toml +++ b/RoboClerk/Configuration/Project/projectConfig.toml @@ -236,7 +236,7 @@ UpdatedDocumentationRequirementIDs = [] UpdatedDocContentIDs = [] -# RoboClerkAI is powered by a state of the art AI system that can provide feedback on the contents +# RoboClerk is powered by a state of the art AI system that can provide feedback on the contents # of the documentation generated by RoboClerk. In the configuration item below, you can indicate # what truth items you want RoboClerk to provide feedback on. Due to the fact that AI review takes # time and costs money, AI review is only enabled when the commandline option AIFeedback is set: diff --git a/RoboClerk/Configuration/TruthItemConfig.cs b/RoboClerk/Configuration/TruthItemConfig.cs new file mode 100644 index 0000000..0b2ed3d --- /dev/null +++ b/RoboClerk/Configuration/TruthItemConfig.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RoboClerk.Configuration +{ + public class TruthItemConfig + { + public string Name { get; } + public bool Filtered { get; } + + public TruthItemConfig() + { + Name = string.Empty; + Filtered = false; + } + + public TruthItemConfig(string name, bool filter) + { + Name = name; + Filtered = filter; + } + } +} diff --git a/RoboClerk/ContentCreators/Document.cs b/RoboClerk/ContentCreators/Document.cs index 5bf9356..94673f1 100644 --- a/RoboClerk/ContentCreators/Document.cs +++ b/RoboClerk/ContentCreators/Document.cs @@ -3,8 +3,14 @@ namespace RoboClerk.ContentCreators { - internal class Document : IContentCreator + public class Document : IContentCreator { + private readonly ITraceabilityAnalysis analysis; + public Document(ITraceabilityAnalysis analysis) + { + this.analysis = analysis; + } + public string GetContent(RoboClerkTag tag, DocumentConfig doc) { if (tag.ContentCreatorID.ToUpper() == "TITLE") @@ -31,6 +37,30 @@ public string GetContent(RoboClerkTag tag, DocumentConfig doc) { return DateTime.Now.ToString("yyyy/MM/dd HH:mm"); } + else if (tag.ContentCreatorID.ToUpper() == "COUNTENTITIES") + { + if (tag.HasParameter("entity")) + { + string entityName = tag.GetParameterOrDefault("entity"); + if (entityName != null) + { + string restart = tag.GetParameterOrDefault("restart"); + TraceEntity te = analysis.GetTraceEntityForAnyProperty(entityName); + if (te == null) + { + throw new Exception($"RoboClerk was unable to find the entity \"{entityName}\" as specified in the document tag: \"{tag.Source}:{tag.ContentCreatorID}\" in \"{doc.RoboClerkID}\"."); + } + if (restart != null && restart.ToUpper() == "TRUE") + { + //reset the counter and return an empty string + doc.ResetEntityCount(te); + return string.Empty; + } + + return doc.GetEntityCount(te).ToString(); + } + } + } throw new Exception($"RoboClerk did not know how to handle the document tag: \"{tag.Source}:{tag.ContentCreatorID}\" in \"{doc.RoboClerkID}\"."); } } diff --git a/RoboClerk/ContentCreators/MultiItemContentCreator.cs b/RoboClerk/ContentCreators/MultiItemContentCreator.cs index be9d65d..89e791a 100644 --- a/RoboClerk/ContentCreators/MultiItemContentCreator.cs +++ b/RoboClerk/ContentCreators/MultiItemContentCreator.cs @@ -41,6 +41,7 @@ public override string GetContent(RoboClerkTag tag, DocumentConfig doc) } index++; } + doc.AddEntityCount(te,(uint)includedItems.Count); //keep track of how many entities we're adding to the document string content = string.Empty; try { diff --git a/RoboClerk/DocTemplates/SoftwareRequirementSpecification.adoc b/RoboClerk/DocTemplates/SoftwareRequirementSpecification.adoc index 0b6fb7e..1ad02ea 100644 --- a/RoboClerk/DocTemplates/SoftwareRequirementSpecification.adoc +++ b/RoboClerk/DocTemplates/SoftwareRequirementSpecification.adoc @@ -96,4 +96,6 @@ This project does not have software requirements of this category. If you still @@@SLMS:SWR(itemCategory=User Interface,NewerThan=2023/02/04 03:07:12 PM) -@@@ \ No newline at end of file +@@@ + +This document contains @@Document:CountEntities(entity=Software Requirement)@@ software requirements. \ No newline at end of file diff --git a/RoboClerk/DocTemplates/SystemLevelTestPlan.adoc b/RoboClerk/DocTemplates/SystemLevelTestPlan.adoc index 2b88ce6..f09ab6f 100644 --- a/RoboClerk/DocTemplates/SystemLevelTestPlan.adoc +++ b/RoboClerk/DocTemplates/SystemLevelTestPlan.adoc @@ -66,6 +66,8 @@ The following tests are to be executed manually. This could be done by printing @@@ +This document contains @@Document:CountEntities(entity=TC)@@ manual test cases.@@Document:CountEntities(entity=TC,restart=true)@@ + @@POST:PAGEBREAK()@@ === Automated Tests @@ -76,6 +78,8 @@ The following tests are automated, they are typically part of the continuous int @@@ +This document contains @@Document:CountEntities(entity=TC)@@ automated test cases. + @@POST:PAGEBREAK()@@ === Unit Tests Brief @@ -94,4 +98,6 @@ It is also possible to create seperate tables for unit tests. Here is the table @@@FILE:UnitTest(brief=false,ItemID=439DA613-EF71-4589-9F3B-8314CB8A11E5) Individual unit test tables -@@@ \ No newline at end of file +@@@ + + diff --git a/RoboClerk/Document.cs b/RoboClerk/Document.cs index 2069240..d19113c 100644 --- a/RoboClerk/Document.cs +++ b/RoboClerk/Document.cs @@ -32,7 +32,7 @@ public void FromString(string text) catch (TagInvalidException e) { e.DocumentTitle = title; - throw e; + throw; } } diff --git a/RoboClerk/ItemTemplates/UnitTest.adoc b/RoboClerk/ItemTemplates/UnitTest.adoc index 483e2ab..b4a73d1 100644 --- a/RoboClerk/ItemTemplates/UnitTest.adoc +++ b/RoboClerk/ItemTemplates/UnitTest.adoc @@ -8,7 +8,9 @@ UnitTestItem item = (UnitTestItem)Item; AddTrace(item.ItemID); ] |==== -| *[csx:te.Name] ID:* | [csx:GetItemLinkString(item)] +| *[csx:te.Name] ID:* | [csx:GetItemLinkString(item)] + +| *Function / File Name:* | [csx:GetValOrDef(item.UnitTestFunctionName,"N/A")] / [csx:GetValOrDef(item.UnitTestFileName,"N/A")] | *Revision:* | [csx:item.ItemRevision] diff --git a/RoboClerk/ItemTemplates/UnitTest_brief.adoc b/RoboClerk/ItemTemplates/UnitTest_brief.adoc index 83260e6..6c7dab1 100644 --- a/RoboClerk/ItemTemplates/UnitTest_brief.adoc +++ b/RoboClerk/ItemTemplates/UnitTest_brief.adoc @@ -12,10 +12,10 @@ string CreateRows() { UnitTestItem ut = (UnitTestItem)item; AddTrace(ut.ItemID); - sb.Append($"| {GetItemLinkString(ut)} | {ut.UnitTestPurpose} | {ut.UnitTestAcceptanceCriteria} \n\n"); + sb.Append($"| {GetItemLinkString(ut)} | {ut.UnitTestFunctionName} / {ut.UnitTestFileName} | {ut.UnitTestPurpose} | {ut.UnitTestAcceptanceCriteria} \n\n"); } return sb.ToString(); }]|==== -| [csx:te.Name] ID | [csx:te.Name] Purpose | Acceptance Criteria +| [csx:te.Name] ID | Function / File Name | [csx:te.Name] Purpose | Acceptance Criteria [csx:CreateRows()]|==== diff --git a/RoboClerk/Items/UnitTestItem.cs b/RoboClerk/Items/UnitTestItem.cs index a6403d4..c75afcb 100644 --- a/RoboClerk/Items/UnitTestItem.cs +++ b/RoboClerk/Items/UnitTestItem.cs @@ -9,6 +9,8 @@ public class UnitTestItem : LinkedItem private string unitTestPurpose = ""; private string unitTestAcceptanceCriteria = ""; private string unitTestFileLocation = ""; + private string unitTestFileName = ""; + private string unitTestFunctionName = ""; public UnitTestItem() { @@ -39,5 +41,17 @@ public string UnitTestFileLocation get { return unitTestFileLocation; } set { unitTestFileLocation = value; } } + + public string UnitTestFileName + { + get { return unitTestFileName; } + set { unitTestFileName = value; } + } + + public string UnitTestFunctionName + { + get { return unitTestFunctionName; } + set { unitTestFunctionName = value; } + } } } diff --git a/RoboClerk/PluginSupport/PluginBase.cs b/RoboClerk/PluginSupport/PluginBase.cs index 6e96fa0..aa4117c 100644 --- a/RoboClerk/PluginSupport/PluginBase.cs +++ b/RoboClerk/PluginSupport/PluginBase.cs @@ -37,12 +37,12 @@ public string Description public abstract void Initialize(IConfiguration config); - protected string GetStringForKey(TomlTable config, string keyName, bool required) + protected T GetObjectForKey(TomlTable config, string keyName, bool required) { string result = string.Empty; if (config.ContainsKey(keyName)) { - return (string)config[keyName]; + return (T)config[keyName]; } else { @@ -53,7 +53,7 @@ protected string GetStringForKey(TomlTable config, string keyName, bool required else { logger.Warn($"Key \\\"{keyName}\\\" missing from configuration file for {name}. Attempting to continue."); - return string.Empty; + return default(T); } } } diff --git a/RoboClerk/PluginSupport/SLMSPluginBase.cs b/RoboClerk/PluginSupport/SLMSPluginBase.cs index ae1b4f2..d14b312 100644 --- a/RoboClerk/PluginSupport/SLMSPluginBase.cs +++ b/RoboClerk/PluginSupport/SLMSPluginBase.cs @@ -1,78 +1,185 @@ -using System; -using System.Collections.Generic; -using System.IO.Abstractions; -using Tomlyn.Model; -using IConfiguration = RoboClerk.Configuration.IConfiguration; - -namespace RoboClerk -{ - public abstract class SLMSPluginBase : DataSourcePluginBase - { - protected string prsName = string.Empty; - protected string srsName = string.Empty; - protected string docName = string.Empty; - protected string cntName = string.Empty; - protected string tcName = string.Empty; - protected string bugName = string.Empty; - protected string riskName = string.Empty; - protected string soupName = string.Empty; - protected TomlArray ignoreList = new TomlArray(); - - public SLMSPluginBase(IFileSystem fileSystem) - : base(fileSystem) - { - - } - - public override void Initialize(IConfiguration configuration) - { - try - { - var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); - prsName = configuration.CommandLineOptionOrDefault("SystemRequirement", GetStringForKey(config, "SystemRequirement", false)); - srsName = configuration.CommandLineOptionOrDefault("SoftwareRequirement", GetStringForKey(config, "SoftwareRequirement", false)); - docName = configuration.CommandLineOptionOrDefault("DocumentationRequirement", GetStringForKey(config, "DocumentationRequirement", false)); - cntName = configuration.CommandLineOptionOrDefault("DocContent", GetStringForKey(config, "DocContent", false)); - tcName = configuration.CommandLineOptionOrDefault("SoftwareSystemTest", GetStringForKey(config, "SoftwareSystemTest", false)); - bugName = configuration.CommandLineOptionOrDefault("Anomaly", GetStringForKey(config, "Anomaly", false)); - riskName = configuration.CommandLineOptionOrDefault("Risk", GetStringForKey(config, "Risk", false)); - soupName = configuration.CommandLineOptionOrDefault("SOUP", GetStringForKey(config, "SOUP", false)); - if (config.ContainsKey("Ignore")) - { - ignoreList = (TomlArray)config["Ignore"]; - } - else - { - logger.Warn($"Key \"Ignore\" missing from configuration file for {name}. Attempting to continue."); - } - } - catch (Exception e) - { - logger.Error($"Error reading configuration file for {name}."); - logger.Error(e); - throw new Exception($"The {name} could not read its configuration. Aborting..."); - } - } - - protected void TrimLinkedItems(List items, List retrievedIDs) - { - foreach (var item in items) - { - LinkedItem linkedItem = item as LinkedItem; - List linkedItemsToRemove = new List(); - foreach (var itemLink in linkedItem.LinkedItems) - { - if (!retrievedIDs.Contains(itemLink.TargetID)) - { - logger.Warn($"Removing a {itemLink.LinkType} link to item with ID \"{itemLink.TargetID}\" because that item has a status that causes it to be ignored."); - linkedItemsToRemove.Add(itemLink); - } - } - foreach (var itemLink in linkedItemsToRemove) - { - linkedItem.RemoveLinkedItem(itemLink); //remove the link to an ignored item - } - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using Tomlyn.Model; +using RoboClerk.Configuration; +using IConfiguration = RoboClerk.Configuration.IConfiguration; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; + +namespace RoboClerk +{ + public abstract class SLMSPluginBase : DataSourcePluginBase + { + private Dictionary truthItemConfig = new Dictionary(); + protected TruthItemConfig PrsConfig => truthItemConfig["SystemRequirement"]; + protected TruthItemConfig SrsConfig => truthItemConfig["SoftwareRequirement"]; + protected TruthItemConfig DocConfig => truthItemConfig["DocumentationRequirement"]; + protected TruthItemConfig CntConfig => truthItemConfig["DocContent"]; + protected TruthItemConfig TcConfig => truthItemConfig["SoftwareSystemTest"]; + protected TruthItemConfig BugConfig => truthItemConfig["Anomaly"]; + protected TruthItemConfig RiskConfig => truthItemConfig["Risk"]; + protected TruthItemConfig SoupConfig => truthItemConfig["SOUP"]; + + protected TomlArray ignoreList = new TomlArray(); + + private Dictionary> inclusionFilters = new Dictionary>(); + private Dictionary> exclusionFilters = new Dictionary>(); + + + public SLMSPluginBase(IFileSystem fileSystem) + : base(fileSystem) + { + + } + + public override void Initialize(IConfiguration configuration) + { + try + { + var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml"); + AddTruthItemConfig("SystemRequirement", config); + AddTruthItemConfig("SoftwareRequirement", config); + AddTruthItemConfig("DocumentationRequirement", config); + AddTruthItemConfig("DocContent", config); + AddTruthItemConfig("SoftwareSystemTest", config); + AddTruthItemConfig("Anomaly",config); + AddTruthItemConfig("Risk", config); + AddTruthItemConfig("SOUP", config); + + if (config.ContainsKey("Ignore")) + { + ignoreList = (TomlArray)config["Ignore"]; + } + else + { + logger.Warn($"Key \"Ignore\" missing from configuration file for {name}. Attempting to continue."); + } + + if (config.ContainsKey("ExcludedItemFilter")) + { + TomlTable excludedFields = (TomlTable)config["ExcludedItemFilter"]; + foreach (var field in excludedFields) + { + exclusionFilters[field.Key] = GetFilterValues((TomlArray)field.Value, "ExcludedItemFilter"); + } + } + + if (config.ContainsKey("IncludedItemFilter")) + { + TomlTable includedFields = (TomlTable)config["IncludedItemFilter"]; + foreach (var field in includedFields) + { + inclusionFilters[field.Key] = GetFilterValues((TomlArray)field.Value, "IncludedItemFilter"); + } + } + } + catch (Exception e) + { + logger.Error($"Error reading configuration file for {name}."); + logger.Error(e); + throw new Exception($"The {name} could not read its configuration. Aborting..."); + } + } + + private HashSet GetFilterValues(TomlArray values, string id) + { + HashSet vs = new HashSet(); + foreach (var value in values) + { + if (value is string str) + { + vs.Add(str); + } + else + { + logger.Error($"One or more values in the {id} list in the {name} configuration file is not a string. Cannot parse."); + throw new Exception($"{id} list value not a string."); + } + } + return vs; + } + + private void AddTruthItemConfig(string itemName, TomlTable config) + { + if (config.ContainsKey(itemName)) + { + try + { + TomlTable item = (TomlTable)config[itemName]; + string nm = GetObjectForKey(item, "name", true); + bool flt = GetObjectForKey(item, "filter", true); + truthItemConfig[itemName] = new TruthItemConfig(nm, flt); + } + catch + { + logger.Error($"{name} configuration file has an entry for {itemName} but it is not valid."); + throw; + } + } + else + { + throw new ArgumentException($"{name} configuration file does not contain valid truth item configuration for \"{itemName}\"."); + } + } + + protected bool ExcludeItem(string fieldName, HashSet values) + { + if(exclusionFilters.Count > 0) + { + if (exclusionFilters.ContainsKey(fieldName)) + { + return exclusionFilters[fieldName].Overlaps(values); + } + else + { + return false; + } + } + else + { + return false; + } + } + + protected bool IncludeItem(string fieldName, HashSet values) + { + if (inclusionFilters.Count > 0) + { + if (inclusionFilters.ContainsKey(fieldName)) + { + return inclusionFilters[fieldName].Overlaps(values); + } + else + { + return true; + } + } + else + { + return true; + } + } + + protected void TrimLinkedItems(List items, List retrievedIDs) + { + foreach (var item in items) + { + LinkedItem linkedItem = item as LinkedItem; + List linkedItemsToRemove = new List(); + foreach (var itemLink in linkedItem.LinkedItems) + { + if (!retrievedIDs.Contains(itemLink.TargetID)) + { + logger.Warn($"Removing a {itemLink.LinkType} link from item \"{linkedItem.ItemID}\" to item with ID \"{itemLink.TargetID}\" because that item has a status that causes it to be ignored."); + linkedItemsToRemove.Add(itemLink); + } + } + foreach (var itemLink in linkedItemsToRemove) + { + linkedItem.RemoveLinkedItem(itemLink); //remove the link to an ignored item + } + } + } + } +} \ No newline at end of file diff --git a/RoboClerk/Program.cs b/RoboClerk/Program.cs index 002a921..05446ce 100644 --- a/RoboClerk/Program.cs +++ b/RoboClerk/Program.cs @@ -9,7 +9,7 @@ using System.Reflection; using Tomlyn; -[assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.2.*")] namespace RoboClerk { diff --git a/RoboClerk/ProtoTag.cs b/RoboClerk/ProtoTag.cs new file mode 100644 index 0000000..5fe0dde --- /dev/null +++ b/RoboClerk/ProtoTag.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RoboClerk +{ + internal class ProtoTag + { + private int startIndex = -1; + private int endIndex = -1; + private bool containerTag = false; + public ProtoTag(int startIndex, bool containerTag = false) + { + this.startIndex = startIndex; + this.containerTag = containerTag; + } + + public bool hasEndIndex() + { + return endIndex != -1; + } + + public int StartIndex { get { return startIndex; } } + public int EndIndex { set { endIndex = value; } get { return endIndex; } } + public bool ContainerTag { get { return containerTag; } } + } +} diff --git a/RoboClerk/RoboClerkAsciiDoc.cs b/RoboClerk/RoboClerkAsciiDoc.cs index a57f5b4..3bef91e 100644 --- a/RoboClerk/RoboClerkAsciiDoc.cs +++ b/RoboClerk/RoboClerkAsciiDoc.cs @@ -9,57 +9,76 @@ public static class RoboClerkAsciiDoc public static List ExtractRoboClerkTags(string asciiDocText) { int index = 0; - List containerIndices = new List(); - List inlineIndices = new List(); + List indices = new List(); while ((index = asciiDocText.IndexOf("@@", index)) >= 0) { if (index + 1 == asciiDocText.Length - 1) { - inlineIndices.Add(index); + AddIndex(indices, index, false); break; } if (asciiDocText[index + 2] != '@') { - inlineIndices.Add(index); + AddIndex(indices, index, false); index += 2; } else { - containerIndices.Add(index); + AddIndex(indices, index, true); index += 3; } } - if (containerIndices.Count % 2 != 0) - { - throw new Exception("Number of @@@ container indices is not even. Template file is invalid."); - } - if (inlineIndices.Count % 2 != 0) + CheckForUnbalancedTags(indices); + + List tags = new List(); + + foreach (ProtoTag tag in indices) { - throw new Exception("Number of @@ inline container indices is not even. Template file is invalid."); + if (tag.ContainerTag) + { + tags.Add(new RoboClerkTag(tag.StartIndex, tag.EndIndex, asciiDocText, false)); + } + else + { + //check for newlines in tag which is illegal + int idx = asciiDocText.IndexOf('\n', tag.StartIndex, tag.EndIndex - tag.StartIndex); + if (idx >= 0 && idx <= tag.EndIndex) + { + throw new Exception($"Inline Roboclerk containers cannot have newline characters in them. Newline found in tag at {tag.StartIndex}."); + } + //check if this inline tag is within a container tag, if so, do not add the tag. The outer container tag will be resolved first + if (tags.Count(t => (!t.Inline && t.ContentStart < tag.StartIndex && t.ContentEnd > tag.EndIndex)) == 0) + tags.Add(new RoboClerkTag(tag.StartIndex, tag.EndIndex, asciiDocText, true)); + } } - List tags = new List(); + return tags; + } - for (int i = 0; i < containerIndices.Count; i += 2) + private static void AddIndex(List tags, int index, bool containerIndex) + { + if (tags.Count > 0 && !tags.Last().hasEndIndex()) { - tags.Add(new RoboClerkTag(containerIndices[i], containerIndices[i + 1], asciiDocText, false)); + tags.Last().EndIndex=index; + return; } + tags.Add(new ProtoTag(index, containerIndex)); + } - for (int i = 0; i < inlineIndices.Count; i += 2) + private static void CheckForUnbalancedTags(List tags) + { + foreach (ProtoTag tag in tags) { - //check for newlines in tag which is illegal - int idx = asciiDocText.IndexOf('\n', inlineIndices[i], inlineIndices[i + 1] - inlineIndices[i]); - if (idx >= 0 && idx <= inlineIndices[i + 1]) + if (!tag.hasEndIndex() && tag.ContainerTag) + { + throw new Exception("Number of @@@ container indices is not even. Template file is invalid."); + } + if (!tag.hasEndIndex() && !tag.ContainerTag) { - throw new Exception($"Inline Roboclerk containers cannot have newline characters in them. Newline found in tag at {inlineIndices[i]}."); + throw new Exception("Number of @@ inline container indices is not even. Template file is invalid."); } - //check if this inline tag is within a container tag, if so, do not add the tag. The outer container tag will be resolved first - if (tags.Count(t => (!t.Inline && t.ContentStart < inlineIndices[i] && t.ContentEnd > inlineIndices[i + 1])) == 0) - tags.Add(new RoboClerkTag(inlineIndices[i], inlineIndices[i + 1], asciiDocText, true)); } - - return tags; } public static string ReInsertRoboClerkTags(string asciiDoc, List tags) diff --git a/RoboClerk/RoboClerkCore.cs b/RoboClerk/RoboClerkCore.cs index 31172cb..7963e8a 100644 --- a/RoboClerk/RoboClerkCore.cs +++ b/RoboClerk/RoboClerkCore.cs @@ -104,7 +104,7 @@ private List ProcessTemplates(IEnumerable configDocume } else if (tag.Source == DataSource.Document) { - IContentCreator cc = new ContentCreators.Document(); + IContentCreator cc = new ContentCreators.Document(traceAnalysis); tag.Contents = cc.GetContent(tag, doc); } else if (tag.Source == DataSource.AI) diff --git a/RoboClerk/RoboClerkTag.cs b/RoboClerk/RoboClerkTag.cs index 899d58d..0027eb5 100644 --- a/RoboClerk/RoboClerkTag.cs +++ b/RoboClerk/RoboClerkTag.cs @@ -183,7 +183,7 @@ private void ProcessRoboClerkContainerInlineTag(int startIndex, int endIndex, st catch (TagInvalidException e) { e.SetLocation(tagStart, rawDocument); - throw e; + throw; } ExtractParameters(contents); var items = contents.Split('(')[0].Split(':'); @@ -248,7 +248,7 @@ private void ProcessRoboClerkContainerTag(int startIndex, int endIndex, string r catch (TagInvalidException e) { e.SetLocation(tagStart, rawDocument); - throw e; + throw; } var items = tagContents.Split(':'); source = GetSource(items[0].Trim().ToUpper()); diff --git a/RoboClerkDocker/Dockerfile b/RoboClerkDocker/Dockerfile index c96667d..930aadc 100644 --- a/RoboClerkDocker/Dockerfile +++ b/RoboClerkDocker/Dockerfile @@ -10,23 +10,28 @@ ENV PYTHONUNBUFFERED=1 ENV PANDOC_VERSION 3.1.8 ENV PANDOC_DOWNLOAD_URL https://github.com/jgm/pandoc/releases/download/$PANDOC_VERSION/pandoc-$PANDOC_VERSION-linux-amd64.tar.gz -RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python \ - && python3 -m ensurepip \ - && python3 -m pip install --no-cache --upgrade pip \ - && pip3 install --no-cache --upgrade bayoo-docx \ - && pip3 install --no-cache --upgrade openpyxl \ +RUN apk add --update --no-cache python3 py3-virtualenv \ + && ln -sf python3 /usr/bin/python \ + && python3 -m venv /opt/venv \ + && . /opt/venv/bin/activate \ + && pip install --no-cache --upgrade pip \ + && pip install --no-cache --upgrade bayoo-docx \ + && pip install --no-cache --upgrade openpyxl \ && wget $PANDOC_DOWNLOAD_URL \ && tar -xzf pandoc-$PANDOC_VERSION-linux-amd64.tar.gz \ && cp /pandoc-$PANDOC_VERSION/bin/pandoc /usr/bin/ \ && rm -rf pandoc-$PANDOC_VERSION \ && rm pandoc-$PANDOC_VERSION-linux-amd64.tar.gz \ - && apk add --no-cache asciidoctor \ - && apk --update add --no-cache ruby ruby-io-console ruby-irb ruby-json ruby-rake ruby-rdoc git \ - && gem install asciidoctor-kroki + && apk add --no-cache asciidoctor \ + && apk --update add --no-cache ruby ruby-io-console ruby-irb ruby-json ruby-rake ruby-rdoc git \ + && gem install asciidoctor-kroki # Install RoboClerk itself ADD Publish /home/RoboClerk WORKDIR /home/RoboClerk ENV PATH="/home/RoboClerk:${PATH}" RUN chmod +x /home/RoboClerk/scaffold -RUN chmod +x /home/RoboClerk/generate \ No newline at end of file +RUN chmod +x /home/RoboClerk/generate + +# Activate the virtual environment and set it as default for subsequent commands +ENV PATH="/opt/venv/bin:$PATH"