Skip to content
Open
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
11 changes: 11 additions & 0 deletions examples/CSharpDev/CSharpDev.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<RootNamespace>CSharpDev</RootNamespace>
</PropertyGroup>

<ItemGroup>
Expand All @@ -18,4 +19,14 @@
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
121 changes: 121 additions & 0 deletions examples/CSharpDev/XUnit/PluginStatsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using NBomber.Contracts;
using NBomber.Contracts.Stats;
using NBomber.CSharp;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace CSharpDev.XUnit
{
public class Plugin1 : IWorkerPlugin
{
public string PluginName => "Plugin1";

public Task Init(IBaseContext context, IConfiguration infraConfig) => Task.CompletedTask;

public Task Start() => Task.CompletedTask;

public Task<DataSet> GetStats(OperationType currentOperation)
{
var pluginStats = new DataSet();
pluginStats.Tables.Add(CreateTable("PluginStats", 5));
return Task.FromResult(pluginStats);
}

public string[] GetHints() => Array.Empty<string>();

public Task Stop() => Task.CompletedTask;

public void Dispose()
{
}

public static string TryGetValueForKey(string key, DataSet pluginStats)
{
var table = pluginStats?.Tables["PluginStats"];
var row = table?.Rows.Cast<DataRow>().FirstOrDefault(r => r["Key"].ToString() == key);
return row?["Value"].ToString();
}

private IEnumerable<DataColumn> CreateTableCols()
{
yield return new DataColumn("Key", Type.GetType("System.String"))
{
Caption = "Key"
};

yield return new DataColumn("Value", Type.GetType("System.String"))
{
Caption = "Value"
};
}

private IEnumerable<DataRow> CreateTableRows(DataTable table, int count)
{
for (var i = 1; i <= count; i++)
{
var row = table.NewRow();
row["Key"] = $"Key{i}";
row["Value"] = $"Value{i}";
yield return row;
}
}

private DataTable CreateTable(string tableName, int rowsCount)
{
var table = new DataTable(tableName);
table.Columns.AddRange(CreateTableCols().ToArray());
var rows = CreateTableRows(table, rowsCount).ToArray();

foreach (var row in rows)
{
table.Rows.Add(row);
}

return table;
}
}

public class PluginStatsTest
{
// in this example we use:
// - XUnit (https://xunit.net/)
// - Fluent Assertions (https://fluentassertions.com/)
// to get more info about test automation, please visit: (https://nbomber.com/docs/test-automation)

Scenario BuildScenario()
{
var step = Step.Create("simple step", async context =>
{
await Task.Delay(TimeSpan.FromSeconds(0.1));
return Response.Ok(sizeBytes: 1024);
});

return ScenarioBuilder.CreateScenario("xunit_plugin_stats", step);
}

[Fact]
public void Test()
{
var scenario = BuildScenario()
.WithoutWarmUp()
.WithLoadSimulations(new[]
{
Simulation.KeepConstant(copies: 1, during: TimeSpan.FromSeconds(2))
});

var plugin1 = new Plugin1();
var nodeStats = NBomberRunner.RegisterScenarios(scenario).WithWorkerPlugins(plugin1).Run();
var (success, pluginStats) = nodeStats.TryFindPluginStats(plugin1);
var pluginStatsValue = Plugin1.TryGetValueForKey("Key1", pluginStats);

success.Should().BeTrue();
pluginStatsValue.Should().Be("Value1");
}
}
}
11 changes: 10 additions & 1 deletion examples/FSharpDev/FSharpDev.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<RootNamespace>FSharpDev</RootNamespace>
</PropertyGroup>

<ItemGroup>
Expand All @@ -30,7 +31,8 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Compile Include="ClientFactory\HttpClientFactory.fs" />
<Compile Include="Program.fs" />
<Compile Include="XUnit\PluginStatsTest.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
Expand All @@ -39,6 +41,13 @@

<ItemGroup>
<PackageReference Include="App.Metrics.Reporting.InfluxDB" Version="4.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="Unquote" Version="5.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion examples/FSharpDev/Program.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp
// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp

open System
open FSharpDev.ClientFactory
Expand Down
95 changes: 95 additions & 0 deletions examples/FSharpDev/XUnit/PluginStatsTest.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
module FSharpDev.XUnit.PluginStatsTest

