Skip to content

Commit 5990ac4

Browse files
authored
Merge pull request #70 from PTCInc/feat_update_project_load
Feat update project load
2 parents a2a67de + 779b1d6 commit 5990ac4

51 files changed

Lines changed: 9494 additions & 208 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Kepware-ConfigAPI-SDK-dotnet.sln

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 18
4-
VisualStudioVersion = 18.3.11505.172 d18.3
4+
VisualStudioVersion = 18.3.11505.172
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kepware.SyncService", "KepwareSync.Service\Kepware.SyncService.csproj", "{19B2841F-01D1-4A7E-BE26-A93CF8C57628}"
77
EndProject
@@ -13,7 +13,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFiles", "SolutionFi
1313
Directory.Build.props = Directory.Build.props
1414
LICENSE.txt = LICENSE.txt
1515
README.md = README.md
16-
version.json = version.json
1716
EndProjectSection
1817
EndProject
1918
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kepware.Api", "Kepware.Api\Kepware.Api.csproj", "{ABB368B9-7D95-4946-9579-F7AA7A5750F7}"
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Reflection;
5+
using System.Threading.Tasks;
6+
using Kepware.Api.Model;
7+
using Kepware.Api.Test.ApiClient;
8+
using Microsoft.Extensions.Logging;
9+
using Moq;
10+
using Moq.Contrib.HttpClient;
11+
using Xunit;
12+
13+
namespace Kepware.Api.Test.ApiClient
14+
{
15+
public class GenericHandler : TestApiClientBase
16+
{
17+
[Fact]
18+
public void AppendQueryString_PrivateMethod_EncodesAndSkipsNullsAndAppendsCorrectly()
19+
{
20+
// Arrange
21+
var method = typeof(Kepware.Api.ClientHandler.GenericApiHandler)
22+
.GetMethod("AppendQueryString", BindingFlags.NonPublic | BindingFlags.Static);
23+
Assert.NotNull(method);
24+
25+
var query = new[]
26+
{
27+
new KeyValuePair<string, string?>("a", "b"),
28+
new KeyValuePair<string, string?>("space", "x y"),
29+
new KeyValuePair<string, string?>("skip", null) // should be skipped
30+
};
31+
32+
// Act
33+
var result1 = (string)method!.Invoke(null, new object[] { "https://api/config", query })!;
34+
var result2 = (string)method!.Invoke(null, new object[] { "https://api/config?existing=1", query })!;
35+
36+
// Assert
37+
Assert.Equal("https://api/config?a=b&space=x%20y", result1);
38+
Assert.Equal("https://api/config?existing=1&a=b&space=x%20y", result2);
39+
}
40+
41+
[Fact]
42+
public async Task LoadCollectionAsync_AppendsQueryAndReturnsCollection()
43+
{
44+
// Arrange
45+
var channelsJson = """
46+
[
47+
{
48+
"PROJECT_ID": 676550906,
49+
"common.ALLTYPES_NAME": "Channel1",
50+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Channel",
51+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator"
52+
},
53+
{
54+
"PROJECT_ID": 676550906,
55+
"common.ALLTYPES_NAME": "Data Type Examples",
56+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Channel",
57+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator"
58+
}
59+
]
60+
""";
61+
62+
var query = new[]
63+
{
64+
new KeyValuePair<string, string?>("status", "active"),
65+
new KeyValuePair<string, string?>("name", "John Doe"),
66+
new KeyValuePair<string, string?>("skip", null)
67+
};
68+
69+
// Expect encoded space in "John Doe" and null entry skipped
70+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels?status=active&name=John%20Doe")
71+
.ReturnsResponse(channelsJson, "application/json");
72+
73+
// Act
74+
var result = await _kepwareApiClient.GenericConfig.LoadCollectionAsync<ChannelCollection, Channel>((string?)null, query);
75+
76+
// Assert
77+
Assert.NotNull(result);
78+
Assert.Equal(2, result.Count);
79+
Assert.Contains(result, c => c.Name == "Channel1");
80+
Assert.Contains(result, c => c.Name == "Data Type Examples");
81+
}
82+
83+
[Fact]
84+
public async Task LoadEntityAsync_AppendsQueryAndReturnsEntity()
85+
{
86+
// Arrange
87+
var channelJson = """
88+
{
89+
"PROJECT_ID": 676550906,
90+
"common.ALLTYPES_NAME": "Channel1",
91+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Channel",
92+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator"
93+
}
94+
""";
95+
96+
var query = new[]
97+
{
98+
new KeyValuePair<string, string?>("status", "active"),
99+
new KeyValuePair<string, string?>("name", "John Doe"),
100+
new KeyValuePair<string, string?>("skip", null)
101+
};
102+
103+
// Expect encoded space in "John Doe" and null entry skipped
104+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?status=active&name=John%20Doe")
105+
.ReturnsResponse(channelJson, "application/json");
106+
107+
// Act
108+
var result = await _kepwareApiClient.GenericConfig.LoadEntityAsync<Channel>("Channel1", query);
109+
110+
// Assert
111+
Assert.NotNull(result);
112+
Assert.Equal("Channel1", result.Name);
113+
Assert.Equal("Example Simulator Channel", result.Description);
114+
}
115+
}
116+
}
File renamed without changes.

