Skip to content

Commit e27b4fc

Browse files
committed
feat(api): Added ProjectLoadTagLimit as a configurable property for controlling optimized recursion load behavior.
1 parent d022f55 commit e27b4fc

12 files changed

Lines changed: 1582 additions & 78 deletions

File tree

Kepware.Api.Test/ApiClient/ProjectLoadTests.cs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Kepware.Api.Test.ApiClient;
1616
using Kepware.Api.Util;
1717
using Shouldly;
18+
using Xunit.Sdk;
1819

1920
namespace Kepware.Api.Test.ApiClient
2021
{
@@ -98,7 +99,7 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport(
9899
await ConfigureToServeEndpoints();
99100
}
100101

101-
var project = await _kepwareApiClient.Project.LoadProjectAsync(true);
102+
var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true);
102103

103104
project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad);
104105

@@ -140,6 +141,71 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport(
140141
}
141142
}
142143

144+
[Theory]
145+
[InlineData("KEPServerEX", "12", 6, 17, true)]
146+
[InlineData("ThingWorxKepwareServer", "12", 6, 17, true)]
147+
[InlineData("ThingWorxKepwareEdge", "13", 1, 10, true)]
148+
[InlineData("Kepware Edge", "13", 1, 0, true)]
149+
public async Task LoadProject_ShouldLoadCorrectly_Serialize_BasedOnProductSupport(
150+
string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad)
151+
{
152+
ConfigureConnectedClient(productName, productId, majorVersion, minorVersion);
153+
154+
if (supportsJsonLoad)
155+
{
156+
await ConfigureToServeEndpoints();
157+
}
158+
else
159+
{
160+
// Skip this test case at runtime because it expects the server to serve a full JSON project.
161+
throw SkipException.ForSkip($"Product {productName} v{majorVersion}.{minorVersion} (id={productId}) does not support JSON project load. Skipping full-project test case.");
162+
}
163+
164+
var tagLimitOverride = 100; // Set a high tag limit to ensure all tags are loaded for comparison
165+
166+
var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true, projectLoadTagLimit: tagLimitOverride);
167+
168+
// Optimized recursion is done for this test, which will result in false.
169+
project.IsLoadedByProjectLoadService.ShouldBeFalse();
170+
171+
project.ShouldNotBeNull();
172+
project.Channels.ShouldNotBeEmpty("Channels list should not be empty.");
173+
174+
var testProject = await LoadJsonTestDataAsync();
175+
var compareResult = EntityCompare.Compare<ChannelCollection, Channel>(testProject?.Project?.Channels, project?.Channels);
176+
177+
compareResult.ShouldNotBeNull();
178+
compareResult.UnchangedItems.ShouldNotBeEmpty("All channels should be unchanged.");
179+
compareResult.ChangedItems.ShouldBeEmpty("No channels should be changed.");
180+
compareResult.ItemsOnlyInLeft.ShouldBeEmpty("No channels should exist only in the test data.");
181+
compareResult.ItemsOnlyInRight.ShouldBeEmpty("No channels should exist only in the loaded project.");
182+
183+
foreach (var (ExpectedChannel, LoadedChannel) in testProject?.Project?.Channels?.Zip(project?.Channels ?? []) ?? [])
184+
{
185+
var deviceCompareResult = EntityCompare.Compare<DeviceCollection, Device>(ExpectedChannel.Devices, LoadedChannel.Devices);
186+
deviceCompareResult.ShouldNotBeNull();
187+
deviceCompareResult.UnchangedItems.ShouldNotBeEmpty($"All devices in channel {ExpectedChannel.Name} should be unchanged.");
188+
deviceCompareResult.ChangedItems.ShouldBeEmpty($"No devices in channel {ExpectedChannel.Name} should be changed.");
189+
deviceCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No devices should exist only in the test data for channel {ExpectedChannel.Name}.");
190+
deviceCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No devices should exist only in the loaded project for channel {ExpectedChannel.Name}.");
191+
192+
foreach (var (ExpectedDevice, LoadedDevice) in ExpectedChannel.Devices?.Zip(LoadedChannel.Devices ?? []) ?? [])
193+
{
194+
if (ExpectedDevice.Tags?.Count > 0 || LoadedDevice.Tags?.Count > 0)
195+
{
196+
var tagCompareResult = EntityCompare.Compare<DeviceTagCollection, Tag>(ExpectedDevice.Tags, LoadedDevice.Tags);
197+
tagCompareResult.ShouldNotBeNull();
198+
tagCompareResult.UnchangedItems.ShouldNotBeEmpty($"All tags in device {ExpectedDevice.Name} should be unchanged.");
199+
tagCompareResult.ChangedItems.ShouldBeEmpty($"No tags in device {ExpectedDevice.Name} should be changed.");
200+
tagCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No tags should exist only in the test data for device {ExpectedDevice.Name}.");
201+
tagCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No tags should exist only in the loaded project for device {ExpectedDevice.Name}.");
202+
}
203+
204+
CompareTagGroupsRecursive(ExpectedDevice.TagGroups, LoadedDevice.TagGroups, ExpectedDevice.Name);
205+
}
206+
}
207+
}
208+
143209
private static void CompareTagGroupsRecursive(DeviceTagGroupCollection? expected, DeviceTagGroupCollection? actual, string parentName)
144210
{
145211
if ((expected?.Count ?? 0) == 0 && (actual?.Count ?? 0) == 0)

Kepware.Api.Test/ApiClient/_TestApiClientBase.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,21 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_
146146
{
147147
var projectData = await LoadJsonTestDataAsync(filePath);
148148

149-
var channels = projectData.Project?.Channels?.Select(c => new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }).ToList() ?? [];
149+
150+
var channels = projectData.Project?.Channels?
151+
.Select(c =>
152+
{
153+
var ch = new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties };
154+
int staticCount = c.Name switch
155+
{
156+
"Channel1" => 2,
157+
"Simulation Examples" => 24,
158+
"Data Type Examples" => 216,
159+
_ => 0
160+
};
161+
ch.SetDynamicProperty(Properties.Channel.StaticTagCount, staticCount);
162+
return ch;
163+
}).ToList() ?? new List<Channel>();
150164