open System
open System.Data
open System.Linq
open System.Threading.Tasks

open FSharp.Control.Tasks.NonAffine
open Swensen.Unquote
open Xunit

open NBomber
open NBomber.Contracts
open NBomber.FSharp

// in this example we use:
// - XUnit (https://xunit.net/)
// - Unquote (https://github.com/SwensenSoftware/unquote)
// to get more info about test automation, please visit: (https://nbomber.com/docs/test-automation)

type Plugin1 () =

let createTableCols () =
let colKey = new DataColumn("Key", Type.GetType("System.String"))
colKey.Caption <- "Key"

let colValue = new DataColumn("Value", Type.GetType("System.String"))
colValue.Caption <- "Value"

[| colKey; colValue |]

let createTableRows (count: int) (table: DataTable) = [|
for i in 1 .. count do
let row = table.NewRow()
row.["Key"] <- sprintf "Key%i" i
row.["Value"] <- sprintf "Value%i" i
yield row
|]

let createTable (rowsCount) (tableName) =
let table = new DataTable(tableName)
table.Columns.AddRange(createTableCols())
table |> createTableRows rowsCount |> Array.iter(table.Rows.Add)
table

let createPluginStats (currentOperation) =
let pluginStats = new DataSet()
pluginStats.Tables.Add(createTable 5 "PluginStats")
Task.FromResult(pluginStats)

static member TryGetValueForKey(key: string, pluginStats: DataSet) =
pluginStats.Tables.Item("PluginStats")
|> fun table -> table.Rows.Cast<DataRow>().ToArray()
|> Array.tryFind(fun row -> row.["Key"].ToString() = key)
|> Option.map(fun row -> row.["Value"].ToString())

interface IWorkerPlugin with
member _.PluginName = "Plugin1"
member _.Init(_, _) = Task.CompletedTask
member _.Start() = Task.CompletedTask
member _.GetStats(currentOperation) = createPluginStats(currentOperation)
member _.GetHints() = Array.empty
member _.Stop() = Task.CompletedTask
member _.Dispose() = ()

[<Fact>]
let ``Plugin stats test`` () =

let plugin = new Plugin1()

let step = Step.create("step_1", fun context -> task {
do! Task.Delay(seconds 1)
return Response.ok()
})

let nodeStats =
Scenario.create "scenario_1" [step]
|> Scenario.withoutWarmUp
|> Scenario.withLoadSimulations [KeepConstant(copies = 1, during = seconds 30)]
|> NBomberRunner.registerScenario
|> NBomberRunner.withWorkerPlugins [plugin]
|> NBomberRunner.withTestSuite "assert_plugin_stats"
|> NBomberRunner.withTestName "simple_test"
|> NBomberRunner.run
|> function
| Ok stats -> stats
| Error e -> failwith e

let pluginStatsValue =
nodeStats
|> WorkerPluginStats.tryFindPluginStats plugin
|> Option.bind(fun pluginStats -> Plugin1.TryGetValueForKey("Key1", pluginStats))
|> Option.defaultWith(fun () -> failwith "No value was found")

test <@ pluginStatsValue = "Value1" @>
2 changes: 1 addition & 1 deletion examples/FSharpProd/FSharpProd.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<RootNamespace>FSharp</RootNamespace>
<RootNamespace>FSharpProd</RootNamespace>
</PropertyGroup>

<ItemGroup>
Expand Down
16 changes: 15 additions & 1 deletion src/NBomber/Api/CSharp.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace NBomber.CSharp
namespace NBomber.CSharp

#nowarn "3211"

Expand All @@ -12,6 +12,7 @@ open Serilog
open NBomber
open NBomber.Contracts
open NBomber.Configuration
open NBomber.Contracts.Stats

/// ClientFactory helps you create and initialize API clients to work with specific API or protocol (HTTP, WebSockets, gRPC, GraphQL).
type ClientFactory =
Expand Down Expand Up @@ -334,6 +335,19 @@ type Simulation =
static member InjectPerSecRandom(minRate:int, maxRate:int, during:TimeSpan) =
LoadSimulation.InjectPerSecRandom(minRate, maxRate, during)

