Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Xunit;
using BannerlordModEditor.Common.Models.SubModule;

namespace BannerlordModEditor.Common.Tests.Models.SubModule
{
public class SubModuleLoaderTests
{
private readonly SubModuleLoader _loader;

public SubModuleLoaderTests()
{
_loader = new SubModuleLoader();
}

[Fact]
public void Load_ReturnsNullForNonExistentFile()
{
var result = _loader.Load("/nonexistent/path/SubModule.xml");
Assert.Null(result);
}

[Fact]
public async Task LoadAsync_ReturnsNullForNonExistentFile()
{
var result = await _loader.LoadAsync("/nonexistent/path/SubModule.xml");
Comment on lines +21 to +28
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a hard-coded absolute path like /nonexistent/path/... can be brittle across OSes/environments (and in rare cases could exist or be accessible). Prefer generating a guaranteed-nonexistent path under the temp directory (e.g., Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml")) so the test remains deterministic.

Suggested change
var result = _loader.Load("/nonexistent/path/SubModule.xml");
Assert.Null(result);
}
[Fact]
public async Task LoadAsync_ReturnsNullForNonExistentFile()
{
var result = await _loader.LoadAsync("/nonexistent/path/SubModule.xml");
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = _loader.Load(nonExistentPath);
Assert.Null(result);
}
[Fact]
public async Task LoadAsync_ReturnsNullForNonExistentFile()
{
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = await _loader.LoadAsync(nonExistentPath);

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +28
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a hard-coded absolute path like /nonexistent/path/... can be brittle across OSes/environments (and in rare cases could exist or be accessible). Prefer generating a guaranteed-nonexistent path under the temp directory (e.g., Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml")) so the test remains deterministic.

Suggested change
var result = _loader.Load("/nonexistent/path/SubModule.xml");
Assert.Null(result);
}
[Fact]
public async Task LoadAsync_ReturnsNullForNonExistentFile()
{
var result = await _loader.LoadAsync("/nonexistent/path/SubModule.xml");
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = _loader.Load(nonExistentPath);
Assert.Null(result);
}
[Fact]
public async Task LoadAsync_ReturnsNullForNonExistentFile()
{
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = await _loader.LoadAsync(nonExistentPath);

Copilot uses AI. Check for mistakes.
Assert.Null(result);
}

[Fact]
public void Load_ReadsValidSubModuleXml()
{
var tempFile = Path.GetTempFileName();
var xml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<Module>
<Name>Test Mod</Name>
<Id>TestMod</Id>
<Version>v1.0.0</Version>
<SingleplayerModule>true</SingleplayerModule>
<MultiplayerModule>false</MultiplayerModule>
<DependedModules>
<Module Id=""Native""/>
</DependedModules>
<SubModules>
<SubModule>
<Name>TestSubModule</Name>
<DLLName>TestMod.dll</DLLName>
<SubModuleClassType>TestMod.SubModule</SubModuleClassType>
<IsOptional>false</IsOptional>
<IsTicked>true</IsTicked>
<Tags>
<Tag Key=""DedicatedServerType"" Value=""none""/>
</Tags>
</SubModule>
</SubModules>
</Module>";

try
{
File.WriteAllText(tempFile, xml);

var result = _loader.Load(tempFile);

Assert.NotNull(result);
Assert.Equal("Test Mod", result.Name);
Assert.Equal("TestMod", result.Id);
Assert.Equal("v1.0.0", result.Version);
Assert.True(result.SingleplayerModule);
Assert.False(result.MultiplayerModule);
Assert.Single(result.DependedModules);
Assert.Equal("Native", result.DependedModules[0].Id);
Assert.Single(result.SubModules);
Assert.Equal("TestSubModule", result.SubModules[0].Name);
Assert.Equal("TestMod.dll", result.SubModules[0].DLLName);
Assert.Single(result.SubModules[0].Tags);
Assert.Equal("DedicatedServerType", result.SubModules[0].Tags[0].Key);
Assert.Equal("none", result.SubModules[0].Tags[0].Value);
}
finally
{
File.Delete(tempFile);
}
}

[Fact]
public void Save_CreatesValidXml()
{
var tempFile = Path.GetTempFileName();
var data = new SubModuleDO
{
Name = "Save Test Mod",
Id = "SaveTestMod",
Version = "v2.0.0",
SingleplayerModule = true,
MultiplayerModule = false,
DependedModules = new()
{
new DependedModuleDO { Id = "Native" },
new DependedModuleDO { Id = "Sandbox" }
},
SubModules = new()
{
new SubModuleItemDO
{
Name = "SaveTestSubModule",
DLLName = "SaveTestMod.dll",
SubModuleClassType = "SaveTestMod.SubModule",
IsOptional = false,
IsTicked = true,
Tags = new()
{
new SubModuleTagDO { Key = "DedicatedServerType", Value = "none" }
}
}
}
};

try
{
_loader.Save(data, tempFile);

Assert.True(File.Exists(tempFile));
var loadedData = _loader.Load(tempFile);
Assert.NotNull(loadedData);
Assert.Equal("Save Test Mod", loadedData.Name);
Assert.Equal("SaveTestMod", loadedData.Id);
Assert.Equal(2, loadedData.DependedModules.Count);
Assert.Single(loadedData.SubModules);
}
finally
{
File.Delete(tempFile);
}
}

[Fact]
public async Task SaveAsync_CreatesValidXml()
{
var tempFile = Path.GetTempFileName();
var data = _loader.CreateDefault("Async Test Mod", "AsyncTestMod");

try
{
await _loader.SaveAsync(data, tempFile);

Assert.True(File.Exists(tempFile));
var loadedData = await _loader.LoadAsync(tempFile);
Assert.NotNull(loadedData);
Assert.Equal("Async Test Mod", loadedData.Name);
Assert.Equal("AsyncTestMod", loadedData.Id);
}
finally
{
File.Delete(tempFile);
}
}

[Fact]
public void CreateDefault_CreatesValidDefault()
{
var result = _loader.CreateDefault("Default Test", "DefaultTest");

Assert.Equal("Default Test", result.Name);
Assert.Equal("DefaultTest", result.Id);
Assert.Equal("v1.0.0", result.Version);
Assert.True(result.SingleplayerModule);
Assert.False(result.MultiplayerModule);
Assert.Empty(result.DependedModules);
Assert.Empty(result.SubModules);
Assert.Empty(result.Xmls);
Assert.Empty(result.OptionalDependedModules);
}
}
}
96 changes: 96 additions & 0 deletions BannerlordModEditor.Common/Models/SubModule/SubModuleDO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Xml.Serialization;

namespace BannerlordModEditor.Common.Models.SubModule
{
[XmlRoot("Module")]
public class SubModuleDO
{
[XmlElement("Name")]
public string Name { get; set; } = string.Empty;

[XmlElement("Id")]
public string Id { get; set; } = string.Empty;

[XmlElement("Version")]
public string Version { get; set; } = string.Empty;

[XmlElement("SingleplayerModule")]
public bool SingleplayerModule { get; set; } = true;

[XmlElement("MultiplayerModule")]
public bool MultiplayerModule { get; set; }

[XmlArray("DependedModules")]
[XmlArrayItem("Module")]
public List<DependedModuleDO> DependedModules { get; set; } = new();

[XmlArray("SubModules")]
[XmlArrayItem("SubModule")]
public List<SubModuleItemDO> SubModules { get; set; } = new();

[XmlArray("Xmls")]
[XmlArrayItem("XmlNode")]
public List<XmlNodeDO> Xmls { get; set; } = new();

[XmlArray("OptionalDependedModules")]
[XmlArrayItem("Module")]
public List<OptionalDependedModuleDO> OptionalDependedModules { get; set; } = new();
}

public class DependedModuleDO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;
}

public class OptionalDependedModuleDO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;
}

public class SubModuleItemDO
{
[XmlElement("Name")]
public string Name { get; set; } = string.Empty;

[XmlElement("DLLName")]
public string DLLName { get; set; } = string.Empty;

[XmlElement("SubModuleClassType")]
public string SubModuleClassType { get; set; } = string.Empty;

[XmlElement("IsOptional")]
public bool IsOptional { get; set; }

[XmlElement("IsTicked")]
public bool IsTicked { get; set; } = true;

[XmlArray("Tags")]
[XmlArrayItem("Tag")]
public List<SubModuleTagDO> Tags { get; set; } = new();
}

public class SubModuleTagDO
{
[XmlAttribute("Key")]
public string Key { get; set; } = string.Empty;

[XmlAttribute("Value")]
public string Value { get; set; } = string.Empty;
}

public class XmlNodeDO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;

[XmlAttribute("Type")]
public string Type { get; set; } = string.Empty;

[XmlText]
Comment on lines +7 to +93
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DO types are decorated with XML serialization attributes even though serialization is performed via SubModuleDTO + XmlSerializer(typeof(SubModuleDTO)). This duplicates the schema across DO/DTO and increases the risk of them drifting out of sync. Consider either (a) removing XML attributes from the DO classes (keep DTO as the serialization contract), or (b) consolidating to a single XML model to eliminate the mapper if separation isn’t needed.

Suggested change
[XmlRoot("Module")]
public class SubModuleDO
{
[XmlElement("Name")]
public string Name { get; set; } = string.Empty;
[XmlElement("Id")]
public string Id { get; set; } = string.Empty;
[XmlElement("Version")]
public string Version { get; set; } = string.Empty;
[XmlElement("SingleplayerModule")]
public bool SingleplayerModule { get; set; } = true;
[XmlElement("MultiplayerModule")]
public bool MultiplayerModule { get; set; }
[XmlArray("DependedModules")]
[XmlArrayItem("Module")]
public List<DependedModuleDO> DependedModules { get; set; } = new();
[XmlArray("SubModules")]
[XmlArrayItem("SubModule")]
public List<SubModuleItemDO> SubModules { get; set; } = new();
[XmlArray("Xmls")]
[XmlArrayItem("XmlNode")]
public List<XmlNodeDO> Xmls { get; set; } = new();
[XmlArray("OptionalDependedModules")]
[XmlArrayItem("Module")]
public List<OptionalDependedModuleDO> OptionalDependedModules { get; set; } = new();
}
public class DependedModuleDO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;
}
public class OptionalDependedModuleDO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;
}
public class SubModuleItemDO
{
[XmlElement("Name")]
public string Name { get; set; } = string.Empty;
[XmlElement("DLLName")]
public string DLLName { get; set; } = string.Empty;
[XmlElement("SubModuleClassType")]
public string SubModuleClassType { get; set; } = string.Empty;
[XmlElement("IsOptional")]
public bool IsOptional { get; set; }
[XmlElement("IsTicked")]
public bool IsTicked { get; set; } = true;
[XmlArray("Tags")]
[XmlArrayItem("Tag")]
public List<SubModuleTagDO> Tags { get; set; } = new();
}
public class SubModuleTagDO
{
[XmlAttribute("Key")]
public string Key { get; set; } = string.Empty;
[XmlAttribute("Value")]
public string Value { get; set; } = string.Empty;
}
public class XmlNodeDO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;
[XmlAttribute("Type")]
public string Type { get; set; } = string.Empty;
[XmlText]
public class SubModuleDO
{
public string Name { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public bool SingleplayerModule { get; set; } = true;
public bool MultiplayerModule { get; set; }
public List<DependedModuleDO> DependedModules { get; set; } = new();
public List<SubModuleItemDO> SubModules { get; set; } = new();
public List<XmlNodeDO> Xmls { get; set; } = new();
public List<OptionalDependedModuleDO> OptionalDependedModules { get; set; } = new();
}
public class DependedModuleDO
{
public string Id { get; set; } = string.Empty;
}
public class OptionalDependedModuleDO
{
public string Id { get; set; } = string.Empty;
}
public class SubModuleItemDO
{
public string Name { get; set; } = string.Empty;
public string DLLName { get; set; } = string.Empty;
public string SubModuleClassType { get; set; } = string.Empty;
public bool IsOptional { get; set; }
public bool IsTicked { get; set; } = true;
public List<SubModuleTagDO> Tags { get; set; } = new();
}
public class SubModuleTagDO
{
public string Key { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
public class XmlNodeDO
{
public string Id { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;

Copilot uses AI. Check for mistakes.
public string Path { get; set; } = string.Empty;
}
}
96 changes: 96 additions & 0 deletions BannerlordModEditor.Common/Models/SubModule/SubModuleDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Xml.Serialization;

namespace BannerlordModEditor.Common.Models.SubModule
{
[XmlRoot("Module")]
public class SubModuleDTO
{
[XmlElement("Name")]
public string Name { get; set; } = string.Empty;

[XmlElement("Id")]
public string Id { get; set; } = string.Empty;

[XmlElement("Version")]
public string Version { get; set; } = string.Empty;

[XmlElement("SingleplayerModule")]
public bool SingleplayerModule { get; set; } = true;

[XmlElement("MultiplayerModule")]
public bool MultiplayerModule { get; set; }

[XmlArray("DependedModules")]
[XmlArrayItem("Module")]
public List<DependedModuleDTO> DependedModules { get; set; } = new();

[XmlArray("SubModules")]
[XmlArrayItem("SubModule")]
public List<SubModuleItemDTO> SubModules { get; set; } = new();

[XmlArray("Xmls")]
[XmlArrayItem("XmlNode")]
public List<XmlNodeDTO> Xmls { get; set; } = new();

[XmlArray("OptionalDependedModules")]
[XmlArrayItem("Module")]
public List<OptionalDependedModuleDTO> OptionalDependedModules { get; set; } = new();
}

public class DependedModuleDTO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;
}

public class OptionalDependedModuleDTO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;
}

public class SubModuleItemDTO
{
[XmlElement("Name")]
public string Name { get; set; } = string.Empty;

[XmlElement("DLLName")]
public string DLLName { get; set; } = string.Empty;

[XmlElement("SubModuleClassType")]
public string SubModuleClassType { get; set; } = string.Empty;

[XmlElement("IsOptional")]
public bool IsOptional { get; set; }

[XmlElement("IsTicked")]
public bool IsTicked { get; set; } = true;

[XmlArray("Tags")]
[XmlArrayItem("Tag")]
public List<SubModuleTagDTO> Tags { get; set; } = new();
}

public class SubModuleTagDTO
{
[XmlAttribute("Key")]
public string Key { get; set; } = string.Empty;

[XmlAttribute("Value")]
public string Value { get; set; } = string.Empty;
}

public class XmlNodeDTO
{
[XmlAttribute("Id")]
public string Id { get; set; } = string.Empty;

[XmlAttribute("Type")]
public string Type { get; set; } = string.Empty;

[XmlText]
public string Path { get; set; } = string.Empty;
}
}
Loading
Loading