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

MeindertN / RoboClerk / 19315366704

12 Nov 2025 05:53PM UTC coverage: 72.366% (-0.7%) from 73.034%
19315366704

push

github

MeindertN
WIP: Added support for listing template files for inclusion in other files, getting and changing the project configuration, refreshing the project (with or without tag calculations) and refreshing the project datasources. TODO: ensure the refresh works as expected in all plugins and add the ability to return the specifications and parameters for all content creators. For this, each individual content creator needs to be able to describe its own parameters.

1747 of 2521 branches covered (69.3%)

Branch coverage included in aggregate %.

61 of 71 new or added lines in 4 files covered. (85.92%)

239 existing lines in 13 files now uncovered.

5685 of 7749 relevant lines covered (73.36%)

54.75 hits per line

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

0.0
/RoboClerk.Core/PluginSupport/PluginLoader.cs
1
#nullable enable
2
using Microsoft.Extensions.DependencyInjection;
3
using System;
4
using System.Collections.Generic;
5
using System.IO;
6
using System.IO.Abstractions;
7
using System.Linq;
8
using System.Reflection;
9
using System.Runtime.Loader;
10

11
namespace RoboClerk
12
{
13
    public class PluginLoadContext : AssemblyLoadContext
14
    {
15
        private static readonly HashSet<string> _sharedAssemblies = new(StringComparer.OrdinalIgnoreCase)
×
16
        {
×
17
            "RoboClerk.Core",
×
18
            "Microsoft.Extensions.DependencyInjection.Abstractions",
×
19
            "Microsoft.Extensions.DependencyInjection"
×
20
        };
×
21

22
        private readonly AssemblyDependencyResolver _resolver;
23

24
        public PluginLoadContext(string pluginPath)
×
25
        {
×
26
            _resolver = new AssemblyDependencyResolver(pluginPath);
×
27
        }
×
28

29
        protected override Assembly? Load(AssemblyName assemblyName)
30
        {
×
31
            if (assemblyName.Name != null && _sharedAssemblies.Contains(assemblyName.Name))
×
32
            {
×
33
                // Use the already loaded version from the default context
34
                return null;
×
35
            }
36

37
            string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
×
38
            if (assemblyPath != null)
×
39
            {
×
40
                return LoadFromAssemblyPath(assemblyPath);
×
41
            }
42

43
            return null;
×
44
        }
×
45

46
        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
47
        {
×
48
            string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
×
49
            if (libraryPath != null)
×
50
            {
×
51
                return LoadUnmanagedDllFromPath(libraryPath);
×
52
            }
53

54
            return IntPtr.Zero;
×
55
        }
×
56
    }
57

58
    public class PluginLoader : IPluginLoader
59
    {
60
        private readonly IFileSystem _fileSystem;
61
        private readonly IFileProviderPlugin _pluginFileProvider;
62
        private readonly PluginAssemblyLoader _assemblyLoader;
63

64
        public PluginLoader(IFileSystem fileSystem, IFileProviderPlugin pluginFileProvider)
×
65
        {
×
66
            _fileSystem = fileSystem;
×
67
            _pluginFileProvider = pluginFileProvider;
×
68
            _assemblyLoader = new PluginAssemblyLoader(_fileSystem);
×
69
        }
×
70

71
        // -------------------------
72
        // PUBLIC: load *all* plugins
73
        // -------------------------
74
        public IServiceProvider LoadAll<TPluginInterface>(
75
            string pluginDir,
76
            Action<IServiceCollection>? configureGlobals = null
77
        ) where TPluginInterface : class, IPlugin
78
        {
×
79
            var (services, _) = BuildContainer<TPluginInterface>(pluginDir, configureGlobals);
×
80
            return services.BuildServiceProvider();
×
81
        }
×
82

83
        // ----------------------------------------
84
        // PUBLIC: load one plugin by its class-name
85
        // ----------------------------------------
86
        public TPluginInterface? LoadByName<TPluginInterface>(
87
            string pluginDir,
88
            string typeName,
89
            Action<IServiceCollection>? configureGlobals = null
90
        ) where TPluginInterface : class, IPlugin
91
        {
×
92
            // 1) Build the container and capture the list of discovered impl types:
93
            var (services, implTypes) = BuildContainer<TPluginInterface>(pluginDir, configureGlobals);
×
94
            var provider = services.BuildServiceProvider();
×
95

96
            // 2) Find the one whose class name matches
97
            var match = implTypes
×
98
                .FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.Ordinal));
×
99
            if (match is null)
×
100
                return null;
×
101

102
            // 3) Resolve via DI (honors ctor injection, modules� registrations, etc.)
103
            return provider.GetService(match) as TPluginInterface;
×
104
        }
×
105

106
        // -------------------------------------------------
