sprints work

This commit is contained in:
master
2026-01-10 11:15:28 +02:00
parent a21d3dbc1f
commit 701eb6b21c
71 changed files with 10854 additions and 136 deletions

View File

@@ -0,0 +1,272 @@
// <copyright file="AgentRegistrationServiceTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace StellaOps.Signals.RuntimeAgent.Tests;
/// <summary>
/// Tests for <see cref="AgentRegistrationService"/>.
/// </summary>
[Trait("Category", "Unit")]
public class AgentRegistrationServiceTests
{
private readonly FakeTimeProvider _timeProvider = new();
private readonly AgentRegistrationService _service;
public AgentRegistrationServiceTests()
{
_service = new AgentRegistrationService(
_timeProvider,
NullLogger<AgentRegistrationService>.Instance);
}
[Fact]
public async Task RegisterAsync_ValidRequest_ReturnsRegistration()
{
var request = CreateRegistrationRequest("agent-1");
var result = await _service.RegisterAsync(request);
result.Should().NotBeNull();
result.AgentId.Should().Be("agent-1");
result.Platform.Should().Be(RuntimePlatform.DotNet);
result.Hostname.Should().Be("host1");
result.State.Should().Be(AgentState.Stopped);
}
[Fact]
public async Task RegisterAsync_DuplicateAgent_UpdatesRegistration()
{
var request1 = CreateRegistrationRequest("agent-1");
var request2 = CreateRegistrationRequest("agent-1") with { ApplicationName = "App2" };
await _service.RegisterAsync(request1);
_timeProvider.Advance(TimeSpan.FromMinutes(1));
var result = await _service.RegisterAsync(request2);
result.ApplicationName.Should().Be("App2");
_service.Count.Should().Be(1);
}
[Fact]
public async Task HeartbeatAsync_RegisteredAgent_UpdatesState()
{
var request = CreateRegistrationRequest("agent-1");
await _service.RegisterAsync(request);
_timeProvider.Advance(TimeSpan.FromSeconds(30));
var heartbeat = new AgentHeartbeatRequest
{
AgentId = "agent-1",
State = AgentState.Running,
Posture = RuntimePosture.Full,
Statistics = CreateStatistics("agent-1", 1000)
};
var response = await _service.HeartbeatAsync(heartbeat);
response.Continue.Should().BeTrue();
var registration = await _service.GetAsync("agent-1");
registration!.State.Should().Be(AgentState.Running);
registration.Posture.Should().Be(RuntimePosture.Full);
}
[Fact]
public async Task HeartbeatAsync_UnknownAgent_ReturnsContinueFalse()
{
var heartbeat = new AgentHeartbeatRequest
{
AgentId = "unknown-agent",
State = AgentState.Running,
Posture = RuntimePosture.Sampled
};
var response = await _service.HeartbeatAsync(heartbeat);
response.Continue.Should().BeFalse();
}
[Fact]
public async Task HeartbeatAsync_WithPendingCommand_ReturnsCommand()
{
var request = CreateRegistrationRequest("agent-1");
await _service.RegisterAsync(request);
await _service.SendCommandAsync("agent-1", AgentCommand.Start);
var heartbeat = new AgentHeartbeatRequest
{
AgentId = "agent-1",
State = AgentState.Stopped,
Posture = RuntimePosture.Sampled
};
var response = await _service.HeartbeatAsync(heartbeat);
response.Command.Should().Be(AgentCommand.Start);
// Second heartbeat should not have command
var response2 = await _service.HeartbeatAsync(heartbeat);
response2.Command.Should().BeNull();
}
[Fact]
public async Task HeartbeatAsync_WithPendingPosture_ReturnsNewPosture()
{
var request = CreateRegistrationRequest("agent-1");
await _service.RegisterAsync(request);
await _service.UpdatePostureAsync("agent-1", RuntimePosture.Full);
var heartbeat = new AgentHeartbeatRequest
{
AgentId = "agent-1",
State = AgentState.Running,
Posture = RuntimePosture.Sampled
};
var response = await _service.HeartbeatAsync(heartbeat);
response.NewPosture.Should().Be(RuntimePosture.Full);
}
[Fact]
public async Task UnregisterAsync_RegisteredAgent_RemovesFromList()
{
var request = CreateRegistrationRequest("agent-1");
await _service.RegisterAsync(request);
await _service.UnregisterAsync("agent-1");
var result = await _service.GetAsync("agent-1");
result.Should().BeNull();
_service.Count.Should().Be(0);
}
[Fact]
public async Task ListAsync_ReturnsAllRegistrations()
{
await _service.RegisterAsync(CreateRegistrationRequest("agent-1"));
await _service.RegisterAsync(CreateRegistrationRequest("agent-2"));
await _service.RegisterAsync(CreateRegistrationRequest("agent-3"));
var result = await _service.ListAsync();
result.Should().HaveCount(3);
}
[Fact]
public async Task ListByPlatformAsync_FiltersCorrectly()
{
await _service.RegisterAsync(CreateRegistrationRequest("agent-1") with
{
Platform = RuntimePlatform.DotNet
});
await _service.RegisterAsync(CreateRegistrationRequest("agent-2") with
{
Platform = RuntimePlatform.Java
});
await _service.RegisterAsync(CreateRegistrationRequest("agent-3") with
{
Platform = RuntimePlatform.DotNet
});
var result = await _service.ListByPlatformAsync(RuntimePlatform.DotNet);
result.Should().HaveCount(2);
result.Should().AllSatisfy(r => r.Platform.Should().Be(RuntimePlatform.DotNet));
}
[Fact]
public async Task ListHealthyAsync_FiltersStaleAgents()
{
_service.HeartbeatTimeout = TimeSpan.FromMinutes(2);
await _service.RegisterAsync(CreateRegistrationRequest("agent-1"));
await _service.RegisterAsync(CreateRegistrationRequest("agent-2"));
// Advance time and only heartbeat agent-1
_timeProvider.Advance(TimeSpan.FromMinutes(1));
await _service.HeartbeatAsync(new AgentHeartbeatRequest
{
AgentId = "agent-1",
State = AgentState.Running,
Posture = RuntimePosture.Sampled
});
// Advance 30 seconds - agent-1 should still be healthy (1.5 min since heartbeat)
// but agent-2 is unhealthy (2.5 min since registration/initial heartbeat)
_timeProvider.Advance(TimeSpan.FromMinutes(1.5));
var healthy = await _service.ListHealthyAsync();
healthy.Should().HaveCount(1);
healthy[0].AgentId.Should().Be("agent-1");
}
[Fact]
public async Task PruneStale_RemovesExpiredRegistrations()
{
_service.HeartbeatTimeout = TimeSpan.FromMinutes(2);
await _service.RegisterAsync(CreateRegistrationRequest("agent-1"));
await _service.RegisterAsync(CreateRegistrationRequest("agent-2"));
_timeProvider.Advance(TimeSpan.FromMinutes(3));
var pruned = _service.PruneStale();
pruned.Should().Be(2);
_service.Count.Should().Be(0);
}
[Fact]
public async Task SendCommandAsync_UnknownAgent_DoesNotThrow()
{
await _service.SendCommandAsync("unknown", AgentCommand.Start);
// Should not throw, just log warning
}
[Fact]
public async Task UpdatePostureAsync_UnknownAgent_DoesNotThrow()
{
await _service.UpdatePostureAsync("unknown", RuntimePosture.Full);
// Should not throw, just log warning
}
private static AgentRegistrationRequest CreateRegistrationRequest(string agentId)
{
return new AgentRegistrationRequest
{
AgentId = agentId,
Platform = RuntimePlatform.DotNet,
Hostname = "host1",
AgentVersion = "1.0.0",
ApplicationName = "TestApp",
InitialPosture = RuntimePosture.Sampled
};
}
private AgentStatistics CreateStatistics(string agentId, long eventsCollected)
{
return new AgentStatistics
{
AgentId = agentId,
Timestamp = _timeProvider.GetUtcNow(),
State = AgentState.Running,
Uptime = TimeSpan.FromMinutes(5),
TotalEventsCollected = eventsCollected,
EventsLastMinute = Math.Min(eventsCollected, 1000),
EventsDropped = 0,
UniqueMethodsObserved = (int)(eventsCollected / 10),
UniqueTypesObserved = (int)(eventsCollected / 100),
UniqueAssembliesObserved = 5,
BufferUtilizationPercent = 25.0,
EstimatedCpuOverheadPercent = 1.5,
MemoryUsageBytes = 50_000_000
};
}
}

View File

@@ -0,0 +1,257 @@
// <copyright file="ClrMethodResolverTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace StellaOps.Signals.RuntimeAgent.Tests;
/// <summary>
/// Tests for <see cref="ClrMethodResolver"/>.
/// </summary>
[Trait("Category", "Unit")]
public class ClrMethodResolverTests
{
private readonly FakeTimeProvider _timeProvider = new();
private readonly ClrMethodResolver _resolver;
public ClrMethodResolverTests()
{
_resolver = new ClrMethodResolver(
_timeProvider,
NullLogger<ClrMethodResolver>.Instance);
}
[Fact]
public void RegisterMethod_IncrementsCacheCount()
{
_resolver.RegisterMethod(
methodId: 0x06000001,
moduleId: 0x00007FF8ABC12340,
methodNamespace: "MyApp.Services",
methodName: "ProcessData",
methodSignature: "(System.String)");
_resolver.CachedMethodCount.Should().Be(1);
}
[Fact]
public void RegisterModule_IncrementsCacheCount()
{
_resolver.RegisterModule(
moduleId: 0x00007FF8ABC12340,
assemblyId: 0x00007FF8DEF00000,
modulePath: @"C:\app\MyApp.dll",
simpleName: "MyApp");
_resolver.CachedModuleCount.Should().Be(1);
}
[Fact]
public void ResolveMethod_RegisteredMethod_ReturnsResolved()
{
const ulong methodId = 0x06000001;
_resolver.RegisterMethod(
methodId: methodId,
moduleId: 0x00007FF8ABC12340,
methodNamespace: "MyApp.Services.DataService",
methodName: "ProcessData",
methodSignature: "(System.String)");
var resolved = _resolver.ResolveMethod(methodId);
resolved.Should().NotBeNull();
resolved!.MethodName.Should().Be("ProcessData");
resolved.Namespace.Should().Be("MyApp.Services.DataService");
resolved.TypeName.Should().Be("DataService");
resolved.Signature.Should().Be("(System.String)");
}
[Fact]
public void ResolveMethod_UnknownMethod_ReturnsNull()
{
var resolved = _resolver.ResolveMethod(0xDEADBEEF);
resolved.Should().BeNull();
}
[Fact]
public void ResolveToEvent_RegisteredMethod_ReturnsEvent()
{
const ulong methodId = 0x06000001;
const ulong moduleId = 0x00007FF8ABC12340;
_resolver.RegisterModule(moduleId, 0, @"C:\app\MyApp.dll", "MyApp");
_resolver.RegisterMethod(
methodId: methodId,
moduleId: moduleId,
methodNamespace: "MyApp.Services.DataService",
methodName: "ProcessData",
methodSignature: "(System.String)");
var @event = _resolver.ResolveToEvent(
methodId,
RuntimeEventKind.MethodEnter,
eventId: "test-event-1",
timestamp: _timeProvider.GetUtcNow(),
processId: 1234);
@event.Should().NotBeNull();
@event!.MethodName.Should().Be("ProcessData");
@event.TypeName.Should().Be("DataService");
@event.AssemblyOrModule.Should().Be("MyApp");
@event.Kind.Should().Be(RuntimeEventKind.MethodEnter);
@event.Platform.Should().Be(RuntimePlatform.DotNet);
@event.ProcessId.Should().Be(1234);
}
[Fact]
public void ResolveToEvent_UnknownMethod_ReturnsNull()
{
var @event = _resolver.ResolveToEvent(
0xDEADBEEF,
RuntimeEventKind.MethodEnter,
eventId: "test-event-1",
timestamp: _timeProvider.GetUtcNow());
@event.Should().BeNull();
}
[Theory]
[InlineData("MethodID=0x06000123", 0x06000123UL)]
[InlineData("MethodID=0x00000001", 0x00000001UL)]
[InlineData("MethodID=0xDEADBEEF", 0xDEADBEEFUL)]
public void TryParseEtwMethodId_ValidInput_ReturnsMethodId(string input, ulong expected)
{
var success = _resolver.TryParseEtwMethodId(input, out var methodId);
success.Should().BeTrue();
methodId.Should().Be(expected);
}
[Theory]
[InlineData("")]
[InlineData("invalid")]
[InlineData("MethodID=invalid")]
[InlineData("ModuleID=0x123")]
public void TryParseEtwMethodId_InvalidInput_ReturnsFalse(string input)
{
var success = _resolver.TryParseEtwMethodId(input, out _);
success.Should().BeFalse();
}
[Fact]
public void ParseEtwMethodWithModule_ValidInput_ReturnsBoth()
{
var result = _resolver.ParseEtwMethodWithModule(
"MethodID=0x06000123 ModuleID=0x00007FF8ABC12340");
result.Should().NotBeNull();
result!.Value.MethodId.Should().Be(0x06000123UL);
result.Value.ModuleId.Should().Be(0x00007FF8ABC12340UL);
}
[Fact]
public void ParseEtwMethodWithModule_MethodOnly_ReturnsZeroModule()
{
var result = _resolver.ParseEtwMethodWithModule("MethodID=0x06000123");
result.Should().NotBeNull();
result!.Value.MethodId.Should().Be(0x06000123UL);
result.Value.ModuleId.Should().Be(0UL);
}
[Fact]
public void FormatSymbolId_ReturnsCorrectFormat()
{
var symbolId = ClrMethodResolver.FormatSymbolId(0x06000123);
symbolId.Should().Be("clr:method:0000000006000123");
}
[Fact]
public void Clear_RemovesAllCaches()
{
_resolver.RegisterMethod(0x1, 0x2, "ns", "method", "()");
_resolver.RegisterModule(0x2, 0x3, "path", "name");
_resolver.RegisterAssembly(0x3, "assembly");
_resolver.Clear();
_resolver.CachedMethodCount.Should().Be(0);
_resolver.CachedModuleCount.Should().Be(0);
_resolver.GetStatistics().CachedAssemblies.Should().Be(0);
}
[Fact]
public void GetStatistics_ReturnsCorrectCounts()
{
_resolver.RegisterMethod(0x1, 0x2, "ns1", "method1", "()");
_resolver.RegisterMethod(0x2, 0x2, "ns2", "method2", "()");
_resolver.RegisterModule(0x10, 0x20, "path1", "name1");
_resolver.RegisterAssembly(0x20, "assembly1");
var stats = _resolver.GetStatistics();
stats.CachedMethods.Should().Be(2);
stats.CachedModules.Should().Be(1);
stats.CachedAssemblies.Should().Be(1);
}
[Fact]
public void ResolvedMethod_FullyQualifiedName_CombinesCorrectly()
{
const ulong methodId = 0x06000001;
_resolver.RegisterMethod(
methodId: methodId,
moduleId: 0,
methodNamespace: "MyApp.Services.DataService",
methodName: "ProcessData",
methodSignature: "()");
var resolved = _resolver.ResolveMethod(methodId);
resolved!.FullyQualifiedName.Should().Be("MyApp.Services.DataService.ProcessData");
resolved.DisplayName.Should().Be("MyApp.Services.DataService.ProcessData()");
}
[Fact]
public void ResolvedMethod_EmptyNamespace_UsesMethodNameOnly()
{
const ulong methodId = 0x06000001;
_resolver.RegisterMethod(
methodId: methodId,
moduleId: 0,
methodNamespace: "",
methodName: "Main",
methodSignature: "()");
var resolved = _resolver.ResolveMethod(methodId);
resolved!.FullyQualifiedName.Should().Be("Main");
}
[Fact]
public void RegisterAssembly_ResolvesInEvent()
{
const ulong methodId = 0x06000001;
const ulong moduleId = 0x00007FF8ABC12340;
const ulong assemblyId = 0x00007FF8DEF00000;
_resolver.RegisterAssembly(assemblyId, "MyApp.Core, Version=1.0.0.0");
_resolver.RegisterModule(moduleId, assemblyId, @"C:\app\MyApp.Core.dll", "MyApp.Core");
_resolver.RegisterMethod(methodId, moduleId, "MyApp.Core.Data", "Load", "()");
var @event = _resolver.ResolveToEvent(
methodId,
RuntimeEventKind.MethodEnter,
eventId: "e1",
timestamp: _timeProvider.GetUtcNow());
@event!.AssemblyOrModule.Should().Be("MyApp.Core, Version=1.0.0.0");
}
}

