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

MeindertN / RoboClerk / 19314579599

12 Nov 2025 10:19PM UTC coverage: 81.831% (-2.2%) from 84.026%
19314579599

push

github

web-flow
Merge pull request #79 from MeindertN/V1.6.0

jumping to V1.7.0

2069 of 2635 branches covered (78.52%)

Branch coverage included in aggregate %.

1293 of 1710 new or added lines in 32 files covered. (75.61%)

17 existing lines in 5 files now uncovered.

6322 of 7619 relevant lines covered (82.98%)

189.57 hits per line

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

70.0
/RoboClerk/PluginSupport/SourceCodeAnalysisPluginBase.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 Tomlyn.Model;
8

9
namespace RoboClerk
10
{
11
    /// <summary>
12
    /// Represents a single test configuration with its own settings and associated files
13
    /// </summary>
14
    public class TestConfiguration
15
    {
16
        // Known/common fields with typed accessors
17
        public string Language { get; set; } = "csharp";
345✔
18
        public string TestDirectory { get; set; } = string.Empty;
275✔
19
        public bool SubDirs { get; set; } = true;
233✔
20
        public List<string> FileMasks { get; set; } = new List<string>();
216✔
21
        public string Project { get; set; } = string.Empty;
320✔
22
        
23
        // Dictionary for all fields (including unknown ones)
24
        public Dictionary<string, object> AllFields { get; private set; } = new Dictionary<string, object>();
817✔
25
        
26
        // Files associated with this configuration
27
        public List<string> SourceFiles { get; private set; } = new List<string>();
368✔
28

29
        public void FromToml(TomlTable toml)
30
        {
52✔
31
            // Store ALL fields from TOML first
32
            AllFields.Clear();
52✔
33
            foreach (var kvp in toml)
816✔
34
            {
330✔
35
                AllFields[kvp.Key] = kvp.Value;
330✔
36
            }
330✔
37
            
38
            // Then populate known typed properties
39
            if (AllFields.TryGetValue("Language", out var lang))
52✔
40
                Language = lang.ToString();
52✔
41
                
42
            if (AllFields.TryGetValue("TestDirectory", out var dir))
52✔
43
                TestDirectory = dir.ToString();
52✔
44
                
45
            if (AllFields.TryGetValue("SubDirs", out var sub))
52✔
46
                SubDirs = (bool)sub;
52✔
47
                
48
            if (AllFields.TryGetValue("FileMasks", out var masks) && masks is TomlArray masksArray)
52!
49
            {
52✔
50
                FileMasks.Clear();
52✔
51
                foreach (var obj in masksArray)
278✔
52
                {
61✔
53
                    FileMasks.Add((string)obj);
61✔
54
                }
61✔
55
            }
52✔
56
            
57
            if (AllFields.TryGetValue("Project", out var proj))
52✔
58
                Project = proj.ToString();
52✔
59
        }
52✔
60
        
61
        /// <summary>
62
        /// Get a typed value from the configuration, with optional default
63
        /// </summary>
64
        public T GetValue<T>(string key, T defaultValue = default(T))
65
        {
123✔
66
            if (AllFields.TryGetValue(key, out var value))
123!
67
            {
123✔
68
                if (value is T typedValue)
123!
69
                    return typedValue;
123✔
70
                
71
                // Try to convert common types
NEW
72
                if (typeof(T) == typeof(string))
×
NEW
73
                    return (T)(object)value.ToString();
×
74
                
NEW
75
                if (typeof(T) == typeof(bool) && bool.TryParse(value.ToString(), out var boolVal))
×
NEW
76
                    return (T)(object)boolVal;
×
77
                    
78
                // Add other type conversions as needed
NEW
79
            }
×
80
            
NEW
81
            return defaultValue;
×
82
        }
123✔
83
        
84
        /// <summary>
85
        /// Check if a field exists in the configuration
86
        /// </summary>
NEW
87
        public bool HasField(string key) => AllFields.ContainsKey(key);
×
88
        
89
        /// <summary>
90
        /// Add a source file to this configuration
91
        /// </summary>
92
        internal void AddSourceFile(string filePath)
93
        {
55✔
94
            SourceFiles.Add(filePath);
55✔
95
        }
55✔
96
        
97
        /// <summary>
98
        /// Clear all source files for this configuration
99
        /// </summary>
100
        internal void ClearSourceFiles()
101
        {
49✔
102
            SourceFiles.Clear();
49✔
103
        }
49✔
104
    }
105

106
    public abstract class SourceCodeAnalysisPluginBase : DataSourcePluginBase
107
    {
108
        // Configuration-based approach
109
        protected List<TestConfiguration> testConfigurations = new List<TestConfiguration>();
51✔
110
        protected List<string> sourceFiles = new List<string>();
51✔
111
        protected GitRepository gitRepo = null;
51✔
112

113
        public SourceCodeAnalysisPluginBase(IFileSystem fileSystem)
114
            : base(fileSystem)
51✔
115
        {
51✔
116
        }
51✔
117

118
        public override void InitializePlugin(IConfiguration configuration)
119
        {
50✔
120
            var config = GetConfigurationTable(configuration.PluginConfigDir, $"{name}.toml");
50✔
121
            
122
            // Check if using TestConfigurations format
123
            if (config.ContainsKey("TestConfigurations"))
48✔
124
            {
47✔
125
                InitializeTestConfigurations(config);
47✔
126
            }
47✔
127
            else
128
            {
1✔
129
                throw new Exception($"TestConfigurations section is required in {name}.toml configuration file.");
1✔
130
            }
131

132
            try
133
            {
47✔
134
                if (config.ContainsKey("UseGit") && (bool)config["UseGit"])
47!
135
                {
×
NEW
136
                    gitRepo = new GitRepository(configuration, fileSystem);
×
137
                }
×
138
            }
47✔
139
            catch (Exception)
×
140
            {
×
141
                logger.Error($"Error opening git repo at project root \"{configuration.ProjectRoot}\" even though the {name}.toml configuration file UseGit setting was set to true.");
×
142
                throw;
×
143
            }
144
        }
47✔
145

146
        private void InitializeTestConfigurations(TomlTable config)
147
        {
47✔
148
            testConfigurations.Clear();
47✔
149
            var configurationsArray = (TomlTableArray)config["TestConfigurations"];
47✔
150
            
151
            foreach (TomlTable configTable in configurationsArray)
245✔
152
            {
52✔
153
                var testConfig = new TestConfiguration();
52✔
154
                testConfig.FromToml(configTable);
52✔
155
                testConfigurations.Add(testConfig);
52✔
156
            }
52✔
157

158
            if (testConfigurations.Count == 0)
47!
NEW
159
            {
×
NEW
160
                throw new Exception($"No test configurations found in {name}.toml. At least one TestConfiguration is required.");
×
161
            }
162
        }
47✔
163

164
        protected void ScanDirectoriesForSourceFiles()
165
        {
45✔
166
            // Clear all files from configurations first
167
            foreach (var testConfig in testConfigurations)
233✔
168
            {
49✔
169
                testConfig.ClearSourceFiles();
49✔
170
            }
49✔
171
            
172
            // Clear the legacy sourceFiles list (keep for backward compatibility)
173
            sourceFiles.Clear();
45✔
174
            
175
            foreach (var testConfig in testConfigurations)
232✔
176
            {
49✔
177
                ScanDirectoryForConfiguration(testConfig);
49✔
178
            }
48✔
179

180
            // Count total files across all configurations
181
            var totalFiles = testConfigurations.Sum(config => config.SourceFiles.Count);
91✔
182
            logger.Info($"Found {totalFiles} source files across {testConfigurations.Count} configurations");
44✔
183
            
184
            // Preload git information for performance if git is enabled and we have files
185
            if (gitRepo != null && totalFiles > 0)
44!
NEW
186
            {
×
NEW
187
                logger.Debug($"Preloading git information for {totalFiles} source files across {testConfigurations.Count} configurations");
×
188
                
189
                // Get unique directories that contain our source files
NEW
190
                var sourceDirectories = testConfigurations
×
NEW
191
                    .SelectMany(config => config.SourceFiles)
×
NEW
192
                    .Select(f => Path.GetDirectoryName(f))
×
NEW
193
                    .Where(d => !string.IsNullOrEmpty(d))
×
NEW
194
                    .Distinct()
×
NEW
195
                    .ToList();
×
196

197
                // Preload git information for each directory
NEW
198
                foreach (var directory in sourceDirectories)
×
UNCOV
199
                {
×
200
                    try
NEW
201
                    {
×
NEW
202
                        gitRepo.PreloadDirectoryInfo(directory);
×
NEW
203
                    }
×
NEW
204
                    catch (Exception ex)
×
UNCOV
205
                    {
×
NEW
206
                        logger.Warn($"Could not preload git information for directory {directory}: {ex.Message}");
×
207
                        // Continue processing other directories even if one fails
UNCOV
208
                    }
×
UNCOV
209
                }
×
NEW
210
            }
×
211
        }
44✔
212

213
        private void ScanDirectoryForConfiguration(TestConfiguration testConfig)
214
        {
49✔
215
            if (string.IsNullOrEmpty(testConfig.TestDirectory))
49!
NEW
216
            {
×
NEW
217
                logger.Warn($"Empty TestDirectory in configuration for project '{testConfig.Project}', skipping");
×
NEW
218
                return;
×
219
            }
220

221
            IDirectoryInfo dir = fileSystem.DirectoryInfo.New(testConfig.TestDirectory);
49✔
222
            try
223
            {
49✔
224
                foreach (var fileMask in testConfig.FileMasks)
262✔
225
                {
58✔
226
                    IFileInfo[] files = dir.GetFiles(fileMask, testConfig.SubDirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
58✔
227
                    foreach (var file in files)
281✔
228
                    {
55✔
229
                        testConfig.AddSourceFile(file.FullName);
55✔
230
                        sourceFiles.Add(file.FullName); // Keep for backward compatibility
55✔
231
                        logger.Debug($"Found source file: {file.FullName} (Project: {testConfig.Project}, Language: {testConfig.Language})");
55✔
232
                    }
55✔
233
                }
57✔
234
                
235
                logger.Info($"Configuration '{testConfig.Project}' ({testConfig.Language}): Found {testConfig.SourceFiles.Count} source files");
48✔
236
            }
48✔
237
            catch (Exception ex)
1✔
238
            {
1✔
239
                logger.Error($"Error reading directory {testConfig.TestDirectory} for project '{testConfig.Project}': {ex.Message}");
1✔
240
                throw;
1✔
241
            }
242
        }
48✔
243

244
        /// <summary>
245
        /// Get the test configuration that applies to a specific source file path
246
        /// </summary>
247
        /// <param name="filePath">The path to the source file</param>
248
        /// <returns>The TestConfiguration that matches this file, or null if not found</returns>
249
        protected TestConfiguration GetConfigurationForFile(string filePath)
250
        {
70✔
251
            foreach (var config in testConfigurations)
280!
252
            {
70✔
253
                if (IsFileInDirectory(filePath, config.TestDirectory, config.SubDirs))
70!
254
                {
70✔
255
                    return config;
70✔
256
                }
NEW
257
            }
×
NEW
258
            return null;
×
259
        }
70✔
260

261
        private bool IsFileInDirectory(string filePath, string directoryPath, bool includeSubDirs)
262
        {
70✔
263
            try
264
            {
70✔
265
                var fileInfo = fileSystem.FileInfo.New(filePath);
70✔
266
                var dirInfo = fileSystem.DirectoryInfo.New(directoryPath);
70✔
267
                
268
                var fileDir = fileInfo.Directory;
70✔
269
                while (fileDir != null)
70!
270
                {
70✔
271
                    if (string.Equals(fileDir.FullName, dirInfo.FullName, StringComparison.OrdinalIgnoreCase))
70!
272
                    {
70✔
273
                        return true;
70✔
274
                    }
275
                    
NEW
276
                    if (!includeSubDirs)
×
NEW
277
                    {
×
NEW
278
                        break;
×
279
                    }
280
                    
NEW
281
                    fileDir = fileDir.Parent;
×
UNCOV
282
                }
×
283
                
NEW
284
                return false;
×
285
            }
NEW
286
            catch
×
NEW
287
            {
×
NEW
288
                return false;
×
289
            }
290
        }
70✔
291

292
        /// <summary>
293
        /// Gets all test configurations
294
        /// </summary>
295
        protected IReadOnlyList<TestConfiguration> TestConfigurations => testConfigurations.AsReadOnly();
118✔
296
    }
297
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc