Skip to content

Commit

Permalink
Merge pull request planetarium#3925 from planetarium/introduce/render…
Browse files Browse the repository at this point in the history
…-observables

✨  Introduce/render observables
  • Loading branch information
riemannulus authored Sep 13, 2024
2 parents acd0bce + 586f0d5 commit ab474be
Show file tree
Hide file tree
Showing 31 changed files with 487 additions and 148 deletions.
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

0 comments on commit ab474be

Please sign in to comment.