107
        // INTERNAL: assemble IServiceCollection + impl types
108
        // -------------------------------------------------
109
        private (IServiceCollection services, List<Type> implTypes) BuildContainer<TPluginInterface>
110
            (string pluginDir,Action<IServiceCollection>? configureGlobals) where TPluginInterface : class, IPlugin
111
        {
×
112
            if (!_fileSystem.Directory.Exists(pluginDir))
×
113
                throw new DirectoryNotFoundException($"Plugin directory not found: {pluginDir}");
×
114

115
            var services = new ServiceCollection();
×
116
            var implTypes = new List<Type>();
×
117

118
            // 1) globals - configure first so we can check what's registered
119
            configureGlobals?.Invoke(services);
×
120
            
121
            // Check if IFileProviderPlugin is already registered, if not add the default
UNCOV
122
            var existingFileProvider = services.FirstOrDefault(s => s.ServiceType == typeof(IFileProviderPlugin));
×
123
            if (existingFileProvider == null)
×
124
            {
×
125
                services.AddSingleton<IFileProviderPlugin>(_pluginFileProvider);
×
126
            }
×
127

128
            // Build a temporary service provider to get the file provider for plugin instantiation
129
            var tempProvider = services.BuildServiceProvider();
×
UNCOV
130
            var fileProviderForPlugins = tempProvider.GetRequiredService<IFileProviderPlugin>();
×
131

132
            // 2) per‐assembly scan
133
            foreach (var asm in _assemblyLoader.LoadFromDirectory(pluginDir))
×
UNCOV
134
            {
×
UNCOV
135
                var pluginTypes = asm
×
UNCOV
136
                    .GetTypes()
×
137
                    .Where(t => typeof(TPluginInterface).IsAssignableFrom(t)
×
UNCOV
138
                             && !t.IsAbstract
×
139
                             && t.GetConstructor(new[] { typeof(IFileProviderPlugin) }) != null);
×
140

141
                foreach (var type in pluginTypes)
×
142
                {
×
UNCOV
143
                    ConstructorInfo? ctor = null;
×
144
                    object?[] args;
145

146
                    // 3) find the single‐arg ctor
147
                    ctor = type.GetConstructor(new[] { typeof(IFileProviderPlugin) });
×
148

UNCOV
149
                    if (ctor != null)
×
150
                    {
×
151
                        args = new object[] { fileProviderForPlugins };
×
UNCOV
152
                    }
×
153
                    else
UNCOV
154
                    {
×
155
                        // Fallback to parameterless constructor
156
                        ctor = type.GetConstructor(Type.EmptyTypes);
×
UNCOV
157
                        if (ctor == null)
×
UNCOV
158
                            throw new InvalidOperationException($"Type {type.FullName} has no supported constructor.");
×
159

160
                        args = Array.Empty<object>();
×
161
                    }
×
162

163
                    implTypes.Add(type);
×
164

165
                    // 4) invoke it to get the PluginBase/IPluginRegistrar
UNCOV
166
                    var metadataInstance = (TPluginInterface)ctor.Invoke(args);
×
167

168
                    // 5) let the plugin register everything it needs,
UNCOV
169
                    metadataInstance.ConfigureServices(services);
×
UNCOV
170
                }
×
UNCOV
171
            }
×
172

173
            // Dispose the temporary provider
UNCOV
174
            tempProvider.Dispose();
×
175

176
            return (services, implTypes);
×
177
        }
×
178
    }
179

180
    // --- helper for loading raw assemblies ---
181
    public class PluginAssemblyLoader
182
    {
183
        private readonly IFileSystem _fs;
184

185
        public PluginAssemblyLoader(IFileSystem fs) => _fs = fs;
×
186

187
        public IEnumerable<Assembly> LoadFromDirectory(string pluginDir)
188
        {
×
189
            var dlls = _fs.Directory.GetFiles(pluginDir, "RoboClerk.*.dll");
×
190
            foreach (var dll in dlls)
×
191
            {
×
192
                var ctx = new PluginLoadContext(dll);
×
193
                var asmName = new AssemblyName(_fs.Path.GetFileNameWithoutExtension(dll));
×
UNCOV
194
                Assembly? asm = null;
×
195
                try
UNCOV
196
                {
×
UNCOV
197
                    asm = ctx.LoadFromAssemblyName(asmName);
×
UNCOV
198
                }
×
UNCOV
199
                catch (Exception ex)
×
UNCOV
200
                {
×
UNCOV
201
                    Console.WriteLine($"Skipping plugin {dll}: {ex.Message}");
×
UNCOV
202
                }
×
UNCOV
203
                if (asm != null)
×
UNCOV
204
                    yield return asm;
×
UNCOV
205
            }
×
UNCOV
206
        }
×
207
    }
208
}
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