DEEP-26 Load Generator added.

This commit is contained in:
Andrey Shabarshov 2023-07-08 15:34:33 +01:00
parent 5d7223e801
commit dc67d69bd2
13 changed files with 787 additions and 0 deletions

View File

@ -0,0 +1,32 @@
using QRBee.Core.Client;
using System.Collections.Concurrent;
namespace QRBee.Load.Generator;
public class ClientPool
{
private readonly ConcurrentDictionary<int, ClientSettings> _clientPool = new();
private readonly ConcurrentDictionary<int, ClientSettings> _merchantPool = new();
private readonly SecurityServiceFactory _securityServiceFactory;
private readonly Client _client;
public ClientPool(SecurityServiceFactory securityServiceFactory, QRBee.Core.Client.Client client)
{
_securityServiceFactory = securityServiceFactory;
this._client = client;
}
public async Task<ClientSettings> GetMerchant(int no)
{
var merchant = _merchantPool.GetOrAdd(no+100_000_000, x => new ClientSettings(_securityServiceFactory, no, true));
await merchant.InitialSetup(_client);
return merchant;
}
public async Task<ClientSettings> GetClient(int no)
{
var customer = _clientPool.GetOrAdd(no, x => new ClientSettings(_securityServiceFactory, no, false));
await customer.InitialSetup(_client);
return customer;
}
}

View File

@ -0,0 +1,68 @@
using QRBee.Core.Data;
using QRBee.Core.Security;
namespace QRBee.Load.Generator;
public class ClientSettings
{
private ISecurityService _securityService;
public ClientSettings(SecurityServiceFactory securityServiceFactory, int no, bool isMerchant)
{
Id = no;
IsMerchant = isMerchant;
CardNumber = IsMerchant ? "": $"123400000000{no:0000}";
CardHolderName = IsMerchant ? $"Merchant {no}" : $"Mr {no}";
CVC = IsMerchant ? "" : $"{no:000}";
ExpirationDate = IsMerchant ? "" : (DateTime.Now.Date + TimeSpan.FromDays(364)).ToString("yyyy-MM");
ValidFrom = IsMerchant ? "" : (DateTime.Now.Date - TimeSpan.FromDays(7)).ToString("yyyy-MM");
Email = IsMerchant ? $"{no}@merchant.org" : $"{no}@client.org";
_securityService = securityServiceFactory(no);
}
public int Id { get; }
public string? ClientId { get; private set; }
public string CardNumber { get; }
public string CardHolderName { get; }
public string CVC { get; }
public int? IssueNo { get; }
public string? ExpirationDate { get; }
public string? ValidFrom { get; }
public bool IsMerchant { get; }
public string Email { get; }
public ISecurityService SecurityService { get => _securityService; }
public async Task InitialSetup(QRBee.Core.Client.Client client)
{
var idFileName = Environment.ExpandEnvironmentVariables($"%TEMP%/!QRBee/QRBee-{Id:8X}.txt");
if (File.Exists(idFileName))
{
var l = File.ReadAllLines(idFileName);
if (l != null && l.Length > 0)
ClientId = l[0];
}
if (!string.IsNullOrWhiteSpace(ClientId) && SecurityService.PrivateKeyHandler.Exists())
return;
var request = new RegistrationRequest
{
Name = CardHolderName,
Email = Email,
DateOfBirth = "2000-01-01",
RegisterAsMerchant = IsMerchant,
CertificateRequest = SecurityService.PrivateKeyHandler.CreateCertificateRequest(Email)
};
var resp = await client.RegisterAsync(request);
_securityService.APIServerCertificate = _securityService.Deserialize(resp.APIServerCertificate);
ClientId = resp.ClientId;
File.WriteAllText(idFileName, ClientId);
}
}

View File

@ -0,0 +1,21 @@
namespace QRBee.Load.Generator;
internal class Anomaly
{
public double Probability { get; set; }
public Dictionary<string,string> Parameters { get; set; } = new();
}
internal class GeneratorSettings
{
public int NumberOfClients { get; set; } = 100;
public int NumberOfMerchants { get; set; } = 10;
public int NumberOfThreads { get; set; } = 20;
public int DelayBetweenMessagesMSec { get; set; } = 100;
public int DelayJitterMSec { get; set; } = 50;
public double MinAmount { get; set; } = 10;
public double MaxAmount { get; set; } = 100;
public Anomaly LoadSpike { get; set; } = new();
public Anomaly LargeAmount { get; set; } = new();
}