Kepware.Api.Test/ApiClient/LoadEntity.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ public async Task LoadEntityAsync_ShouldThrowInvalidOperationException_WhenLoadR
379379

380380
#endregion
381381

382-
#region LoadEntityAsync - Single Tag mit DynamicProperties
382+
#region LoadEntityAsync - Single Tag with DynamicProperties
383383

384384
[Fact]
385385
public async Task LoadEntityAsync_ShouldReturnTag_WithCorrectDynamicProperties()

Kepware.Api.Test/ApiClient/ProjectLoadTests.cs

Lines changed: 90 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,92 +15,120 @@
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
{
2122
public class ProjectLoadTests : TestApiClientBase
2223
{
2324

24-
//private async Task ConfigureToServeEndpoints()
25-
//{
26-
// var projectData = await LoadJsonTestDataAsync();
27-
28-
// var channels = projectData.Project?.Channels?.Select(c => new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }).ToList() ?? [];
29-
30-
// // Serve project details
31-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project")
32-
// .ReturnsResponse(JsonSerializer.Serialize(new Project { Description = projectData?.Project?.Description, DynamicProperties = projectData?.Project?.DynamicProperties ?? [] }), "application/json");
33-
34-
// // Serve channels without nested devices
35-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels")
36-
// .ReturnsResponse(JsonSerializer.Serialize(channels), "application/json");
25+
[Theory]
26+
[InlineData("KEPServerEX", "12", 6, 17, true)]
27+
[InlineData("KEPServerEX", "12", 6, 16, false)]
28+
[InlineData("ThingWorxKepwareEdge", "13", 1, 10, true)]
29+
[InlineData("ThingWorxKepwareEdge", "13", 1, 9, false)]
30+
[InlineData("UnknownProduct", "99", 10, 0, false)]
31+
public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport(
32+
string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad)
33+
{
34+
// This test will validate that the LoadProjectAsync method correctly loads the project structure and
35+
// content based on whether the connected server version supports JsonProjectLoad. It will compare the loaded project against expected test data to ensure accuracy.
36+
// For servers that support JsonProjectLoad, the test will configure the mock server to serve a full JSON project
37+
// and validate that the loaded project matches the test data exactly.
3738

38-
// foreach (var channel in projectData?.Project?.Channels ?? [])
39-
// {
40-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}")
41-
// .ReturnsResponse(JsonSerializer.Serialize(new Channel { Name = channel.Name, Description = channel.Description, DynamicProperties = channel.DynamicProperties }), "application/json");
39+
// Arrange
40+
ConfigureConnectedClient(productName, productId, majorVersion, minorVersion);
4241

43-
// if (channel.Devices != null)
44-
// {
45-
// var devices = channel.Devices.Select(d => new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }).ToList();
46-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices")
47-
// .ReturnsResponse(JsonSerializer.Serialize(devices), "application/json");
42+
if (supportsJsonLoad)
43+
{
44+
await ConfigureToServeFullProject();
45+
}
46+
else
47+
{
48+
await ConfigureToServeEndpoints();
49+
}
4850

49-
// foreach (var device in channel.Devices)
50-
// {
51-
// var deviceEndpoint = TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}";
52-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint)
53-
// .ReturnsResponse(JsonSerializer.Serialize(new Device { Name = device.Name, Description = device.Description, DynamicProperties = device.DynamicProperties }), "application/json");
51+
// Act
52+
var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true);
5453

54+
// Assert
55+
project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad);
5556

56-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint + "/tags")
57-
// .ReturnsResponse(JsonSerializer.Serialize(device.Tags), "application/json");
57+
project.ShouldNotBeNull();
58+
project.Channels.ShouldNotBeEmpty("Channels list should not be empty.");
5859

59-
// ConfigureToServeEndpointsTagGroupsRecursive(deviceEndpoint, device.TagGroups ?? []);
60-
// }
61-
// }
62-
// }
63-
//}
60+
var testProject = await LoadJsonTestDataAsync();
61+
var compareResult = EntityCompare.Compare<ChannelCollection, Channel>(testProject?.Project?.Channels, project?.Channels);
6462

65-
//private void ConfigureToServeEndpointsTagGroupsRecursive(string endpoint, IEnumerable<DeviceTagGroup> tagGroups)
66-
//{
67-
// var tagGroupEndpoint = endpoint + "/tag_groups";
63+
compareResult.ShouldNotBeNull();
64+
compareResult.UnchangedItems.ShouldNotBeEmpty("All channels should be unchanged.");
65+
compareResult.ChangedItems.ShouldBeEmpty("No channels should be changed.");
66+
compareResult.ItemsOnlyInLeft.ShouldBeEmpty("No channels should exist only in the test data.");
67+
compareResult.ItemsOnlyInRight.ShouldBeEmpty("No channels should exist only in the loaded project.");
6868

69-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, tagGroupEndpoint)
70-
// .ReturnsResponse(JsonSerializer.Serialize(tagGroups), "application/json");
69+
foreach (var (ExpectedChannel, LoadedChannel) in testProject?.Project?.Channels?.Zip(project?.Channels ?? []) ?? [])
70+
{
71+
var deviceCompareResult = EntityCompare.Compare<DeviceCollection, Device>(ExpectedChannel.Devices, LoadedChannel.Devices);
72+
deviceCompareResult.ShouldNotBeNull();
73+
deviceCompareResult.UnchangedItems.ShouldNotBeEmpty($"All devices in channel {ExpectedChannel.Name} should be unchanged.");
74+
deviceCompareResult.ChangedItems.ShouldBeEmpty($"No devices in channel {ExpectedChannel.Name} should be changed.");
75+
deviceCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No devices should exist only in the test data for channel {ExpectedChannel.Name}.");
76+
deviceCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No devices should exist only in the loaded project for channel {ExpectedChannel.Name}.");
7177

72-
// foreach (var tagGroup in tagGroups)
73-
// {
74-
// _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, string.Concat(tagGroupEndpoint, "/", tagGroup.Name, "/tags"))
75-
// .ReturnsResponse(JsonSerializer.Serialize(tagGroup.Tags), "application/json");
78+
foreach (var (ExpectedDevice, LoadedDevice) in ExpectedChannel.Devices?.Zip(LoadedChannel.Devices ?? []) ?? [])
79+
{
80+
if (ExpectedDevice.Tags?.Count > 0 || LoadedDevice.Tags?.Count > 0)
81+
{
82+
var tagCompareResult = EntityCompare.Compare<DeviceTagCollection, Tag>(ExpectedDevice.Tags, LoadedDevice.Tags);
83+
tagCompareResult.ShouldNotBeNull();
84+
tagCompareResult.UnchangedItems.ShouldNotBeEmpty($"All tags in device {ExpectedDevice.Name} should be unchanged.");
85+
tagCompareResult.ChangedItems.ShouldBeEmpty($"No tags in device {ExpectedDevice.Name} should be changed.");
86+
tagCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No tags should exist only in the test data for device {ExpectedDevice.Name}.");
87+
tagCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No tags should exist only in the loaded project for device {ExpectedDevice.Name}.");
88+
}
7689

77-
// ConfigureToServeEndpointsTagGroupsRecursive(string.Concat(tagGroupEndpoint, "/", tagGroup.Name), tagGroup.TagGroups ?? []);
78-
// }
79-
//}
90+
CompareTagGroupsRecursive(ExpectedDevice.TagGroups, LoadedDevice.TagGroups, ExpectedDevice.Name);
91+
}
92+
}
93+
}
8094