/// WorkerPluginStats helps to find worker plugin stats.
[<Extension>]
type WorkerPluginStats =

/// Tries to find stats for given worker plugin. Returns Tuple<bool, DataSet>.
[<Extension>]
static member TryFindPluginStats(nodeStats: NodeStats, plugin: IWorkerPlugin) =
nodeStats
|> FSharp.WorkerPluginStats.tryFindPluginStats plugin
|> function
| Some pluginStats -> (true, pluginStats)
| None -> (false, null)

namespace NBomber.CSharp.SyncApi

open System
Expand Down
11 changes: 10 additions & 1 deletion src/NBomber/Api/FSharp.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace NBomber.FSharp
namespace NBomber.FSharp

open System
open System.IO
Expand All @@ -19,6 +19,7 @@ open NBomber.Errors
open NBomber.Domain
open NBomber.Domain.DomainTypes
open NBomber.Domain.ClientPool
open NBomber.Contracts.Stats
open NBomber.DomainServices

type CommandLineArgs = {
Expand Down Expand Up @@ -337,6 +338,14 @@ module NBomberRunner =
|> runWithResult args
|> Result.mapError(AppError.toString)

/// WorkerPluginStats helps to find worker plugin stats.
module WorkerPluginStats =

/// Tries to find stats for given worker plugin. Returns Some DataSet if plugin exists or None otherwise.
let tryFindPluginStats (plugin: IWorkerPlugin) (nodeStats: NodeStats) =
let pluginFullName = WorkerPlugin.getFullName(plugin)
nodeStats.PluginStats |> WorkerPlugin.tryFindPluginStats pluginFullName

namespace NBomber.FSharp.SyncApi

open NBomber
Expand Down
6 changes: 6 additions & 0 deletions src/NBomber/Domain/Errors.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type ValidationError =
| CopiesCountIsZeroOrNegative of simulation:string
| RateIsZeroOrNegative of simulation:string

// Plugin errors
| DuplicatePluginFullName of pluginFullNames: string list

type AppError =
| Domain of DomainError
| Validation of ValidationError
Expand Down Expand Up @@ -111,6 +114,9 @@ type AppError =
| RateIsZeroOrNegative simulation ->
$"Simulation: '{simulation}' has invalid rate value. The value should be bigger than 0."

| DuplicatePluginFullName pluginFullNames ->
sprintf "Plugins: registered plugins with duplicated ful names: %s"
(String.concatWithCommaAndQuotes(pluginFullNames))

static member toString (error: AppError) =
match error with
Expand Down
11 changes: 11 additions & 0 deletions src/NBomber/DomainServices/NBomberContext.fs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ module Validation =
|> Option.map(fun invalidScenario -> Error <| LoadSimulationConfigValueHasInvalidFormat(invalidScenario.ScenarioName))
|> Option.defaultValue(Ok settings)

let checkWorkerPlugins (plugins: IWorkerPlugin list) =
plugins
|> Seq.groupBy(WorkerPlugin.getFullName)
|> Seq.filter(fun (_, plugins) -> Seq.length(plugins) > 1)
|> Seq.map fst
|> fun duplicatedPluginNames ->
if Seq.length(duplicatedPluginNames) = 0 then
Ok plugins
else duplicatedPluginNames |> List.ofSeq |> DuplicatePluginFullName |> Error

let empty = {
TestSuite = Constants.DefaultTestSuite
TestName = Constants.DefaultTestName
Expand Down Expand Up @@ -209,6 +219,7 @@ let createSessionArgs (testInfo: TestInfo) (context: NBomberContext) =
let! targetScenarios = context |> getTargetScenarios |> Validation.checkAvailableTargets(context.RegisteredScenarios)
let! reportName = context |> getReportFileName |> Validation.checkReportName
let! reportFolder = context |> getReportFolder |> Validation.checkReportFolder
let! workerPlugins = context.WorkerPlugins |> Validation.checkWorkerPlugins
let! sendStatsInterval = context |> getSendStatsInterval
let! scenariosSettings = context |> getScenariosSettings
let clientFactorySettings = context |> getClientFactorySettings
Expand Down
Loading