Conversation
- Create IProjectTypeDetector interface - Implement ProjectTypeDetector with BUTR template detection - Support detection of SubModule.xml, .csproj files, and BUTR markers - Add comprehensive unit tests - Register service in DI container Closes #11
There was a problem hiding this comment.
Pull request overview
Adds an auto-detection service for identifying Bannerlord mod project types (including BUTR template markers) and wires it into the app’s DI container, with unit tests validating common detection scenarios.
Changes:
- Introduce
IProjectTypeDetectorandProjectTypeDetectorwith heuristics for SubModule.xml,.csproj, and BUTR markers - Add unit tests covering the main detection paths
- Register the detector (and scanner) in the UI DI container
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| BannerlordModEditor.UI/App.axaml.cs | Registers the new detector (and scanner) in DI |
| BannerlordModEditor.Common/Services/ProjectTypeDetector.cs | Implements project-type detection and marker scanning |
| BannerlordModEditor.Common.Tests/Services/ProjectTypeDetectorTests.cs | Adds unit tests for detection behavior |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private bool CheckForButrMarkers(string projectPath) | ||
| { | ||
| var allFiles = GetAllFiles(projectPath); | ||
|
|
||
| foreach (var file in allFiles) | ||
| { | ||
| var fileName = Path.GetFileName(file); | ||
| if (ButrMarkers.Any(marker => fileName.Contains(marker, StringComparison.OrdinalIgnoreCase))) | ||
| return true; | ||
|
|
||
| var content = SafeReadFile(file); | ||
| if (content != null && content.Contains("BUTR", StringComparison.OrdinalIgnoreCase)) | ||
| return true; | ||
| } | ||
|
|
||
| var directories = Directory.GetDirectories(projectPath, "*", SearchOption.AllDirectories); | ||
| foreach (var dir in directories) | ||
| { | ||
| var dirName = new DirectoryInfo(dir).Name; | ||
| if (ButrDirectories.Any(marker => dirName.Contains(marker, StringComparison.OrdinalIgnoreCase))) | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
CheckForButrMarkers traverses the directory tree twice (once via GetAllFiles, then again via Directory.GetDirectories(..., AllDirectories)) and attempts to read the full contents of every file. This can become very slow for large repos and can cause significant IO/memory pressure. Consider a single-pass traversal (e.g., using Directory.EnumerateFileSystemEntries/EnumerateFiles + EnumerateDirectories with early-exit) that checks directory names as they’re discovered, and only reads a small, filtered subset of files (by extension and/or max size) when content scanning is needed.
| catch | ||
| { | ||
| } |
There was a problem hiding this comment.
The empty catch { } in GetAllFiles will silently hide IO/permission/path errors and may lead to non-obvious mis-detections (e.g., returning Unknown/missing markers without any diagnosability). Prefer catching specific exceptions (e.g., UnauthorizedAccessException, IOException, PathTooLongException) and either (a) surface a meaningful result (like “inconclusive”) or (b) log at debug/trace level so failures can be investigated without crashing.
| catch | |
| { | |
| } | |
| catch (UnauthorizedAccessException ex) | |
| { | |
| System.Diagnostics.Debug.WriteLine($"[ProjectTypeDetector] Access denied while enumerating directory '{directory}': {ex}"); | |
| } | |
| catch (IOException ex) | |
| { | |
| System.Diagnostics.Debug.WriteLine($"[ProjectTypeDetector] I/O error while enumerating directory '{directory}': {ex}"); | |
| } | |
| catch (PathTooLongException ex) | |
| { | |
| System.Diagnostics.Debug.WriteLine($"[ProjectTypeDetector] Path too long while enumerating directory '{directory}': {ex}"); | |
| } |
| var csprojFiles = Directory.GetFiles(projectPath, "*.csproj"); | ||
| if (csprojFiles.Any()) | ||
| { | ||
| var csprojPath = csprojFiles.First(); |
There was a problem hiding this comment.
Directory.GetFiles(...).First() is not deterministic when multiple .csproj files exist, so GetModName can return different results across machines/filesystems or as files are added. To make this stable and predictable, select deterministically (e.g., sort by name/path), and/or prefer a .csproj matching the directory name, or extract the module name from SubModule.xml when present.
| var csprojPath = csprojFiles.First(); | |
| var directoryName = new DirectoryInfo(projectPath).Name; | |
| // Prefer a .csproj whose name matches the directory name, otherwise choose deterministically. | |
| var matchingCsproj = csprojFiles.FirstOrDefault(f => | |
| string.Equals( | |
| Path.GetFileNameWithoutExtension(f), | |
| directoryName, | |
| StringComparison.OrdinalIgnoreCase)); | |
| var csprojPath = matchingCsproj ?? | |
| csprojFiles | |
| .OrderBy(f => f, StringComparer.OrdinalIgnoreCase) | |
| .First(); |
| [Fact] | ||
| public void DetectProjectType_ReturnsUnknownForNonExistentPath() | ||
| { | ||
| var result = _detector.DetectProjectType("/nonexistent/path"); |
There was a problem hiding this comment.
The hard-coded "/nonexistent/path" is OS/path-format dependent and can make intent less clear on Windows. Prefer generating a guaranteed-nonexistent path under Path.GetTempPath() (e.g., combining a GUID) to keep the test portable and explicit.
| var result = _detector.DetectProjectType("/nonexistent/path"); | |
| var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); | |
| var result = _detector.DetectProjectType(nonExistentPath); |
| var tempDir = Path.Combine(Path.GetTempPath(), "TestModProject"); | ||
| Directory.CreateDirectory(tempDir); | ||
|
|
||
| try | ||
| { | ||
| var result = _detector.GetModName(tempDir); | ||
| Assert.Equal("TestModProject", result); |
There was a problem hiding this comment.
Using a fixed temp directory name (TestModProject) risks cross-test/process collisions and can make the test flaky if the directory already exists (or contains leftover files like a .csproj). Use a unique directory name (GUID) like the other tests to ensure isolation.
| var tempDir = Path.Combine(Path.GetTempPath(), "TestModProject"); | |
| Directory.CreateDirectory(tempDir); | |
| try | |
| { | |
| var result = _detector.GetModName(tempDir); | |
| Assert.Equal("TestModProject", result); | |
| var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); | |
| Directory.CreateDirectory(tempDir); | |
| try | |
| { | |
| var result = _detector.GetModName(tempDir); | |
| Assert.Equal(Path.GetFileName(tempDir), result); |
Summary
IProjectTypeDetectorinterfaceProjectTypeDetectorwith BUTR template detectionChanges
BannerlordModEditor.Common/Services/ProjectTypeDetector.cs- ImplementationBannerlordModEditor.Common.Tests/Services/ProjectTypeDetectorTests.cs- Unit testsBannerlordModEditor.UI/App.axaml.cs- DI registrationCloses #11