• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MeindertN / RoboClerk / 17880219182

20 Sep 2025 01:05PM UTC coverage: 84.152% (+0.8%) from 83.388%
17880219182

push

github

MeindertN
WIP: all tests for all languages passing

1897 of 2345 branches covered (80.9%)

Branch coverage included in aggregate %.

33 of 36 new or added lines in 1 file covered. (91.67%)

5808 of 6811 relevant lines covered (85.27%)

127.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.9
/RoboClerk.UnitTestFN/UnitTestFNPlugin.cs
1
using RoboClerk.Configuration;
2
using System;
3
using System.Collections.Generic;
4
using System.IO;
5
using System.IO.Abstractions;
6
using System.Linq;
7
using System.Text;
8
using TreeSitter;
9

10
namespace RoboClerk
11
{
12
    public class UnitTestFNPlugin : SourceCodeAnalysisPluginBase
13
    {
14
        private List<string> functionMaskElements = new List<string>();
13✔
15
        private string sectionSeparator = string.Empty;
13✔
16
        private string selectedLanguage = "csharp";
13✔
17

18
        public UnitTestFNPlugin(IFileSystem fileSystem)
19
            : base(fileSystem)
13✔
20
        {
13✔
21
            SetBaseParam();
13✔
22
        }
13✔
23

24
        private void SetBaseParam()
25
        {
13✔
26
            name = "UnitTestFNPlugin";
13✔
27
            description = "A plugin that analyzes a project's source code to extract unit test information for RoboClerk using TreeSitter.";
13✔
28
        }
13✔
29

30
        public override void InitializePlugin(IConfiguration configuration)
31
        {
13✔
32
            logger.Info("Initializing the Unit Test Function Name Plugin");
13✔
33
            try
34
            {
13✔
35
                base.InitializePlugin(configuration);
13✔
36
                var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml");
12✔
37

38
                // Language selection (make it optional)
39
                selectedLanguage = configuration.CommandLineOptionOrDefault("Language", GetObjectForKey<string>(config, "Language", false) ?? "csharp");
12!
40
                
41
                var functionMask = configuration.CommandLineOptionOrDefault("FunctionMask", GetObjectForKey<string>(config, "FunctionMask", true));
12✔
42
                functionMaskElements = ParseFunctionMask(functionMask);
12✔
43
                ValidateFunctionMaskElements(functionMaskElements);
12✔
44
                sectionSeparator = configuration.CommandLineOptionOrDefault("SectionSeparator", GetObjectForKey<string>(config, "SectionSeparator", true));
11✔
45
            }
11✔
46
            catch (Exception e)
2✔
47
            {
2✔
48
                logger.Error("Error reading configuration file for Unit Test FN plugin.");
2✔
49
                logger.Error(e);
2✔
50
                throw new Exception("The Unit Test FN plugin could not read its configuration. Aborting...");
2✔
51
            }
52
            ScanDirectoriesForSourceFiles();
11✔
53
        }
11✔
54

55
        private List<string> ParseFunctionMask(string functionMask)
56
        {
12✔
57
            List<string> elements = new List<string>();
12✔
58
            bool inElement = false;
12✔
59
            StringBuilder sb = new StringBuilder();
12✔
60
            foreach (char c in functionMask)
952✔
61
            {
458✔
62
                if (c == '<' && !inElement)
458✔
63
                {
25✔
64
                    inElement = true;
25✔
65
                    if (sb.Length > 0)
25✔
66
                    {
14✔
67
                        elements.Add(sb.ToString());
14✔
68
                        sb.Clear();
14✔
69
                    }
14✔
70
                }
25✔
71
                if (inElement)
458✔
72
                {
293✔
73
                    sb.Append(c);
293✔
74
                    if (c == '>')
293✔
75
                    {
25✔
76
                        inElement = false;
25✔
77
                        elements.Add(sb.ToString());
25✔
78
                        sb.Clear();
25✔
79
                    }
25✔
80
                }
293✔
81
                else
82
                {
165✔
83
                    sb.Append(c);
165✔
84
                }
165✔
85
            }
458✔
86
            // Add any remaining content
87
            if (sb.Length > 0)
12✔
88
            {
1✔
89
                elements.Add(sb.ToString());
1✔
90
                sb.Clear();
1✔
91
            }
1✔
92
            return elements;
12✔
93
        }
12✔
94

95
        private void ValidateFunctionMaskElements(List<string> elements)
96
        {
12✔
97
            if (!elements.First().StartsWith('<'))
12✔
98
            {
1✔
99
                throw new Exception("Error in UnitTestFNPlugin, Function Mask must start with an element identifier (e.g. <IGNORE>)");
1✔
100
            }
101
            foreach (string element in elements)
111✔
102
            {
39✔
103
                if (element.StartsWith('<'))
39✔
104
                {
25✔
105
                    if (element.ToUpper() != "<PURPOSE>" &&
25!
106
                        element.ToUpper() != "<POSTCONDITION>" &&
25✔
107
                        element.ToUpper() != "<IDENTIFIER>" &&
25✔
108
                        element.ToUpper() != "<TRACEID>" &&
25✔
109
                        element.ToUpper() != "<IGNORE>")
25✔
110
                    {
×
111
                        throw new Exception($"Error in UnitTestFNPlugin, unknown function mask element found: \"{element}\"");
×
112
                    }
113
                }
25✔
114
                else
115
                {
14✔
116
                    if (element.Any(x => Char.IsWhiteSpace(x)))
140!
117
                    {
×
118
                        throw new Exception($"Error in UnitTestFNPlugin, whitespace found in an element of the function mask: \"{element}\"");
×
119
                    }
120
                }
14✔
121
            }
39✔
122
        }
11✔
123

124
        private string SeparateSection(string section)
125
        {
42✔
126
            if (sectionSeparator.ToUpper() == "CAMELCASE")
42✔
127
            {
2✔
128
                StringBuilder sb = new StringBuilder();
2✔
129
                bool first = true;
2✔
130
                //don't care if the first word is capitalized
131
                foreach (char c in section)
72✔
132
                {
33✔
133
                    if (first)
33✔
134
                    {
2✔
135
                        sb.Append(c);
2✔
136
                        first = false;
2✔
137
                    }
2✔
138
                    else
139
                    {
31✔
140
                        if (Char.IsUpper(c))
31✔
141
                        {
7✔
142
                            sb.Append(' ');
7✔
143
                            sb.Append(c);
7✔
144
                        }
7✔
145
                        else
146
                        {
24✔
147
                            sb.Append(c);
24✔
148
                        }
24✔
149
                    }
31✔
150
                }
33✔
151
                return sb.ToString();
2✔
152
            }
153
            else
154
            {
40✔
155
                return section.Replace(sectionSeparator, " ");
40✔
156
            }
157
        }
42✔
158

159
        private List<(string, string)> ApplyFunctionNameMask(string functionName)
160
        {
27✔
161
            List<(string, string)> resultingElements = new List<(string, string)>();
27✔
162
            
163
            // Check if the function name matches the non-element parts of the mask
164
            bool foundMatch = true;
27✔
165
            foreach (var functionMaskElement in functionMaskElements)
246✔
166
            {
86✔
167
                if (!functionMaskElement.StartsWith('<'))
86✔
168
                {
33✔
169
                    if (!functionName.Contains(functionMaskElement))
33✔
170
                    {
7✔
171
                        foundMatch = false;
7✔
172
                        break;
7✔
173
                    }
174
                }
26✔
175
            }
79✔
176
            
177
            if (foundMatch)
27✔
178
            {
20✔
179
                string remainingFunctionName = functionName;
20✔
180
                
181
                // Process pairs: identifier, separator, identifier, separator, etc.
182
                for (int i = 0; i < functionMaskElements.Count; i++)
144✔
183
                {
72✔
184
                    if (functionMaskElements[i].StartsWith('<'))
72✔
185
                    {
46✔
186
                        // This is an element identifier
187
                        if (i + 1 < functionMaskElements.Count && !functionMaskElements[i + 1].StartsWith('<'))
46✔
188
                        {
26✔
189
                            // Next element is a separator
190
                            var separator = functionMaskElements[i + 1];
26✔
191
                            var separatorIndex = remainingFunctionName.IndexOf(separator);
26✔
192
                            
193
                            if (separatorIndex >= 0)
26!
194
                            {
26✔
195
                                var extractedContent = remainingFunctionName.Substring(0, separatorIndex);
26✔
196
                                resultingElements.Add((functionMaskElements[i], extractedContent));
26✔
197
                                remainingFunctionName = remainingFunctionName.Substring(separatorIndex + separator.Length);
26✔
198
                            }
26✔
199
                            else
NEW
200
                            {
×
NEW
201
                                foundMatch = false;
×
NEW
202
                                break;
×
203
                            }
204
                        }
26✔
205
                        else
206
                        {
20✔
207
                            // This is the last element - take all remaining content
208
                            resultingElements.Add((functionMaskElements[i], remainingFunctionName));
20✔
209
                            break;
20✔
210
                        }
211
                    }
26✔
212
                }
52✔
213
            }
20✔
214
            
215
            if (!foundMatch)
27✔
216
            {
7✔
217
                resultingElements.Clear();
7✔
218
            }
7✔
219
            
220
            return resultingElements;
27✔
221
        }
27✔
222

223
        private void AddUnitTest(List<(string, string)> els, string fileName, int lineNumber, string functionName)
224
        {
20✔
225
            var unitTest = new UnitTestItem();
20✔
226
            bool identified = false;
20✔
227
            string shortFileName = Path.GetFileName(fileName);
20✔
228
            foreach (var el in els)
152✔
229
            {
46✔
230
                switch (el.Item1.ToUpper())
46!
231
                {
232
                    case "<PURPOSE>": unitTest.UnitTestPurpose = SeparateSection(el.Item2); break;
40✔
233
                    case "<POSTCONDITION>": unitTest.UnitTestAcceptanceCriteria = SeparateSection(el.Item2); break;
40✔
234
                    case "<IDENTIFIER>": unitTest.ItemID = SeparateSection(el.Item2); identified = true; break;
6✔
235
                    case "<TRACEID>": unitTest.AddLinkedItem(new ItemLink(el.Item2, ItemLinkType.UnitTests)); break;
4✔
236
                    case "<IGNORE>": break;
2✔
237
                    default: throw new Exception($"Unknown element identifier in FunctionMask: {el.Item1.ToUpper()}");
×
238
                }
239
            }
46✔
240
            unitTest.UnitTestFileName = shortFileName;
20✔
241
            unitTest.UnitTestFunctionName = functionName;
20✔
242
            if (!identified)
20✔
243
            {
18✔
244
                unitTest.ItemID = $"{shortFileName}:{lineNumber}";
18✔
245
            }
18✔
246
            if (gitRepo != null && !gitRepo.GetFileLocallyUpdated(fileName))
20!
247
            {
×
248
                //if gitInfo is not null, this means some item data elements should be collected through git
249
                unitTest.ItemLastUpdated = gitRepo.GetFileLastUpdated(fileName);
×
250
                unitTest.ItemRevision = gitRepo.GetFileVersion(fileName);
×
251
            }
×
252
            else
253
            {
20✔
254
                //the last time the local file was updated is our best guess
255
                unitTest.ItemLastUpdated = File.GetLastWriteTime(fileName);
20✔
256
                unitTest.ItemRevision = File.GetLastWriteTime(fileName).ToString("yyyy/MM/dd HH:mm:ss");
20✔
257
            }
20✔
258
            if (unitTests.FindIndex(x => x.ItemID == unitTest.ItemID) != -1)
34!
259
            {
×
260
                throw new Exception($"Duplicate unit test identifier detected in {shortFileName} in the annotation starting on line {lineNumber}. Check other unit tests to ensure all unit tests have a unique identifier.");
×
261
            }
262
            unitTests.Add(unitTest);
20✔
263
        }
20✔
264

265
        public override void RefreshItems()
266
        {
10✔
267
            foreach (var sourceFile in sourceFiles)
50✔
268
            {
10✔
269
                var text = fileSystem.File.ReadAllText(sourceFile);
10✔
270
                FindAndProcessFunctions(text, sourceFile);
10✔
271
            }
10✔
272
        }
10✔
273

274
        private void FindAndProcessFunctions(string sourceText, string filename)
275
        {
10✔
276
            var languageId = GetTreeSitterLanguageId(selectedLanguage);
10✔
277
            
278
            using var language = new Language(languageId);
10✔
279
            using var parser = new Parser(language);
10✔
280
            using var tree = parser.Parse(sourceText);
10✔
281

282
            var queryString = GetQueryForLanguage(selectedLanguage);
10✔
283
            using var query = new Query(language, queryString);
10✔
284
            var exec = query.Execute(tree.RootNode);
10✔
285

286
            // Process all methods found by TreeSitter
287
            var processedMethods = new HashSet<(string methodName, int line)>();
10✔
288

289
            foreach (var match in exec.Matches)
84✔
290
            {
27✔
291
                string methodName = string.Empty;
27✔
292
                int methodLine = 0;
27✔
293

294
                foreach (var cap in match.Captures)
108✔
295
                {
27✔
296
                    if (cap.Name == "method_name")
27!
297
                    {
27✔
298
                        methodName = cap.Node.Text;
27✔
299
                        methodLine = (int)cap.Node.StartPosition.Row + 1;
27✔
300
                        break;
27✔
301
                    }
302
                }
×
303

304
                if (!string.IsNullOrEmpty(methodName))
27✔
305
                {
27✔
306
                    var methodKey = (methodName, methodLine);
27✔
307
                    if (processedMethods.Contains(methodKey))
27✔
308
                        continue;
×
309
                    processedMethods.Add(methodKey);
27✔
310
                                        
311
                    // Try to apply the function name mask
312
                    var els = ApplyFunctionNameMask(methodName);
27✔
313
                    if (els.Count > 0)
27✔
314
                    {
20✔
315
                        AddUnitTest(els, filename, methodLine, methodName);
20✔
316
                    }
20✔
317
                }
27✔
318
            }
27✔
319
        }
20✔
320

321
        private string GetTreeSitterLanguageId(string lang) => lang.ToLowerInvariant() switch
10!
322
        {
10✔
323
            "c#" or "csharp" or "cs" => "C_SHARP",
5✔
324
            "java" => "JAVA",
1✔
325
            "python" or "py" => "PYTHON",
2✔
326
            "typescript" or "ts" => "TYPESCRIPT",
1✔
327
            "javascript" or "js" => "TYPESCRIPT", // use TS grammar to parse JS
1✔
328
            _ => "C_SHARP"
×
329
        };
10✔
330

331
        private string GetQueryForLanguage(string lang)
332
        {
10✔
333
            return lang.ToLowerInvariant() switch
10!
334
            {
10✔
335
                "c#" or "csharp" or "cs" => @"
5✔
336
(method_declaration
5✔
337
  name: (identifier) @method_name
5✔
338
)",
5✔
339

10✔
340
                "java" => @"
1✔
341
(method_declaration
1✔
342
  name: (identifier) @method_name
1✔
343
)",
1✔
344

10✔
345
                "python" or "py" => @"
2✔
346
(function_definition 
2✔
347
  name: (identifier) @method_name
2✔
348
)",
2✔
349

10✔
350
                "typescript" or "ts" or "javascript" or "js" => @"
2✔
351
; One pattern that matches class methods and standalone functions
2✔
352
(
2✔
353
  [
2✔
354
    (method_definition
2✔
355
      name: (_) @method_name      ; property_identifier, string, number, computed — all OK
2✔
356
    )
2✔
357
    (function_declaration
2✔
358
      name: (identifier) @method_name
2✔
359
    )
2✔
360
  ]
2✔
361
)
2✔
362
",
2✔
363

10✔
364
                _ => @"
×
365
(method_declaration
×
366
  name: (identifier) @method_name
×
367
)"
×
368
            };
10✔
369
        }
10✔
370
    }
371
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc