Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Introduce/render observables #3925

Merged
merged 5 commits into from
Sep 13, 2024
Merged
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
28 changes: 28 additions & 0 deletions sdk/node/Libplanet.Node.Executable/BlockChainRendererTracer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Libplanet.Node.Services;

namespace Libplanet.Node.API;

internal sealed class BlockChainRendererTracer(
IRendererService rendererService, ILogger<BlockChainRendererTracer> logger)
: IHostedService
{
private readonly ILogger<BlockChainRendererTracer> _logger = logger;
private IDisposable? _observer;

public Task StartAsync(CancellationToken cancellationToken)
{
rendererService.RenderBlockEnd.Subscribe(
info => _logger.LogInformation(
"-Pattern2- #{Height} Block end: {Hash}",
info.NewTip.Index,
info.NewTip.Hash));
return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
_observer?.Dispose();
_observer = null;
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Libplanet.Node.API.Explorer;

[Options(Position)]
public sealed class ExplorerOptions : OptionsBase<ExplorerOptions>, IEnabledOptions
public sealed class ExplorerOptions : OptionsBase<ExplorerOptions>
{
public const string Position = "Explorer";

Expand Down
2 changes: 2 additions & 0 deletions sdk/node/Libplanet.Node.Executable/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Libplanet.Node.API;
using Libplanet.Node.API.Explorer;
using Libplanet.Node.API.Services;
using Libplanet.Node.Extensions;
Expand Down Expand Up @@ -27,6 +28,7 @@
builder.Services.AddGrpc();
builder.Services.AddGrpcReflection();
builder.Services.AddLibplanetNode(builder.Configuration);
builder.Services.AddHostedService<BlockChainRendererTracer>();

if (builder.IsExplorerEnabled())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public static ILibplanetNodeBuilder AddLibplanetNode(
services.AddSingleton(s => (IStoreService)s.GetRequiredService<StoreService>());
services.AddSingleton<ActionService>();
services.AddSingleton(s => (IActionService)s.GetRequiredService<ActionService>());
services.AddSingleton<RendererService>();
services.AddSingleton(s => (IRendererService)s.GetRequiredService<RendererService>());
services.AddSingleton<IBlockChainService, BlockChainService>();
services.AddSingleton<IReadChainService, ReadChainService>();
services.AddSingleton<TransactionService>();
Expand Down
34 changes: 32 additions & 2 deletions sdk/node/Libplanet.Node.Tests/BlockChainUtility.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Libplanet.Action;
using Libplanet.Blockchain;
using Libplanet.Crypto;
using Libplanet.Types.Blocks;
using Libplanet.Types.Tx;

namespace Libplanet.Node.Tests;

Expand All @@ -14,8 +16,8 @@ public static async Task<Block> AppendBlockAsync(BlockChain blockChain, PrivateK
var tip = blockChain.Tip;
var height = tip.Index + 1;
var block = blockChain.ProposeBlock(
privateKey,
blockChain.GetBlockCommit(tip.Hash));
proposer: privateKey,
lastCommit: blockChain.GetBlockCommit(tip.Hash));
blockChain.Append(
block,
blockChain.GetBlockCommit(tip.Hash),
Expand All @@ -30,4 +32,32 @@ public static async Task<Block> AppendBlockAsync(BlockChain blockChain, PrivateK

return block;
}

public static void StageTransaction(
BlockChain blockChain, IAction[] actions)
=> StageTransaction(blockChain, new PrivateKey(), actions);

public static void StageTransaction(
BlockChain blockChain, PrivateKey privateKey, IAction[] actions)
{
var transaction = CreateTransaction(blockChain, privateKey, actions);
blockChain.StageTransaction(transaction);
}

public static Transaction CreateTransaction(
BlockChain blockChain, IAction[] actions)
=> CreateTransaction(blockChain, new PrivateKey(), actions);

public static Transaction CreateTransaction(
BlockChain blockChain, PrivateKey privateKey, IAction[] actions)
{
var genesisBlock = blockChain.Genesis;
var nonce = blockChain.GetNextTxNonce(privateKey.Address);
var values = actions.Select(item => item.PlainValue).ToArray();
return Transaction.Create(
nonce: nonce,
privateKey: privateKey,
genesisHash: genesisBlock.Hash,
actions: new TxActionList(values));
}
}
36 changes: 36 additions & 0 deletions sdk/node/Libplanet.Node.Tests/DumbAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Diagnostics;
using Bencodex.Types;
using Libplanet.Action;
using Libplanet.Action.State;

namespace Libplanet.Node.Tests;

public class DumbAction : IAction
{
public string ErrorMessage { get; set; } = string.Empty;

public IValue PlainValue => Dictionary.Empty
.Add("error_message", ErrorMessage);

public void LoadPlainValue(IValue plainValue)
{
if (plainValue is Dictionary dictionary)
{
ErrorMessage = (Text)dictionary["error_message"];
}
else
{
throw new UnreachableException("The plain value of DumbAction must be a dictionary.");
}
}

public IWorld Execute(IActionContext context)
{
if (ErrorMessage != string.Empty)
{
throw new InvalidOperationException(ErrorMessage);
}

return context.PreviousState;
}
}
21 changes: 21 additions & 0 deletions sdk/node/Libplanet.Node.Tests/DumbActionLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Bencodex.Types;
using Libplanet.Action;
using Libplanet.Action.Loader;
using Libplanet.Action.Sys;

namespace Libplanet.Node.Tests;

public sealed class DumbActionLoader : IActionLoader
{
public IAction LoadAction(long index, IValue value)
{
if (Registry.IsSystemAction(value))
{
return Registry.Deserialize(value);
}

var action = new DumbAction();
action.LoadPlainValue(value);
return action;
}
}
16 changes: 0 additions & 16 deletions sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,4 @@ public void Create_Test()

Assert.Equal(1, blockChain.Count);
}

[Fact]
public async Task BlockAppended_TestAsync()
{
var serviceProvider = TestUtility.CreateServiceProvider();
var blockChainService = serviceProvider.GetRequiredService<IBlockChainService>();
var blockChain = blockChainService.BlockChain;

var args = await Assert.RaisesAsync<BlockEventArgs>(
handler => blockChainService.BlockAppended += handler,
handler => blockChainService.BlockAppended -= handler,
async () => await BlockChainUtility.AppendBlockAsync(blockChain));

Assert.Equal(args.Arguments.Block, blockChain.Tip);
Assert.Equal(2, blockChain.Count);
}
}
108 changes: 108 additions & 0 deletions sdk/node/Libplanet.Node.Tests/Services/RendererServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Libplanet.Action;
using Libplanet.Node.Options;
using Libplanet.Node.Services;
using Microsoft.Extensions.DependencyInjection;

namespace Libplanet.Node.Tests.Services;

public class RendererServiceTest
{
[Fact]
public async Task RenderBlock_TestAsync()
{
var serviceProvider = TestUtility.CreateServiceProvider();
var blockChainService = serviceProvider.GetRequiredService<IBlockChainService>();
var rendererService = serviceProvider.GetRequiredService<IRendererService>();
var blockChain = blockChainService.BlockChain;

using var observer = new TestObserver<RenderBlockInfo>(rendererService.RenderBlock);
await Assert.RaisesAnyAsync<RenderBlockInfo>(
attach: handler => observer.Next += handler,
detach: handler => observer.Next -= handler,
testCode: async () => await BlockChainUtility.AppendBlockAsync(blockChain));
}

[Fact]
public async Task RenderAction_TestAsync()
{
var settings = new Dictionary<string, string?>
{
[$"{ActionOptions.Position}:{nameof(ActionOptions.ModulePath)}"]
= typeof(DumbActionLoader).Assembly.Location,
[$"{ActionOptions.Position}:{nameof(ActionOptions.ActionLoaderType)}"]
= typeof(DumbActionLoader).FullName,
};

var serviceProvider = TestUtility.CreateServiceProvider(settings);
var blockChainService = serviceProvider.GetRequiredService<IBlockChainService>();
var rendererService = serviceProvider.GetRequiredService<IRendererService>();
var blockChain = blockChainService.BlockChain;

var actions = new IAction[]
{
new DumbAction(),
new DumbAction(),
new DumbAction(),
};

using var observer = new TestObserver<RenderActionInfo>(rendererService.RenderAction);
await Assert.RaisesAnyAsync<RenderActionInfo>(
attach: handler => observer.Next += handler,
detach: handler => observer.Next -= handler,
testCode: async () =>
{
BlockChainUtility.StageTransaction(blockChain, actions);
await BlockChainUtility.AppendBlockAsync(blockChain);
});
}

[Fact]
public async Task RenderActionError_TestAsync()
{
var settings = new Dictionary<string, string?>
{
[$"{ActionOptions.Position}:{nameof(ActionOptions.ModulePath)}"]
= typeof(DumbActionLoader).Assembly.Location,
[$"{ActionOptions.Position}:{nameof(ActionOptions.ActionLoaderType)}"]
= typeof(DumbActionLoader).FullName,
};

var serviceProvider = TestUtility.CreateServiceProvider(settings);
var blockChainService = serviceProvider.GetRequiredService<IBlockChainService>();
var rendererService = serviceProvider.GetRequiredService<IRendererService>();
var blockChain = blockChainService.BlockChain;
var errorMessage = "123";

var actions = new IAction[]
{
new DumbAction() { ErrorMessage = errorMessage },
};

using var observer = new TestObserver<RenderActionErrorInfo>(
rendererService.RenderActionError);
var errorInfo = await Assert.RaisesAnyAsync<RenderActionErrorInfo>(
attach: handler => observer.Next += handler,
detach: handler => observer.Next -= handler,
testCode: async () =>
{
BlockChainUtility.StageTransaction(blockChain, actions);
await BlockChainUtility.AppendBlockAsync(blockChain);
});
Assert.Equal(errorMessage, errorInfo.Arguments.Exception.InnerException!.Message);
}

[Fact]
public async Task RenderBlockEnd_TestAsync()
{
var serviceProvider = TestUtility.CreateServiceProvider();
var blockChainService = serviceProvider.GetRequiredService<IBlockChainService>();
var rendererService = serviceProvider.GetRequiredService<IRendererService>();
var blockChain = blockChainService.BlockChain;

using var observer = new TestObserver<RenderBlockInfo>(rendererService.RenderBlockEnd);
await Assert.RaisesAnyAsync<RenderBlockInfo>(
attach: handler => observer.Next += handler,
detach: handler => observer.Next -= handler,
testCode: async () => await BlockChainUtility.AppendBlockAsync(blockChain));
}
}
15 changes: 9 additions & 6 deletions sdk/node/Libplanet.Node.Tests/Services/SwarmServiceTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Libplanet.Node.Options;
using Libplanet.Node.Services;
using Microsoft.Extensions.DependencyInjection;
using R3;

namespace Libplanet.Node.Tests.Services;

Expand Down Expand Up @@ -33,9 +34,10 @@ public async Task Start_TestAsync()
var swarmService = serviceProvider.GetRequiredService<ISwarmService>();
var swarmServiceHost = serviceProvider.GetRequiredService<SwarmService>();

await Assert.RaisesAnyAsync(
handler => swarmService.Started += handler,
handler => swarmService.Started -= handler,
using var observer = new TestObserver<Unit>(swarmService.Started);
await Assert.RaisesAnyAsync<Unit>(
attach: handler => observer.Next += handler,
detach: handler => observer.Next -= handler,
async () => await swarmServiceHost.StartAsync(default));
Assert.True(swarmService.IsRunning);
}
Expand All @@ -60,9 +62,10 @@ public async Task Stop_TestAsync()
var swarmServiceHost = serviceProvider.GetRequiredService<SwarmService>();
await swarmServiceHost.StartAsync(default);

await Assert.RaisesAnyAsync(
handler => swarmService.Stopped += handler,
handler => swarmService.Stopped -= handler,
using var observer = new TestObserver<Unit>(swarmService.Stopped);
await Assert.RaisesAnyAsync<Unit>(
attach: handler => observer.Next += handler,
detach: handler => observer.Next -= handler,
async () => await swarmServiceHost.StopAsync(default));
Assert.False(swarmService.IsRunning);
}
Expand Down
32 changes: 32 additions & 0 deletions sdk/node/Libplanet.Node.Tests/TestObserver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Libplanet.Node.Tests;

internal sealed class TestObserver<T> : IObserver<T>, IDisposable
{
private IDisposable? _subscription;

public TestObserver(IObservable<T> observable)
{
_subscription = observable.Subscribe(this);
}

public event EventHandler? Completed;

public event EventHandler? Error;

public event EventHandler<T>? Next;

public void Dispose()
{
if (_subscription is not null)
{
_subscription.Dispose();
_subscription = null;
}
}

void IObserver<T>.OnCompleted() => Completed?.Invoke(this, EventArgs.Empty);

void IObserver<T>.OnError(Exception error) => Error?.Invoke(this, EventArgs.Empty);

void IObserver<T>.OnNext(T value) => Next?.Invoke(this, value);
}
Loading
Loading