Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,23 @@
using Centron.Tests.EndToEnd.Infrastructure.Verifier;
using Xunit.Abstractions;
using Centron.Core;
using Centron.Tests.EndToEnd.Infrastructure.Fixtures;
using Xunit.Extensions.AssemblyFixture;
namespace Centron.Tests.EndToEnd.Infrastructure
{
public abstract class CentronTest :
IAssemblyFixture<CultureAssemblyFixture>
{
public ITestOutputHelper TestOutputHelper { get; }
public CentronVerifier Verifier { get; }
public CentronTest(ITestOutputHelper testOutputHelper)
{
Guard.NotNull(testOutputHelper, nameof(testOutputHelper));
this.TestOutputHelper = testOutputHelper;
this.Verifier = new CentronVerifier();
}
}
}
@@ -0,0 +1,357 @@
using System;
using System.Data.SqlClient;
using System.IO;
using System.IO.Compression;
using System.Runtime.ExceptionServices;
using Centron.BusinessLogic;
using Centron.BusinessLogic.Administration.Scripts;
using Centron.BusinessLogic.Sales.Receipts;
using Centron.Common;
using Centron.Core.Extensions;
using Centron.Core.Utils;
using Centron.DAO;
using Centron.DAO.DAOConnections;
using Centron.Interfaces.BL;
using Xunit;
namespace Centron.Tests.EndToEnd.Infrastructure
{
public class Database
{
#region Database Server
public static string Server
{
get
{
var server = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_SERVER");
if (string.IsNullOrWhiteSpace(server) == false)
return server;
throw new Exception("You have to configure the SQL server for EndToEndTests");
// return "YOUR-MACHINE\\SQL2017"; //Enter your local SQL server here
}
}
public static string Username
{
get
{
var username = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_USERNAME");
if (string.IsNullOrWhiteSpace(username) == false)
return username;
throw new Exception("You have to configure the SQL server username for EndToEndTests");
// return "sql-username"; //Enter your local SQL username here
}
}
public static string Password
{
get
{
var password = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_PASSWORD");
if (string.IsNullOrWhiteSpace(password) == false)
return password;
throw new Exception("You have to configure the SQL server password for EndToEndTests");
// return "password"; //Enter your local SQL password here
}
}
#endregion
private static object _lock = new object();
private static Exception _initializeException;
private static string _databaseName;
private static string _snapshotName;
public static IDisposable Initialize()
{
lock (_lock)
{
if (string.IsNullOrWhiteSpace(_databaseName) == false || string.IsNullOrWhiteSpace(_snapshotName) == false)
throw new Exception("Database.Initialize can only be called once.");
// If there was an error when initializing the Database before
// Rethrow that exception here, so we don't waste a lot of time restoring the database again
if (_initializeException is not null)
ExceptionDispatchInfo.Throw(_initializeException); // This keeps the original exception stack-trace
// Start database initialization in the beginning to let it run parallel to database restore
DAOFactory.Instance.InitializeAsync();
string databaseName = GetDatabaseName();
string snapshotName = GetSnapshotName(databaseName);
RestoreDatabase(databaseName);
try
{
Connect(databaseName);
UpdateDatabase();
}
catch (Exception e)
{
try
{
// If something fails when connecting to the database, or executing scripts
// we want to Drop the database again.
// The test will not be able to run, but we were leaving old database backups behind.
// So, lets clean up after ourselves and Drop the database if something goes wrong.
DropDatabase(databaseName);
}
catch
{
// Dropping the database failed. Unlucky, but not much we can do.
}
_initializeException = e;
throw;
}
try
{
CreateSnapshot(snapshotName, databaseName);
}
catch (Exception e)
{
// The test will not be able to run, but we were leaving old database backups behind.
// So, lets clean up after ourselves and Drop the database if something goes wrong.
try
{
DropSnapshot(snapshotName);
}
catch
{
//Dropping the snapshot failed. Unlucky, but not much we can do.
}
try
{
DropDatabase(databaseName);
}
catch
{
//Dropping the database failed. Unlucky, but not much we can do.
}
_initializeException = e;
throw;
}
_databaseName = databaseName;
_snapshotName = snapshotName;
return new DisposableAction(() =>
{
DropSnapshot(snapshotName);
DropDatabase(databaseName);
});
}
}
public static void RestoreFromSnapshot()
{
RestoreFromSnapshot(_databaseName, _snapshotName);
ReceiptItemSpecialArticleHelperBL.ClearSpecialArticlesCache();
}
#region Names
private static string GetDatabaseName()
{
var user = Environment.UserName;
var time = DateTime.Now.ToString("dd-MM-yyyy-HH:mm:ss");
var guid = Guid.NewGuid().ToString("N");
return $"EndToEndTests-{user}-{time}-{guid}".Truncate(120); // The MSSQL server database name max length is 128 characters, just make sure to not go too long
}
private static string GetSnapshotName(string databaseName)
{
return databaseName + "-snapshot";
}
#endregion
#region Snapshots
private static void CreateSnapshot(string snapshotName, string databaseName)
{
ExecuteSql(command =>
{
command.CommandText = GetCreateSnapshotSQL(snapshotName, databaseName);
command.ExecuteNonQuery();
});
}
private static string GetCreateSnapshotSQL(string snapshotName, string databaseName)
{
string dataPath = null;
ExecuteSql(command =>
{
command.CommandText = "SELECT serverproperty('InstanceDefaultDataPath')";
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
dataPath = reader.GetString(0);
}
}
});
return $@"CREATE DATABASE [{snapshotName}] ON
(
NAME = CentronWork_Data,
FILENAME = N'{dataPath}{databaseName.Replace("-", "").Replace(":", "")}.ss'
)
AS SNAPSHOT OF [{databaseName}]";
}
private static void DropSnapshot(string snapshotName)
{
ExecuteSql(command =>
{
command.CommandText = GetDropSnapshotSQL(snapshotName);
command.ExecuteNonQuery();
});
}
private static string GetDropSnapshotSQL(string snapshotName)
{
return $"DROP DATABASE [{snapshotName}]";
}
private static void RestoreFromSnapshot(string databaseName, string snapshotName)
{
ExecuteSql(command =>
{
command.CommandText = GetRestoreFromSnapshotSQL(databaseName, snapshotName);
command.ExecuteNonQuery();
});
}
private static string GetRestoreFromSnapshotSQL(string databaseName, string snapshotName)
{
return $@"
ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
RESTORE DATABASE [{databaseName}]
FROM DATABASE_SNAPSHOT = '{snapshotName}';
ALTER DATABASE [{databaseName}] SET MULTI_USER WITH ROLLBACK IMMEDIATE; ";
}
#endregion
#region Database
private static void RestoreDatabase(string databaseName)
{
ExecuteSql(command =>
{
command.CommandText = GetRestoreDatabaseSQL(databaseName);
command.CommandTimeout = (int)TimeSpan.FromMinutes(2).TotalSeconds;
command.ExecuteNonQuery();
});
}
private static string GetRestoreDatabaseSQL(string databaseName)
{
var backupPath = GetEnvironmentVariable("DATABASE_BACKUP_PATH");
if (string.IsNullOrEmpty(backupPath))
backupPath = Path.GetFullPath(".\\Infrastructure\\DatabaseBackup.bak");
string dataPath = null;
string logPath = null;
ExecuteSql(command =>
{
command.CommandText = "SELECT serverproperty('InstanceDefaultDataPath'), serverproperty('InstanceDefaultLogPath')";
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
dataPath = reader.GetString(0);
logPath = reader.GetString(1);
}
}
});
return $@"RESTORE DATABASE [{databaseName}] FROM DISK = N'{backupPath}'
WITH
REPLACE,
MOVE N'CentronWork_Data' TO N'{dataPath}{databaseName.Replace("-", "").Replace(":", "")}.mdf',
MOVE N'CentronWork_Log' TO N'{logPath}{databaseName.Replace("-", "").Replace(":", "")}.ldf'";
}
private static void DropDatabase(string databaseName)
{
ExecuteSql(command =>
{
command.CommandText = GetDropDatabaseSQL(databaseName);
command.ExecuteNonQuery();
});
}
private static string GetDropDatabaseSQL(string databaseName)
{
return $"ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{databaseName}]";
}
#endregion
#region Private Methods
private static void Connect(string databaseName)
{
DAOFactory.Instance.SetConnection(new InCodeDAOConnection(Server, databaseName, Username, Password), "c-entron Tests");
DAOFactory.Instance.InitializeAsync().Wait();
Assert.True(DAOFactory.Instance.IsInitialized, "DAOFactory.Instance.IsInitialized");
Assert.True(DAOFactory.Instance.HasConnection, "DAOFactory.Instance.HasConnection");
}
private static void UpdateDatabase()
{
using (var session = new BLSession())
{
var scriptEngineBL = session.GetBL<ScriptEngineBL>();
var version = new Version(20, 0, 0, 0);
var result = scriptEngineBL.ExecuteScripts(currentVersionOverride: version);
Assert.Null(result.Message);
Assert.Equal(ResultStatus.Success, result.Status);
}
}
private static void ExecuteSql(Action<SqlCommand> execute)
{
var builder = new SqlConnectionStringBuilder
{
DataSource = Server,
UserID = Username,
Password = Password,
InitialCatalog = "master"
};
using (var connection = new SqlConnection(builder.ToString()))
{
connection.Open();
using (var command = connection.CreateCommand())
{
execute(command);
}
}
}
private static string GetEnvironmentVariable(string name)
{
var targets = new[]
{
EnvironmentVariableTarget.Process,
EnvironmentVariableTarget.User,
EnvironmentVariableTarget.Machine
};
foreach (var target in targets)
{
var value = Environment.GetEnvironmentVariable(name, target);
if (string.IsNullOrWhiteSpace(value) == false)
return value;
}
return null;
}
#endregion
}
}
@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Centron.BusinessLogic;
using Centron.BusinessLogic.Administration.Logins;
using Centron.BusinessLogic.EmployeeArea;
using Centron.BusinessLogic.Mail.Factory;
using Centron.DAO;
using Centron.Data.Entities.Administration.Logins;
using Centron.Tests.EndToEnd.Infrastructure.Fixtures;
using Xunit;
using Xunit.Abstractions;
using Xunit.Extensions.AssemblyFixture;
using Centron.Core;
using Centron.Interfaces.Administration.Logins;
using Centron.Interfaces.BL;
namespace Centron.Tests.EndToEnd.Infrastructure
{
public abstract class EndToEndTest : CentronTest,
IAssemblyFixture<DatabaseAssemblyFixture>,
IAssemblyFixture<LicensingAssemblyFixture>,
IAssemblyFixture<InitializeObjectMapperAssemblyFixture>
{
[Fact]
public abstract void Execute();
protected LoggedInUser GetLoggedInUser(int userI3D = 11)
{
using var session = new BLSession();
return new LoggedInUser(session.GetBL<AppUserBL>().GetAppUser(f => f.I3D == userI3D));
}
private Dictionary<int, string> _ticketId = new();
protected LoggedInUser GetLoggedInUserWebAccount(int webAccountI3D = 6)
{
using var session = new BLSession();
var webAccountAppUser = session.GetBL<AppUserBL>().GetAppUserForWebaccounts().ThrowIfError();
var emp = webAccountAppUser.Employee; //Make sure the employee is loaded too
var appUser = emp.AppUser; //Make sure this is loaded as well
var webAccount = session.GetBL<WebAccountBL>().GetWebAccountByI3D(webAccountI3D);
var rights = webAccount.WebRights.ToList(); //Make sure the rights are loaded too
var existingTicketId = this._ticketId.TryGetValue(webAccountI3D, out var i) ? i : null;
var ticket = existingTicketId switch
{
not null => session.GetBL<TicketBL>().GetTicket(existingTicketId).ThrowIfError(),
null => session.GetBL<TicketBL>().CreateNewTicket(
ApplicationKind.Developer,
LicenseGuids.Developer,
"Test-Device",
webAccountAppUser,
webAccount).ThrowIfError()
};
// Update web-account, to make sure we get current version of the web-account, with the current rights
ticket.WebAccount = webAccount;
this._ticketId[webAccountI3D] = ticket.TicketId;
return new LoggedInUser(webAccountAppUser, ticket);
}
public EndToEndTest(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
Guard.NotNull(testOutputHelper, nameof(testOutputHelper));
Database.RestoreFromSnapshot();
}
public void Sql(string statement, Action<IDbCommand> addParametersAction = null)
{
using (var session = new DAOSession())
{
session.Advanced.RawSqlAccess.ExecuteNonQueryTransactionSave(statement, addParametersAction).ThrowIfError();
}
}
public T Sql<T>(string statement, Action<IDbCommand> addParametersAction = null)
{
using (var session = new DAOSession())
{
return session.Advanced.RawSqlAccess.ExecuteScalarTransactionSave<T>(statement, addParametersAction).ThrowIfError();
}
}
public void ExecuteMatrix<T1>(T1[] input1, Action<T1> execute)
{
this.ExecuteMatrix<T1, object>(
input1,
new object[]{null},
(inputItem1, _) => execute(inputItem1));
}
public void ExecuteMatrix<T1, T2>(T1[] input1, T2[] input2, Action<T1, T2> execute)
{
this.ExecuteMatrix<T1, T2, object>(
input1,
input2,
new object[] {null},
(inputItem1, inputItem2, _) => execute(inputItem1, inputItem2));
}
public void ExecuteMatrix<T1, T2, T3>(T1[] input1, T2[] input2, T3[] input3, Action<T1, T2, T3> execute)
{
foreach (var inputItem1 in input1)
{
foreach (var inputItem2 in input2)
{
foreach (var inputItem3 in input3)
{
execute(inputItem1, inputItem2, inputItem3);
}
}
}
}
public void WithSession(Action<BLSession> action)
{
using (var session = new BLSession())
{
action(session);
}
}
public T WithSession<T>(Func<BLSession, T> getter)
{
using (var session = new BLSession())
{
return getter(session);
}
}
}
}
@@ -0,0 +1,18 @@
using System.Globalization;
using Centron.Common.UtilClasses;
namespace Centron.Tests.EndToEnd.Infrastructure.Fixtures
{
public class CultureAssemblyFixture
{
public CultureAssemblyFixture()
{
// Even if this application is started on a windows machine with different language
// The c-entron.NET and Web-Service don't work with all cultures, so use the CultureUtils to get one that is compatible with us
CultureInfo.DefaultThreadCurrentCulture = CultureUtils.GetCultureInfoForThisMachine();
// Also set the text-language
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.DefaultThreadCurrentCulture;
}
}
}
@@ -0,0 +1,19 @@
using System;
namespace Centron.Tests.EndToEnd.Infrastructure.Fixtures
{
public class DatabaseAssemblyFixture : IDisposable
{
private readonly IDisposable _database;
public DatabaseAssemblyFixture()
{
this._database = Database.Initialize();
}
public void Dispose()
{
this._database.Dispose();
}
}
}
@@ -0,0 +1,12 @@
using Centron.BusinessLogic.WebServices;
namespace Centron.Tests.EndToEnd.Infrastructure.Fixtures
{
public class InitializeObjectMapperAssemblyFixture
{
public InitializeObjectMapperAssemblyFixture()
{
ObjectMapper.InitializeAsync().Wait();
}
}
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using Centron.BusinessLogic.Administration.Licensing;
using Centron.Interfaces.Administration.Logins;
namespace Centron.Tests.EndToEnd.Infrastructure.Fixtures
{
public class LicensingAssemblyFixture
{
public LicensingAssemblyFixture()
{
var products = new Dictionary<Guid, string>
{
[LicenseGuids.Centron] = "c-entron.NET",
[LicenseGuids.RiversuiteInventory] = "Riversuite Inventory",
[LicenseGuids.TaskManagementReportServer] = "Reportserver",
[LicenseGuids.CRMPro] = "CRMPro",
[LicenseGuids.ProductionManagement] = "Production",
[LicenseGuids.ServiceBoardWebDev] = "Tasks",
[LicenseGuids.ServiceBoard] = "Serviceboard",
[LicenseGuids.MspModule] = "Modul MSP",
[LicenseGuids.DocumentProcessing] = "Modul Dokumentenweiterverarbeitung",
[LicenseGuids.WebCart2] = "WebCart 2.0",
[LicenseGuids.OnlineBanking_FinApi] = "Online-Banking (finAPI)",
[LicenseGuids.PasswordManager] = "Passwort-Manager"
};
LicenseManager.Initialize(LicenseManager.SettingsForTests(products));
LicenseManager.Instance.LoadLicenses().Wait();
}
}
}
@@ -0,0 +1,66 @@
using System;
using System.Diagnostics;
using Xunit;
using Xunit.Abstractions;
namespace Centron.Tests.EndToEnd.Infrastructure
{
public abstract class PerformanceTest : EndToEndTest
{
protected PerformanceTest(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}
public override void Execute()
{
this.Prepare();
var watch = Stopwatch.StartNew();
this.Measure();
watch.Stop();
this.AssertResults();
bool FinishedInTime(double factor)
{
return this.AllowedDuration == null ||
watch.Elapsed.TotalSeconds <= this.AllowedDuration.Value.TotalSeconds * factor;
}
bool finishedPerfect = FinishedInTime(factor: 1.0);
bool finishedGood = FinishedInTime(factor: 1.1);
bool finishedBad = FinishedInTime(factor: 1.5);
var status = finishedPerfect
? "PERFECT"
: finishedGood
? "GOOD"
: finishedBad
? "BAD"
: string.Empty;
if (string.IsNullOrWhiteSpace(status))
{
this.TestOutputHelper.WriteLine($"{this.GetType().Name} did not finish in time. Max duration is {this.AllowedDuration?.ToString() ?? "(infinite)"}, but it took {watch.Elapsed}");
}
else
{
this.TestOutputHelper.WriteLine($"{this.GetType().Name} did finish {status} in {watch.Elapsed}");
}
}
protected virtual TimeSpan? AllowedDuration { get; }
protected virtual void Prepare()
{
}
protected abstract void Measure();
protected virtual void AssertResults()
{
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Centron.Tests.EndToEnd.Infrastructure
{
public class RegexHelper
{
public const string Date = "\\d{2}\\.\\d{2}\\.\\d{4}"; //Beispiel: 01.01.2010
public const string RtfCreatedDate = "{\\\\creatim\\\\yr\\d{4}\\\\mo\\d{1,2}\\\\dy\\d{1,2}\\\\hr\\d{1,2}\\\\min\\d{1,2}}"; //Beispiel: {\\creatim\\yr2020\\mo1\\dy7\\hr13\\min41}
// Matches the whole RTF \info author region up to the trailing {\version token. This swallows the
// \upr/\ud unicode-author fallback DevExpress emits when the OS user name contains non-ANSI chars
// (e.g. "Müller"), so the normalized baseline stays "{\info{\author John Doe}{\version1}}" regardless
// of who regenerates it. Replacement is "{\info{\author John Doe}", leaving {\version...} intact.
public const string RtfInfo = "{\\\\info.*?(?={\\\\version)";
public const string Guid = "[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}";
}
}
@@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
using Xunit;
namespace Centron.Tests.EndToEnd.Infrastructure;
public sealed class SkipOnLinuxFact: FactAttribute
{
public SkipOnLinuxFact() {
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
Skip = "Ignore on Linux";
}
}
}
@@ -0,0 +1,197 @@
using Centron.DAO;
using Centron.Data.WebServices.Sales.Receipts;
using Centron.Tests.EndToEnd.Infrastructure.Verifier;
using DevExpress.CodeParser;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows.Forms;
using Xunit;
using Centron.Core;
using Centron.Core.Utils;
using DevExpress.Spreadsheet;
namespace Centron.Tests.EndToEnd.Infrastructure.Verifier
{
public class CentronVerifier
{
public CentronVerifier()
{
this.Settings = new CentronVerifierSettings();
this.Settings.IgnoreProperty<ReceiptBaseDTO>(f => f.CreatedThroughApplicationVersion);
this.Settings.IgnoreProperty<ReceiptBaseDTO>(f => f.ChangedThroughApplicationVersion);
}
public CentronVerifierSettings Settings { get; }
public bool OverrideExpectedFiles { get; set; }
public void VerifyTable(string name, string tableName, string where = null, CentronVerifierSettings settings = null, [CallerFilePath] string sourceFile = "")
{
where = where ?? "1 = 1";
string sqlStatement = $"SELECT * FROM {tableName} WHERE {where}";
this.VerifySql(name, sqlStatement, settings, sourceFile);
}
public void VerifySql(string name, string sqlStatement, CentronVerifierSettings settings = null, [CallerFilePath] string sourceFile = "")
{
using (var session = new DAOSession())
{
var table = session.Advanced.RawSqlAccess.GetRawSqlResult(sqlStatement);
this.Verify(name, table, settings, sourceFile: sourceFile);
}
}
public void VerifyExcel(string fileName, byte[] excel, [CallerFilePath] string sourceFile = "")
{
var workbook = new Workbook();
workbook.LoadDocument(excel);
var worksheet = workbook.Worksheets[0];
string[,] excelCellValues = new string[worksheet.Rows.LastUsedIndex + 1, worksheet.Columns.LastUsedIndex + 1];
for (int i = 0; i <= worksheet.Rows.LastUsedIndex; i++)
for (int j = 0; j <= worksheet.Columns.LastUsedIndex; j++)
{
var testCell = worksheet.Cells[i, j].Value.ToString();
excelCellValues[i, j] = testCell;
}
this.Verify(fileName, excelCellValues, sourceFile: sourceFile);
}
public void Verify(string name, object data, CentronVerifierSettings settings = null, [CallerFilePath] string sourceFile = "")
{
Guard.NotNullOrWhiteSpace(name, nameof(name));
//data can be NULL
//differentPerTargetFramework can be anything
//settings can be NULL
Guard.NotNullOrWhiteSpace(sourceFile, nameof(sourceFile));
if (settings != null)
{
settings.MergeWith(this.Settings);
}
else
{
settings = this.Settings;
}
var json = this.SerializeAsJson(data, settings);
var expectedFileName = this.GetExpectedFileName(name, sourceFile);
var actualFileName = this.GetActualFileName(name, sourceFile);
var directory = Path.GetDirectoryName(expectedFileName);
if (Directory.Exists(directory) == false)
Directory.CreateDirectory(directory);
if (this.OverrideExpectedFiles)
{
File.WriteAllText(expectedFileName, json, Encoding.UTF8);
return;
}
File.WriteAllText(actualFileName, json, Encoding.UTF8);
var expected = File.Exists(expectedFileName)
? File.ReadAllText(expectedFileName, Encoding.UTF8)
: string.Empty;
expected = expected.Replace(@"\r", @"");
json = json.Replace(@"\r", @"");
try
{
Assert.Equal(expected, json, ignoreLineEndingDifferences:true);
}
catch (Exception e)
{
if(Debugger.IsAttached)
this.StartDiffTool(expectedFileName, actualFileName);
Console.WriteLine(json);
throw new Exception(name + " failed", e);
}
}
private string SerializeAsJson(object data, CentronVerifierSettings verifierSettings)
{
var settings = new JsonSerializerSettings
{
Formatting = Newtonsoft.Json.Formatting.Indented,
Converters = new List<JsonConverter>
{
verifierSettings.CleanupGuidValues ? new CleanupValuesConverter<Guid>() : null,
verifierSettings.CleanupDateTimeValues ? new CleanupValuesConverter<DateTime>(new DateTimeEqualityComparer()) : null,
verifierSettings.CleanupDateTimeOffsetValues ? new CleanupValuesConverter<DateTimeOffset>(new DateTimeOffsetEqualityComparer()) : null,
}
.Where(f => f != null)
.ToList(),
ContractResolver = verifierSettings.GetContractResolver(),
};
var ignoredColumns = verifierSettings.GetIgnoredColumns();
if (ignoredColumns.Any() && data is DataTable table)
{
foreach (var ignoredColumn in ignoredColumns)
{
if (table.Columns.Contains(ignoredColumn))
table.Columns.Remove(ignoredColumn);
}
}
var serializer = JsonSerializer.Create(settings);
var builder = new StringBuilder();
using (var stringWriter = new StringWriter(builder))
using (var writer = new JsonTextWriter(stringWriter) { QuoteChar = '\'', QuoteName = false })
{
serializer.Serialize(writer, data);
}
builder.Replace(@"\\", @"\");
return builder.ToString();
}
private string GetExpectedFileName(string name, string sourceFile)
{
return $"{Path.GetDirectoryName(sourceFile)}/{name}.expected.txt";
}
private string GetActualFileName(string name, string sourceFile)
{
return $"{Path.GetDirectoryName(sourceFile)}/{name}.actual.txt";
}
private void StartDiffTool(string expectedFileName, string actualFileName)
{
try
{
if (File.Exists(expectedFileName) == false || File.Exists(actualFileName) == false)
return;
string vsPath = @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\devenv.exe";
if (File.Exists(vsPath))
ProcessHelper.Start(vsPath, $"/diff \"{actualFileName}\" \"{expectedFileName}\"");
else
ProcessHelper.Start("code", $"--diff \"{actualFileName}\" \"{expectedFileName}\"");
}
catch (Exception)
{
//Ooops
}
}
}
}
@@ -0,0 +1,273 @@
using DevExpress.CodeParser;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
using Centron.Core;
namespace Centron.Tests.EndToEnd.Infrastructure.Verifier
{
public class CentronVerifierSettings
{
private readonly List<string> _ignoredColumns;
private readonly Dictionary<Type, List<string>> _ignoredProperties;
private readonly Dictionary<(Type type, string propertyName), List<(string regex, string replace)>> _cleanupProperties;
private readonly Dictionary<Type, List<string>> _noDateCleanupProperties;
public CentronVerifierSettings()
{
this._ignoredColumns = new List<string>();
this._ignoredProperties = new Dictionary<Type, List<string>>();
this._cleanupProperties = new Dictionary<(Type type, string propertyName), List<(string regex, string replace)>>();
this._noDateCleanupProperties = new Dictionary<Type, List<string>>();
this.CleanupGuidValues = true;
this.CleanupDateTimeValues = true;
this.CleanupDateTimeOffsetValues = true;
}
public void IgnoreColumn(string columnName)
{
this._ignoredColumns.Add(columnName);
}
public void IgnoreProperties<T>(params Expression<Func<T, object>> [] properties)
{
foreach (var property in properties)
this.IgnoreProperty(property);
}
public void IgnoreProperty<T>(Expression<Func<T, object>> property)
{
Guard.NotNull(property, nameof(property));
if (this._ignoredProperties.ContainsKey(typeof(T)) == false)
{
this._ignoredProperties[typeof(T)] = new List<string>();
}
this._ignoredProperties[typeof(T)].Add(this.GetPropertyName(property));
}
public void CleanupProperty<T>(Expression<Func<T, object>> property, string regex, string replace)
{
Guard.NotNull(property, nameof(property));
Guard.NotNullOrWhiteSpace(regex, nameof(regex));
//replace can be null
var propertyName = this.GetPropertyName(property);
var key = (typeof(T), propertyName);
if (this._cleanupProperties.ContainsKey(key) == false)
{
this._cleanupProperties[key] = new List<(string regex, string replace)>();
}
this._cleanupProperties[key].Add((regex, replace));
}
public void NoDateCleanup<T>(Expression<Func<T, string>> property)
{
Guard.NotNull(property, nameof(property));
var propertyName = this.GetPropertyName(property);
if (this._noDateCleanupProperties.ContainsKey(typeof(T)) == false)
{
this._noDateCleanupProperties[typeof(T)] = new List<string>();
}
this._noDateCleanupProperties[typeof(T)].Add(propertyName);
}
public void MergeWith(CentronVerifierSettings settings)
{
Guard.NotNull(settings, nameof(settings));
foreach (var ignoredProperty in settings._ignoredProperties)
{
if (this._ignoredProperties.ContainsKey(ignoredProperty.Key) == false)
{
this._ignoredProperties.Add(ignoredProperty.Key, new List<string>(ignoredProperty.Value)); //Make sure to create a copy of the list, so we can't change the original one by accident
}
else
{
this._ignoredProperties[ignoredProperty.Key].AddRange(ignoredProperty.Value);
}
}
foreach (var cleanupProperty in settings._cleanupProperties)
{
if (this._cleanupProperties.ContainsKey(cleanupProperty.Key) == false)
{
this._cleanupProperties.Add(cleanupProperty.Key, new List<(string, string)>(cleanupProperty.Value)); //Make sure to create a copy of the list, so we can't change the original one by accident
}
else
{
this._cleanupProperties[cleanupProperty.Key].AddRange(cleanupProperty.Value);
}
}
foreach (var noDate in settings._noDateCleanupProperties)
{
if (this._noDateCleanupProperties.ContainsKey(noDate.Key) == false)
{
this._noDateCleanupProperties.Add(noDate.Key, new List<string>(noDate.Value)); //Make sure to create a copy of the list, so we can't change the original one by accident
}
else
{
this._noDateCleanupProperties[noDate.Key].AddRange(noDate.Value);
}
}
this.CleanupGuidValues = settings.CleanupGuidValues || this.CleanupGuidValues;
this.CleanupDateTimeValues = settings.CleanupDateTimeValues || this.CleanupDateTimeValues;
this.CleanupDateTimeOffsetValues = settings.CleanupDateTimeOffsetValues || this.CleanupDateTimeOffsetValues;
}
public IContractResolver GetContractResolver()
{
return new VerifierContractResolver(this);
}
public List<string> GetIgnoredColumns()
{
return this._ignoredColumns;
}
public bool CleanupGuidValues { get; set; }
public bool CleanupDateTimeValues { get; set; }
public bool CleanupDateTimeOffsetValues { get; set; }
private string GetPropertyName<T, TResult>(Expression<Func<T, TResult>> property)
{
var unary = property.Body as UnaryExpression;
var expr = property.Body as MemberExpression ?? unary?.Operand as MemberExpression;
var prop = expr.Member as PropertyInfo;
return prop.Name;
}
#region Internal
private class VerifierContractResolver : DefaultContractResolver
{
private readonly CentronVerifierSettings _settings;
public VerifierContractResolver(CentronVerifierSettings settings)
{
Guard.NotNull(settings, nameof(settings));
this._settings = settings;
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
return base.CreateProperties(type, memberSerialization).OrderBy(f => f.PropertyName).ToList();
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
foreach (var ignoredProperty in this._settings._ignoredProperties)
{
if (property.DeclaringType.IsAssignableFrom(ignoredProperty.Key) && ignoredProperty.Value.Contains(property.PropertyName))
{
property.Ignored = true;
}
}
foreach (var cleanupProperty in this._settings._cleanupProperties)
{
if (property.DeclaringType.IsAssignableFrom(cleanupProperty.Key.type) && cleanupProperty.Key.propertyName == property.PropertyName)
{
foreach (var cleanup in cleanupProperty.Value)
{
property.ValueProvider = new CleanupValueProvider(property.ValueProvider, cleanup.regex, cleanup.replace);
}
}
}
if (property.PropertyType == typeof(string))
{
bool doNotCleanupDate = this._settings._noDateCleanupProperties
.Where(f => property.DeclaringType.IsAssignableFrom(f.Key))
.Where(f => f.Value.Contains(property.PropertyName))
.Any();
if (doNotCleanupDate == false)
property.ValueProvider = new CleanupDateValueProvider(property.ValueProvider);
}
return property;
}
}
private class CleanupValueProvider : IValueProvider
{
private readonly IValueProvider _inner;
private readonly string _regex;
private readonly string _replace;
public CleanupValueProvider(IValueProvider inner, string regex, string replace)
{
Guard.NotNull(inner, nameof(inner));
Guard.NotNullOrWhiteSpace(regex, nameof(regex));
//replace can be NULL
this._inner = inner;
this._regex = regex;
this._replace = replace;
}
public object GetValue(object target)
{
var value = this._inner.GetValue(target);
if (value == null)
return null;
return Regex.Replace((string)value, this._regex, this._replace);
}
public void SetValue(object target, object value)
{
this._inner.SetValue(target, value);
}
}
private class CleanupDateValueProvider : IValueProvider
{
private readonly IValueProvider _inner;
public CleanupDateValueProvider(IValueProvider inner)
{
Guard.NotNull(inner, nameof(inner));
this._inner = inner;
}
public object GetValue(object target)
{
var value = this._inner.GetValue(target);
if (value == null)
return null;
value = Regex.Replace((string)value, RegexHelper.Date, "EinDatum");
value = Regex.Replace((string)value, RegexHelper.RtfCreatedDate, string.Empty);
return value;
}
public void SetValue(object target, object value)
{
this._inner.SetValue(target, value);
}
}
#endregion
}
}
@@ -0,0 +1,34 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Centron.Tests.EndToEnd.Infrastructure.Verifier
{
internal class CleanupValuesConverter<T> : JsonConverter
where T : struct
{
public CleanupValuesConverter(IEqualityComparer<T> equalityComparer = null)
{
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T) || objectType == typeof(T?);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotSupportedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
return;
writer.WriteRawValue(typeof(T).Name);
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace Centron.Tests.EndToEnd.Infrastructure.Verifier
{
internal class DateTimeEqualityComparer : EqualityComparer<DateTime>
{
public override bool Equals(DateTime x, DateTime y)
{
return x.Year == y.Year &&
x.Month == y.Month &&
x.Day == y.Day &&
x.Hour == y.Hour &&
x.Minute == y.Minute &&
x.Second == y.Second;
}
public override int GetHashCode(DateTime obj)
{
return Tuple.Create(obj.Year, obj.Month, obj.Day, obj.Hour, obj.Minute, obj.Second).GetHashCode();
}
}
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace Centron.Tests.EndToEnd.Infrastructure.Verifier
{
internal class DateTimeOffsetEqualityComparer : EqualityComparer<DateTimeOffset>
{
public override bool Equals(DateTimeOffset x, DateTimeOffset y)
{
return x.Year == y.Year &&
x.Month == y.Month &&
x.Day == y.Day &&
x.Hour == y.Hour &&
x.Minute == y.Minute &&
x.Second == y.Second &&
x.Offset == y.Offset;
}
public override int GetHashCode(DateTimeOffset obj)
{
return Tuple.Create(obj.Year, obj.Month, obj.Day, obj.Hour, obj.Minute, obj.Second, obj.Offset).GetHashCode();
}
}
}