View File

@@ -0,0 +1,248 @@
// <copyright file="RuntimeFactsIngestServiceTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace StellaOps.Signals.RuntimeAgent.Tests;
/// <summary>
/// Tests for <see cref="RuntimeFactsIngestService"/>.
/// </summary>
[Trait("Category", "Unit")]
public class RuntimeFactsIngestServiceTests : IAsyncLifetime
{
private readonly FakeTimeProvider _timeProvider = new();
private readonly AgentRegistrationService _registrationService;
private readonly RuntimeFactsIngestService _service;
public RuntimeFactsIngestServiceTests()
{
_registrationService = new AgentRegistrationService(
_timeProvider,
NullLogger<AgentRegistrationService>.Instance);
_service = new RuntimeFactsIngestService(
_timeProvider,
_registrationService,
NullLogger<RuntimeFactsIngestService>.Instance);
}
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync()
{
await _service.DisposeAsync();
}
[Fact]
public async Task IngestAsync_EmptyEvents_ReturnsZero()
{
var count = await _service.IngestAsync("agent-1", [], CancellationToken.None);
count.Should().Be(0);
}
[Fact]
public async Task IngestAsync_ValidEvents_ReturnsCount()
{
var events = CreateEvents(5);
var count = await _service.IngestAsync("agent-1", events, CancellationToken.None);
count.Should().Be(5);
}
[Fact]
public async Task IngestAsync_ProcessesEventsThroughChannel()
{
var events = CreateEvents(10);
await _service.IngestAsync("agent-1", events, CancellationToken.None);
// Allow time for background processing
await Task.Delay(100);
var stats = _service.GetStatistics();
stats.TotalEventsIngested.Should().Be(10);
stats.TotalBatchesProcessed.Should().Be(1);
}
[Fact]
public async Task IngestAsync_AggregatesSymbolObservations()
{
var now = _timeProvider.GetUtcNow();
var events = new List<RuntimeMethodEvent>
{
CreateEvent("symbol-1", "Method1", now),
CreateEvent("symbol-1", "Method1", now.AddSeconds(1)),
CreateEvent("symbol-1", "Method1", now.AddSeconds(2)),
CreateEvent("symbol-2", "Method2", now)
};
await _service.IngestAsync("agent-1", events, CancellationToken.None);
await Task.Delay(100);
var observation = _service.GetObservation("symbol-1");
observation.Should().NotBeNull();
observation!.ObservationCount.Should().Be(3);
observation.MethodName.Should().Be("Method1");
}
[Fact]
public async Task IngestAsync_TracksMultipleAgents()
{
var now = _timeProvider.GetUtcNow();
var event1 = CreateEvent("symbol-1", "Method1", now);
var event2 = CreateEvent("symbol-1", "Method1", now.AddSeconds(1));
await _service.IngestAsync("agent-1", [event1], CancellationToken.None);
await _service.IngestAsync("agent-2", [event2], CancellationToken.None);
await Task.Delay(100);
var observation = _service.GetObservation("symbol-1");
observation!.AgentIds.Should().Contain("agent-1");
observation.AgentIds.Should().Contain("agent-2");
}
[Fact]
public async Task GetObservedSymbols_ReturnsAllUniqueSymbols()
{
var now = _timeProvider.GetUtcNow();
var events = new List<RuntimeMethodEvent>
{
CreateEvent("symbol-1", "Method1", now),
CreateEvent("symbol-2", "Method2", now),
CreateEvent("symbol-3", "Method3", now),
CreateEvent("symbol-1", "Method1", now) // Duplicate
};
await _service.IngestAsync("agent-1", events, CancellationToken.None);
await Task.Delay(100);
var symbols = _service.GetObservedSymbols();
symbols.Should().HaveCount(3);
symbols.Should().Contain("symbol-1");
symbols.Should().Contain("symbol-2");
symbols.Should().Contain("symbol-3");
}
[Fact]
public async Task GetObservationsSince_FiltersCorrectly()
{
var baseTime = _timeProvider.GetUtcNow();
var events1 = new List<RuntimeMethodEvent>
{
CreateEvent("symbol-1", "Method1", baseTime)
};
await _service.IngestAsync("agent-1", events1, CancellationToken.None);
await Task.Delay(100);
_timeProvider.Advance(TimeSpan.FromHours(1));
var laterTime = _timeProvider.GetUtcNow();
var events2 = new List<RuntimeMethodEvent>
{
CreateEvent("symbol-2", "Method2", laterTime)
};
await _service.IngestAsync("agent-1", events2, CancellationToken.None);
await Task.Delay(100);
var recentObservations = _service.GetObservationsSince(baseTime.AddMinutes(30));
recentObservations.Should().HaveCount(1);
recentObservations[0].SymbolId.Should().Be("symbol-2");
}
[Fact]
public async Task GetStatistics_ReturnsCorrectCounts()
{
var events1 = CreateEvents(5);
var events2 = CreateEvents(3);
await _service.IngestAsync("agent-1", events1, CancellationToken.None);
await _service.IngestAsync("agent-1", events2, CancellationToken.None);
await Task.Delay(100);
var stats = _service.GetStatistics();
stats.TotalEventsIngested.Should().Be(8);
stats.TotalBatchesProcessed.Should().Be(2);
stats.UniqueSymbolsObserved.Should().BeGreaterThan(0);
}
[Fact]
public async Task RegisterAgentAsync_DoesNotThrow()
{
var registration = new AgentRegistration
{
AgentId = "agent-1",
Platform = RuntimePlatform.DotNet,
Hostname = "host1",
AgentVersion = "1.0.0",
RegisteredAt = _timeProvider.GetUtcNow(),
LastHeartbeat = _timeProvider.GetUtcNow()
};
await _service.RegisterAgentAsync(registration, CancellationToken.None);
}
[Fact]
public async Task HeartbeatAsync_DoesNotThrow()
{
var stats = CreateStatistics("agent-1", 100);
await _service.HeartbeatAsync("agent-1", stats, CancellationToken.None);
}
[Fact]
public async Task UnregisterAgentAsync_DoesNotThrow()
{
await _service.UnregisterAgentAsync("agent-1", CancellationToken.None);
}
private List<RuntimeMethodEvent> CreateEvents(int count)
{
var now = _timeProvider.GetUtcNow();
return Enumerable.Range(0, count)
.Select(i => CreateEvent($"symbol-{i}", $"Method{i}", now.AddMilliseconds(i)))
.ToList();
}
private AgentStatistics CreateStatistics(string agentId, long eventsCollected)
{
return new AgentStatistics
{
AgentId = agentId,
Timestamp = _timeProvider.GetUtcNow(),
State = AgentState.Running,
Uptime = TimeSpan.FromMinutes(5),
TotalEventsCollected = eventsCollected,
EventsLastMinute = Math.Min(eventsCollected, 1000),
EventsDropped = 0,
UniqueMethodsObserved = (int)(eventsCollected / 10),
UniqueTypesObserved = (int)(eventsCollected / 100),
UniqueAssembliesObserved = 5,
BufferUtilizationPercent = 25.0,
EstimatedCpuOverheadPercent = 1.5,
MemoryUsageBytes = 50_000_000
};
}
private static RuntimeMethodEvent CreateEvent(string symbolId, string methodName, DateTimeOffset timestamp)
{
return new RuntimeMethodEvent
{
EventId = Guid.NewGuid().ToString("N"),
SymbolId = symbolId,
MethodName = methodName,
TypeName = "TestType",
AssemblyOrModule = "TestAssembly",
Timestamp = timestamp,
Kind = RuntimeEventKind.MethodEnter,
Platform = RuntimePlatform.DotNet
};
}
}