View File

@ -0,0 +1,273 @@
// See https://aka.ms/new-console-template for more information
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QRBee.Core.Client;
using QRBee.Core.Data;
using QRBee.Load.Generator;
internal class LoadGenerator : IHostedService
{
private readonly Client _client;
private readonly ClientPool _clientPool;
private readonly PaymentRequestGenerator _paymentRequestGenerator;
private readonly ILogger<LoadGenerator> _logger;
private readonly IOptions<GeneratorSettings> _settings;
public LoadGenerator(
QRBee.Core.Client.Client client,
ClientPool clientPool,
PaymentRequestGenerator paymentRequestGenerator,
ILogger<LoadGenerator> logger,
IOptions<GeneratorSettings> settings
)
{
_client = client;
_clientPool = clientPool;
_paymentRequestGenerator = paymentRequestGenerator;
_logger = logger;
_settings = settings;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await InitClients();
_ = Task.Run(ReportingThread);
_ = Task.Run(ConfirmationThread);
_ = Task.Run(ReceivingThread);
_ = Task.Run(GenerateLoad);
}
public Task StopAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
private async Task InitClients()
{
_logger.LogInformation($"Initializing {_settings.Value.NumberOfClients} clients...");
for ( var i = 1; i < _settings.Value.NumberOfClients+1; i++ )
{
await _clientPool.GetClient(i);
}
_logger.LogInformation($"Initializing {_settings.Value.NumberOfMerchants} merchants...");
for (var i = 1; i < _settings.Value.NumberOfMerchants + 1; i++)
{
await _clientPool.GetMerchant(i);
}
_logger.LogInformation($"=== Initialization complete ===");
}
private async Task ReportingThread()
{
DateTime nextReport = DateTime.MinValue;
while (true)
{
if (DateTime.Now > nextReport)
{
_logger.LogInformation($"S: {_paymentsGenerated,10:N0} R: {_paymentsProcessed,10:N0} C: {_paymentsConfirmed,10:N0} F: {_paymentsFailed,10:N0}");
nextReport = DateTime.Now + TimeSpan.FromSeconds(1);
}
await Task.Delay(1000);
}
}
private async Task GenerateLoad()
{
_logger.LogInformation("Generator started");
var threadTasks = Enumerable.Range(0, _settings.Value.NumberOfThreads)
.Select(_ => GenerationThread())
.ToArray();
await Task.WhenAll(threadTasks);
_logger.LogInformation("Generator finished");
}
private List<Task<PaymentResponse>> _responseQueue = new();
private List<Task> _confirmationQueue = new();
private ThreadSafeRandom _rng = new();
private object _lock = new();
private long _paymentsGenerated;
private long _paymentsProcessed;
private long _paymentsConfirmed;
private long _paymentsFailed;
private async Task ConfirmationThread()
{
while (true)
{
try
{
var newQueue = new List<Task>();
lock (_lock)
newQueue = Interlocked.Exchange(ref _confirmationQueue, newQueue);
if (newQueue.Count == 0)
{
await Task.Delay( _rng.NextInRange(300, 600));
continue;
}
var tasks = newQueue.ToList();
while (tasks.Any(x => !x.IsCompleted))
{
try
{
var t = await Task.WhenAny(tasks);
tasks.Remove(t);
Interlocked.Increment(ref _paymentsConfirmed);
}
catch (Exception ex)
{
Interlocked.Increment(ref _paymentsFailed);
_logger.LogError(ex, "Confirmation thread");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Receining thread");
}
}
}
private async Task ReceivingThread()
{
while (true)
{
try
{
var newQueue = new List<Task<PaymentResponse>>();
lock (_lock)
newQueue = Interlocked.Exchange(ref _responseQueue, newQueue);
if (newQueue.Count == 0)
{
await Task.Delay(_rng.NextInRange(300, 600));
continue;
}
var tasks = newQueue.ToList();
while ( tasks.Any() )
{
try
{
var t = await Task.WhenAny(tasks);
tasks.Remove(t);
var res = await t;
Interlocked.Increment(ref _paymentsProcessed);
if (res?.Success ?? false)
{
var paymentConfirmation = new PaymentConfirmation
{
MerchantId = res.PaymentRequest.ClientResponse.MerchantRequest.MerchantId,
MerchantTransactionId = res.PaymentRequest.ClientResponse.MerchantRequest.MerchantTransactionId,
GatewayTransactionId = res.GatewayTransactionId
};
var confirmationTask = _client.ConfirmPayAsync(paymentConfirmation);
_confirmationQueue.Add(confirmationTask);
}
else
Interlocked.Increment(ref _paymentsFailed);
}
catch (Exception ex)
{
Interlocked.Increment(ref _paymentsFailed);
_logger.LogError(ex, "Receining thread (confirmation)");
}
}
}
catch ( Exception ex )
{
_logger.LogError(ex, "Receining thread");
}
}
}
private async Task GenerationThread()
{
// initial delay
await Task.Delay(500 + _rng.Next() % 124);
var spikeEnd = DateTime.MinValue;
var loadSpike = _settings.Value.LoadSpike;
var spikeDuration = TimeSpan.Zero;
var spikeDelay = TimeSpan.Zero;
var spikeProbability = loadSpike?.Probability ?? 0.0;
if ( loadSpike != null && loadSpike.Probability > 0.0 )
{
if ( !loadSpike.Parameters.TryGetValue("Duration", out var duration)
|| !TimeSpan.TryParse( duration, out spikeDuration ) )
{
spikeProbability = 0.0;
}
else
{
if (!loadSpike.Parameters.TryGetValue("Delay", out duration)
|| !TimeSpan.TryParse(duration, out spikeDelay))
{
spikeDelay = TimeSpan.FromMilliseconds(10);
}
}
}
while (true)
{
try
{
var req = await _paymentRequestGenerator.GeneratePaymentRequest(
_rng.NextInRange(1, _settings.Value.NumberOfClients + 1),
_rng.NextInRange(1, _settings.Value.NumberOfMerchants + 1)
);
var resp = _client.PayAsync(req);
_responseQueue.Add(resp);
Interlocked.Increment(ref _paymentsGenerated);
}
catch (Exception ex)
{
Interlocked.Increment(ref _paymentsFailed);
_logger.LogError(ex, "Generation thread");
}
if (DateTime.Now > spikeEnd)
{
if (loadSpike != null && _rng.NextDouble() < spikeProbability)
{
// start load spike
spikeEnd = DateTime.Now + spikeDuration;
_logger.LogWarning($"Anomaly: Load spike until {spikeEnd}");
await Task.Delay(spikeDuration);
}
else
{
await Task.Delay(_rng.NextInRange(
_settings.Value.DelayBetweenMessagesMSec,
_settings.Value.DelayBetweenMessagesMSec + _settings.Value.DelayJitterMSec
));
}
}
else
{
await Task.Delay(spikeDuration);
}
}
}
}

View File

@ -0,0 +1,111 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QRBee.Core.Data;
using System.Text;
namespace QRBee.Load.Generator;
internal class PaymentRequestGenerator
{
private readonly ClientPool _clientPool;
private readonly ILogger<PaymentRequestGenerator> _logger;
private ThreadSafeRandom _rng = new ThreadSafeRandom();
private double _minAmount = 1;
private double _maxAmount = 100;
private double _largeAmountProbability;
private double _largeAmountValue;
public PaymentRequestGenerator(ClientPool clientPool, IOptions<GeneratorSettings> settings, ILogger<PaymentRequestGenerator> logger)
{
_clientPool = clientPool;
_logger = logger;
_minAmount = settings.Value.MinAmount;
_maxAmount = settings.Value.MaxAmount;
var largeAmount = settings.Value.LargeAmount;
_largeAmountProbability = largeAmount.Probability;
if (_largeAmountProbability > 0)
{
if ( largeAmount.Parameters.TryGetValue("Value", out var s))
_largeAmountValue = Double.Parse(s);
}
if (_largeAmountValue <= 0.0)
_largeAmountProbability = 0.0;
}
public async Task<PaymentRequest> GeneratePaymentRequest(int clientId, int merchantId)
{
var merchant = await GetMerchant(merchantId);
var merchantReq = new MerchantToClientRequest()
{
MerchantId = merchant.ClientId,
MerchantTransactionId = Guid.NewGuid().ToString(),
Name = merchant.CardHolderName,
Amount = GetAmount(),
TimeStampUTC = DateTime.UtcNow
};
var merchantSignature = merchant.SecurityService.Sign(Encoding.UTF8.GetBytes(merchantReq.AsDataForSignature()));
merchantReq.MerchantSignature = Convert.ToBase64String(merchantSignature);
var clientResp = await CreateClientResponse(merchantReq, clientId);
var req = new PaymentRequest()
{
ClientResponse = clientResp
};
return req;
}
private async Task<ClientToMerchantResponse> CreateClientResponse(MerchantToClientRequest merchantReq, int clientId)
{
var client = await GetClient(clientId);
var response = new ClientToMerchantResponse
{
ClientId = client.ClientId,
TimeStampUTC = DateTime.UtcNow,
MerchantRequest = merchantReq,
EncryptedClientCardData = EncryptCardData(client, merchantReq.MerchantTransactionId)
};
var clientSignature = client.SecurityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ClientSignature = Convert.ToBase64String(clientSignature);
return response;
}
private decimal GetAmount()
{
if (_rng.NextDouble() < _largeAmountProbability)
{
_logger.LogWarning($"Anomaly: Large amount");
return Convert.ToDecimal(_rng.NextDoubleInRange(_largeAmountValue, _largeAmountValue* 1.10));
}
return Convert.ToDecimal(_rng.NextDoubleInRange(_minAmount, _maxAmount));
}
private Task<ClientSettings> GetMerchant(int id) => _clientPool.GetMerchant(id);
private Task<ClientSettings> GetClient(int id) => _clientPool.GetClient(id);
private string EncryptCardData(ClientSettings settings, string transactionId)
{
var clientCardData = new ClientCardData
{
TransactionId = transactionId,
CardNumber = settings.CardNumber,
ExpirationDateYYYYMM = string.IsNullOrWhiteSpace(settings.ExpirationDate) ? null : DateTime.Parse(settings.ExpirationDate).ToString("yyyy-MM"),
ValidFromYYYYMM = string.IsNullOrWhiteSpace(settings.ValidFrom) ? null : DateTime.Parse(settings.ValidFrom).ToString("yyyy-MM"),
CardHolderName = settings.CardHolderName,
CVC = settings.CVC,
IssueNo = settings.IssueNo
};
var bytes = settings.SecurityService.Encrypt(Encoding.UTF8.GetBytes(clientCardData.AsString()), settings.SecurityService.APIServerCertificate);
return Convert.ToBase64String(bytes);
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using QRBee.Api.Services;
using QRBee.Core.Security;
namespace QRBee.Load.Generator;
public delegate IPrivateKeyHandler PrivateKeyHandlerFactory(int keyNo);
public delegate ISecurityService SecurityServiceFactory(int keyNo);
internal class PrivateKeyHandler : ServerPrivateKeyHandler
{
public PrivateKeyHandler(ILogger<ServerPrivateKeyHandler> logger, IConfiguration config, int keyNo) : base(logger, config)
{
PrivateKeyFileName = Environment.ExpandEnvironmentVariables($"%TEMP%/!QRBee/QRBee-{keyNo:8X}.key");
}
}

View File

@ -0,0 +1,42 @@
// See https://aka.ms/new-console-template for more information
using log4net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using QRBee.Api.Services;
using QRBee.Droid.Services;
using QRBee.Load.Generator;
using Microsoft.Extensions.Configuration;
Console.WriteLine("=== QRBee artificaial load generator ===");
var builder = Host.CreateDefaultBuilder();
builder.ConfigureServices((context, services) =>
{
services.AddLogging(logging =>
{
logging.ClearProviders();
GlobalContext.Properties["LOGS_ROOT"] = Environment.GetEnvironmentVariable("LOGS_ROOT") ?? "";
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
logging.AddLog4Net("log4net.config");
});
services
.AddHttpClient<QRBee.Core.Client.Client, QRBee.Core.Client.Client>(httpClient => new QRBee.Core.Client.Client("https://localhost:7000/", httpClient))
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator });
;
services
.Configure<GeneratorSettings>(context.Configuration.GetSection("GeneratorSettings"))
.AddSingleton<ClientPool>()
.AddSingleton<PaymentRequestGenerator>()
.AddSingleton<PrivateKeyHandlerFactory>(x => no => new PrivateKeyHandler(x.GetRequiredService<ILogger<ServerPrivateKeyHandler>>(), x.GetRequiredService<IConfiguration>(), no))
.AddSingleton<SecurityServiceFactory>(x => no => new AndroidSecurityService(x.GetRequiredService<PrivateKeyHandlerFactory>()(no)))
.AddHostedService<LoadGenerator>()
;
});
var host = builder.Build();
host.Run();

View File

@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.Development.json" />
<None Remove="appsettings.json" />
<None Remove="log4net.config" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="log4net" Version="2.0.15" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Log4Net.AspNetCore" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\QRBee.Core\QRBee.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\QRBee\QRBee.Android\Services\AndroidSecurityService.cs" />
<Compile Include="..\QRBeeApi\Services\ServerPrivateKeyHandler.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="log4net.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="private_key.p12">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,47 @@
namespace QRBee.Load.Generator;
/// <summary>
/// https://stackoverflow.com/questions/3049467/is-c-sharp-random-number-generator-thread-safe
/// </summary>
public class ThreadSafeRandom
{
private static readonly Random _global = new Random();
[ThreadStatic] private static Random? _local;
public int Next()
{
Init();
return _local!.Next();
}
public double NextDouble()
{
Init();
return _local!.NextDouble();
}
public int NextInRange(int start, int end)
{
var n = Next();
return start + n % (end - start);
}
public double NextDoubleInRange(double min, double max)
{
var n = NextDouble();
return min + (max - min) * n;
}
private static void Init()
{
if (_local == null)
{
int seed;
lock (_global)
{
seed = _global.Next();
}
_local = new Random(seed);
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,38 @@
{
"PrivateKey" : {
"FileName": "private_key.p12",
"Password": ""
},
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft.AspNetCore": "Information"
}
},
"GeneratorSettings": {
"NumberOfClients": 100,
"NumberOfMerchants": 10,
"NumberOfThreads": 20,
"DelayBetweenMessagesMSec": 100,
"DelayJitterMSec": 50,
"MinAmount": 10,
"MaxAmount": 100,
"LoadSpike": {
"Probability": 0.001,
"Parameters": {
"Duration": "00:00:05",
"Delay": "00:00:00.0100000"
}
},
"LargeAmount": {
"Probability": 0.03,
"Parameters": {
"Valie": "1_000"
}
}
}
}

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="DebugAppender" type="log4net.Appender.DebugAppender" >
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%-2thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<!--
<appender name="Console" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%-2thread] %-5level %-20logger - %message%newline" />
</layout>
</appender>
-->
<!--Console appender-->
<appender name="Console" type="log4net.Appender.ManagedColoredConsoleAppender">
<mapping>
<level value="INFO" />
<forecolor value="Green" />
</mapping>
<mapping>
<level value="WARN" />
<forecolor value="Yellow" />
</mapping>
<mapping>
<level value="ERROR" />
<forecolor value="Red" />
</mapping>
<mapping>
<level value="DEBUG" />
<forecolor value="Blue" />
</mapping>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date{HH:mm:ss,fff} [%-2thread] %-5level %-15logger{1} %message%newline" />
</layout>
</appender>
<appender name="BufferingForwardingAppender" type="log4net.Appender.BufferingForwardingAppender" >
<bufferSize value="1"/>
<appender-ref ref="DebugAppender" />
<appender-ref ref="Console" />
<!-- <appender-ref ref="RollingFile" /> -->
</appender>
<logger name="System.Net.Http">
<level value="ERROR"/>
</logger>
<root>
<level value="ALL"/>
<appender-ref ref="BufferingForwardingAppender" />
</root>
</log4net>

View File

@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRBee.Core", "QRBee.Core\QR
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRBee.Tests", "QRBee.Tests\QRBee.Tests.csproj", "{C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRBee.Tests", "QRBee.Tests\QRBee.Tests.csproj", "{C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRBee.Load.Generator", "QRBee.Load.Generator\QRBee.Load.Generator.csproj", "{656BE747-ADE3-4E85-A70C-13167E716260}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -95,6 +97,18 @@ Global
{C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhone.Build.0 = Release|Any CPU {C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhone.Build.0 = Release|Any CPU
{C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Debug|Any CPU.Build.0 = Debug|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhone.Build.0 = Debug|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Release|Any CPU.ActiveCfg = Release|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Release|Any CPU.Build.0 = Release|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhone.ActiveCfg = Release|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhone.Build.0 = Release|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE