Skip to content

Commit

Permalink
PocketIC integration (#136)
Browse files Browse the repository at this point in the history
* Adding pocketic

* Copy binaries to output

* Fixing

* Adding line test

* Client

* Update EdjCase.ICP.PocketIC.csproj

* Update PocketIcClient.cs

* Update PocketIcClient.cs

* Removing lf enforcement

* Rename folder

* Fixing tests

* a

* Tests work

* Fixing tests

* Removing dead code

* Adding missing doc

* Adding PocketIC Tests and .net8

* Adding to solution and XUnit

* Adding tests

* Adding more methods

* IPocketIcHttpClient

* Adding tests

* Adding routes

* HttpGateway

* Auto progress time

* Documenting PocketIc.cs

* IPocketIcHttpClient.cs docs

* PocketIcHttpClient.cs docs

* PocketIcServer + Request/Response models docs

* Docs

* Misc Candid/PocketIC tweaks and tests

* Updating README for pocketic

* Fixing Async
  • Loading branch information
Gekctek authored Nov 5, 2024
1 parent d729cd8 commit 7117394
Show file tree
Hide file tree
Showing 45 changed files with 5,391 additions and 34 deletions.
1 change: 0 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
- uses: actions/checkout@v2

- name: Setup Dotnet
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x

- name: Pack Candid
run: dotnet pack src/Candid/EdjCase.ICP.Candid.csproj --configuration Release /p:Version=${{ github.event.release.tag_name }} --output . --include-symbols --include-source
Expand All @@ -33,6 +33,12 @@ jobs:
- name: Push Agent
run: dotnet nuget push EdjCase.ICP.Agent.${{ github.event.release.tag_name }}.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json

- name: Pack Agent
run: dotnet pack src/PocketIC/EdjCase.ICP.PocketIC.csproj --configuration Release /p:Version=${{ github.event.release.tag_name }} --output . --include-symbols --include-source

- name: Push PocketIC
run: dotnet nuget push EdjCase.ICP.PocketIC.${{ github.event.release.tag_name }}.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json

- name: Pack WebSockets
run: dotnet pack src/WebSockets/EdjCase.ICP.WebSockets.csproj --configuration Release /p:Version=${{ github.event.release.tag_name }} --output . --include-symbols --include-source

Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/Sample/bin/Debug/net6.0/Sample.dll",
"program": "${workspaceFolder}/src/Sample/bin/Debug/net8.0/Sample.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Sample",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
Expand Down
22 changes: 21 additions & 1 deletion ICP.sln
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.WebSockets", "sample
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSockets.Tests", "test\WebSockets.Tests\WebSockets.Tests.csproj", "{3015AFBC-B866-459F-B25C-4BEA00C2A91E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLS.Tests", "test\BLS.Tests\BLS.Tests.csproj", "{213F30BA-D147-4291-93A3-13A8A006126D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLS.Tests", "test\BLS.Tests\BLS.Tests.csproj", "{213F30BA-D147-4291-93A3-13A8A006126D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdjCase.ICP.PocketIC", "src\PocketIC\EdjCase.ICP.PocketIC.csproj", "{051EE789-4283-48DA-81EF-4B8ADFD406D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.PocketIC", "samples\Sample.PocketIC\Sample.PocketIC.csproj", "{E674253B-7B51-40D0-9F3D-805007174D31}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PocketIC.Tests", "test\PocketIC.Tests\PocketIC.Tests.csproj", "{02D1DDA8-7A9A-4355-BE95-DE1720D56055}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -117,6 +123,18 @@ Global
{213F30BA-D147-4291-93A3-13A8A006126D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{213F30BA-D147-4291-93A3-13A8A006126D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{213F30BA-D147-4291-93A3-13A8A006126D}.Release|Any CPU.Build.0 = Release|Any CPU
{051EE789-4283-48DA-81EF-4B8ADFD406D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{051EE789-4283-48DA-81EF-4B8ADFD406D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{051EE789-4283-48DA-81EF-4B8ADFD406D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{051EE789-4283-48DA-81EF-4B8ADFD406D0}.Release|Any CPU.Build.0 = Release|Any CPU
{E674253B-7B51-40D0-9F3D-805007174D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E674253B-7B51-40D0-9F3D-805007174D31}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E674253B-7B51-40D0-9F3D-805007174D31}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E674253B-7B51-40D0-9F3D-805007174D31}.Release|Any CPU.Build.0 = Release|Any CPU
{02D1DDA8-7A9A-4355-BE95-DE1720D56055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{02D1DDA8-7A9A-4355-BE95-DE1720D56055}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02D1DDA8-7A9A-4355-BE95-DE1720D56055}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02D1DDA8-7A9A-4355-BE95-DE1720D56055}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -133,6 +151,8 @@ Global
{B4F9E828-BC3D-4EFF-9E21-53DD82928AB0} = {7FADA9D9-5FDA-4CFB-8A53-A578A61FBBA9}
{3015AFBC-B866-459F-B25C-4BEA00C2A91E} = {F71B8320-C279-4A79-A8D4-4039DB39D522}
{213F30BA-D147-4291-93A3-13A8A006126D} = {F71B8320-C279-4A79-A8D4-4039DB39D522}
{E674253B-7B51-40D0-9F3D-805007174D31} = {7FADA9D9-5FDA-4CFB-8A53-A578A61FBBA9}
{02D1DDA8-7A9A-4355-BE95-DE1720D56055} = {F71B8320-C279-4A79-A8D4-4039DB39D522}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3103E11A-E792-49EE-98C5-B2F3709DB088}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/Sample.CLI/Sample.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>ICP.Sample.CLI</RootNamespace>
Expand Down
Binary file not shown.
153 changes: 153 additions & 0 deletions samples/Sample.PocketIC/PocketIc.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System.Net;
using EdjCase.ICP.Agent.Agents;
using EdjCase.ICP.Agent.Responses;
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.PocketIC;
using EdjCase.ICP.PocketIC.Client;
using EdjCase.ICP.PocketIC.Models;
using Newtonsoft.Json;
using Org.BouncyCastle.Asn1.Cms;
using Xunit;

namespace Sample.PocketIC
{
public class PocketIcServerFixture : IDisposable
{
public PocketIcServer Server { get; private set; }

public PocketIcServerFixture()
{
// Start the server for all tests
this.Server = PocketIcServer.Start().GetAwaiter().GetResult();
}

public void Dispose()
{
// Stop the server after all tests
if (this.Server != null)
{
this.Server.StopAsync().GetAwaiter().GetResult();
this.Server.DisposeAsync().GetAwaiter().GetResult();
}
}
}

public class PocketIcTests : IClassFixture<PocketIcServerFixture>
{
private readonly PocketIcServerFixture fixture;
private string url => this.fixture.Server.GetUrl();

public PocketIcTests(PocketIcServerFixture fixture)
{
this.fixture = fixture;
}

[Fact]
public async Task UpdateCallAsync_CounterWasm__Basic__Valid()
{
byte[] wasmModule = File.ReadAllBytes("CanisterWasmModules/counter.wasm");
CandidArg arg = CandidArg.FromCandid();

// Create new pocketic instance for test, then dispose it
await using (PocketIc pocketIc = await PocketIc.CreateAsync(this.url))
{
Principal canisterId = await pocketIc.CreateAndInstallCanisterAsync(wasmModule, arg);

UnboundedUInt value = await pocketIc.QueryCallAsync<UnboundedUInt>(
Principal.Anonymous(),
canisterId,
"get"
);
Assert.Equal((UnboundedUInt)0, value);


await pocketIc.UpdateCallNoResponseAsync(
Principal.Anonymous(),
canisterId,
"inc"
);

value = await pocketIc.QueryCallAsync<UnboundedUInt>(
Principal.Anonymous(),
canisterId,
"get"
);
Assert.Equal((UnboundedUInt)1, value);

await pocketIc.UpdateCallNoResponseAsync(
Principal.Anonymous(),
canisterId,
"set",
(UnboundedUInt)10
);

value = await pocketIc.QueryCallAsync<UnboundedUInt>(
Principal.Anonymous(),
canisterId,
"get"
);

Assert.Equal((UnboundedUInt)10, value);
}
}


[Fact]
public async Task HttpGateway_CounterWasm__Basic__Valid()
{
byte[] wasmModule = File.ReadAllBytes("CanisterWasmModules/counter.wasm");
CandidArg arg = CandidArg.FromCandid();


SubnetConfig nnsSubnet = SubnetConfig.New(); // NNS subnet required for HttpGateway

await using (PocketIc pocketIc = await PocketIc.CreateAsync(this.url, nnsSubnet: nnsSubnet))
{
Principal canisterId = await pocketIc.CreateAndInstallCanisterAsync(wasmModule, arg);

await pocketIc.StartCanisterAsync(canisterId);

// Let time progress so that update calls get processed
await using (await pocketIc.AutoProgressTimeAsync())
{
await using (HttpGateway httpGateway = await pocketIc.RunHttpGatewayAsync())
{
HttpAgent agent = httpGateway.BuildHttpAgent();
QueryResponse getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty());
CandidArg getResponseArg = getResponse.ThrowOrGetReply();
UnboundedUInt getResponseValue = getResponseArg.ToObjects<UnboundedUInt>();
Assert.Equal((UnboundedUInt)0, getResponseValue);


CancellationTokenSource cts = new(TimeSpan.FromSeconds(5));
CandidArg incResponseArg = await agent.CallAndWaitAsync(canisterId, "inc", CandidArg.Empty(), cancellationToken: cts.Token);
Assert.Equal(CandidArg.Empty(), incResponseArg);

// This alternative also doesnt work
// RequestId requestId = await agent.CallAsync(canisterId, "inc", CandidArg.Empty());
// ICTimestamp currentTime = await pocketIc.GetTimeAsync();
// await pocketIc.SetTimeAsync(currentTime + TimeSpan.FromSeconds(5));
// await pocketIc.TickAsync(5);
// CandidArg incResponseArg = await agent.WaitForRequestAsync(canisterId, requestId);
// Assert.Equal(CandidArg.Empty(), incResponseArg);

getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty());
getResponseArg = getResponse.ThrowOrGetReply();
getResponseValue = getResponseArg.ToObjects<UnboundedUInt>();
Assert.Equal((UnboundedUInt)1, getResponseValue);

CandidArg setRequestArg = CandidArg.FromObjects((UnboundedUInt)10);
CandidArg setResponseArg = await agent.CallAndWaitAsync(canisterId, "set", setRequestArg);
Assert.Equal(CandidArg.Empty(), setResponseArg);

getResponse = await agent.QueryAsync(canisterId, "get", CandidArg.Empty());
getResponseArg = getResponse.ThrowOrGetReply();
getResponseValue = getResponseArg.ToObjects<UnboundedUInt>();
Assert.Equal((UnboundedUInt)10, getResponseValue);

}
}
}
}
}
}
39 changes: 39 additions & 0 deletions samples/Sample.PocketIC/Sample.PocketIC.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.11.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeRefactoring.Testing.XUnit" Version="1.1.2" />
</ItemGroup>

<ItemGroup>
<Content Include="CanisterWasmModules/*.wasm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\PocketIC\EdjCase.ICP.PocketIC.csproj" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion samples/Sample.RestAPI/Sample.RestAPI.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/Sample.WebSockets/Sample.WebSockets.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
14 changes: 13 additions & 1 deletion src/Agent/API.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions src/Agent/Agents/IAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public static class IAgentExtensions
/// <param name="arg">The candid arg to send with the request</param>
/// <param name="effectiveCanisterId">Optional. Specifies the relevant canister id if calling the root canister</param>
/// <param name="cancellationToken">Optional. Token to cancel request</param>
/// <returns>The id of the request that can be used to look up its status with `GetRequestStatusAsync`</returns>
/// <returns>The raw candid arg response</returns>
public static async Task<CandidArg> CallAndWaitAsync(
this IAgent agent,
Principal canisterId,
Expand All @@ -100,12 +100,31 @@ public static async Task<CandidArg> CallAndWaitAsync(
CancellationToken? cancellationToken = null)
{
RequestId id = await agent.CallAsync(canisterId, method, arg, effectiveCanisterId);
return await agent.WaitForRequestAsync(canisterId, id, cancellationToken);
}

/// <summary>
/// Waits for a request to be processed and returns the candid response to the call. This is a helper
/// method built on top of `GetRequestStatusAsync` to wait for the response so it doesn't need to be
/// implemented manually
/// </summary>
/// <param name="agent">The agent to use for the call</param>
/// <param name="canisterId">Canister to read state for</param>
/// <param name="requestId">The unique identifier for the request</param>
/// <param name="cancellationToken">Optional. Token to cancel request</param>
/// <returns>The raw candid arg response</returns>
public static async Task<CandidArg> WaitForRequestAsync(
this IAgent agent,
Principal canisterId,
RequestId requestId,
CancellationToken? cancellationToken = null
)
{
while (true)
{
cancellationToken?.ThrowIfCancellationRequested();

RequestStatus? requestStatus = await agent.GetRequestStatusAsync(canisterId, id);
RequestStatus? requestStatus = await agent.GetRequestStatusAsync(canisterId, requestId);

cancellationToken?.ThrowIfCancellationRequested();

Expand Down
Loading

0 comments on commit 7117394

Please sign in to comment.