-
Notifications
You must be signed in to change notification settings - Fork 1
feat(#9): Tpac file parser service #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Threading.Tasks; | ||
| using Xunit; | ||
| using BannerlordModEditor.Common.Services; | ||
|
|
||
| namespace BannerlordModEditor.Common.Tests.Services | ||
| { | ||
| public class TpacParserServiceTests | ||
| { | ||
| private readonly TpacParserService _service; | ||
|
|
||
| public TpacParserServiceTests() | ||
| { | ||
| _service = new TpacParserService(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void IsTpacFile_ReturnsTrueForTpacExtension() | ||
| { | ||
| Assert.True(_service.IsTpacFile("test.tpac")); | ||
| Assert.True(_service.IsTpacFile("path/to/file.TPAC")); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void IsTpacFile_ReturnsFalseForNonTpacExtension() | ||
| { | ||
| Assert.False(_service.IsTpacFile("test.xml")); | ||
| Assert.False(_service.IsTpacFile("test.txt")); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseTpacFile_ReturnsInvalidForNonExistentFile() | ||
| { | ||
| var result = _service.ParseTpacFile("/nonexistent/file.tpac"); | ||
| Assert.False(result.IsValid); | ||
| Assert.Equal("File not found", result.ErrorMessage); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ParseTpacFileAsync_ReturnsInvalidForNonExistentFile() | ||
| { | ||
| var result = await _service.ParseTpacFileAsync("/nonexistent/file.tpac"); | ||
| Assert.False(result.IsValid); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseTpacFile_ExtractsModelsFromValidFile() | ||
| { | ||
| var tempFile = Path.GetTempFileName(); | ||
| var content = @"model_id: 12345 | ||
| model_name: TestModel | ||
| model_type: Character | ||
| property1: value1 | ||
| property2: value2 | ||
| model_id: 67890 | ||
| model_name: AnotherModel | ||
| model_type: Item"; | ||
|
|
||
| try | ||
| { | ||
| File.WriteAllText(tempFile, content); | ||
|
|
||
| var result = _service.ParseTpacFile(tempFile); | ||
|
|
||
| Assert.True(result.IsValid); | ||
| Assert.Equal(2, result.Models.Count); | ||
| Assert.Equal("12345", result.Models[0].ModelId); | ||
| Assert.Equal("TestModel", result.Models[0].ModelName); | ||
| Assert.Equal("Character", result.Models[0].ModelType); | ||
| Assert.Equal("67890", result.Models[1].ModelId); | ||
| } | ||
| finally | ||
| { | ||
| File.Delete(tempFile); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ParseTpacFileAsync_ExtractsModelsFromValidFile() | ||
| { | ||
| var tempFile = Path.GetTempFileName(); | ||
| var content = @"model_id: 99999 | ||
| model_name: AsyncModel | ||
| model_type: Weapon"; | ||
|
Comment on lines
+82
to
+85
|
||
|
|
||
| try | ||
| { | ||
| File.WriteAllText(tempFile, content); | ||
|
|
||
| var result = await _service.ParseTpacFileAsync(tempFile); | ||
|
|
||
| Assert.True(result.IsValid); | ||
| Assert.Single(result.Models); | ||
| Assert.Equal("99999", result.Models[0].ModelId); | ||
| Assert.Equal("AsyncModel", result.Models[0].ModelName); | ||
| } | ||
| finally | ||
| { | ||
| File.Delete(tempFile); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ExtractModels_ReturnsEmptyForNonExistentFile() | ||
| { | ||
| var result = _service.ExtractModels("/nonexistent/file.tpac"); | ||
| Assert.Empty(result); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.IO; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Linq; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace BannerlordModEditor.Common.Services | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class TpacModelInfo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string ModelId { get; set; } = string.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string ModelName { get; set; } = string.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string ModelType { get; set; } = string.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Dictionary<string, string> Properties { get; set; } = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class TpacFileInfo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string FilePath { get; set; } = string.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string FileName { get; set; } = string.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public List<TpacModelInfo> Models { get; set; } = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool IsValid { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string? ErrorMessage { get; set; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public interface ITpacParserService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Task<TpacFileInfo> ParseTpacFileAsync(string filePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TpacFileInfo ParseTpacFile(string filePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<TpacModelInfo> ExtractModels(string tpacFilePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool IsTpacFile(string filePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class TpacParserService : ITpacParserService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const string TpacExtension = ".tpac"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task<TpacFileInfo> ParseTpacFileAsync(string filePath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await Task.Run(() => ParseTpacFile(filePath)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+40
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task<TpacFileInfo> ParseTpacFileAsync(string filePath) | |
| { | |
| return await Task.Run(() => ParseTpacFile(filePath)); | |
| public Task<TpacFileInfo> ParseTpacFileAsync(string filePath) | |
| { | |
| return Task.FromResult(ParseTpacFile(filePath)); |
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The behavior for invalid extensions (setting IsValid = false and the specific error message) isn’t covered by tests. Add a unit test that writes valid .tpac-formatted content to a non-.tpac filename (or passes such a path) and asserts IsValid is false and ErrorMessage equals "Not a valid .tpac file".
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Property extraction into TpacModelInfo.Properties is a core part of the parser, but the tests currently don’t assert that properties are captured correctly. Add assertions in ParseTpacFile_ExtractsModelsFromValidFile to verify property1/property2 exist with expected values on the first model.
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExtractModels swallows all exceptions and returns whatever it has collected (often an empty list). Because ParseTpacFile treats “no exception” as success, an IO/parsing failure can incorrectly produce IsValid = true with Models = []. Remove the empty catch and let exceptions propagate to ParseTpacFile (so it can set IsValid = false), or catch and rethrow a more specific exception that preserves the original as an inner exception.
| try | |
| { | |
| var content = File.ReadAllText(tpacFilePath); | |
| var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries); | |
| var currentModel = new TpacModelInfo(); | |
| foreach (var line in lines) | |
| { | |
| var trimmed = line.Trim(); | |
| if (trimmed.StartsWith("model_id:", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| if (!string.IsNullOrEmpty(currentModel.ModelId)) | |
| { | |
| models.Add(currentModel); | |
| } | |
| currentModel = new TpacModelInfo | |
| { | |
| ModelId = trimmed["model_id:".Length..].Trim() | |
| }; | |
| } | |
| else if (trimmed.StartsWith("model_name:", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| currentModel.ModelName = trimmed["model_name:".Length..].Trim(); | |
| } | |
| else if (trimmed.StartsWith("model_type:", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| currentModel.ModelType = trimmed["model_type:".Length..].Trim(); | |
| } | |
| else if (trimmed.Contains(':')) | |
| { | |
| var parts = trimmed.Split(':', 2); | |
| if (parts.Length == 2) | |
| { | |
| currentModel.Properties[parts[0].Trim()] = parts[1].Trim(); | |
| } | |
| } | |
| } | |
| if (!string.IsNullOrEmpty(currentModel.ModelId)) | |
| { | |
| models.Add(currentModel); | |
| } | |
| } | |
| catch | |
| { | |
| var content = File.ReadAllText(tpacFilePath); | |
| var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries); | |
| var currentModel = new TpacModelInfo(); | |
| foreach (var line in lines) | |
| { | |
| var trimmed = line.Trim(); | |
| if (trimmed.StartsWith("model_id:", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| if (!string.IsNullOrEmpty(currentModel.ModelId)) | |
| { | |
| models.Add(currentModel); | |
| } | |
| currentModel = new TpacModelInfo | |
| { | |
| ModelId = trimmed["model_id:".Length..].Trim() | |
| }; | |
| } | |
| else if (trimmed.StartsWith("model_name:", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| currentModel.ModelName = trimmed["model_name:".Length..].Trim(); | |
| } | |
| else if (trimmed.StartsWith("model_type:", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| currentModel.ModelType = trimmed["model_type:".Length..].Trim(); | |
| } | |
| else if (trimmed.Contains(':')) | |
| { | |
| var parts = trimmed.Split(':', 2); | |
| if (parts.Length == 2) | |
| { | |
| currentModel.Properties[parts[0].Trim()] = parts[1].Trim(); | |
| } | |
| } | |
| } | |
| if (!string.IsNullOrEmpty(currentModel.ModelId)) | |
| { | |
| models.Add(currentModel); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Path.GetTempFileName()creates a file with a.tmpextension, butParseTpacFilerejects non-.tpacextensions viaIsTpacFile. This test will fail by returningIsValid = falsewith "Not a valid .tpac file". Create the temp path with a.tpacextension (e.g.,Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".tpac")) and write to that path instead.