Skip to content

Commit 9150740

Browse files
CopilotBoBiene
andauthored
test: add sync idempotency regression coverage
Agent-Logs-Url: https://github.com/PTCInc/Kepware-ConfigAPI-SDK-dotnet/sessions/86ad7ece-bec0-429d-a50a-3d4ff8092bde Co-authored-by: BoBiene <23037659+BoBiene@users.noreply.github.com>
1 parent 4365c8e commit 9150740

4 files changed

Lines changed: 613 additions & 0 deletions

File tree

Kepware.Api.Test/Kepware.Api.Test.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
<ProjectReference Include="..\Kepware.Api\Kepware.Api.csproj" />
3030
</ItemGroup>
3131

32+
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
33+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
34+
<ProjectReference Include="..\KepwareSync.Service\Kepware.SyncService.csproj" />
35+
</ItemGroup>
36+
3237
<ItemGroup>
3338
<Using Include="Xunit" />
3439
</ItemGroup>
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
using System.IO;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using Kepware.Api.Model;
5+
using Kepware.Api.Serializer;
6+
using Kepware.Api.Util;
7+
using Microsoft.Extensions.Logging;
8+
using Moq;
9+
using Shouldly;
10+
11+
namespace Kepware.Api.Test.Serializer
12+
{
13+
public class RoundtripIdempotencyTests
14+
{
15+
private static YamlSerializer CreateYamlSerializer() =>
16+
new(Mock.Of<ILogger<YamlSerializer>>());
17+
18+
private static CsvTagSerializer CreateCsvTagSerializer() =>
19+
new(Mock.Of<ILogger<CsvTagSerializer>>());
20+
21+
private static DataTypeEnumConverterProvider CreateDataTypeConverterProvider() => new();
22+
23+
[Fact]
24+
public async Task YamlSerializer_Roundtrip_ProjectEntities_ShouldPreserveHashes()
25+
{
26+
var serializer = CreateYamlSerializer();
27+
var tempRoot = Path.Combine(Path.GetTempPath(), nameof(YamlSerializer_Roundtrip_ProjectEntities_ShouldPreserveHashes), Path.GetRandomFileName());
28+
29+
try
30+
{
31+
var project = CreateProjectEntity();
32+
var channel = CreateChannelEntity();
33+
var device = CreateDeviceEntity();
34+
var projectFile = Path.Combine(tempRoot, "project", "project.yaml");
35+
var channelFile = Path.Combine(tempRoot, channel.Name, "channel.yaml");
36+
var deviceFile = Path.Combine(tempRoot, channel.Name, device.Name, "device.yaml");
37+
38+
await serializer.SaveAsYaml(projectFile, project);
39+
await serializer.SaveAsYaml(channelFile, channel);
40+
await serializer.SaveAsYaml(deviceFile, device);
41+
42+
var savedChannelYaml = await File.ReadAllTextAsync(channelFile);
43+
var savedDeviceYaml = await File.ReadAllTextAsync(deviceFile);
44+
45+
var loadedProject = await serializer.LoadFromYaml<Project>(projectFile);
46+
var loadedChannel = await serializer.LoadFromYaml<Channel>(channelFile);
47+
var loadedDevice = await serializer.LoadFromYaml<Device>(deviceFile);
48+
49+
loadedProject.Description.ShouldBe(project.Description);
50+
loadedProject.ProjectProperties.Title.ShouldBe(project.ProjectProperties.Title);
51+
52+
loadedChannel.Hash.ShouldBe(channel.Hash);
53+
loadedChannel.Name.ShouldBe(channel.Name);
54+
loadedChannel.Description.ShouldBe(channel.Description);
55+
loadedChannel.DeviceDriver.ShouldBe(channel.DeviceDriver);
56+
57+
loadedDevice.Hash.ShouldBe(device.Hash);
58+
loadedDevice.Name.ShouldBe(device.Name);
59+
loadedDevice.Description.ShouldBe(device.Description);
60+
loadedDevice.GetDynamicProperty<string>(Properties.Channel.DeviceDriver).ShouldBe(device.GetDynamicProperty<string>(Properties.Channel.DeviceDriver));
61+
62+
await serializer.SaveAsYaml(channelFile, loadedChannel);
63+
await serializer.SaveAsYaml(deviceFile, loadedDevice);
64+
65+
(await File.ReadAllTextAsync(channelFile)).ShouldBe(savedChannelYaml);
66+
(await File.ReadAllTextAsync(deviceFile)).ShouldBe(savedDeviceYaml);
67+
}
68+
finally
69+
{
70+
if (Directory.Exists(tempRoot))
71+
{
72+
Directory.Delete(tempRoot, recursive: true);
73+
}
74+
}
75+
}
76+
77+
[Fact]
78+
public async Task CsvTagSerializer_Roundtrip_Tags_ShouldNotIntroduceHashDifferences()
79+
{
80+
var serializer = CreateCsvTagSerializer();
81+
var converter = CreateDataTypeConverterProvider().GetDataTypeEnumConverter("Simulator");
82+
var tempRoot = Path.Combine(Path.GetTempPath(), nameof(CsvTagSerializer_Roundtrip_Tags_ShouldNotIntroduceHashDifferences), Path.GetRandomFileName());
83+
var tagsFile = Path.Combine(tempRoot, "tags.csv");
84+
var secondTagsFile = Path.Combine(tempRoot, "tags-roundtrip.csv");
85+
86+
Directory.CreateDirectory(tempRoot);
87+
88+
try
89+
{
90+
var sourceTags = new DeviceTagCollection
91+
{
92+
CreateScaledTag("ScaledTag"),
93+
CreateUnscaledTag("DiscreteTag")
94+
};
95+
96+
await serializer.ExportTagsAsync(tagsFile, sourceTags.ToList(), converter);
97+
var importedTags = await serializer.ImportTagsAsync(tagsFile, converter);
98+
await serializer.ExportTagsAsync(secondTagsFile, importedTags, converter);
99+
100+
importedTags.Count.ShouldBe(sourceTags.Count);
101+
102+
foreach (var sourceTag in sourceTags)
103+
{
104+
var importedTag = importedTags.Single(tag => tag.Name == sourceTag.Name);
105+
importedTag.Hash.ShouldBe(sourceTag.Hash);
106+
importedTag.Description.ShouldBe(sourceTag.Description);
107+
importedTag.TagAddress.ShouldBe(sourceTag.TagAddress);
108+
importedTag.DataType.ShouldBe(sourceTag.DataType);
109+
importedTag.ReadWriteAccess.ShouldBe(sourceTag.ReadWriteAccess);
110+
importedTag.ScanRateMilliseconds.ShouldBe(sourceTag.ScanRateMilliseconds);
111+
importedTag.ScalingType.ShouldBe(sourceTag.ScalingType);
112+
importedTag.ScalingUnits.ShouldBe(sourceTag.ScalingUnits);
113+
importedTag.ScalingClampLow.ShouldBe(sourceTag.ScalingClampLow);
114+
importedTag.ScalingClampHigh.ShouldBe(sourceTag.ScalingClampHigh);
115+
importedTag.ScalingNegateValue.ShouldBe(sourceTag.ScalingNegateValue);
116+
}
117+
118+
var compareResult = EntityCompare.Compare<DeviceTagCollection, Tag>(sourceTags, [.. importedTags]);
119+
compareResult.ChangedItems.ShouldBeEmpty();
120+
compareResult.ItemsOnlyInLeft.ShouldBeEmpty();
121+
compareResult.ItemsOnlyInRight.ShouldBeEmpty();
122+
compareResult.UnchangedItems.Count.ShouldBe(sourceTags.Count);
123+
124+
(await File.ReadAllTextAsync(secondTagsFile)).ShouldBe(await File.ReadAllTextAsync(tagsFile));
125+
}
126+
finally
127+
{
128+
if (Directory.Exists(tempRoot))
129+
{
130+
Directory.Delete(tempRoot, recursive: true);
131+
}
132+
}
133+
}
134+
135+
private static Project CreateProjectEntity()
136+
{
137+
var project = new Project
138+
{
139+
Description = "Project description"
140+
};
141+
project.ProjectProperties.Title = "Project title";
142+
project.SetDynamicProperty(Properties.ProjectSettings.OpcDa.EnableOpcDa3, true);
143+
return project;
144+
}
145+
146+
private static Channel CreateChannelEntity()
147+
{
148+
var channel = new Channel
149+
{
150+
Name = "Channel-01",
151+
Description = "Channel description",
152+
DeviceDriver = "Simulator",
153+
DiagnosticsCapture = true,
154+
};
155+
channel.SetDynamicProperty(Properties.NonUpdatable.ChannelUniqueId, 101L);
156+
return channel;
157+
}
158+
159+
private static Device CreateDeviceEntity()
160+
{
161+
var device = new Device
162+
{
163+
Name = "Device-01",
164+
Description = "Device description",
165+
};
166+
device.SetDynamicProperty(Properties.NonUpdatable.DeviceUniqueId, 201L);
167+
device.SetDynamicProperty(Properties.Channel.DeviceDriver, "Simulator");
168+
device.SetDynamicProperty(Properties.Device.DeviceDriver, "Simulator");
169+
return device;
170+
}
171+
172+
private static Tag CreateScaledTag(string name)
173+
{
174+
var tag = new Tag
175+
{
176+
Name = name,
177+
Description = "Scaled tag"
178+
};
179+
180+
tag.TagAddress = "RAMP";
181+
tag.DataType = 8;
182+
tag.ReadWriteAccess = 1;
183+
tag.ScanRateMilliseconds = 250;
184+
tag.ScalingType = 1;
185+
tag.ScalingRawLow = 0;
186+
tag.ScalingRawHigh = 100;
187+
tag.ScalingScaledLow = 0;
188+
tag.ScalingScaledHigh = 1000;
189+
tag.ScalingScaledDataType = 8;
190+
tag.ScalingClampLow = true;
191+
tag.ScalingClampHigh = false;
192+
tag.ScalingUnits = "psi";
193+
tag.ScalingNegateValue = true;
194+
195+
return tag;
196+
}
197+
198+
private static Tag CreateUnscaledTag(string name)
199+
{
200+
var tag = new Tag
201+
{
202+
Name = name,
203+
Description = "Discrete tag"
204+
};
205+
206+
tag.TagAddress = "SWITCH";
207+
tag.DataType = 1;
208+
tag.ReadWriteAccess = 0;
209+
tag.ScanRateMilliseconds = 100;
210+
tag.ScalingType = 0;
211+
return tag;
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)