151165
// Serve project details
152166
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project")
@@ -163,7 +177,21 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_
163177

164178
if (channel.Devices != null)
165179
{
166-
var devices = channel.Devices.Select(d => new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }).ToList();
180+
var devices = channel.Devices
181+
.Select(d =>
182+
{
183+
var dev = new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties };
184+
int staticCount = d.Name switch
185+
{
186+
"Device1" => 2,
187+
"Functions" => 24,
188+
"16 Bit Device" => 98,
189+
"8 Bit Device" => 118,
190+
_ => 0
191+
};
192+
dev.SetDynamicProperty(Properties.Device.StaticTagCount, staticCount);
193+
return dev;
194+
}).ToList() ?? new List<Device>();
167195
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices")
168196
.ReturnsResponse(JsonSerializer.Serialize(devices), "application/json");
169197

@@ -181,6 +209,22 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_
181209
}
182210
}
183211
}
212+
213+
// Additional endpoints for content=serialize mocking
214+
var projectPropertiesString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/projectProperties.json");
215+
var channel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/channel1.json");
216+
var sixteenBitDeviceString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dataTypeExamples.16BitDevice.json");
217+
var simExamplesChannelString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/simulationExamples.json");
218+
219+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project")
220+
.ReturnsResponse(projectPropertiesString, "application/json");
221+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?content=serialize")
222+
.ReturnsResponse(channel1String, "application/json");
223+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/16 Bit Device?content=serialize")
224+
.ReturnsResponse(sixteenBitDeviceString, "application/json");
225+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Simulation Examples?content=serialize")
226+
.ReturnsResponse(simExamplesChannelString, "application/json");
227+
184228
}
185229
private void ConfigureToServeEndpointsTagGroupsRecursive(string endpoint, IEnumerable<DeviceTagGroup> tagGroups)
186230
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</ItemGroup>
3535

3636
<ItemGroup>
37-
<None Update="_data/*.*">
37+
<None Update="_data/**">
3838
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3939
</None>
4040
</ItemGroup>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"channels": {
3+
"common.ALLTYPES_NAME": "Channel1",
4+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Channel",
5+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator",
6+
"servermain.CHANNEL_DIAGNOSTICS_CAPTURE": false,
7+
"servermain.CHANNEL_UNIQUE_ID": 1704486747,
8+
"servermain.CHANNEL_WRITE_OPTIMIZATIONS_METHOD": 2,
9+
"servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10,
10+
"servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0,
11+
"simulator.CHANNEL_ITEM_PERSISTENCE": false,
12+
"simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Channel1.dat",
13+
"devices": [
14+
{
15+
"common.ALLTYPES_NAME": "Device1",
16+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Device",
17+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator",
18+
"servermain.DEVICE_MODEL": 0,
19+
"servermain.DEVICE_UNIQUE_ID": 1808204482,
20+
"servermain.DEVICE_ID_FORMAT": 1,
21+
"servermain.DEVICE_ID_STRING": "1",
22+
"servermain.DEVICE_ID_HEXADECIMAL": 1,
23+
"servermain.DEVICE_ID_DECIMAL": 1,
24+
"servermain.DEVICE_ID_OCTAL": 1,
25+
"servermain.DEVICE_DATA_COLLECTION": true,
26+
"servermain.DEVICE_SCAN_MODE": 0,
27+
"servermain.DEVICE_SCAN_MODE_RATE_MS": 1000,
28+
"servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false,
29+
"tags": [
30+
{
31+
"common.ALLTYPES_NAME": "Tag1",
32+
"common.ALLTYPES_DESCRIPTION": "Ramping Read/Write tag used to verify client connection",
33+
"servermain.TAG_ADDRESS": "R0001",
34+
"servermain.TAG_DATA_TYPE": 5,
35+
"servermain.TAG_READ_WRITE_ACCESS": 1,
36+
"servermain.TAG_SCAN_RATE_MILLISECONDS": 100,
37+
"servermain.TAG_SCALING_TYPE": 0
38+
},
39+
{
40+
"common.ALLTYPES_NAME": "Tag2",
41+
"common.ALLTYPES_DESCRIPTION": "Constant Read/Write tag used to verify client connection",
42+
"servermain.TAG_ADDRESS": "K0001",
43+
"servermain.TAG_DATA_TYPE": 5,
44+
"servermain.TAG_READ_WRITE_ACCESS": 1,
45+
"servermain.TAG_SCAN_RATE_MILLISECONDS": 100,
46+
"servermain.TAG_SCALING_TYPE": 0
47+
}
48+
]
49+
}
50+
]
51+
}
52+
}

0 commit comments

Comments
 (0)