8195
[Theory]
8296
[InlineData("KEPServerEX", "12", 6, 17, true)]
83-
[InlineData("KEPServerEX", "12", 6, 16, false)]
97+
[InlineData("ThingWorxKepwareServer", "12", 6, 17, true)]
8498
[InlineData("ThingWorxKepwareEdge", "13", 1, 10, true)]
85-
[InlineData("ThingWorxKepwareEdge", "13", 1, 9, false)]
86-
[InlineData("UnknownProduct", "99", 10, 0, false)]
87-
public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport(
99+
[InlineData("Kepware Edge", "13", 1, 0, true)]
100+
public async Task LoadProject_ShouldLoadCorrectly_Serialize_BasedOnProductSupport(
88101
string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad)
89102
{
103+
// This test will validate that the LoadProjectAsync method correctly loads the project structure using the optimized recursion method.
104+
// It will compare the loaded project against expected test data to ensure accuracy. The test will configure the mock server to serve
105+
// endpoints to support an optimized recursion load and validate that the loaded project matches the test data exactly.
106+
107+
// Arrange
90108
ConfigureConnectedClient(productName, productId, majorVersion, minorVersion);
91109

92110
if (supportsJsonLoad)
93111
{
94-
await ConfigureToServeFullProject();
112+
await ConfigureToServeEndpoints();
95113
}
96114
else
97115
{
98-
await ConfigureToServeEndpoints();
116+
// Skip this test case at runtime because it expects the server to serve a full JSON project.
117+
throw SkipException.ForSkip($"Product {productName} v{majorVersion}.{minorVersion} (id={productId}) does not support JSON project load. Skipping full-project test case.");
99118
}
100119

101-
var project = await _kepwareApiClient.Project.LoadProject(true);
120+
// Override the tag limit to ensure that we are testing the optimized recursion and selectively load objects based on the tag limit.
121+
// See _data/simdemo_en.json and json chunks in _data/projectLoadSerializeData for data that is served by the mock server for this test.
122+
var tagLimitOverride = 100;
102123

103-
project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad);
124+
125+
// Act
126+
var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true, projectLoadTagLimit: tagLimitOverride);
127+
128+
129+
// Assert
130+
// Optimized recursion is done for this test, which will result in false.
131+
project.IsLoadedByProjectLoadService.ShouldBeFalse();
104132

105133
project.ShouldNotBeNull();
106134
project.Channels.ShouldNotBeEmpty("Channels list should not be empty.");
@@ -138,6 +166,12 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport(
138166
CompareTagGroupsRecursive(ExpectedDevice.TagGroups, LoadedDevice.TagGroups, ExpectedDevice.Name);
139167
}
140168
}
169+
170+
// Verify expected number of calls to the project load endpoints to ensure that the optimized recursion is selectively loading objects based on the tag limit.
171+
foreach (var uri in _optimizedRecursionUris)
172+
{
173+
_httpMessageHandlerMock.VerifyRequest(HttpMethod.Get, uri);
174+
}
141175
}
142176

143177
private static void CompareTagGroupsRecursive(DeviceTagGroupCollection? expected, DeviceTagGroupCollection? actual, string parentName)
@@ -182,7 +216,7 @@ public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport(
182216

183217
await ConfigureToServeEndpoints();
184218

185-
var project = await _kepwareApiClient.Project.LoadProject(blnLoadFullProject: false);
219+
var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: false);
186220

187221
project.ShouldNotBeNull();
188222
project.Channels.ShouldBeNull("Channels list should be null.");
@@ -201,7 +235,7 @@ public async Task LoadProject_ShouldReturnEmptyProject_WhenHttpRequestFails()
201235
.ThrowsAsync(new HttpRequestException());
202236

203237
// Act
204-
var project = await _kepwareApiClient.Project.LoadProject(true);
238+
var project = await _kepwareApiClient.Project.LoadProjectAsync(true);
205239

206240
// Assert
207241
project.ShouldNotBeNull();
File renamed without changes.

0 commit comments

Comments
 (0)