From f224affedff8c995267e0f39f6bda243b2b35c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 23 Oct 2022 20:33:45 +0200 Subject: [PATCH 01/44] Adding counter infrastructure --- .../Exceptions/CounterThresholdException.cs | 50 ++++++ .../CounterDataCollectorExtensions.cs | 60 +++++++ .../NavigationUITestContextExtensions.cs | 6 +- .../Services/CounterConfiguration.cs | 62 +++++++ .../Services/CounterDataCollector.cs | 34 ++++ .../Services/Counters/CounterKey.cs | 13 ++ .../Services/Counters/CounterProbe.cs | 64 +++++++ .../Services/Counters/CounterProbeBase.cs | 39 ++++ .../Counters/Data/DbCommandCounterKey.cs | 50 ++++++ .../Counters/Data/DbExecuteCounterKey.cs | 20 +++ .../Counters/Data/DbReadCounterKey.cs | 20 +++ .../Services/Counters/Data/ProbedDbCommand.cs | 128 +++++++++++++ .../Counters/Data/ProbedDbConnection.cs | 90 ++++++++++ .../Counters/Data/ProbedDbDataReader.cs | 168 ++++++++++++++++++ .../Counters/ICounterDataCollector.cs | 29 +++ .../Services/Counters/ICounterKey.cs | 15 ++ .../Services/Counters/ICounterProbe.cs | 38 ++++ .../Services/Counters/ICounterValue.cs | 13 ++ .../Services/Counters/NavigationProbe.cs | 14 ++ .../Services/Counters/Value/CounterValue.cs | 12 ++ .../Counters/Value/IntegerCounterValue.cs | 8 + .../OrchardApplicationFactory.cs | 20 ++- .../Services/OrchardCoreInstance.cs | 9 +- .../OrchardCoreUITestExecutorConfiguration.cs | 5 + .../Services/PhaseCounterConfiguration.cs | 13 ++ .../Services/ProbedConnectionFactory.cs | 23 +++ Lombiq.Tests.UI/Services/UITestContext.cs | 9 +- .../Services/UITestExecutionSession.cs | 16 +- 28 files changed, 1016 insertions(+), 12 deletions(-) create mode 100644 Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs create mode 100644 Lombiq.Tests.UI/Extensions/CounterDataCollectorExtensions.cs create mode 100644 Lombiq.Tests.UI/Services/CounterConfiguration.cs create mode 100644 Lombiq.Tests.UI/Services/CounterDataCollector.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/CounterKey.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/CounterProbe.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/ICounterKey.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/ICounterValue.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs create mode 100644 Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs create mode 100644 Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs diff --git a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs new file mode 100644 index 000000000..694f28cca --- /dev/null +++ b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs @@ -0,0 +1,50 @@ +using Lombiq.Tests.UI.Services.Counters; +using System; +using System.Text; + +namespace Lombiq.Tests.UI.Exceptions; + +// We need constructors with required informations. +#pragma warning disable CA1032 // Implement standard exception constructors +public class CounterThresholdException : Exception +#pragma warning restore CA1032 // Implement standard exception constructors +{ + public CounterThresholdException( + ICounterProbe probe, + ICounterKey counter, + ICounterValue value) + : this(probe, counter, value, message: null, innerException: null) + { + } + + public CounterThresholdException( + ICounterProbe probe, + ICounterKey counter, + ICounterValue value, + string message) + : this(probe, counter, value, message, innerException: null) + { + } + + public CounterThresholdException( + ICounterProbe probe, + ICounterKey counter, + ICounterValue value, + string message, + Exception innerException) + : base(FormatMessage(probe, counter, value, message), innerException) + { + } + + private static string FormatMessage( + ICounterProbe probe, + ICounterKey counter, + ICounterValue value, + string message) => + new StringBuilder() + .AppendLine(probe.DumpHeadline()) + .AppendLine(counter.Dump()) + .AppendLine(value.Dump()) + .AppendLine(message ?? string.Empty) + .ToString(); +} diff --git a/Lombiq.Tests.UI/Extensions/CounterDataCollectorExtensions.cs b/Lombiq.Tests.UI/Extensions/CounterDataCollectorExtensions.cs new file mode 100644 index 000000000..57ace2172 --- /dev/null +++ b/Lombiq.Tests.UI/Extensions/CounterDataCollectorExtensions.cs @@ -0,0 +1,60 @@ +using Lombiq.Tests.UI.Services.Counters; +using Lombiq.Tests.UI.Services.Counters.Data; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.Extensions; + +public static class CounterDataCollectorExtensions +{ + public static int DbCommandExecuteNonQuery(this ICounterDataCollector collector, DbCommand dbCommand) + { + collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteNonQuery(); + } + + public static Task DbCommandExecuteNonQueryAsync( + this ICounterDataCollector collector, + DbCommand dbCommand, + CancellationToken cancellationToken) + { + collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteNonQueryAsync(cancellationToken); + } + + public static object DbCommandExecuteScalar(this ICounterDataCollector collector, DbCommand dbCommand) + { + collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteScalar(); + } + + public static Task DbCommandExecuteScalarAsync( + this ICounterDataCollector collector, + DbCommand dbCommand, + CancellationToken cancellationToken) + { + collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteScalarAsync(cancellationToken); + } + + public static DbDataReader DbCommandExecuteDbDatareader( + this ICounterDataCollector collector, + DbCommand dbCommand, + CommandBehavior behavior) + { + collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteReader(behavior); + } + + public static Task DbCommandExecuteDbDatareaderAsync( + this ICounterDataCollector collector, + DbCommand dbCommand, + CommandBehavior behavior, + CancellationToken cancellationToken) + { + collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteReaderAsync(behavior, cancellationToken); + } +} diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index 48341e808..3bc1ac9b0 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -2,6 +2,7 @@ using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Pages; using Lombiq.Tests.UI.Services; +using Lombiq.Tests.UI.Services.Counters; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using System; @@ -39,7 +40,10 @@ public static Task GoToAbsoluteUrlAsync(this UITestContext context, Uri absolute await context.Configuration.Events.BeforeNavigation .InvokeAsync(eventHandler => eventHandler(context, absoluteUri)); - context.Driver.Navigate().GoToUrl(absoluteUri); + using (new NavigationProbe(context.CounterDataCollector, absoluteUri)) + { + context.Driver.Navigate().GoToUrl(absoluteUri); + } await context.Configuration.Events.AfterNavigation .InvokeAsync(eventHandler => eventHandler(context, absoluteUri)); diff --git a/Lombiq.Tests.UI/Services/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/CounterConfiguration.cs new file mode 100644 index 000000000..da39a36f9 --- /dev/null +++ b/Lombiq.Tests.UI/Services/CounterConfiguration.cs @@ -0,0 +1,62 @@ +using Lombiq.Tests.UI.Exceptions; +using Lombiq.Tests.UI.Services.Counters; +using Lombiq.Tests.UI.Services.Counters.Data; +using Lombiq.Tests.UI.Services.Counters.Value; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lombiq.Tests.UI.Services; + +public class CounterConfiguration +{ + /// + /// Gets the counter configuration used in the setup phase. + /// + public PhaseCounterConfiguration Setup { get; } = new(); + + /// + /// Gets the counter configuration used in the running phase. + /// + public PhaseCounterConfiguration Running { get; } = new(); + + public static Action DefaultAssertCounterData(PhaseCounterConfiguration configuration) => + probe => + { + if (probe is NavigationProbe or CounterDataCollector) + { + var executeThreshold = probe is NavigationProbe + ? configuration.DbCommandExecutionRepetitionPerNavigationThreshold + : configuration.DbCommandExecutionRepetitionThreshold; + var executeThresholdName = probe is NavigationProbe + ? nameof(configuration.DbCommandExecutionRepetitionPerNavigationThreshold) + : nameof(configuration.DbCommandExecutionRepetitionThreshold); + var readThreshold = probe is NavigationProbe + ? configuration.DbReaderReadPerNavigationThreshold + : configuration.DbReaderReadThreshold; + var readThresholdName = probe is NavigationProbe + ? nameof(configuration.DbReaderReadPerNavigationThreshold) + : nameof(configuration.DbReaderReadThreshold); + + AssertIntegerCounterValue(probe, executeThresholdName, executeThreshold); + AssertIntegerCounterValue(probe, readThresholdName, readThreshold); + } + }; + + public static void AssertIntegerCounterValue(ICounterProbe probe, string thresholdName, int threshold) + where TKey : ICounterKey => + probe.Counters.Keys + .OfType() + .ForEach(key => + { + if (probe.Counters[key] is IntegerCounterValue counterValue + && counterValue.Value > threshold) + { + throw new CounterThresholdException( + probe, + key, + counterValue, + $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}"); + } + }); +} diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs new file mode 100644 index 000000000..fad375359 --- /dev/null +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -0,0 +1,34 @@ +using Lombiq.Tests.UI.Services.Counters; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Lombiq.Tests.UI.Services; + +public sealed class CounterDataCollector : CounterProbeBase, ICounterDataCollector +{ + private readonly ConcurrentBag _probes = new(); + public override bool IsRunning => true; + public Action AssertCounterData { get; set; } + + public void AttachProbe(ICounterProbe probe) => _probes.Add(probe); + + public void Reset() + { + _probes.Clear(); + Clear(); + } + + public override void Increment(ICounterKey counter) + { + _probes.SelectWhere(probe => probe, probe => probe.IsRunning) + .ForEach(probe => probe.Increment(counter)); + + base.Increment(counter); + } + + public override string DumpHeadline() => nameof(CounterDataCollector); + public override string Dump() => DumpHeadline(); + public void AssertCounter(ICounterProbe probe) => AssertCounterData?.Invoke(probe); + public void AssertCounter() => AssertCounter(this); +} diff --git a/Lombiq.Tests.UI/Services/Counters/CounterKey.cs b/Lombiq.Tests.UI/Services/Counters/CounterKey.cs new file mode 100644 index 000000000..73e505d9c --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/CounterKey.cs @@ -0,0 +1,13 @@ +namespace Lombiq.Tests.UI.Services.Counters; + +// The Equals must be implemented in consumer classes. +#pragma warning disable S4035 // Classes implementing "IEquatable" should be sealed +public abstract class CounterKey : ICounterKey +#pragma warning restore S4035 // Classes implementing "IEquatable" should be sealed +{ + public abstract bool Equals(ICounterKey other); + protected abstract int HashCode(); + public override bool Equals(object obj) => Equals(obj as ICounterKey); + public override int GetHashCode() => HashCode(); + public abstract string Dump(); +} diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs new file mode 100644 index 000000000..7e292ccf0 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs @@ -0,0 +1,64 @@ +using System; +using System.Text; + +namespace Lombiq.Tests.UI.Services.Counters; + +public abstract class CounterProbe : CounterProbeBase, IDisposable +{ + private bool _disposed; + + public override bool IsRunning => !_disposed; + public ICounterDataCollector CounterDataCollector { get; init; } + + public override string Dump() + { + var builder = new StringBuilder(); + + builder.AppendLine(DumpHeadline()); + + foreach (var entry in Counters) + { + builder.AppendLine(entry.Key.Dump()) + .AppendLine(entry.Value.Dump()); + } + + return builder.ToString(); + } + + protected CounterProbe(ICounterDataCollector counterDataCollector) + { + CounterDataCollector = counterDataCollector; + CounterDataCollector.AttachProbe(this); + } + + protected virtual void OnAssertData() => + CounterDataCollector.AssertCounter(this); + + protected virtual void OnDispose() + { + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + try { OnAssertData(); } + catch { throw; } + finally + { + if (disposing) + { + OnDispose(); + } + + _disposed = true; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs new file mode 100644 index 000000000..4e471fc1e --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs @@ -0,0 +1,39 @@ +using Lombiq.Tests.UI.Services.Counters.Value; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Lombiq.Tests.UI.Services.Counters; + +public abstract class CounterProbeBase : ICounterProbe +{ + private readonly ConcurrentDictionary _counters = new(); + + public abstract bool IsRunning { get; } + public IDictionary Counters => _counters; + + protected void Clear() => _counters.Clear(); + + public virtual void Increment(ICounterKey counter) => + _counters.AddOrUpdate( + counter, + new IntegerCounterValue { Value = 1 }, + (_, current) => TryConvertAndUpdate(current, current => current.Value++)); + + public abstract string DumpHeadline(); + + public abstract string Dump(); + + private static TCounter TryConvertAndUpdate(ICounterValue counter, Action update) + { + if (counter is not TCounter value) + { + throw new ArgumentException( + $"The type of ${nameof(counter)} is not compatible with ${typeof(TCounter).Name}"); + } + + update.Invoke(value); + + return value; + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs new file mode 100644 index 000000000..5161ae4b1 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +public abstract class DbCommandCounterKey : CounterKey +{ + private readonly List> _parameters = new(); + public string CommandText { get; private set; } + public IEnumerable> Parameters => _parameters; + + protected DbCommandCounterKey(string commandText, IEnumerable> parameters) + { + _parameters.AddRange(parameters); + CommandText = commandText; + } + + public override bool Equals(ICounterKey other) + { + if (ReferenceEquals(this, other)) return true; + + return other is DbExecuteCounterKey otherKey + && GetType() == otherKey.GetType() + && CommandText == otherKey.CommandText + && Parameters + .Select(param => (param.Key, param.Value)) + .SequenceEqual(otherKey.Parameters.Select(param => (param.Key, param.Value))); + } + + public override string Dump() + { + var builder = new StringBuilder(); + + builder.AppendLine(GetType().Name) + .AppendLine(CultureInfo.InvariantCulture, $"\t{CommandText}"); + var commandParams = Parameters.Select((parameter, index) => + FormattableString.Invariant( + $"[{index.ToTechnicalString()}]{parameter.Key ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) + .Join(", "); + builder.AppendLine(CultureInfo.InvariantCulture, $"\t\t{commandParams}"); + + return builder.ToString(); + } + + protected override int HashCode() => StringComparer.Ordinal.GetHashCode(CommandText); + public override string ToString() => $"[{GetType().Name}] {CommandText}"; +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs new file mode 100644 index 000000000..64b79e8ff --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +public sealed class DbExecuteCounterKey : DbCommandCounterKey +{ + private DbExecuteCounterKey(string commandText, IEnumerable> parameters) + : base(commandText, parameters) + { + } + + public static DbExecuteCounterKey CreateFrom(DbCommand dbCommand) => + new( + dbCommand.CommandText, + dbCommand.Parameters + .OfType() + .Select(parameter => new KeyValuePair(parameter.ParameterName, parameter.Value))); +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs new file mode 100644 index 000000000..9cd0a2b97 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +public class DbReadCounterKey : DbCommandCounterKey +{ + private DbReadCounterKey(string commandText, IEnumerable> parameters) + : base(commandText, parameters) + { + } + + public static DbReadCounterKey CreateFrom(DbCommand dbCommand) => + new( + dbCommand.CommandText, + dbCommand.Parameters + .OfType() + .Select(parameter => new KeyValuePair(parameter.ParameterName, parameter.Value))); +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs new file mode 100644 index 000000000..c6947519e --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs @@ -0,0 +1,128 @@ +using Lombiq.Tests.UI.Extensions; +using System; +using System.ComponentModel; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +[DesignerCategory("")] +public class ProbedDbCommand : DbCommand +{ + private readonly CounterDataCollector _counterDataCollector; + private DbConnection _dbConnection; + + internal DbCommand ProbedCommand { get; private set; } + + public override string CommandText + { + get => ProbedCommand.CommandText; + set => ProbedCommand.CommandText = value; + } + + public override int CommandTimeout + { + get => ProbedCommand.CommandTimeout; + set => ProbedCommand.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => ProbedCommand.CommandType; + set => ProbedCommand.CommandType = value; + } + + protected override DbConnection DbConnection + { + get => _dbConnection; + set + { + _dbConnection = value; + UnwrapAndAssignConnection(value); + } + } + + protected override DbParameterCollection DbParameterCollection => ProbedCommand.Parameters; + + protected override DbTransaction DbTransaction + { + get => ProbedCommand.Transaction; + set => ProbedCommand.Transaction = value; + } + + public override bool DesignTimeVisible + { + get => ProbedCommand.DesignTimeVisible; + set => ProbedCommand.DesignTimeVisible = value; + } + + public override UpdateRowSource UpdatedRowSource + { + get => ProbedCommand.UpdatedRowSource; + set => ProbedCommand.UpdatedRowSource = value; + } + + public ProbedDbCommand(DbCommand command, DbConnection connection, CounterDataCollector counterDataCollector) + { + _counterDataCollector = counterDataCollector ?? throw new ArgumentNullException(nameof(counterDataCollector)); + ProbedCommand = command ?? throw new ArgumentNullException(nameof(command)); + + if (connection != null) + { + _dbConnection = connection; + UnwrapAndAssignConnection(connection); + } + } + + public override int ExecuteNonQuery() => + _counterDataCollector.DbCommandExecuteNonQuery(ProbedCommand); + + public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) => + _counterDataCollector.DbCommandExecuteNonQueryAsync(ProbedCommand, cancellationToken); + + public override object ExecuteScalar() => + _counterDataCollector.DbCommandExecuteScalar(ProbedCommand); + + public override Task ExecuteScalarAsync(CancellationToken cancellationToken) => + _counterDataCollector.DbCommandExecuteScalarAsync(ProbedCommand, cancellationToken); + + public override void Cancel() => ProbedCommand.Cancel(); + + public override void Prepare() => ProbedCommand.Prepare(); + + protected override DbParameter CreateDbParameter() => ProbedCommand.CreateParameter(); + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => + new ProbedDbDataReader( + _counterDataCollector.DbCommandExecuteDbDatareader(ProbedCommand, behavior), + behavior, + this, + _counterDataCollector); + + protected override async Task ExecuteDbDataReaderAsync( + CommandBehavior behavior, + CancellationToken cancellationToken) => + new ProbedDbDataReader( + await _counterDataCollector.DbCommandExecuteDbDatareaderAsync(ProbedCommand, behavior, cancellationToken), + behavior, + this, + _counterDataCollector); + + private void UnwrapAndAssignConnection(DbConnection value) => + ProbedCommand.Connection = value is ProbedDbConnection probedConnection + ? probedConnection.ProbedConnection + : value; + + protected override void Dispose(bool disposing) + { + if (disposing && ProbedCommand != null) + { + ProbedCommand.Dispose(); + } + + ProbedCommand = null; + base.Dispose(disposing); + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs new file mode 100644 index 000000000..673eb79a3 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs @@ -0,0 +1,90 @@ +using System; +using System.ComponentModel; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +[DesignerCategory("")] +public class ProbedDbConnection : DbConnection +{ + private readonly CounterDataCollector _counterDataCollector; + + internal DbConnection ProbedConnection { get; private set; } + + public override string ConnectionString + { + get => ProbedConnection.ConnectionString; + set => ProbedConnection.ConnectionString = value; + } + + public override int ConnectionTimeout => ProbedConnection.ConnectionTimeout; + + public override string Database => ProbedConnection.Database; + + public override string DataSource => ProbedConnection.DataSource; + + public override string ServerVersion => ProbedConnection.ServerVersion; + + public override ConnectionState State => ProbedConnection.State; + + protected override bool CanRaiseEvents => true; + + public ProbedDbConnection(DbConnection connection, CounterDataCollector counterDataCollector) + { + _counterDataCollector = counterDataCollector ?? throw new ArgumentNullException(nameof(counterDataCollector)); + ProbedConnection = connection ?? throw new ArgumentNullException(nameof(connection)); + ProbedConnection.StateChange += StateChangeHandler; + } + + public override void ChangeDatabase(string databaseName) => + ProbedConnection.ChangeDatabase(databaseName); + + public override void Close() => + ProbedConnection.Close(); + + public override void Open() => + ProbedConnection.Open(); + + public override Task OpenAsync(CancellationToken cancellationToken) => + ProbedConnection.OpenAsync(cancellationToken); + + protected override DbTransaction BeginDbTransaction(System.Data.IsolationLevel isolationLevel) => + ProbedConnection.BeginTransaction(isolationLevel); + + protected virtual DbCommand CreateDbCommand(DbCommand original) => + new ProbedDbCommand(original, this, _counterDataCollector); + + protected override DbCommand CreateDbCommand() => + CreateDbCommand(ProbedConnection.CreateCommand()); + + private void StateChangeHandler(object sender, StateChangeEventArgs stateChangeEventArguments) => + OnStateChange(stateChangeEventArguments); + + public override void EnlistTransaction(Transaction transaction) => + ProbedConnection.EnlistTransaction(transaction); + + public override DataTable GetSchema() => + ProbedConnection.GetSchema(); + + public override DataTable GetSchema(string collectionName) => + ProbedConnection.GetSchema(collectionName); + + public override DataTable GetSchema(string collectionName, string[] restrictionValues) => + ProbedConnection.GetSchema(collectionName, restrictionValues); + + protected override void Dispose(bool disposing) + { + if (disposing && ProbedConnection != null) + { + ProbedConnection.StateChange -= StateChangeHandler; + ProbedConnection.Dispose(); + } + + base.Dispose(disposing); + ProbedConnection = null; + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs new file mode 100644 index 000000000..5bbf7ec1a --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +// Generic IEnumerable<> interface is not implemented in DbDatareader. +#pragma warning disable CA1010 // Generic interface should also be implemented +public class ProbedDbDataReader : DbDataReader +#pragma warning restore CA1010 // Generic interface should also be implemented +{ + private CounterDataCollector CounterDataCollector { get; init; } + + private ProbedDbCommand ProbedCommand { get; init; } + + private DbReadCounterKey CounterKey { get; init; } + + internal DbDataReader ProbedReader { get; private set; } + + public CommandBehavior Behavior { get; } + + public override int Depth => ProbedReader.Depth; + + public override int FieldCount => ProbedReader.FieldCount; + + public override bool HasRows => ProbedReader.HasRows; + + public override bool IsClosed => ProbedReader.IsClosed; + + public override int RecordsAffected => ProbedReader.RecordsAffected; + + public override object this[string name] => ProbedReader[name]; + + public override object this[int ordinal] => ProbedReader[ordinal]; + + public ProbedDbDataReader( + DbDataReader reader, + ProbedDbCommand probedCommand, + CounterDataCollector counterDataCollector) + : this(reader, CommandBehavior.Default, probedCommand, counterDataCollector) { } + + public ProbedDbDataReader( + DbDataReader reader, + CommandBehavior behavior, + ProbedDbCommand probedCommand, + CounterDataCollector counterDataCollector) + { + CounterDataCollector = counterDataCollector ?? throw new ArgumentNullException(nameof(counterDataCollector)); + ProbedCommand = probedCommand ?? throw new ArgumentNullException(nameof(probedCommand)); + ProbedReader = reader ?? throw new ArgumentNullException(nameof(reader)); + Behavior = behavior; + CounterKey = DbReadCounterKey.CreateFrom(ProbedCommand); + } + + public override bool GetBoolean(int ordinal) => ProbedReader.GetBoolean(ordinal); + + public override byte GetByte(int ordinal) => ProbedReader.GetByte(ordinal); + + public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) => + ProbedReader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); + + public override char GetChar(int ordinal) => ProbedReader.GetChar(ordinal); + + public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) => + ProbedReader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); + + public new DbDataReader GetData(int ordinal) => ProbedReader.GetData(ordinal); + + public override string GetDataTypeName(int ordinal) => ProbedReader.GetDataTypeName(ordinal); + + public override DateTime GetDateTime(int ordinal) => ProbedReader.GetDateTime(ordinal); + + public override decimal GetDecimal(int ordinal) => ProbedReader.GetDecimal(ordinal); + + public override double GetDouble(int ordinal) => ProbedReader.GetDouble(ordinal); + + public override IEnumerator GetEnumerator() => new ReaderEnumerator(this); + + public override Type GetFieldType(int ordinal) => ProbedReader.GetFieldType(ordinal); + + public override T GetFieldValue(int ordinal) => ProbedReader.GetFieldValue(ordinal); + + public override Task GetFieldValueAsync(int ordinal, CancellationToken cancellationToken) => + ProbedReader.GetFieldValueAsync(ordinal, cancellationToken); + + public override float GetFloat(int ordinal) => ProbedReader.GetFloat(ordinal); + + public override Guid GetGuid(int ordinal) => ProbedReader.GetGuid(ordinal); + + public override short GetInt16(int ordinal) => ProbedReader.GetInt16(ordinal); + + public override int GetInt32(int ordinal) => ProbedReader.GetInt32(ordinal); + + public override long GetInt64(int ordinal) => ProbedReader.GetInt64(ordinal); + + public override string GetName(int ordinal) => ProbedReader.GetName(ordinal); + + public override int GetOrdinal(string name) => ProbedReader.GetOrdinal(name); + + public override string GetString(int ordinal) => ProbedReader.GetString(ordinal); + + public override object GetValue(int ordinal) => ProbedReader.GetValue(ordinal); + + public override int GetValues(object[] values) => ProbedReader.GetValues(values); + + public override bool IsDBNull(int ordinal) => ProbedReader.IsDBNull(ordinal); + + public override Task IsDBNullAsync(int ordinal, CancellationToken cancellationToken) => + ProbedReader.IsDBNullAsync(ordinal, cancellationToken); + + public override bool NextResult() => ProbedReader.NextResult(); + + public override Task NextResultAsync(CancellationToken cancellationToken) => + ProbedReader.NextResultAsync(cancellationToken); + + public override bool Read() + { + var result = ProbedReader.Read(); + if (result) CounterDataCollector.Increment(CounterKey); + + return result; + } + + public override async Task ReadAsync(CancellationToken cancellationToken) + { + var result = await ProbedReader.ReadAsync(cancellationToken); + if (result) CounterDataCollector.Increment(CounterKey); + + return result; + } + + public override void Close() => ProbedReader.Close(); + + public override DataTable GetSchemaTable() => ProbedReader.GetSchemaTable(); + + protected override void Dispose(bool disposing) + { + ProbedReader.Dispose(); + base.Dispose(disposing); + } + + private sealed class ReaderEnumerator : IEnumerator + { + private readonly ProbedDbDataReader _probedReader; + private readonly IEnumerator _probedEnumerator; + + public ReaderEnumerator(ProbedDbDataReader probedReader) + { + _probedReader = probedReader; + _probedEnumerator = (probedReader.ProbedReader as IEnumerable).GetEnumerator(); + } + + public object Current => _probedEnumerator.Current; + + public bool MoveNext() + { + var haveNext = _probedEnumerator.MoveNext(); + if (haveNext) _probedReader.CounterDataCollector.Increment(_probedReader.CounterKey); + + return haveNext; + } + + public void Reset() => _probedEnumerator.Reset(); + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs b/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs new file mode 100644 index 000000000..cad73948e --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs @@ -0,0 +1,29 @@ +namespace Lombiq.Tests.UI.Services.Counters; + +/// +/// Represents a data collector which collects and asserts data from probes attached to it. +/// +public interface ICounterDataCollector : ICounterProbe +{ + /// + /// Attaches a instance given by to the current collector instance. + /// + /// The instance to attach. + void AttachProbe(ICounterProbe probe); + + /// + /// Resets the collected counters and probes. + /// + void Reset(); + + /// + /// Asserts the data collected by . + /// + /// The instance to assert. + void AssertCounter(ICounterProbe probe); + + /// + /// Asserts the data collected by the instance. + /// + void AssertCounter(); +} diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs new file mode 100644 index 000000000..72cad3d02 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs @@ -0,0 +1,15 @@ +using System; + +namespace Lombiq.Tests.UI.Services.Counters; + +/// +/// Represents a key in . +/// +public interface ICounterKey : IEquatable +{ + /// + /// Dumps the key content to a human readable format. + /// + /// A human readable string representation of instance. + string Dump(); +} diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs new file mode 100644 index 000000000..5291a71d3 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace Lombiq.Tests.UI.Services.Counters; + +/// +/// Represents a probe for the counter infrastructure. +/// +public interface ICounterProbe +{ + /// + /// Gets a value indicating whether the instance is running. + /// + bool IsRunning { get; } + + /// + /// Gets the collected values. + /// + IDictionary Counters { get; } + + /// + /// Increments the selected by . If the + /// does not exists, creates a new instance. + /// + /// The counter key. + void Increment(ICounterKey counter); + + /// + /// Dumps the probe headline to a human readable format. + /// + /// A human readable string representation of instance in one line. + public string DumpHeadline(); + + /// + /// Dumps the probe content to a human readable format. + /// + /// A human readable string representation of instance. + string Dump(); +} diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs new file mode 100644 index 000000000..dd44e1678 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs @@ -0,0 +1,13 @@ +namespace Lombiq.Tests.UI.Services.Counters; + +/// +/// Represents a value in . +/// +public interface ICounterValue +{ + /// + /// Dumps the value content to a human readable format. + /// + /// A human readable string representation of instance. + string Dump(); +} diff --git a/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs new file mode 100644 index 000000000..b459772c3 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs @@ -0,0 +1,14 @@ +using System; + +namespace Lombiq.Tests.UI.Services.Counters; + +public class NavigationProbe : CounterProbe +{ + public Uri AbsoluteUri { get; init; } + + public NavigationProbe(ICounterDataCollector counterDataCollector, Uri absoluteUri) + : base(counterDataCollector) => + AbsoluteUri = absoluteUri; + + public override string DumpHeadline() => $"{nameof(NavigationProbe)}, AbsoluteUri={AbsoluteUri}"; +} diff --git a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs new file mode 100644 index 000000000..4cc41da40 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs @@ -0,0 +1,12 @@ +using System; + +namespace Lombiq.Tests.UI.Services.Counters.Value; + +public abstract class CounterValue : ICounterValue + where TValue : struct +{ + public TValue Value { get; set; } + + public virtual string Dump() => + FormattableString.Invariant($"{GetType().Name} value: {Value}"); +} diff --git a/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs new file mode 100644 index 000000000..5a55355df --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs @@ -0,0 +1,8 @@ +using System; + +namespace Lombiq.Tests.UI.Services.Counters.Value; + +public class IntegerCounterValue : CounterValue +{ + public override string ToString() => Value.ToTechnicalString(); +} diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index d6a8b7248..a6f0be589 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -1,4 +1,5 @@ using Lombiq.Tests.Integration.Services; +using Lombiq.Tests.UI.Services.Counters; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Testing; @@ -11,6 +12,7 @@ using NLog; using NLog.Web; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -24,14 +26,17 @@ public sealed class OrchardApplicationFactory : WebApplicationFactory< { private readonly Action _configuration; private readonly Action _configureOrchard; - private readonly List _createdStores = new(); + private readonly ConcurrentBag _createdStores = new(); + private readonly CounterDataCollector _counterDataCollector; public OrchardApplicationFactory( + CounterDataCollector counterDataCollector, Action configuration = null, Action configureOrchard = null) { _configuration = configuration; _configureOrchard = configureOrchard; + _counterDataCollector = counterDataCollector; } public Uri BaseAddress => ClientOptions.BaseAddress; @@ -90,13 +95,14 @@ private void AddFakeStore(IServiceCollection services) return null; } - lock (_createdStores) - { - var fakeStore = new FakeStore((IStore)storeDescriptor.ImplementationFactory.Invoke(serviceProvider)); - _createdStores.Add(fakeStore); + store.Configuration.ConnectionFactory = new ProbedConnectionFactory( + store.Configuration.ConnectionFactory, + _counterDataCollector); - return fakeStore; - } + var fakeStore = new FakeStore(store); + _createdStores.Add(fakeStore); + + return fakeStore; }); } diff --git a/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs b/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs index d2cc957c6..e9cb2c03e 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs @@ -59,17 +59,23 @@ public sealed class OrchardCoreInstance : IWebApplicationInstance private readonly OrchardCoreConfiguration _configuration; private readonly string _contextId; private readonly ITestOutputHelper _testOutputHelper; + private readonly CounterDataCollector _counterDataCollector; private string _contentRootPath; private bool _isDisposed; private OrchardApplicationFactory _orchardApplication; private string _url; private TestReverseProxy _reverseProxy; - public OrchardCoreInstance(OrchardCoreConfiguration configuration, string contextId, ITestOutputHelper testOutputHelper) + public OrchardCoreInstance( + OrchardCoreConfiguration configuration, + string contextId, + ITestOutputHelper testOutputHelper, + CounterDataCollector counterDataCollector) { _configuration = configuration; _contextId = contextId; _testOutputHelper = testOutputHelper; + _counterDataCollector = counterDataCollector; } public IServiceProvider Services => _orchardApplication?.Services; @@ -181,6 +187,7 @@ await _configuration.BeforeAppStart Path.Combine(Path.GetDirectoryName(typeof(OrchardCoreInstance<>).Assembly.Location), "refs"), 60); _orchardApplication = new OrchardApplicationFactory( + _counterDataCollector, builder => builder .UseContentRoot(_contentRootPath) .UseWebRoot(Path.Combine(_contentRootPath, "wwwroot")) diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index 79eae2b78..d1886d9e0 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -141,6 +141,11 @@ public class OrchardCoreUITestExecutorConfiguration Justification = "Deliberately modifiable by consumer code.")] public Dictionary CustomConfiguration { get; } = new(); + /// + /// Gets or sets configuration for performance counting and monitoring. + /// + public CounterConfiguration CounterConfiguration { get; set; } = new(); + public async Task AssertAppLogsMaybeAsync(IWebApplicationInstance instance, Action log) { if (instance == null || AssertAppLogsAsync == null) return; diff --git a/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs new file mode 100644 index 000000000..e1d231628 --- /dev/null +++ b/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs @@ -0,0 +1,13 @@ +using Lombiq.Tests.UI.Services.Counters; +using System; + +namespace Lombiq.Tests.UI.Services; + +public class PhaseCounterConfiguration +{ + public Action AssertCounterData { get; set; } + public int DbCommandExecutionRepetitionPerNavigationThreshold { get; set; } = 11; + public int DbCommandExecutionRepetitionThreshold { get; set; } = 22; + public int DbReaderReadPerNavigationThreshold { get; set; } = 11; + public int DbReaderReadThreshold { get; set; } = 11; +} diff --git a/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs new file mode 100644 index 000000000..1f4dc3afd --- /dev/null +++ b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs @@ -0,0 +1,23 @@ +using Lombiq.Tests.UI.Services.Counters.Data; +using System; +using System.Data.Common; +using YesSql; + +namespace Lombiq.Tests.UI.Services; + +public class ProbedConnectionFactory : IConnectionFactory +{ + private readonly IConnectionFactory _connectionFactory; + private readonly CounterDataCollector _counterDataCollector; + + public Type DbConnectionType => typeof(ProbedDbConnection); + + public ProbedConnectionFactory(IConnectionFactory connectionFactory, CounterDataCollector counterDataCollector) + { + _connectionFactory = connectionFactory; + _counterDataCollector = counterDataCollector; + } + + public DbConnection CreateConnection() => + new ProbedDbConnection(_connectionFactory.CreateConnection(), _counterDataCollector); +} diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index 538cf2280..f66b79091 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -95,13 +95,19 @@ public class UITestContext /// public string AdminUrlPrefix { get; set; } = "/Admin"; + /// + /// Gets the currently running instance. + /// + public CounterDataCollector CounterDataCollector { get; init; } + public UITestContext( string id, UITestManifest testManifest, OrchardCoreUITestExecutorConfiguration configuration, IWebApplicationInstance application, AtataScope scope, - RunningContextContainer runningContextContainer) + RunningContextContainer runningContextContainer, + CounterDataCollector counterDataCollector) { Id = id; TestManifest = testManifest; @@ -111,6 +117,7 @@ public UITestContext( Scope = scope; SmtpServiceRunningContext = runningContextContainer.SmtpServiceRunningContext; AzureBlobStorageRunningContext = runningContextContainer.AzureBlobStorageRunningContext; + CounterDataCollector = counterDataCollector; } /// diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 5a17e0307..4227ef57c 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -103,6 +103,9 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) _context ??= await CreateContextAsync(); _context.FailureDumpContainer.Clear(); + _context.CounterDataCollector.Reset(); + _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Running.AssertCounterData + ?? CounterConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Running); failureDumpContainer = _context.FailureDumpContainer; _context.SetDefaultBrowserSize(); @@ -110,6 +113,7 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) await _testManifest.TestAsync(_context); await _context.AssertLogsAsync(); + _context.CounterDataCollector.AssertCounter(); return true; } @@ -443,6 +447,8 @@ private async Task SetupAsync() // Note that the context creation needs to be done here too because the Orchard app needs the snapshot // config to be available at startup too. _context = await CreateContextAsync(); + _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Setup.AssertCounterData + ?? CounterConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Setup); SetupSqlServerSnapshot(); SetupAzureBlobStorageSnapshot(); @@ -452,6 +458,7 @@ private async Task SetupAsync() var result = (_context, await setupConfiguration.SetupOperation(_context)); await _context.AssertLogsAsync(); + _context.CounterDataCollector.AssertCounter(); _testOutputHelper.WriteLineTimestampedAndDebug("Finished setup operation."); return result; @@ -470,6 +477,7 @@ private async Task SetupAsync() await _context.GoToRelativeUrlAsync(resultUri.PathAndQuery); } + catch (CounterThresholdException) { throw; } catch (Exception ex) when (ex is not SetupFailedFastException) { if (setupConfiguration.FastFailSetup) @@ -573,10 +581,13 @@ Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommand _configuration.OrchardCoreConfiguration.BeforeAppStart.RemoveAll(UITestingBeforeAppStartHandlerAsync); _configuration.OrchardCoreConfiguration.BeforeAppStart += UITestingBeforeAppStartHandlerAsync; + var counterDataCollector = new CounterDataCollector(); + _applicationInstance = new OrchardCoreInstance( _configuration.OrchardCoreConfiguration, contextId, - _testOutputHelper); + _testOutputHelper, + counterDataCollector); var uri = await _applicationInstance.StartUpAsync(); _configuration.SetUpEvents(); @@ -611,7 +622,8 @@ Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommand _configuration, _applicationInstance, atataScope, - new RunningContextContainer(sqlServerContext, smtpContext, azureBlobStorageContext)); + new RunningContextContainer(sqlServerContext, smtpContext, azureBlobStorageContext), + counterDataCollector); } private string GetSetupHashCode() => From 34b538b4a07297b8e0b5f76309020d0251e2461e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 25 Oct 2022 01:06:29 +0200 Subject: [PATCH 02/44] Using interfaces --- Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs | 7 +++++-- .../Services/Counters/Data/ProbedDbConnection.cs | 4 ++-- .../Services/Counters/Data/ProbedDbDataReader.cs | 6 +++--- .../OrchardCoreHosting/OrchardApplicationFactory.cs | 4 ++-- Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs | 5 +++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs index c6947519e..061477be1 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs @@ -11,16 +11,19 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; [DesignerCategory("")] public class ProbedDbCommand : DbCommand { - private readonly CounterDataCollector _counterDataCollector; + private readonly ICounterDataCollector _counterDataCollector; private DbConnection _dbConnection; internal DbCommand ProbedCommand { get; private set; } + // The ProbedDbCommand scope is performance counting. +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities public override string CommandText { get => ProbedCommand.CommandText; set => ProbedCommand.CommandText = value; } +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities public override int CommandTimeout { @@ -64,7 +67,7 @@ public override UpdateRowSource UpdatedRowSource set => ProbedCommand.UpdatedRowSource = value; } - public ProbedDbCommand(DbCommand command, DbConnection connection, CounterDataCollector counterDataCollector) + public ProbedDbCommand(DbCommand command, DbConnection connection, ICounterDataCollector counterDataCollector) { _counterDataCollector = counterDataCollector ?? throw new ArgumentNullException(nameof(counterDataCollector)); ProbedCommand = command ?? throw new ArgumentNullException(nameof(command)); diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs index 673eb79a3..432621b1d 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs @@ -11,7 +11,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; [DesignerCategory("")] public class ProbedDbConnection : DbConnection { - private readonly CounterDataCollector _counterDataCollector; + private readonly ICounterDataCollector _counterDataCollector; internal DbConnection ProbedConnection { get; private set; } @@ -33,7 +33,7 @@ public override string ConnectionString protected override bool CanRaiseEvents => true; - public ProbedDbConnection(DbConnection connection, CounterDataCollector counterDataCollector) + public ProbedDbConnection(DbConnection connection, ICounterDataCollector counterDataCollector) { _counterDataCollector = counterDataCollector ?? throw new ArgumentNullException(nameof(counterDataCollector)); ProbedConnection = connection ?? throw new ArgumentNullException(nameof(connection)); diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs index 5bbf7ec1a..235d8b6df 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs @@ -12,7 +12,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public class ProbedDbDataReader : DbDataReader #pragma warning restore CA1010 // Generic interface should also be implemented { - private CounterDataCollector CounterDataCollector { get; init; } + private ICounterDataCollector CounterDataCollector { get; init; } private ProbedDbCommand ProbedCommand { get; init; } @@ -39,14 +39,14 @@ public class ProbedDbDataReader : DbDataReader public ProbedDbDataReader( DbDataReader reader, ProbedDbCommand probedCommand, - CounterDataCollector counterDataCollector) + ICounterDataCollector counterDataCollector) : this(reader, CommandBehavior.Default, probedCommand, counterDataCollector) { } public ProbedDbDataReader( DbDataReader reader, CommandBehavior behavior, ProbedDbCommand probedCommand, - CounterDataCollector counterDataCollector) + ICounterDataCollector counterDataCollector) { CounterDataCollector = counterDataCollector ?? throw new ArgumentNullException(nameof(counterDataCollector)); ProbedCommand = probedCommand ?? throw new ArgumentNullException(nameof(probedCommand)); diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index a6f0be589..ffd043d2b 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -27,10 +27,10 @@ public sealed class OrchardApplicationFactory : WebApplicationFactory< private readonly Action _configuration; private readonly Action _configureOrchard; private readonly ConcurrentBag _createdStores = new(); - private readonly CounterDataCollector _counterDataCollector; + private readonly ICounterDataCollector _counterDataCollector; public OrchardApplicationFactory( - CounterDataCollector counterDataCollector, + ICounterDataCollector counterDataCollector, Action configuration = null, Action configureOrchard = null) { diff --git a/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs index 1f4dc3afd..ed8a164c8 100644 --- a/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs +++ b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs @@ -1,3 +1,4 @@ +using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.Counters.Data; using System; using System.Data.Common; @@ -8,11 +9,11 @@ namespace Lombiq.Tests.UI.Services; public class ProbedConnectionFactory : IConnectionFactory { private readonly IConnectionFactory _connectionFactory; - private readonly CounterDataCollector _counterDataCollector; + private readonly ICounterDataCollector _counterDataCollector; public Type DbConnectionType => typeof(ProbedDbConnection); - public ProbedConnectionFactory(IConnectionFactory connectionFactory, CounterDataCollector counterDataCollector) + public ProbedConnectionFactory(IConnectionFactory connectionFactory, ICounterDataCollector counterDataCollector) { _connectionFactory = connectionFactory; _counterDataCollector = counterDataCollector; From beef4865c838fc91d7f460ce6b71620e6ab6cb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 25 Oct 2022 01:20:59 +0200 Subject: [PATCH 03/44] Implementing required exception constructors --- .../Exceptions/CounterThresholdException.cs | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs index 694f28cca..59eaa40c1 100644 --- a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs +++ b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs @@ -4,11 +4,22 @@ namespace Lombiq.Tests.UI.Exceptions; -// We need constructors with required informations. -#pragma warning disable CA1032 // Implement standard exception constructors public class CounterThresholdException : Exception -#pragma warning restore CA1032 // Implement standard exception constructors { + public CounterThresholdException() + { + } + + public CounterThresholdException(string message) + : this(probe: null, counter: null, value: null, message) + { + } + + public CounterThresholdException(string message, Exception innerException) + : this(probe: null, counter: null, value: null, message, innerException) + { + } + public CounterThresholdException( ICounterProbe probe, ICounterKey counter, @@ -40,11 +51,14 @@ private static string FormatMessage( ICounterProbe probe, ICounterKey counter, ICounterValue value, - string message) => - new StringBuilder() - .AppendLine(probe.DumpHeadline()) - .AppendLine(counter.Dump()) - .AppendLine(value.Dump()) - .AppendLine(message ?? string.Empty) - .ToString(); + string message) + { + var builder = new StringBuilder(); + if (probe is not null) builder.AppendLine(probe.DumpHeadline()); + if (counter is not null) builder.AppendLine(counter.Dump()); + if (value is not null) builder.AppendLine(value.Dump()); + if (!string.IsNullOrEmpty(message)) builder.AppendLine(message); + + return builder.ToString(); + } } From 6c3602f159d7caa15bc96600c699a2e746e6030e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 25 Oct 2022 11:12:12 +0200 Subject: [PATCH 04/44] Adjusting tests Fixing typos --- .../Tests/BasicOrchardFeaturesTests.cs | 12 +++++++++++- Lombiq.Tests.UI/Services/Counters/CounterProbe.cs | 1 - .../Services/Counters/Data/ProbedDbCommand.cs | 6 +++--- .../Services/Counters/Data/ProbedDbConnection.cs | 2 +- .../Extensions/ICounterDataCollectorExtensions.cs} | 9 ++++----- 5 files changed, 19 insertions(+), 11 deletions(-) rename Lombiq.Tests.UI/{Extensions/CounterDataCollectorExtensions.cs => Services/Counters/Extensions/ICounterDataCollectorExtensions.cs} (87%) diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs index 230f00d3e..4aa844fdf 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs @@ -25,7 +25,17 @@ public BasicOrchardFeaturesTests(ITestOutputHelper testOutputHelper) public Task BasicOrchardFeaturesShouldWork(Browser browser) => ExecuteTestAsync( context => context.TestBasicOrchardFeaturesAsync(RecipeIds.BasicOrchardFeaturesTests), - browser); + browser, + configuration => + { + // The UI Testing Toolbox includes DbCommand execution counter, after the end of test it checks the + // number of executed commands with the same Sql command and parameter set against the threshold value + // in its configuration. If the executed command count is greater then the threshold, it raises a + // CounterThresholdException. So here we set the minimum required value to avoid it. + configuration.CounterConfiguration.Running.DbCommandExecutionRepetitionThreshold = 26; + + return Task.CompletedTask; + }); } // END OF TRAINING SECTION: Basic Orchard features tests. diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs index 7e292ccf0..87d2438f1 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs @@ -43,7 +43,6 @@ protected virtual void Dispose(bool disposing) if (!_disposed) { try { OnAssertData(); } - catch { throw; } finally { if (disposing) diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs index 061477be1..d205fca34 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbCommand.cs @@ -1,4 +1,4 @@ -using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Services.Counters.Extensions; using System; using System.ComponentModel; using System.Data; @@ -99,7 +99,7 @@ public override Task ExecuteScalarAsync(CancellationToken cancellationTo protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => new ProbedDbDataReader( - _counterDataCollector.DbCommandExecuteDbDatareader(ProbedCommand, behavior), + _counterDataCollector.DbCommandExecuteDbDataReader(ProbedCommand, behavior), behavior, this, _counterDataCollector); @@ -108,7 +108,7 @@ protected override async Task ExecuteDbDataReaderAsync( CommandBehavior behavior, CancellationToken cancellationToken) => new ProbedDbDataReader( - await _counterDataCollector.DbCommandExecuteDbDatareaderAsync(ProbedCommand, behavior, cancellationToken), + await _counterDataCollector.DbCommandExecuteDbDataReaderAsync(ProbedCommand, behavior, cancellationToken), behavior, this, _counterDataCollector); diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs index 432621b1d..4c8854913 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbConnection.cs @@ -78,7 +78,7 @@ public override DataTable GetSchema(string collectionName, string[] restrictionV protected override void Dispose(bool disposing) { - if (disposing && ProbedConnection != null) + if (disposing && ProbedConnection is not null) { ProbedConnection.StateChange -= StateChangeHandler; ProbedConnection.Dispose(); diff --git a/Lombiq.Tests.UI/Extensions/CounterDataCollectorExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs similarity index 87% rename from Lombiq.Tests.UI/Extensions/CounterDataCollectorExtensions.cs rename to Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs index 57ace2172..ea49c6ac2 100644 --- a/Lombiq.Tests.UI/Extensions/CounterDataCollectorExtensions.cs +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs @@ -1,13 +1,12 @@ -using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.Counters.Data; using System.Data; using System.Data.Common; using System.Threading; using System.Threading.Tasks; -namespace Lombiq.Tests.UI.Extensions; +namespace Lombiq.Tests.UI.Services.Counters.Extensions; -public static class CounterDataCollectorExtensions +public static class ICounterDataCollectorExtensions { public static int DbCommandExecuteNonQuery(this ICounterDataCollector collector, DbCommand dbCommand) { @@ -39,7 +38,7 @@ public static Task DbCommandExecuteScalarAsync( return dbCommand.ExecuteScalarAsync(cancellationToken); } - public static DbDataReader DbCommandExecuteDbDatareader( + public static DbDataReader DbCommandExecuteDbDataReader( this ICounterDataCollector collector, DbCommand dbCommand, CommandBehavior behavior) @@ -48,7 +47,7 @@ public static DbDataReader DbCommandExecuteDbDatareader( return dbCommand.ExecuteReader(behavior); } - public static Task DbCommandExecuteDbDatareaderAsync( + public static Task DbCommandExecuteDbDataReaderAsync( this ICounterDataCollector collector, DbCommand dbCommand, CommandBehavior behavior, From 06f8eaa175e3cb5f9e54dc153a64501a423a7e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 25 Oct 2022 11:22:34 +0200 Subject: [PATCH 05/44] Fixing typo --- Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs index 235d8b6df..36f570480 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs @@ -7,7 +7,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; -// Generic IEnumerable<> interface is not implemented in DbDatareader. +// Generic IEnumerable<> interface is not implemented in DbDataReader. #pragma warning disable CA1010 // Generic interface should also be implemented public class ProbedDbDataReader : DbDataReader #pragma warning restore CA1010 // Generic interface should also be implemented From 914c291322fc9b489a8113cf3c191c2ff7522f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 3 Dec 2022 18:41:51 +0100 Subject: [PATCH 06/44] Update Lombiq.Tests.UI/Services/CounterDataCollector.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- Lombiq.Tests.UI/Services/CounterDataCollector.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index fad375359..ad7ce59ed 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -21,10 +21,8 @@ public void Reset() public override void Increment(ICounterKey counter) { - _probes.SelectWhere(probe => probe, probe => probe.IsRunning) - .ForEach(probe => probe.Increment(counter)); - - base.Increment(counter); + _probes.Where(probe => probe.IsRunning).ForEach(probe => probe.Increment(counter)); + base. Increment(counter); } public override string DumpHeadline() => nameof(CounterDataCollector); From 1100237d3366b0fc17800c44b25ea2d5eb49ea0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 3 Dec 2022 18:42:11 +0100 Subject: [PATCH 07/44] Update Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs index 4e471fc1e..293f60aac 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs @@ -29,7 +29,7 @@ private static TCounter TryConvertAndUpdate(ICounterValue counter, Act if (counter is not TCounter value) { throw new ArgumentException( - $"The type of ${nameof(counter)} is not compatible with ${typeof(TCounter).Name}"); + $"The type of ${nameof(counter)} is not compatible with ${typeof(TCounter).Name}."); } update.Invoke(value); From df3c3c2f2757a5744359c5506ddbcb39e41a73c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 3 Dec 2022 18:42:33 +0100 Subject: [PATCH 08/44] Update Lombiq.Tests.UI/Services/Counters/ICounterValue.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- Lombiq.Tests.UI/Services/Counters/ICounterValue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs index dd44e1678..81e47aa4b 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs @@ -8,6 +8,6 @@ public interface ICounterValue /// /// Dumps the value content to a human readable format. /// - /// A human readable string representation of instance. + /// A human-readable string representation of the instance. string Dump(); } From 1a0fd1a85600fd9903089edb34a633b9a4e5aa8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 3 Dec 2022 18:42:54 +0100 Subject: [PATCH 09/44] Update Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs index b459772c3..e57a93818 100644 --- a/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs @@ -10,5 +10,5 @@ public NavigationProbe(ICounterDataCollector counterDataCollector, Uri absoluteU : base(counterDataCollector) => AbsoluteUri = absoluteUri; - public override string DumpHeadline() => $"{nameof(NavigationProbe)}, AbsoluteUri={AbsoluteUri}"; + public override string DumpHeadline() => $"{nameof(NavigationProbe)}, AbsoluteUri = {AbsoluteUri}"; } From 8e61c2231db9e10b9e1ec7c42ea823f94236da87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 3 Dec 2022 18:43:09 +0100 Subject: [PATCH 10/44] Update Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs index 4aa844fdf..37c3ad9cd 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs @@ -28,9 +28,9 @@ public Task BasicOrchardFeaturesShouldWork(Browser browser) => browser, configuration => { - // The UI Testing Toolbox includes DbCommand execution counter, after the end of test it checks the - // number of executed commands with the same Sql command and parameter set against the threshold value - // in its configuration. If the executed command count is greater then the threshold, it raises a + // The UI Testing Toolbox includes a DbCommand execution counter. After the end of the test, it checks the + // number of executed commands with the same SQL command and parameter set against the threshold value + // in its configuration. If the executed command count is greater than the threshold, it raises a // CounterThresholdException. So here we set the minimum required value to avoid it. configuration.CounterConfiguration.Running.DbCommandExecutionRepetitionThreshold = 26; From a20311c4f995469e880324ba33a1fabb07ff5b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 3 Dec 2022 18:43:56 +0100 Subject: [PATCH 11/44] Update Lombiq.Tests.UI/Services/CounterConfiguration.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- Lombiq.Tests.UI/Services/CounterConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/CounterConfiguration.cs index da39a36f9..702e6e2cc 100644 --- a/Lombiq.Tests.UI/Services/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/CounterConfiguration.cs @@ -56,7 +56,7 @@ public static void AssertIntegerCounterValue(ICounterProbe probe, string t probe, key, counterValue, - $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}"); + $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); } }); } From 169fa2645aa66d94235fba0cf8aecdf7ffcf543b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 3 Dec 2022 21:50:27 +0100 Subject: [PATCH 12/44] Adding SessionProbe --- .../Services/CounterConfiguration.cs | 33 ++++++++++- .../Services/CounterDataCollector.cs | 5 +- .../Services/Counters/CounterProbe.cs | 9 ++- .../Services/Counters/SessionProbe.cs | 59 +++++++++++++++++++ .../OrchardApplicationFactory.cs | 27 +++++++++ .../Services/PhaseCounterConfiguration.cs | 1 + 6 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 Lombiq.Tests.UI/Services/Counters/SessionProbe.cs diff --git a/Lombiq.Tests.UI/Services/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/CounterConfiguration.cs index 702e6e2cc..749f9f52e 100644 --- a/Lombiq.Tests.UI/Services/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/CounterConfiguration.cs @@ -1,3 +1,4 @@ +using Atata; using Lombiq.Tests.UI.Exceptions; using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.Counters.Data; @@ -38,15 +39,28 @@ public static Action DefaultAssertCounterData(PhaseCounterConfigu ? nameof(configuration.DbReaderReadPerNavigationThreshold) : nameof(configuration.DbReaderReadThreshold); - AssertIntegerCounterValue(probe, executeThresholdName, executeThreshold); - AssertIntegerCounterValue(probe, readThresholdName, readThreshold); + AssertIntegerCounterValue( + probe, + configuration.ExcludeFilter ?? (key => false), + executeThresholdName, + executeThreshold); + AssertIntegerCounterValue( + probe, + configuration.ExcludeFilter ?? (key => false), + readThresholdName, + readThreshold); } }; - public static void AssertIntegerCounterValue(ICounterProbe probe, string thresholdName, int threshold) + public static void AssertIntegerCounterValue( + ICounterProbe probe, + Func excludeFilter, + string thresholdName, + int threshold) where TKey : ICounterKey => probe.Counters.Keys .OfType() + .Where(key => !excludeFilter(key)) .ForEach(key => { if (probe.Counters[key] is IntegerCounterValue counterValue @@ -59,4 +73,17 @@ public static void AssertIntegerCounterValue(ICounterProbe probe, string t $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); } }); + + public static bool DefaultExcludeFilter(ICounterKey key) + { + if (key is DbExecuteCounterKey dbExecuteCounter) + { + if (dbExecuteCounter.CommandText == @"SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex] AS [WorkflowTypeStartActivitiesIndex_a1] ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id] WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0) and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))") + { + return true; + } + } + + return false; + } } diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index ad7ce59ed..5aab9042f 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -21,8 +21,9 @@ public void Reset() public override void Increment(ICounterKey counter) { - _probes.Where(probe => probe.IsRunning).ForEach(probe => probe.Increment(counter)); - base. Increment(counter); + _probes.SelectWhere(probe => probe, probe => probe.IsRunning) + .ForEach(probe => probe.Increment(counter)); + base.Increment(counter); } public override string DumpHeadline() => nameof(CounterDataCollector); diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs index 87d2438f1..727dbc2c0 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs @@ -34,7 +34,11 @@ protected CounterProbe(ICounterDataCollector counterDataCollector) protected virtual void OnAssertData() => CounterDataCollector.AssertCounter(this); - protected virtual void OnDispose() + protected virtual void OnDisposing() + { + } + + protected virtual void OnDisposed() { } @@ -42,12 +46,13 @@ protected virtual void Dispose(bool disposing) { if (!_disposed) { + OnDisposing(); try { OnAssertData(); } finally { if (disposing) { - OnDispose(); + OnDisposed(); } _disposed = true; diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs new file mode 100644 index 000000000..ed0c1b3a2 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Threading.Tasks; +using YesSql; +using YesSql.Indexes; + +namespace Lombiq.Tests.UI.Services.Counters; + +public sealed class SessionProbe : CounterProbe, ISession +{ + private readonly ISession _session; + public Uri AbsoluteUri { get; init; } + DbTransaction ISession.CurrentTransaction => _session.CurrentTransaction; + IStore ISession.Store => _session.Store; + + public SessionProbe(ICounterDataCollector counterDataCollector, Uri absoluteUri, ISession session) + : base(counterDataCollector) + { + AbsoluteUri = absoluteUri; + _session = session; + } + + public override string DumpHeadline() => $"{nameof(NavigationProbe)}, AbsoluteUri = {AbsoluteUri}"; + + protected override void OnDisposing() => _session.Dispose(); + + void ISession.Save(object obj, bool checkConcurrency, string collection) => + _session.Save(obj, checkConcurrency, collection); + void ISession.Delete(object item, string collection) => + _session.Delete(item, collection); + bool ISession.Import(object item, int id, int version, string collection) => + _session.Import(item, id, version, collection); + void ISession.Detach(object item, string collection) => + _session.Detach(item, collection); + Task> ISession.GetAsync(int[] ids, string collection) => + _session.GetAsync(ids, collection); + IQuery ISession.Query(string collection) => + _session.Query(collection); + IQuery ISession.ExecuteQuery(ICompiledQuery compiledQuery, string collection) => + _session.ExecuteQuery(compiledQuery, collection); + Task ISession.CancelAsync() => _session.CancelAsync(); + Task ISession.FlushAsync() => _session.FlushAsync(); + Task ISession.SaveChangesAsync() => _session.SaveChangesAsync(); + Task ISession.CreateConnectionAsync() => _session.CreateConnectionAsync(); + Task ISession.BeginTransactionAsync() => _session.BeginTransactionAsync(); + Task ISession.BeginTransactionAsync(IsolationLevel isolationLevel) => + _session.BeginTransactionAsync(isolationLevel); + ISession ISession.RegisterIndexes(IIndexProvider[] indexProviders, string collection) => + _session.RegisterIndexes(indexProviders, collection); + async ValueTask IAsyncDisposable.DisposeAsync() + { + await _session.DisposeAsync(); + // Should be at the end because, the Session implementation calls CommitOrRollbackTransactionAsync in DisposeAsync + // and we should count the executed db commands in it. + Dispose(); + } +} diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index 95c019288..22bf1a0ca 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -1,6 +1,8 @@ using Lombiq.Tests.Integration.Services; using Lombiq.Tests.UI.Services.Counters; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; @@ -18,6 +20,7 @@ using System.Linq; using System.Threading.Tasks; using YesSql; +using ISession = YesSql.ISession; namespace Lombiq.Tests.UI.Services.OrchardCoreHosting; @@ -87,6 +90,7 @@ .ImplementationInstance as ConfigurationManager { AddFakeStore(builderServices); AddFakeViewCompilerProvider(builderServices); + AddSessionProbe(builderServices); }); } @@ -115,6 +119,29 @@ private void AddFakeStore(IServiceCollection services) }); } + private void AddSessionProbe(IServiceCollection services) + { + var sessionDescriptor = services.LastOrDefault(descriptor => descriptor.ServiceType == typeof(ISession)); + + services.RemoveAll(); + + services.AddScoped(serviceProvider => + { + var session = (ISession)sessionDescriptor.ImplementationFactory.Invoke(serviceProvider); + if (session is null) + { + return null; + } + + var httpContextAccessor = serviceProvider.GetRequiredService(); + + return new SessionProbe( + _counterDataCollector, + new Uri(httpContextAccessor.HttpContext.Request.GetEncodedUrl()), + session); + }); + } + // This is required because OrchardCore adds OrchardCore.Mvc.SharedViewCompilerProvider as IViewCompilerProvider but // it holds a IViewCompiler(Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.RuntimeViewCompiler) instance // reference in a static member(_compiler) and it not get released on IHost.StopAsync() call, and this cause an diff --git a/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs index e1d231628..56f894957 100644 --- a/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs @@ -6,6 +6,7 @@ namespace Lombiq.Tests.UI.Services; public class PhaseCounterConfiguration { public Action AssertCounterData { get; set; } + public Func ExcludeFilter { get; set; } = CounterConfiguration.DefaultExcludeFilter; public int DbCommandExecutionRepetitionPerNavigationThreshold { get; set; } = 11; public int DbCommandExecutionRepetitionThreshold { get; set; } = 22; public int DbReaderReadPerNavigationThreshold { get; set; } = 11; From cc941b0aa04562089388c5b85ec6a997371fde66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 4 Dec 2022 13:55:18 +0100 Subject: [PATCH 13/44] Adding better exclusion and defaults --- .../Services/CounterConfiguration.cs | 13 ------ .../Counters/Data/DbExecuteCounterKey.cs | 2 +- .../Counters/Data/DbReadCounterKey.cs | 2 +- .../Services/PhaseCounterConfiguration.cs | 44 ++++++++++++++++++- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/Lombiq.Tests.UI/Services/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/CounterConfiguration.cs index 749f9f52e..0b0a433aa 100644 --- a/Lombiq.Tests.UI/Services/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/CounterConfiguration.cs @@ -73,17 +73,4 @@ public static void AssertIntegerCounterValue( $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); } }); - - public static bool DefaultExcludeFilter(ICounterKey key) - { - if (key is DbExecuteCounterKey dbExecuteCounter) - { - if (dbExecuteCounter.CommandText == @"SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex] AS [WorkflowTypeStartActivitiesIndex_a1] ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id] WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0) and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))") - { - return true; - } - } - - return false; - } } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs index 64b79e8ff..9b5bbdada 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs @@ -6,7 +6,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public sealed class DbExecuteCounterKey : DbCommandCounterKey { - private DbExecuteCounterKey(string commandText, IEnumerable> parameters) + public DbExecuteCounterKey(string commandText, IEnumerable> parameters) : base(commandText, parameters) { } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs index 9cd0a2b97..8bfe47195 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs @@ -6,7 +6,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public class DbReadCounterKey : DbCommandCounterKey { - private DbReadCounterKey(string commandText, IEnumerable> parameters) + public DbReadCounterKey(string commandText, IEnumerable> parameters) : base(commandText, parameters) { } diff --git a/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs index 56f894957..8877a2198 100644 --- a/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs @@ -1,14 +1,56 @@ using Lombiq.Tests.UI.Services.Counters; +using Lombiq.Tests.UI.Services.Counters.Data; using System; +using System.Collections.Generic; +using System.Linq; namespace Lombiq.Tests.UI.Services; public class PhaseCounterConfiguration { public Action AssertCounterData { get; set; } - public Func ExcludeFilter { get; set; } = CounterConfiguration.DefaultExcludeFilter; + public Func ExcludeFilter { get; set; } = DefaultExcludeFilter; public int DbCommandExecutionRepetitionPerNavigationThreshold { get; set; } = 11; public int DbCommandExecutionRepetitionThreshold { get; set; } = 22; public int DbReaderReadPerNavigationThreshold { get; set; } = 11; public int DbReaderReadThreshold { get; set; } = 11; + + public static IEnumerable DefaultExcludeList { get; } = new List + { + new DbExecuteCounterKey( + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + new List> + { + new("p0", "ContentCreatedEvent"), + new("p1", value: true), + }), + new DbExecuteCounterKey( + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + new List> + { + new("p0", "ContentPublishedEvent"), + new("p1", value: true), + }), + new DbExecuteCounterKey( + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + new List> + { + new("p0", "ContentUpdatedEvent"), + new("p1", value: true), + }), + }; + + public static bool DefaultExcludeFilter(ICounterKey key) => DefaultExcludeList.Contains(key); } From 8e854fcb06088467fed05ea6e9806534a08f8268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 6 Dec 2022 20:37:45 +0100 Subject: [PATCH 14/44] Addressing "Add docs to the properties." Addressing "This needs to be possible to be completely disabled too." Addressing ""human-readable" everywhere." Addressing counting on page load not just navigation request Addressing counting command text with and without parameters Addressing "Keep the order of private fields and ctor assignments the same as the ctor parameters." Addressing "Comment why this is necessary." Addressing "After a page change, write all counters' values to the TestOutput for debugging." --- .../Tests/BasicOrchardFeaturesTests.cs | 2 +- .../Exceptions/CounterThresholdException.cs | 5 +- .../Services/CounterConfiguration.cs | 76 ------------------- .../Services/CounterDataCollector.cs | 33 +++++++- .../Configuration/CounterConfiguration.cs | 75 ++++++++++++++++++ .../CounterThresholdConfiguration.cs | 29 +++++++ .../PhaseCounterConfiguration.cs | 63 ++++++++++++--- .../Services/Counters/CounterKey.cs | 4 +- .../Services/Counters/CounterProbe.cs | 26 ++++--- .../Services/Counters/CounterProbeBase.cs | 35 ++++++++- .../Counters/Data/DbCommandCounterKey.cs | 38 +++++----- ...erKey.cs => DbCommandExecuteCounterKey.cs} | 6 +- .../Data/DbCommandTextExecuteCounterKey.cs | 26 +++++++ ...ounterKey.cs => DbReaderReadCounterKey.cs} | 6 +- .../Counters/Data/ProbedDbDataReader.cs | 4 +- .../ICounterDataCollectorExtensions.cs | 18 +++-- .../Services/Counters/ICounterKey.cs | 7 +- .../Services/Counters/ICounterProbe.cs | 16 ++-- .../Services/Counters/ICounterValue.cs | 8 +- .../Middlewares/PageLoadProbeMiddleware.cs | 24 ++++++ .../Services/Counters/PageLoadProbe.cs | 18 +++++ .../Services/Counters/SessionProbe.cs | 6 +- .../Services/Counters/Value/CounterValue.cs | 5 +- .../OrchardApplicationFactory.cs | 39 ++++++---- .../OrchardCoreUITestExecutorConfiguration.cs | 1 + .../Services/UITestExecutionSession.cs | 8 +- 26 files changed, 410 insertions(+), 168 deletions(-) delete mode 100644 Lombiq.Tests.UI/Services/CounterConfiguration.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs rename Lombiq.Tests.UI/Services/{ => Counters/Configuration}/PhaseCounterConfiguration.cs (52%) rename Lombiq.Tests.UI/Services/Counters/Data/{DbReadCounterKey.cs => DbCommandExecuteCounterKey.cs} (61%) create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs rename Lombiq.Tests.UI/Services/Counters/Data/{DbExecuteCounterKey.cs => DbReaderReadCounterKey.cs} (63%) create mode 100644 Lombiq.Tests.UI/Services/Counters/Middlewares/PageLoadProbeMiddleware.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs index fb0b0a841..7255fbcc3 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs @@ -32,7 +32,7 @@ public Task BasicOrchardFeaturesShouldWork(Browser browser) => // number of executed commands with the same SQL command and parameter set against the threshold value // in its configuration. If the executed command count is greater than the threshold, it raises a // CounterThresholdException. So here we set the minimum required value to avoid it. - configuration.CounterConfiguration.Running.DbCommandExecutionRepetitionThreshold = 26; + configuration.CounterConfiguration.Running.PhaseThreshold.DbCommandExecutionThreshold = 26; return Task.CompletedTask; }); diff --git a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs index 59eaa40c1..b35efe12a 100644 --- a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs +++ b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs @@ -1,5 +1,6 @@ using Lombiq.Tests.UI.Services.Counters; using System; +using System.Collections.Generic; using System.Text; namespace Lombiq.Tests.UI.Exceptions; @@ -55,8 +56,8 @@ private static string FormatMessage( { var builder = new StringBuilder(); if (probe is not null) builder.AppendLine(probe.DumpHeadline()); - if (counter is not null) builder.AppendLine(counter.Dump()); - if (value is not null) builder.AppendLine(value.Dump()); + if (counter is not null) counter.Dump().ForEach(line => builder.AppendLine(line)); + if (value is not null) value.Dump().ForEach(line => builder.AppendLine(line)); if (!string.IsNullOrEmpty(message)) builder.AppendLine(message); return builder.ToString(); diff --git a/Lombiq.Tests.UI/Services/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/CounterConfiguration.cs deleted file mode 100644 index 0b0a433aa..000000000 --- a/Lombiq.Tests.UI/Services/CounterConfiguration.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Atata; -using Lombiq.Tests.UI.Exceptions; -using Lombiq.Tests.UI.Services.Counters; -using Lombiq.Tests.UI.Services.Counters.Data; -using Lombiq.Tests.UI.Services.Counters.Value; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Lombiq.Tests.UI.Services; - -public class CounterConfiguration -{ - /// - /// Gets the counter configuration used in the setup phase. - /// - public PhaseCounterConfiguration Setup { get; } = new(); - - /// - /// Gets the counter configuration used in the running phase. - /// - public PhaseCounterConfiguration Running { get; } = new(); - - public static Action DefaultAssertCounterData(PhaseCounterConfiguration configuration) => - probe => - { - if (probe is NavigationProbe or CounterDataCollector) - { - var executeThreshold = probe is NavigationProbe - ? configuration.DbCommandExecutionRepetitionPerNavigationThreshold - : configuration.DbCommandExecutionRepetitionThreshold; - var executeThresholdName = probe is NavigationProbe - ? nameof(configuration.DbCommandExecutionRepetitionPerNavigationThreshold) - : nameof(configuration.DbCommandExecutionRepetitionThreshold); - var readThreshold = probe is NavigationProbe - ? configuration.DbReaderReadPerNavigationThreshold - : configuration.DbReaderReadThreshold; - var readThresholdName = probe is NavigationProbe - ? nameof(configuration.DbReaderReadPerNavigationThreshold) - : nameof(configuration.DbReaderReadThreshold); - - AssertIntegerCounterValue( - probe, - configuration.ExcludeFilter ?? (key => false), - executeThresholdName, - executeThreshold); - AssertIntegerCounterValue( - probe, - configuration.ExcludeFilter ?? (key => false), - readThresholdName, - readThreshold); - } - }; - - public static void AssertIntegerCounterValue( - ICounterProbe probe, - Func excludeFilter, - string thresholdName, - int threshold) - where TKey : ICounterKey => - probe.Counters.Keys - .OfType() - .Where(key => !excludeFilter(key)) - .ForEach(key => - { - if (probe.Counters[key] is IntegerCounterValue counterValue - && counterValue.Value > threshold) - { - throw new CounterThresholdException( - probe, - key, - counterValue, - $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); - } - }); -} diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index 5aab9042f..6266d89d8 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -2,16 +2,27 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; namespace Lombiq.Tests.UI.Services; public sealed class CounterDataCollector : CounterProbeBase, ICounterDataCollector { + private readonly ITestOutputHelper _testOutputHelper; private readonly ConcurrentBag _probes = new(); public override bool IsRunning => true; public Action AssertCounterData { get; set; } + public string Phase { get; set; } - public void AttachProbe(ICounterProbe probe) => _probes.Add(probe); + public CounterDataCollector(ITestOutputHelper testOutputHelper) => + _testOutputHelper = testOutputHelper; + + public void AttachProbe(ICounterProbe probe) + { + probe.CaptureCompleted = ProbeCaptureCompleted; + _probes.Add(probe); + } public void Reset() { @@ -21,13 +32,27 @@ public void Reset() public override void Increment(ICounterKey counter) { - _probes.SelectWhere(probe => probe, probe => probe.IsRunning) + _probes.Where(probe => probe.IsRunning) .ForEach(probe => probe.Increment(counter)); base.Increment(counter); } - public override string DumpHeadline() => nameof(CounterDataCollector); - public override string Dump() => DumpHeadline(); + public override string DumpHeadline() => $"{nameof(CounterDataCollector)}, Phase = {Phase}"; + public override IEnumerable Dump() + { + var lines = new List + { + DumpHeadline(), + }; + + lines.AddRange(DumpSummary().Select(line => $"\t{line}")); + + return lines; + } + public void AssertCounter(ICounterProbe probe) => AssertCounterData?.Invoke(probe); public void AssertCounter() => AssertCounter(this); + + private void ProbeCaptureCompleted(ICounterProbe probe) => + probe.Dump().ForEach(line => _testOutputHelper.WriteLine(line)); } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs new file mode 100644 index 000000000..83c186864 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs @@ -0,0 +1,75 @@ +using Lombiq.Tests.UI.Exceptions; +using Lombiq.Tests.UI.Services.Counters.Data; +using Lombiq.Tests.UI.Services.Counters.Value; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lombiq.Tests.UI.Services.Counters.Configuration; + +public class CounterConfiguration +{ + /// + /// Gets the counter configuration used in the setup phase of the web application. + /// + public PhaseCounterConfiguration Setup { get; } = new(); + + /// + /// Gets the counter configuration used in the running phase of the web application. + /// + public PhaseCounterConfiguration Running { get; } = new(); + + public static Action DefaultAssertCounterData(PhaseCounterConfiguration configuration) => + probe => + { + (CounterThresholdConfiguration Settings, string Name)? threshold = probe switch + { + NavigationProbe => (Settings: configuration.NavigationThreshold, Name: nameof(configuration.NavigationThreshold)), + PageLoadProbe => (Settings: configuration.PageLoadThreshold, Name: nameof(configuration.NavigationThreshold)), + SessionProbe => (Settings: configuration.SessionThreshold, Name: nameof(configuration.NavigationThreshold)), + CounterDataCollector => (Settings: configuration.PhaseThreshold, Name: nameof(configuration.NavigationThreshold)), + _ => null, + }; + + if (threshold is { } settings && settings.Settings.Disable is not true) + { + AssertIntegerCounterValue( + probe, + configuration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandExecutionThreshold)}", + settings.Settings.DbCommandExecutionThreshold); + AssertIntegerCounterValue( + probe, + configuration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandTextExecutionThreshold)}", + settings.Settings.DbCommandTextExecutionThreshold); + AssertIntegerCounterValue( + probe, + configuration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", + settings.Settings.DbReaderReadThreshold); + } + }; + + public static void AssertIntegerCounterValue( + ICounterProbe probe, + Func excludeFilter, + string thresholdName, + int threshold) + where TKey : ICounterKey => + probe.Counters.Keys + .OfType() + .Where(key => !excludeFilter(key)) + .ForEach(key => + { + if (probe.Counters[key] is IntegerCounterValue counterValue + && counterValue.Value > threshold) + { + throw new CounterThresholdException( + probe, + key, + counterValue, + $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); + } + }); +} diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs new file mode 100644 index 000000000..75a56d454 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs @@ -0,0 +1,29 @@ +namespace Lombiq.Tests.UI.Services.Counters.Configuration; + +public class CounterThresholdConfiguration +{ + /// + /// Gets or sets a value indicating whether the current threshold configuration and checking is disabled. + /// + public bool Disable { get; set; } = true; + + /// + /// Gets or sets the threshold of executed s. Uses + /// and + /// for counting. + /// + public int DbCommandExecutionThreshold { get; set; } = 11; + + /// + /// Gets or sets the threshold of executed s. Uses + /// for counting. + /// + public int DbCommandTextExecutionThreshold { get; set; } = 11; + + /// + /// Gets or sets the threshold of readings of s. Uses + /// and + /// for counting. + /// + public int DbReaderReadThreshold { get; set; } = 11; +} diff --git a/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs similarity index 52% rename from Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs rename to Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs index 8877a2198..bf09d0ba4 100644 --- a/Lombiq.Tests.UI/Services/PhaseCounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs @@ -1,23 +1,68 @@ -using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.Counters.Data; using System; using System.Collections.Generic; using System.Linq; -namespace Lombiq.Tests.UI.Services; +namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class PhaseCounterConfiguration { + /// + /// Gets or sets the counter assertion method. + /// public Action AssertCounterData { get; set; } + + /// + /// Gets or sets the exclude filter. Can be used to exclude counted values before assertion. + /// public Func ExcludeFilter { get; set; } = DefaultExcludeFilter; - public int DbCommandExecutionRepetitionPerNavigationThreshold { get; set; } = 11; - public int DbCommandExecutionRepetitionThreshold { get; set; } = 22; - public int DbReaderReadPerNavigationThreshold { get; set; } = 11; - public int DbReaderReadThreshold { get; set; } = 11; + + /// + /// Gets or sets threshold configuration used under navigation requests. See: + /// . + /// See: . + /// + public CounterThresholdConfiguration NavigationThreshold { get; set; } = new CounterThresholdConfiguration + { + DbCommandExecutionThreshold = 11, + DbCommandTextExecutionThreshold = 22, + DbReaderReadThreshold = 11, + }; + + /// + /// Gets or sets threshold configuration used per lifetime. See: + /// . + /// + public CounterThresholdConfiguration SessionThreshold { get; set; } = new CounterThresholdConfiguration + { + DbCommandExecutionThreshold = 22, + DbCommandTextExecutionThreshold = 44, + DbReaderReadThreshold = 11, + }; + + /// + /// Gets or sets threshold configuration used per page load. See: . + /// + public CounterThresholdConfiguration PageLoadThreshold { get; set; } = new CounterThresholdConfiguration + { + DbCommandExecutionThreshold = 22, + DbCommandTextExecutionThreshold = 44, + DbReaderReadThreshold = 11, + }; + + /// + /// Gets or sets threshold configuration used under app phase(setup, running) lifetime. + /// + public CounterThresholdConfiguration PhaseThreshold { get; set; } = new CounterThresholdConfiguration + { + DbCommandExecutionThreshold = 22, + DbCommandTextExecutionThreshold = 44, + DbReaderReadThreshold = 11, + }; public static IEnumerable DefaultExcludeList { get; } = new List { - new DbExecuteCounterKey( + new DbCommandExecuteCounterKey( "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + " AS [WorkflowTypeStartActivitiesIndex_a1]" + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" @@ -28,7 +73,7 @@ public class PhaseCounterConfiguration new("p0", "ContentCreatedEvent"), new("p1", value: true), }), - new DbExecuteCounterKey( + new DbCommandExecuteCounterKey( "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + " AS [WorkflowTypeStartActivitiesIndex_a1]" + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" @@ -39,7 +84,7 @@ public class PhaseCounterConfiguration new("p0", "ContentPublishedEvent"), new("p1", value: true), }), - new DbExecuteCounterKey( + new DbCommandExecuteCounterKey( "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + " AS [WorkflowTypeStartActivitiesIndex_a1]" + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" diff --git a/Lombiq.Tests.UI/Services/Counters/CounterKey.cs b/Lombiq.Tests.UI/Services/Counters/CounterKey.cs index 73e505d9c..368e533d0 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterKey.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Lombiq.Tests.UI.Services.Counters; // The Equals must be implemented in consumer classes. @@ -9,5 +11,5 @@ public abstract class CounterKey : ICounterKey protected abstract int HashCode(); public override bool Equals(object obj) => Equals(obj as ICounterKey); public override int GetHashCode() => HashCode(); - public abstract string Dump(); + public abstract IEnumerable Dump(); } diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs index 727dbc2c0..563ac68ba 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs @@ -1,5 +1,6 @@ using System; -using System.Text; +using System.Collections.Generic; +using System.Linq; namespace Lombiq.Tests.UI.Services.Counters; @@ -10,19 +11,21 @@ public abstract class CounterProbe : CounterProbeBase, IDisposable public override bool IsRunning => !_disposed; public ICounterDataCollector CounterDataCollector { get; init; } - public override string Dump() + public override IEnumerable Dump() { - var builder = new StringBuilder(); - - builder.AppendLine(DumpHeadline()); - - foreach (var entry in Counters) + var lines = new List { - builder.AppendLine(entry.Key.Dump()) - .AppendLine(entry.Value.Dump()); - } + DumpHeadline(), + }; + + lines.AddRange( + Counters.SelectMany(entry => + entry.Key.Dump() + .Concat(entry.Value.Dump().Select(line => $"\t{line}"))) + .Concat(DumpSummary()) + .Select(line => $"\t{line}")); - return builder.ToString(); + return lines; } protected CounterProbe(ICounterDataCollector counterDataCollector) @@ -56,6 +59,7 @@ protected virtual void Dispose(bool disposing) } _disposed = true; + OnCaptureCompleted(); } } } diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs index 293f60aac..3f9501e59 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace Lombiq.Tests.UI.Services.Counters; @@ -10,6 +11,7 @@ public abstract class CounterProbeBase : ICounterProbe private readonly ConcurrentDictionary _counters = new(); public abstract bool IsRunning { get; } + public Action CaptureCompleted { get; set; } public IDictionary Counters => _counters; protected void Clear() => _counters.Clear(); @@ -22,7 +24,38 @@ public virtual void Increment(ICounterKey counter) => public abstract string DumpHeadline(); - public abstract string Dump(); + public abstract IEnumerable Dump(); + + public virtual IEnumerable DumpSummary() + { + var lines = new List + { + "Summary:", + }; + + lines.AddRange( + Counters.GroupBy(entry => entry.Key.GetType()) + .SelectMany(keyGroup => + { + var keyGroupLines = new List + { + keyGroup.Key.Name, + }; + + var integerCounter = keyGroup.Select(entry => entry.Value) + .OfType() + .Select(value => value.Value) + .Max(); + keyGroupLines.Add($"\t{nameof(IntegerCounterValue)} max: {integerCounter.ToTechnicalString()}"); + + return keyGroupLines.Select(line => $"\t{line}"); + })); + + return lines; + } + + protected virtual void OnCaptureCompleted() => + CaptureCompleted?.Invoke(this); private static TCounter TryConvertAndUpdate(ICounterValue counter, Action update) { diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs index 5161ae4b1..b8eafb6d0 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Text; namespace Lombiq.Tests.UI.Services.Counters.Data; @@ -22,29 +20,35 @@ public override bool Equals(ICounterKey other) { if (ReferenceEquals(this, other)) return true; - return other is DbExecuteCounterKey otherKey + return other is DbCommandCounterKey otherKey + && other.GetType() == GetType() && GetType() == otherKey.GetType() - && CommandText == otherKey.CommandText + && string.Equals(CommandText, otherKey.CommandText, StringComparison.OrdinalIgnoreCase) && Parameters .Select(param => (param.Key, param.Value)) .SequenceEqual(otherKey.Parameters.Select(param => (param.Key, param.Value))); } - public override string Dump() + public override IEnumerable Dump() { - var builder = new StringBuilder(); - - builder.AppendLine(GetType().Name) - .AppendLine(CultureInfo.InvariantCulture, $"\t{CommandText}"); - var commandParams = Parameters.Select((parameter, index) => - FormattableString.Invariant( - $"[{index.ToTechnicalString()}]{parameter.Key ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) - .Join(", "); - builder.AppendLine(CultureInfo.InvariantCulture, $"\t\t{commandParams}"); - - return builder.ToString(); + var lines = new List + { + GetType().Name, + $"\t{CommandText}", + }; + + if (Parameters.Any()) + { + var commandParams = Parameters.Select((parameter, index) => + FormattableString.Invariant( + $"[{index.ToTechnicalString()}]{parameter.Key ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) + .Join(", "); + lines.Add($"\t\t{commandParams}"); + } + + return lines; } - protected override int HashCode() => StringComparer.Ordinal.GetHashCode(CommandText); + protected override int HashCode() => StringComparer.Ordinal.GetHashCode(CommandText.ToUpperInvariant()); public override string ToString() => $"[{GetType().Name}] {CommandText}"; } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs similarity index 61% rename from Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs rename to Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs index 8bfe47195..80edb9c39 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbReadCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs @@ -4,14 +4,14 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; -public class DbReadCounterKey : DbCommandCounterKey +public sealed class DbCommandExecuteCounterKey : DbCommandCounterKey { - public DbReadCounterKey(string commandText, IEnumerable> parameters) + public DbCommandExecuteCounterKey(string commandText, IEnumerable> parameters) : base(commandText, parameters) { } - public static DbReadCounterKey CreateFrom(DbCommand dbCommand) => + public static DbCommandExecuteCounterKey CreateFrom(DbCommand dbCommand) => new( dbCommand.CommandText, dbCommand.Parameters diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs new file mode 100644 index 000000000..db221af22 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +public sealed class DbCommandTextExecuteCounterKey : DbCommandCounterKey +{ + public DbCommandTextExecuteCounterKey(string commandText) + : base(commandText, Enumerable.Empty>()) + { + } + + public static DbCommandTextExecuteCounterKey CreateFrom(DbCommand dbCommand) => + new(dbCommand.CommandText); + + public override bool Equals(ICounterKey other) + { + if (ReferenceEquals(this, other)) return true; + + return other is DbCommandTextExecuteCounterKey otherKey + && GetType() == otherKey.GetType() + && string.Equals(CommandText, otherKey.CommandText, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs similarity index 63% rename from Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs rename to Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs index 9b5bbdada..a95e6549d 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbExecuteCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs @@ -4,14 +4,14 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; -public sealed class DbExecuteCounterKey : DbCommandCounterKey +public class DbReaderReadCounterKey : DbCommandCounterKey { - public DbExecuteCounterKey(string commandText, IEnumerable> parameters) + public DbReaderReadCounterKey(string commandText, IEnumerable> parameters) : base(commandText, parameters) { } - public static DbExecuteCounterKey CreateFrom(DbCommand dbCommand) => + public static DbReaderReadCounterKey CreateFrom(DbCommand dbCommand) => new( dbCommand.CommandText, dbCommand.Parameters diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs index 36f570480..a0586233e 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedDbDataReader.cs @@ -16,7 +16,7 @@ public class ProbedDbDataReader : DbDataReader private ProbedDbCommand ProbedCommand { get; init; } - private DbReadCounterKey CounterKey { get; init; } + private DbReaderReadCounterKey CounterKey { get; init; } internal DbDataReader ProbedReader { get; private set; } @@ -52,7 +52,7 @@ public ProbedDbDataReader( ProbedCommand = probedCommand ?? throw new ArgumentNullException(nameof(probedCommand)); ProbedReader = reader ?? throw new ArgumentNullException(nameof(reader)); Behavior = behavior; - CounterKey = DbReadCounterKey.CreateFrom(ProbedCommand); + CounterKey = DbReaderReadCounterKey.CreateFrom(ProbedCommand); } public override bool GetBoolean(int ordinal) => ProbedReader.GetBoolean(ordinal); diff --git a/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs index ea49c6ac2..8ae7a9162 100644 --- a/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs @@ -10,7 +10,8 @@ public static class ICounterDataCollectorExtensions { public static int DbCommandExecuteNonQuery(this ICounterDataCollector collector, DbCommand dbCommand) { - collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); return dbCommand.ExecuteNonQuery(); } @@ -19,13 +20,15 @@ public static Task DbCommandExecuteNonQueryAsync( DbCommand dbCommand, CancellationToken cancellationToken) { - collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); return dbCommand.ExecuteNonQueryAsync(cancellationToken); } public static object DbCommandExecuteScalar(this ICounterDataCollector collector, DbCommand dbCommand) { - collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); return dbCommand.ExecuteScalar(); } @@ -34,7 +37,8 @@ public static Task DbCommandExecuteScalarAsync( DbCommand dbCommand, CancellationToken cancellationToken) { - collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); return dbCommand.ExecuteScalarAsync(cancellationToken); } @@ -43,7 +47,8 @@ public static DbDataReader DbCommandExecuteDbDataReader( DbCommand dbCommand, CommandBehavior behavior) { - collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); return dbCommand.ExecuteReader(behavior); } @@ -53,7 +58,8 @@ public static Task DbCommandExecuteDbDataReaderAsync( CommandBehavior behavior, CancellationToken cancellationToken) { - collector.Increment(DbExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); + collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); return dbCommand.ExecuteReaderAsync(behavior, cancellationToken); } } diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs index 72cad3d02..759bf293a 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Lombiq.Tests.UI.Services.Counters; @@ -8,8 +9,8 @@ namespace Lombiq.Tests.UI.Services.Counters; public interface ICounterKey : IEquatable { /// - /// Dumps the key content to a human readable format. + /// Dumps the key content to a human-readable format. /// - /// A human readable string representation of instance. - string Dump(); + /// A human-readable representation of instance. + IEnumerable Dump(); } diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs index 5291a71d3..a64d44c59 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Lombiq.Tests.UI.Services.Counters; @@ -12,6 +13,11 @@ public interface ICounterProbe /// bool IsRunning { get; } + /// + /// Gets or sets a callback which is called when the probe completed capturing the data. + /// + Action CaptureCompleted { get; set; } + /// /// Gets the collected values. /// @@ -25,14 +31,14 @@ public interface ICounterProbe void Increment(ICounterKey counter); /// - /// Dumps the probe headline to a human readable format. + /// Dumps the probe headline to a human-readable format. /// - /// A human readable string representation of instance in one line. + /// A human-readable string representation of instance in one line. public string DumpHeadline(); /// - /// Dumps the probe content to a human readable format. + /// Dumps the probe content to a human-readable format. /// - /// A human readable string representation of instance. - string Dump(); + /// A human-readable representation of instance. + IEnumerable Dump(); } diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs index 81e47aa4b..82059dca3 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Lombiq.Tests.UI.Services.Counters; /// @@ -6,8 +8,8 @@ namespace Lombiq.Tests.UI.Services.Counters; public interface ICounterValue { /// - /// Dumps the value content to a human readable format. + /// Dumps the value content to a human-readable format. /// - /// A human-readable string representation of the instance. - string Dump(); + /// A human-readable representation of the instance. + IEnumerable Dump(); } diff --git a/Lombiq.Tests.UI/Services/Counters/Middlewares/PageLoadProbeMiddleware.cs b/Lombiq.Tests.UI/Services/Counters/Middlewares/PageLoadProbeMiddleware.cs new file mode 100644 index 000000000..3259fbc66 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Middlewares/PageLoadProbeMiddleware.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using System; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.Services.Counters.Middlewares; + +public class PageLoadProbeMiddleware +{ + private readonly RequestDelegate _next; + private readonly ICounterDataCollector _counterDataCollector; + + public PageLoadProbeMiddleware(RequestDelegate next, ICounterDataCollector counterDataCollector) + { + _next = next; + _counterDataCollector = counterDataCollector; + } + + public async Task InvokeAsync(HttpContext context) + { + using (new PageLoadProbe(_counterDataCollector, context.Request.Method, new Uri(context.Request.GetEncodedUrl()))) + await _next.Invoke(context); + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs b/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs new file mode 100644 index 000000000..db2b49e8f --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs @@ -0,0 +1,18 @@ +using System; + +namespace Lombiq.Tests.UI.Services.Counters; + +public class PageLoadProbe : CounterProbe +{ + public string RequestMethod { get; init; } + public Uri AbsoluteUri { get; init; } + + public PageLoadProbe(ICounterDataCollector counterDataCollector, string requestMethod, Uri absoluteUri) + : base(counterDataCollector) + { + RequestMethod = requestMethod; + AbsoluteUri = absoluteUri; + } + + public override string DumpHeadline() => $"{nameof(PageLoadProbe)}, [{RequestMethod}]{AbsoluteUri}"; +} diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs index ed0c1b3a2..cbd5da1d6 100644 --- a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -11,18 +11,20 @@ namespace Lombiq.Tests.UI.Services.Counters; public sealed class SessionProbe : CounterProbe, ISession { private readonly ISession _session; + public string RequestMethod { get; init; } public Uri AbsoluteUri { get; init; } DbTransaction ISession.CurrentTransaction => _session.CurrentTransaction; IStore ISession.Store => _session.Store; - public SessionProbe(ICounterDataCollector counterDataCollector, Uri absoluteUri, ISession session) + public SessionProbe(ICounterDataCollector counterDataCollector, string requestMethod, Uri absoluteUri, ISession session) : base(counterDataCollector) { + RequestMethod = requestMethod; AbsoluteUri = absoluteUri; _session = session; } - public override string DumpHeadline() => $"{nameof(NavigationProbe)}, AbsoluteUri = {AbsoluteUri}"; + public override string DumpHeadline() => $"{nameof(SessionProbe)}, [{RequestMethod}]{AbsoluteUri}"; protected override void OnDisposing() => _session.Dispose(); diff --git a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs index 4cc41da40..e7ec48570 100644 --- a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Lombiq.Tests.UI.Services.Counters.Value; @@ -7,6 +8,6 @@ public abstract class CounterValue : ICounterValue { public TValue Value { get; set; } - public virtual string Dump() => - FormattableString.Invariant($"{GetType().Name} value: {Value}"); + public virtual IEnumerable Dump() => + new[] { FormattableString.Invariant($"{GetType().Name} value: {Value}") }; } diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index 22bf1a0ca..c2a8ee275 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -1,5 +1,7 @@ using Lombiq.Tests.Integration.Services; using Lombiq.Tests.UI.Services.Counters; +using Lombiq.Tests.UI.Services.Counters.Middlewares; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -27,11 +29,11 @@ namespace Lombiq.Tests.UI.Services.OrchardCoreHosting; public sealed class OrchardApplicationFactory : WebApplicationFactory, IProxyConnectionProvider where TStartup : class { + private readonly ICounterDataCollector _counterDataCollector; private readonly Action _configureHost; private readonly Action _configuration; private readonly Action _configureOrchard; private readonly ConcurrentBag _createdStores = new(); - private readonly ICounterDataCollector _counterDataCollector; public OrchardApplicationFactory( ICounterDataCollector counterDataCollector, @@ -39,10 +41,10 @@ public OrchardApplicationFactory( Action configuration = null, Action configureOrchard = null) { + _counterDataCollector = counterDataCollector; _configureHost = configureHost; _configuration = configuration; _configureOrchard = configureOrchard; - _counterDataCollector = counterDataCollector; } public Uri BaseAddress => ClientOptions.BaseAddress; @@ -50,6 +52,7 @@ public OrchardApplicationFactory( protected override IHost CreateHost(IHostBuilder builder) { builder.ConfigureHostConfiguration(configurationBuilder => _configureHost?.Invoke(configurationBuilder)); + return base.CreateHost(builder); } @@ -73,25 +76,28 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) private void ConfigureTestServices(IServiceCollection services) { + services.AddSingleton(_counterDataCollector); + var builder = services - .LastOrDefault(descriptor => descriptor.ServiceType == typeof(OrchardCoreBuilder))? - .ImplementationInstance as OrchardCoreBuilder - ?? throw new InvalidOperationException( - "Please call WebApplicationBuilder.Services.AddOrchardCms() in your Program.cs!"); + .LastOrDefault(descriptor => descriptor.ServiceType == typeof(OrchardCoreBuilder))? + .ImplementationInstance as OrchardCoreBuilder + ?? throw new InvalidOperationException( + "Please call WebApplicationBuilder.Services.AddOrchardCms() in your Program.cs!"); var configuration = services - .LastOrDefault(descriptor => descriptor.ServiceType == typeof(ConfigurationManager))? - .ImplementationInstance as ConfigurationManager - ?? throw new InvalidOperationException( - $"Please add {nameof(ConfigurationManager)} instance to WebApplicationBuilder.Services in your Program.cs!"); + .LastOrDefault(descriptor => descriptor.ServiceType == typeof(ConfigurationManager))? + .ImplementationInstance as ConfigurationManager + ?? throw new InvalidOperationException( + $"Please add {nameof(ConfigurationManager)} instance to WebApplicationBuilder.Services in your Program.cs!"); _configureOrchard?.Invoke(configuration, builder); - builder.ConfigureServices(builderServices => - { - AddFakeStore(builderServices); - AddFakeViewCompilerProvider(builderServices); - AddSessionProbe(builderServices); - }); + builder.Configure(app => app.UseMiddleware()) + .ConfigureServices(builderServices => + { + AddFakeStore(builderServices); + AddFakeViewCompilerProvider(builderServices); + AddSessionProbe(builderServices); + }); } private void AddFakeStore(IServiceCollection services) @@ -137,6 +143,7 @@ private void AddSessionProbe(IServiceCollection services) return new SessionProbe( _counterDataCollector, + httpContextAccessor.HttpContext.Request.Method, new Uri(httpContextAccessor.HttpContext.Request.GetEncodedUrl()), session); }); diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index d7ec52986..10a2eec42 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -1,4 +1,5 @@ using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Services.Counters.Configuration; using Lombiq.Tests.UI.Services.GitHub; using OpenQA.Selenium; using Shouldly; diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 3a1bd03e1..bade74602 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -6,6 +6,7 @@ using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Models; +using Lombiq.Tests.UI.Services.Counters.Configuration; using Lombiq.Tests.UI.Services.GitHub; using Microsoft.VisualBasic.FileIO; using Mono.Unix; @@ -106,6 +107,7 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) _context.FailureDumpContainer.Clear(); _context.CounterDataCollector.Reset(); + _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.Running); _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Running.AssertCounterData ?? CounterConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Running); failureDumpContainer = _context.FailureDumpContainer; @@ -115,6 +117,7 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) await _testManifest.TestAsync(_context); await _context.AssertLogsAsync(); + _context.CounterDataCollector.Dump().ForEach(line => _testOutputHelper.WriteLine(line)); _context.CounterDataCollector.AssertCounter(); return true; @@ -476,6 +479,8 @@ private async Task SetupAsync() // Note that the context creation needs to be done here too because the Orchard app needs the snapshot // config to be available at startup too. _context = await CreateContextAsync(); + + _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.Setup); _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Setup.AssertCounterData ?? CounterConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Setup); @@ -487,6 +492,7 @@ private async Task SetupAsync() var result = (_context, await setupConfiguration.SetupOperation(_context)); await _context.AssertLogsAsync(); + _context.CounterDataCollector.Dump().ForEach(line => _testOutputHelper.WriteLine(line)); _context.CounterDataCollector.AssertCounter(); _testOutputHelper.WriteLineTimestampedAndDebug("Finished setup operation."); @@ -610,7 +616,7 @@ Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommand _configuration.OrchardCoreConfiguration.BeforeAppStart.RemoveAll(UITestingBeforeAppStartHandlerAsync); _configuration.OrchardCoreConfiguration.BeforeAppStart += UITestingBeforeAppStartHandlerAsync; - var counterDataCollector = new CounterDataCollector(); + var counterDataCollector = new CounterDataCollector(_testOutputHelper); _applicationInstance = new OrchardCoreInstance( _configuration.OrchardCoreConfiguration, From 33fd4fe70eaa535731db1c8707956d70bf8c63a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 6 Dec 2022 20:43:35 +0100 Subject: [PATCH 15/44] Fixes after merge --- Lombiq.Tests.UI/Services/Counters/SessionProbe.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs index cbd5da1d6..93e730bab 100644 --- a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -32,11 +32,11 @@ void ISession.Save(object obj, bool checkConcurrency, string collection) => _session.Save(obj, checkConcurrency, collection); void ISession.Delete(object item, string collection) => _session.Delete(item, collection); - bool ISession.Import(object item, int id, int version, string collection) => + bool ISession.Import(object item, long id, long version, string collection) => _session.Import(item, id, version, collection); void ISession.Detach(object item, string collection) => _session.Detach(item, collection); - Task> ISession.GetAsync(int[] ids, string collection) => + Task> ISession.GetAsync(long[] ids, string collection) => _session.GetAsync(ids, collection); IQuery ISession.Query(string collection) => _session.Query(collection); From 5d32c2f72602c776326ef6f2f871fea8839550b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Fri, 13 Jan 2023 23:47:00 +0100 Subject: [PATCH 16/44] Fixing analyzer errors --- Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs index b35efe12a..59d1dbb03 100644 --- a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs +++ b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs @@ -56,8 +56,8 @@ private static string FormatMessage( { var builder = new StringBuilder(); if (probe is not null) builder.AppendLine(probe.DumpHeadline()); - if (counter is not null) counter.Dump().ForEach(line => builder.AppendLine(line)); - if (value is not null) value.Dump().ForEach(line => builder.AppendLine(line)); + counter?.Dump().ForEach(line => builder.AppendLine(line)); + value?.Dump().ForEach(line => builder.AppendLine(line)); if (!string.IsNullOrEmpty(message)) builder.AppendLine(message); return builder.ToString(); From b416eb59af0b87438c31ef55558fcb9b5ca3b072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Wed, 22 Mar 2023 10:15:30 +0100 Subject: [PATCH 17/44] Fixes after merge --- .../OrchardApplicationFactory.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index 63b50e25f..222a9e92d 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -117,13 +117,15 @@ .ImplementationInstance as ConfigurationManager _configureOrchard?.Invoke(configuration, builder); builder.Configure(app => app.UseMiddleware()) - .ConfigureServices(builderServices => - { - AddFakeStore(builderServices); - AddFakeViewCompilerProvider(builderServices); - ReplaceRecipeHarvester(builderServices); - AddSessionProbe(builderServices); - }); + .ConfigureServices( + builderServices => + { + AddFakeStore(builderServices); + AddFakeViewCompilerProvider(builderServices); + ReplaceRecipeHarvester(builderServices); + AddSessionProbe(builderServices); + }, + int.MaxValue); } private void AddFakeStore(IServiceCollection services) From bc91080ddd65811d4000705feaa285f1b006cf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Fri, 24 Mar 2023 11:15:30 +0100 Subject: [PATCH 18/44] Fixing possible thread safety violation --- .../ShortcutsUITestContextExtensions.cs | 23 +++++-------------- Lombiq.Tests.UI/Services/UITestExecutor.cs | 15 ++++++------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs index 00a18c433..cc1dc5bb2 100644 --- a/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ShortcutsUITestContextExtensions.cs @@ -361,12 +361,8 @@ public static Task ExecuteRecipeDirectlyAsync( .AwaitEachAsync(harvester => harvester.HarvestRecipesAsync()); var recipe = recipeCollections .SelectMany(recipeCollection => recipeCollection) - .SingleOrDefault(recipeDescriptor => recipeDescriptor.Name == recipeName); - - if (recipe == null) - { - throw new RecipeNotFoundException($"Recipe with the name \"{recipeName}\" not found."); - } + .SingleOrDefault(recipeDescriptor => recipeDescriptor.Name == recipeName) + ?? throw new RecipeNotFoundException($"Recipe with the name \"{recipeName}\" not found."); // Logic copied from OrchardCore.Recipes.Controllers.AdminController. var executionId = Guid.NewGuid().ToString("n"); @@ -443,12 +439,8 @@ public static Task SelectThemeAsync( { var shellFeatureManager = serviceProvider.GetRequiredService(); var themeFeature = (await shellFeatureManager.GetAvailableFeaturesAsync()) - .FirstOrDefault(feature => feature.IsTheme() && feature.Id == id); - - if (themeFeature == null) - { - throw new ThemeNotFoundException($"Theme with the feature ID {id} not found."); - } + .FirstOrDefault(feature => feature.IsTheme() && feature.Id == id) + ?? throw new ThemeNotFoundException($"Theme with the feature ID {id} not found."); if (IsAdminTheme(themeFeature.Extension.Manifest)) { @@ -560,11 +552,8 @@ await context.Application.UsingScopeAsync( { var workflowTypeStore = serviceProvider.GetRequiredService(); - var workflowType = await workflowTypeStore.GetAsync(workflowTypeId); - if (workflowType == null) - { - throw new WorkflowTypeNotFoundException($"Workflow type with the ID {workflowTypeId} not found."); - } + var workflowType = await workflowTypeStore.GetAsync(workflowTypeId) + ?? throw new WorkflowTypeNotFoundException($"Workflow type with the ID {workflowTypeId} not found."); var securityTokenService = serviceProvider.GetRequiredService(); var token = securityTokenService.CreateToken( diff --git a/Lombiq.Tests.UI/Services/UITestExecutor.cs b/Lombiq.Tests.UI/Services/UITestExecutor.cs index caa8bae97..5e84ba37f 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutor.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutor.cs @@ -12,13 +12,13 @@ namespace Lombiq.Tests.UI.Services; public static class UITestExecutor { - private static readonly object _numberOfTestsLimitLock = new(); + private static readonly SemaphoreSlim _numberOfTestsLimitLock = new(1, 1); private static SemaphoreSlim _numberOfTestsLimit; /// /// Executes a test on a new Orchard Core web app instance within a newly created Atata scope. /// - public static Task ExecuteOrchardCoreTestAsync( + public static async Task ExecuteOrchardCoreTestAsync( UITestManifest testManifest, OrchardCoreUITestExecutorConfiguration configuration) where TEntryPoint : class @@ -51,15 +51,16 @@ public static Task ExecuteOrchardCoreTestAsync( configuration.TestOutputHelper.WriteLineTimestampedAndDebug("Finished preparation for {0}.", testManifest.Name); + await _numberOfTestsLimitLock.WaitAsync(); + if (_numberOfTestsLimit == null && configuration.MaxParallelTests > 0) { - lock (_numberOfTestsLimitLock) - { - _numberOfTestsLimit ??= new SemaphoreSlim(configuration.MaxParallelTests); - } + _numberOfTestsLimit = new SemaphoreSlim(configuration.MaxParallelTests); } - return ExecuteOrchardCoreTestInnerAsync(testManifest, configuration, dumpRootPath); + _numberOfTestsLimitLock.Release(); + + await ExecuteOrchardCoreTestInnerAsync(testManifest, configuration, dumpRootPath); } private static async Task ExecuteOrchardCoreTestInnerAsync( From 278fdccd8786cb62417d755986c5030a5cccb9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Mon, 27 Mar 2023 10:29:37 +0200 Subject: [PATCH 19/44] Addressing "S4457: Split this method into two..." analyzer error --- Lombiq.Tests.UI/Services/UITestExecutor.cs | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Lombiq.Tests.UI/Services/UITestExecutor.cs b/Lombiq.Tests.UI/Services/UITestExecutor.cs index 5e84ba37f..f09262e55 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutor.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutor.cs @@ -18,7 +18,7 @@ public static class UITestExecutor /// /// Executes a test on a new Orchard Core web app instance within a newly created Atata scope. /// - public static async Task ExecuteOrchardCoreTestAsync( + public static Task ExecuteOrchardCoreTestAsync( UITestManifest testManifest, OrchardCoreUITestExecutorConfiguration configuration) where TEntryPoint : class @@ -51,16 +51,7 @@ public static async Task ExecuteOrchardCoreTestAsync( configuration.TestOutputHelper.WriteLineTimestampedAndDebug("Finished preparation for {0}.", testManifest.Name); - await _numberOfTestsLimitLock.WaitAsync(); - - if (_numberOfTestsLimit == null && configuration.MaxParallelTests > 0) - { - _numberOfTestsLimit = new SemaphoreSlim(configuration.MaxParallelTests); - } - - _numberOfTestsLimitLock.Release(); - - await ExecuteOrchardCoreTestInnerAsync(testManifest, configuration, dumpRootPath); + return ExecuteOrchardCoreTestInnerAsync(testManifest, configuration, dumpRootPath); } private static async Task ExecuteOrchardCoreTestInnerAsync( @@ -69,6 +60,8 @@ private static async Task ExecuteOrchardCoreTestInnerAsync( string dumpRootPath) where TEntryPoint : class { + await PrepareTestLimitAsync(configuration); + var retryCount = 0; var passed = false; while (!passed) @@ -91,7 +84,6 @@ private static async Task ExecuteOrchardCoreTestInnerAsync( catch (Exception ex) { // When the last try failed. - if (configuration.ExtendGitHubActionsOutput && configuration.GitHubActionsOutputConfiguration.EnableErrorAnnotations && GitHubHelper.IsGitHubEnvironment) @@ -177,4 +169,16 @@ private static string PrepareDumpFolder( return dumpRootPath; } + + private static async Task PrepareTestLimitAsync(OrchardCoreUITestExecutorConfiguration configuration) + { + await _numberOfTestsLimitLock.WaitAsync(); + + if (_numberOfTestsLimit == null && configuration.MaxParallelTests > 0) + { + _numberOfTestsLimit = new SemaphoreSlim(configuration.MaxParallelTests); + } + + _numberOfTestsLimitLock.Release(); + } } From 2fd58bdf986305a650daf2ed5d1876ba5288015e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Mon, 3 Apr 2023 09:20:37 +0200 Subject: [PATCH 20/44] Adding per url threshold configuration support --- .../Configuration/CounterConfiguration.cs | 27 ++++++++---- .../RelativeUrlConfigurationKey.cs | 15 +++++++ .../RunningPhaseCounterConfiguration.cs | 43 +++++++++++++++++++ ...s.cs => CounterDataCollectorExtensions.cs} | 2 +- .../RelativeUrlConfigurationKeyExtensions.cs | 13 ++++++ .../Counters/ICounterConfigurationKey.cs | 10 +++++ .../Counters/IRelativeUrlConfigurationKey.cs | 14 ++++++ .../Services/Counters/NavigationProbe.cs | 11 ++++- .../Services/Counters/PageLoadProbe.cs | 11 ++++- .../Services/Counters/SessionProbe.cs | 11 ++++- 10 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs rename Lombiq.Tests.UI/Services/Counters/Extensions/{ICounterDataCollectorExtensions.cs => CounterDataCollectorExtensions.cs} (98%) create mode 100644 Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/ICounterConfigurationKey.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs index 83c186864..1d21d877c 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs @@ -17,17 +17,28 @@ public class CounterConfiguration /// /// Gets the counter configuration used in the running phase of the web application. /// - public PhaseCounterConfiguration Running { get; } = new(); + public RunningPhaseCounterConfiguration Running { get; } = new(); public static Action DefaultAssertCounterData(PhaseCounterConfiguration configuration) => probe => { + var phaseConfiguration = configuration; + if (phaseConfiguration is RunningPhaseCounterConfiguration runningPhaseCounterConfiguration + && probe is ICounterConfigurationKey counterConfigurationKey) + { + phaseConfiguration = runningPhaseCounterConfiguration.GetMaybe(counterConfigurationKey) ?? configuration; + } + (CounterThresholdConfiguration Settings, string Name)? threshold = probe switch { - NavigationProbe => (Settings: configuration.NavigationThreshold, Name: nameof(configuration.NavigationThreshold)), - PageLoadProbe => (Settings: configuration.PageLoadThreshold, Name: nameof(configuration.NavigationThreshold)), - SessionProbe => (Settings: configuration.SessionThreshold, Name: nameof(configuration.NavigationThreshold)), - CounterDataCollector => (Settings: configuration.PhaseThreshold, Name: nameof(configuration.NavigationThreshold)), + NavigationProbe => + (Settings: phaseConfiguration.NavigationThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), + PageLoadProbe => + (Settings: phaseConfiguration.PageLoadThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), + SessionProbe => + (Settings: phaseConfiguration.SessionThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), + CounterDataCollector => + (Settings: phaseConfiguration.PhaseThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), _ => null, }; @@ -35,17 +46,17 @@ public static Action DefaultAssertCounterData(PhaseCounterConfigu { AssertIntegerCounterValue( probe, - configuration.ExcludeFilter ?? (key => false), + phaseConfiguration.ExcludeFilter ?? (key => false), $"{settings.Name}.{nameof(settings.Settings.DbCommandExecutionThreshold)}", settings.Settings.DbCommandExecutionThreshold); AssertIntegerCounterValue( probe, - configuration.ExcludeFilter ?? (key => false), + phaseConfiguration.ExcludeFilter ?? (key => false), $"{settings.Name}.{nameof(settings.Settings.DbCommandTextExecutionThreshold)}", settings.Settings.DbCommandTextExecutionThreshold); AssertIntegerCounterValue( probe, - configuration.ExcludeFilter ?? (key => false), + phaseConfiguration.ExcludeFilter ?? (key => false), $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", settings.Settings.DbReaderReadThreshold); } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs new file mode 100644 index 000000000..b5502bfef --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs @@ -0,0 +1,15 @@ +using Lombiq.Tests.UI.Services.Counters.Extensions; +using System; + +namespace Lombiq.Tests.UI.Services.Counters.Configuration; + +public sealed class RelativeUrlConfigurationKey : IRelativeUrlConfigurationKey +{ + public Uri Url { get; private init; } + + public RelativeUrlConfigurationKey(Uri url) => + Url = url; + + public bool Equals(ICounterConfigurationKey other) => + this.EqualsWith(other as IRelativeUrlConfigurationKey); +} diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs new file mode 100644 index 000000000..eee5e3647 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs @@ -0,0 +1,43 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Lombiq.Tests.UI.Services.Counters.Configuration; + +public class RunningPhaseCounterConfiguration : PhaseCounterConfiguration, + IDictionary +{ + private readonly IDictionary _configurations = + new Dictionary(); + + public PhaseCounterConfiguration this[ICounterConfigurationKey key] + { + get => _configurations[key]; + set => _configurations[key] = value; + } + + public ICollection Keys => _configurations.Keys; + + public ICollection Values => _configurations.Values; + + public int Count => _configurations.Count; + + public bool IsReadOnly => false; + + public void Add(ICounterConfigurationKey key, PhaseCounterConfiguration value) => _configurations.Add(key, value); + public void Add(KeyValuePair item) => _configurations.Add(item); + public void Clear() => _configurations.Clear(); + public bool Contains(KeyValuePair item) => + _configurations.Contains(item); + public bool ContainsKey(ICounterConfigurationKey key) => _configurations.ContainsKey(key); + public void CopyTo(KeyValuePair[] array, int arrayIndex) => + _configurations.CopyTo(array, arrayIndex); + public IEnumerator> GetEnumerator() => + _configurations.GetEnumerator(); + public bool Remove(ICounterConfigurationKey key) => _configurations.Remove(key); + public bool Remove(KeyValuePair item) => + _configurations.Remove(item); + public bool TryGetValue(ICounterConfigurationKey key, [MaybeNullWhen(false)] out PhaseCounterConfiguration value) => + _configurations.TryGetValue(key, out value); + IEnumerator IEnumerable.GetEnumerator() => _configurations.GetEnumerator(); +} diff --git a/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/CounterDataCollectorExtensions.cs similarity index 98% rename from Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs rename to Lombiq.Tests.UI/Services/Counters/Extensions/CounterDataCollectorExtensions.cs index 8ae7a9162..f2516cdba 100644 --- a/Lombiq.Tests.UI/Services/Counters/Extensions/ICounterDataCollectorExtensions.cs +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/CounterDataCollectorExtensions.cs @@ -6,7 +6,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Extensions; -public static class ICounterDataCollectorExtensions +public static class CounterDataCollectorExtensions { public static int DbCommandExecuteNonQuery(this ICounterDataCollector collector, DbCommand dbCommand) { diff --git a/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs new file mode 100644 index 000000000..4a9ec0170 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs @@ -0,0 +1,13 @@ +namespace Lombiq.Tests.UI.Services.Counters.Extensions; + +public static class RelativeUrlConfigurationKeyExtensions +{ + public static bool EqualsWith(this IRelativeUrlConfigurationKey left, IRelativeUrlConfigurationKey right) + { + if (ReferenceEquals(left, right)) return true; + + if (left is null || right is null) return false; + + return left.Url?.PathAndQuery == right.Url?.PathAndQuery; + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterConfigurationKey.cs b/Lombiq.Tests.UI/Services/Counters/ICounterConfigurationKey.cs new file mode 100644 index 000000000..7e35a9350 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/ICounterConfigurationKey.cs @@ -0,0 +1,10 @@ +using System; + +namespace Lombiq.Tests.UI.Services.Counters; + +/// +/// Represents a key in . +/// +public interface ICounterConfigurationKey : IEquatable +{ +} diff --git a/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs b/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs new file mode 100644 index 000000000..cb96fb5f7 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs @@ -0,0 +1,14 @@ +using System; + +namespace Lombiq.Tests.UI.Services.Counters; + +/// +/// Represents a relative URL configuration key in . +/// +public interface IRelativeUrlConfigurationKey : ICounterConfigurationKey +{ + /// + /// Gets the URL. + /// + Uri Url { get; } +} diff --git a/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs index e57a93818..63238bc0c 100644 --- a/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs @@ -1,8 +1,9 @@ +using Lombiq.Tests.UI.Services.Counters.Extensions; using System; namespace Lombiq.Tests.UI.Services.Counters; -public class NavigationProbe : CounterProbe +public sealed class NavigationProbe : CounterProbe, IRelativeUrlConfigurationKey { public Uri AbsoluteUri { get; init; } @@ -11,4 +12,12 @@ public NavigationProbe(ICounterDataCollector counterDataCollector, Uri absoluteU AbsoluteUri = absoluteUri; public override string DumpHeadline() => $"{nameof(NavigationProbe)}, AbsoluteUri = {AbsoluteUri}"; + + #region IRelativeUrlConfigurationKey implementation + + Uri IRelativeUrlConfigurationKey.Url => AbsoluteUri; + bool IEquatable.Equals(ICounterConfigurationKey other) => + this.EqualsWith(other as IRelativeUrlConfigurationKey); + + #endregion } diff --git a/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs b/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs index db2b49e8f..6d7670b31 100644 --- a/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs @@ -1,8 +1,9 @@ +using Lombiq.Tests.UI.Services.Counters.Extensions; using System; namespace Lombiq.Tests.UI.Services.Counters; -public class PageLoadProbe : CounterProbe +public sealed class PageLoadProbe : CounterProbe, IRelativeUrlConfigurationKey { public string RequestMethod { get; init; } public Uri AbsoluteUri { get; init; } @@ -15,4 +16,12 @@ public PageLoadProbe(ICounterDataCollector counterDataCollector, string requestM } public override string DumpHeadline() => $"{nameof(PageLoadProbe)}, [{RequestMethod}]{AbsoluteUri}"; + + #region IRelativeUrlConfigurationKey implementation + + Uri IRelativeUrlConfigurationKey.Url => AbsoluteUri; + bool IEquatable.Equals(ICounterConfigurationKey other) => + this.EqualsWith(other as IRelativeUrlConfigurationKey); + + #endregion } diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs index 93e730bab..d3151e50c 100644 --- a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -1,3 +1,4 @@ +using Lombiq.Tests.UI.Services.Counters.Extensions; using System; using System.Collections.Generic; using System.Data; @@ -8,7 +9,7 @@ namespace Lombiq.Tests.UI.Services.Counters; -public sealed class SessionProbe : CounterProbe, ISession +public sealed class SessionProbe : CounterProbe, ISession, IRelativeUrlConfigurationKey { private readonly ISession _session; public string RequestMethod { get; init; } @@ -58,4 +59,12 @@ async ValueTask IAsyncDisposable.DisposeAsync() // and we should count the executed db commands in it. Dispose(); } + + #region IRelativeUrlConfigurationKey implementation + + Uri IRelativeUrlConfigurationKey.Url => AbsoluteUri; + bool IEquatable.Equals(ICounterConfigurationKey other) => + this.EqualsWith(other as IRelativeUrlConfigurationKey); + + #endregion } From 8c05e71b4d1bab60974cb57ed427475eca4421a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Fri, 19 May 2023 23:14:45 +0200 Subject: [PATCH 21/44] Adding per page configuration implementation --- .../Configuration/CounterConfiguration.cs | 138 +++++++++--------- .../Configuration/CounterConfigurations.cs | 88 +++++++++++ .../PhaseCounterConfiguration.cs | 89 +---------- .../RelativeUrlConfigurationKey.cs | 9 +- .../RunningPhaseCounterConfiguration.cs | 25 ++-- .../RelativeUrlConfigurationKeyExtensions.cs | 11 +- ...ningPhaseCounterConfigurationExtensions.cs | 61 ++++++++ .../Counters/IRelativeUrlConfigurationKey.cs | 5 + .../Services/Counters/NavigationProbe.cs | 1 + .../Services/Counters/PageLoadProbe.cs | 1 + .../Services/Counters/SessionProbe.cs | 1 + .../OrchardCoreUITestExecutorConfiguration.cs | 2 +- .../Services/UITestExecutionSession.cs | 4 +- 13 files changed, 262 insertions(+), 173 deletions(-) create mode 100644 Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/Extensions/RunningPhaseCounterConfigurationExtensions.cs diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs index 4a0233900..7786682c7 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs @@ -1,6 +1,4 @@ -using Lombiq.Tests.UI.Exceptions; using Lombiq.Tests.UI.Services.Counters.Data; -using Lombiq.Tests.UI.Services.Counters.Value; using System; using System.Collections.Generic; using System.Linq; @@ -10,78 +8,84 @@ namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class CounterConfiguration { /// - /// Gets the counter configuration used in the setup phase of the web application. + /// Gets or sets the counter assertion method. /// - public PhaseCounterConfiguration Setup { get; } = new(); + public Action AssertCounterData { get; set; } /// - /// Gets the counter configuration used in the running phase of the web application. + /// Gets or sets the exclude filter. Can be used to exclude counted values before assertion. /// - public RunningPhaseCounterConfiguration Running { get; } = new(); + public Func ExcludeFilter { get; set; } = DefaultExcludeFilter; - public static Action DefaultAssertCounterData(PhaseCounterConfiguration configuration) => - probe => - { - var phaseConfiguration = configuration; - if (phaseConfiguration is RunningPhaseCounterConfiguration runningPhaseCounterConfiguration - && probe is ICounterConfigurationKey counterConfigurationKey) - { - phaseConfiguration = runningPhaseCounterConfiguration.GetMaybe(counterConfigurationKey) - ?? configuration; - } + /// + /// Gets or sets threshold configuration used under navigation requests. See: + /// . + /// See: . + /// + public CounterThresholdConfiguration NavigationThreshold { get; set; } = new CounterThresholdConfiguration + { + DbCommandExecutionThreshold = 11, + DbCommandTextExecutionThreshold = 22, + DbReaderReadThreshold = 11, + }; - (CounterThresholdConfiguration Settings, string Name)? threshold = probe switch - { - NavigationProbe => - (Settings: phaseConfiguration.NavigationThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), - PageLoadProbe => - (Settings: phaseConfiguration.PageLoadThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), - SessionProbe => - (Settings: phaseConfiguration.SessionThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), - CounterDataCollector => - (Settings: phaseConfiguration.PhaseThreshold, Name: nameof(phaseConfiguration.NavigationThreshold)), - _ => null, - }; + /// + /// Gets or sets threshold configuration used per lifetime. See: + /// . + /// + public CounterThresholdConfiguration SessionThreshold { get; set; } = new CounterThresholdConfiguration + { + DbCommandExecutionThreshold = 22, + DbCommandTextExecutionThreshold = 44, + DbReaderReadThreshold = 11, + }; - if (threshold is { } settings && settings.Settings.Disable is not true) - { - AssertIntegerCounterValue( - probe, - phaseConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandExecutionThreshold)}", - settings.Settings.DbCommandExecutionThreshold); - AssertIntegerCounterValue( - probe, - phaseConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandTextExecutionThreshold)}", - settings.Settings.DbCommandTextExecutionThreshold); - AssertIntegerCounterValue( - probe, - phaseConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", - settings.Settings.DbReaderReadThreshold); - } - }; + /// + /// Gets or sets threshold configuration used per page load. See: . + /// + public CounterThresholdConfiguration PageLoadThreshold { get; set; } = new CounterThresholdConfiguration + { + DbCommandExecutionThreshold = 22, + DbCommandTextExecutionThreshold = 44, + DbReaderReadThreshold = 11, + }; - public static void AssertIntegerCounterValue( - ICounterProbe probe, - Func excludeFilter, - string thresholdName, - int threshold) - where TKey : ICounterKey => - probe.Counters.Keys - .OfType() - .Where(key => !excludeFilter(key)) - .ForEach(key => + public static IEnumerable DefaultExcludeList { get; } = new List + { + new DbCommandExecuteCounterKey( + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + new List> { - if (probe.Counters[key] is IntegerCounterValue counterValue - && counterValue.Value > threshold) - { - throw new CounterThresholdException( - probe, - key, - counterValue, - $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); - } - }); + new("p0", "ContentCreatedEvent"), + new("p1", value: true), + }), + new DbCommandExecuteCounterKey( + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + new List> + { + new("p0", "ContentPublishedEvent"), + new("p1", value: true), + }), + new DbCommandExecuteCounterKey( + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + new List> + { + new("p0", "ContentUpdatedEvent"), + new("p1", value: true), + }), + }; + + public static bool DefaultExcludeFilter(ICounterKey key) => DefaultExcludeList.Contains(key); } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs new file mode 100644 index 000000000..cefea1c73 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs @@ -0,0 +1,88 @@ +using Lombiq.Tests.UI.Exceptions; +using Lombiq.Tests.UI.Services.Counters.Data; +using Lombiq.Tests.UI.Services.Counters.Extensions; +using Lombiq.Tests.UI.Services.Counters.Value; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lombiq.Tests.UI.Services.Counters.Configuration; + +public class CounterConfigurations +{ + /// + /// Gets the counter configuration used in the setup phase of the web application. + /// + public PhaseCounterConfiguration Setup { get; } = new(); + + /// + /// Gets the counter configuration used in the running phase of the web application. + /// + public RunningPhaseCounterConfiguration Running { get; } = new(); + + public static Action DefaultAssertCounterData(PhaseCounterConfiguration configuration) => + probe => + { + var counterConfiguration = configuration as CounterConfiguration; + if (counterConfiguration is RunningPhaseCounterConfiguration runningPhaseCounterConfiguration + && probe is ICounterConfigurationKey counterConfigurationKey) + { + counterConfiguration = runningPhaseCounterConfiguration.GetMaybeByKey(counterConfigurationKey) + ?? configuration; + } + + (CounterThresholdConfiguration Settings, string Name)? threshold = probe switch + { + NavigationProbe => + (Settings: counterConfiguration.NavigationThreshold, Name: nameof(counterConfiguration.NavigationThreshold)), + PageLoadProbe => + (Settings: counterConfiguration.PageLoadThreshold, Name: nameof(counterConfiguration.PageLoadThreshold)), + SessionProbe => + (Settings: counterConfiguration.SessionThreshold, Name: nameof(counterConfiguration.SessionThreshold)), + CounterDataCollector when counterConfiguration is PhaseCounterConfiguration phaseCounterConfiguration => + (Settings: phaseCounterConfiguration.PhaseThreshold, Name: nameof(phaseCounterConfiguration.PhaseThreshold)), + _ => null, + }; + + if (threshold is { } settings && settings.Settings.Disable is not true) + { + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandExecutionThreshold)}", + settings.Settings.DbCommandExecutionThreshold); + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandTextExecutionThreshold)}", + settings.Settings.DbCommandTextExecutionThreshold); + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", + settings.Settings.DbReaderReadThreshold); + } + }; + + public static void AssertIntegerCounterValue( + ICounterProbe probe, + Func excludeFilter, + string thresholdName, + int threshold) + where TKey : ICounterKey => + probe.Counters.Keys + .OfType() + .Where(key => !excludeFilter(key)) + .ForEach(key => + { + if (probe.Counters[key] is IntegerCounterValue counterValue + && counterValue.Value > threshold) + { + throw new CounterThresholdException( + probe, + key, + counterValue, + $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); + } + }); +} diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs index bf09d0ba4..62b903018 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs @@ -1,55 +1,7 @@ -using Lombiq.Tests.UI.Services.Counters.Data; -using System; -using System.Collections.Generic; -using System.Linq; - namespace Lombiq.Tests.UI.Services.Counters.Configuration; -public class PhaseCounterConfiguration +public class PhaseCounterConfiguration : CounterConfiguration { - /// - /// Gets or sets the counter assertion method. - /// - public Action AssertCounterData { get; set; } - - /// - /// Gets or sets the exclude filter. Can be used to exclude counted values before assertion. - /// - public Func ExcludeFilter { get; set; } = DefaultExcludeFilter; - - /// - /// Gets or sets threshold configuration used under navigation requests. See: - /// . - /// See: . - /// - public CounterThresholdConfiguration NavigationThreshold { get; set; } = new CounterThresholdConfiguration - { - DbCommandExecutionThreshold = 11, - DbCommandTextExecutionThreshold = 22, - DbReaderReadThreshold = 11, - }; - - /// - /// Gets or sets threshold configuration used per lifetime. See: - /// . - /// - public CounterThresholdConfiguration SessionThreshold { get; set; } = new CounterThresholdConfiguration - { - DbCommandExecutionThreshold = 22, - DbCommandTextExecutionThreshold = 44, - DbReaderReadThreshold = 11, - }; - - /// - /// Gets or sets threshold configuration used per page load. See: . - /// - public CounterThresholdConfiguration PageLoadThreshold { get; set; } = new CounterThresholdConfiguration - { - DbCommandExecutionThreshold = 22, - DbCommandTextExecutionThreshold = 44, - DbReaderReadThreshold = 11, - }; - /// /// Gets or sets threshold configuration used under app phase(setup, running) lifetime. /// @@ -59,43 +11,4 @@ public class PhaseCounterConfiguration DbCommandTextExecutionThreshold = 44, DbReaderReadThreshold = 11, }; - - public static IEnumerable DefaultExcludeList { get; } = new List - { - new DbCommandExecuteCounterKey( - "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" - + " AS [WorkflowTypeStartActivitiesIndex_a1]" - + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" - + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" - + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", - new List> - { - new("p0", "ContentCreatedEvent"), - new("p1", value: true), - }), - new DbCommandExecuteCounterKey( - "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" - + " AS [WorkflowTypeStartActivitiesIndex_a1]" - + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" - + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" - + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", - new List> - { - new("p0", "ContentPublishedEvent"), - new("p1", value: true), - }), - new DbCommandExecuteCounterKey( - "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" - + " AS [WorkflowTypeStartActivitiesIndex_a1]" - + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" - + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" - + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", - new List> - { - new("p0", "ContentUpdatedEvent"), - new("p1", value: true), - }), - }; - - public static bool DefaultExcludeFilter(ICounterKey key) => DefaultExcludeList.Contains(key); } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs index b5502bfef..3a59811a5 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/RelativeUrlConfigurationKey.cs @@ -6,10 +6,17 @@ namespace Lombiq.Tests.UI.Services.Counters.Configuration; public sealed class RelativeUrlConfigurationKey : IRelativeUrlConfigurationKey { public Uri Url { get; private init; } + public bool ExactMatch { get; private init; } - public RelativeUrlConfigurationKey(Uri url) => + public RelativeUrlConfigurationKey(Uri url, bool exactMatch = true) + { Url = url; + ExactMatch = exactMatch; + } public bool Equals(ICounterConfigurationKey other) => this.EqualsWith(other as IRelativeUrlConfigurationKey); + + public override int GetHashCode() => + (Url, ExactMatch).GetHashCode(); } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs index eee5e3647..64bcd9740 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/RunningPhaseCounterConfiguration.cs @@ -1,16 +1,17 @@ using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class RunningPhaseCounterConfiguration : PhaseCounterConfiguration, - IDictionary + IDictionary { - private readonly IDictionary _configurations = - new Dictionary(); + private readonly IDictionary _configurations = + new ConcurrentDictionary(); - public PhaseCounterConfiguration this[ICounterConfigurationKey key] + public CounterConfiguration this[ICounterConfigurationKey key] { get => _configurations[key]; set => _configurations[key] = value; @@ -18,26 +19,26 @@ public PhaseCounterConfiguration this[ICounterConfigurationKey key] public ICollection Keys => _configurations.Keys; - public ICollection Values => _configurations.Values; + public ICollection Values => _configurations.Values; public int Count => _configurations.Count; public bool IsReadOnly => false; - public void Add(ICounterConfigurationKey key, PhaseCounterConfiguration value) => _configurations.Add(key, value); - public void Add(KeyValuePair item) => _configurations.Add(item); + public void Add(ICounterConfigurationKey key, CounterConfiguration value) => _configurations.Add(key, value); + public void Add(KeyValuePair item) => _configurations.Add(item); public void Clear() => _configurations.Clear(); - public bool Contains(KeyValuePair item) => + public bool Contains(KeyValuePair item) => _configurations.Contains(item); public bool ContainsKey(ICounterConfigurationKey key) => _configurations.ContainsKey(key); - public void CopyTo(KeyValuePair[] array, int arrayIndex) => + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _configurations.CopyTo(array, arrayIndex); - public IEnumerator> GetEnumerator() => + public IEnumerator> GetEnumerator() => _configurations.GetEnumerator(); public bool Remove(ICounterConfigurationKey key) => _configurations.Remove(key); - public bool Remove(KeyValuePair item) => + public bool Remove(KeyValuePair item) => _configurations.Remove(item); - public bool TryGetValue(ICounterConfigurationKey key, [MaybeNullWhen(false)] out PhaseCounterConfiguration value) => + public bool TryGetValue(ICounterConfigurationKey key, [MaybeNullWhen(false)] out CounterConfiguration value) => _configurations.TryGetValue(key, out value); IEnumerator IEnumerable.GetEnumerator() => _configurations.GetEnumerator(); } diff --git a/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs index 4a9ec0170..dce8d90a3 100644 --- a/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs @@ -1,3 +1,5 @@ +using System; + namespace Lombiq.Tests.UI.Services.Counters.Extensions; public static class RelativeUrlConfigurationKeyExtensions @@ -6,8 +8,13 @@ public static bool EqualsWith(this IRelativeUrlConfigurationKey left, IRelativeU { if (ReferenceEquals(left, right)) return true; - if (left is null || right is null) return false; + if (left?.Url is null || right?.Url is null) return false; + + var leftUrl = left.Url.IsAbsoluteUri ? left.Url.PathAndQuery : left.Url.OriginalString; + var rightUrl = right.Url.IsAbsoluteUri ? right.Url.PathAndQuery : right.Url.OriginalString; - return left.Url?.PathAndQuery == right.Url?.PathAndQuery; + return (left.ExactMatch || right.ExactMatch) + ? string.Equals(leftUrl, rightUrl, StringComparison.OrdinalIgnoreCase) + : leftUrl.StartsWithOrdinalIgnoreCase(rightUrl) || rightUrl.StartsWithOrdinalIgnoreCase(leftUrl); } } diff --git a/Lombiq.Tests.UI/Services/Counters/Extensions/RunningPhaseCounterConfigurationExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/RunningPhaseCounterConfigurationExtensions.cs new file mode 100644 index 000000000..45f7a4779 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/RunningPhaseCounterConfigurationExtensions.cs @@ -0,0 +1,61 @@ +using Lombiq.HelpfulLibraries.OrchardCore.Mvc; +using Lombiq.Tests.UI.Services.Counters.Configuration; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace Lombiq.Tests.UI.Services.Counters.Extensions; + +public static class RunningPhaseCounterConfigurationExtensions +{ + public static CounterConfiguration AddIfMissingAndConfigure( + this RunningPhaseCounterConfiguration configuration, + ICounterConfigurationKey key, + Action configure) + { + var phaseConfiguration = configuration.GetMaybeByKey(key); + if (phaseConfiguration is not null) + { + phaseConfiguration = configuration[key]; + configure?.Invoke(phaseConfiguration); + + return phaseConfiguration; + } + + phaseConfiguration = new PhaseCounterConfiguration(); + configure.Invoke(phaseConfiguration); + configuration.Add(key, phaseConfiguration); + + return phaseConfiguration; + } + + public static void ConfigureForRelativeUrl( + this RunningPhaseCounterConfiguration configuration, + Action configure, + Expression> actionExpressionAsync, + bool exactMatch, + params (string Key, object Value)[] additionalArguments) + where TController : ControllerBase => + configuration.AddIfMissingAndConfigure( + new RelativeUrlConfigurationKey( + new Uri( + TypedRoute.CreateFromExpression(actionExpressionAsync.StripResult(), additionalArguments).ToString(), + UriKind.Relative), + exactMatch), + configure); + + public static CounterConfiguration GetMaybeByKey( + this RunningPhaseCounterConfiguration configuration, + ICounterConfigurationKey key) + { + var configurationKey = configuration.Keys.FirstOrDefault(item => item.Equals(key)); + if (configurationKey is null) + { + return null; + } + + return configuration[configurationKey]; + } +} diff --git a/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs b/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs index cb96fb5f7..2b8f34ee0 100644 --- a/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs @@ -11,4 +11,9 @@ public interface IRelativeUrlConfigurationKey : ICounterConfigurationKey /// Gets the URL. /// Uri Url { get; } + + /// + /// Gets a value indicating whether the URL should be matched exactly or substring match enough. + /// + bool ExactMatch { get; } } diff --git a/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs index 63238bc0c..96f33b8bf 100644 --- a/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/NavigationProbe.cs @@ -16,6 +16,7 @@ public NavigationProbe(ICounterDataCollector counterDataCollector, Uri absoluteU #region IRelativeUrlConfigurationKey implementation Uri IRelativeUrlConfigurationKey.Url => AbsoluteUri; + bool IRelativeUrlConfigurationKey.ExactMatch => false; bool IEquatable.Equals(ICounterConfigurationKey other) => this.EqualsWith(other as IRelativeUrlConfigurationKey); diff --git a/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs b/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs index 6d7670b31..ef50b522f 100644 --- a/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs @@ -20,6 +20,7 @@ public PageLoadProbe(ICounterDataCollector counterDataCollector, string requestM #region IRelativeUrlConfigurationKey implementation Uri IRelativeUrlConfigurationKey.Url => AbsoluteUri; + bool IRelativeUrlConfigurationKey.ExactMatch => false; bool IEquatable.Equals(ICounterConfigurationKey other) => this.EqualsWith(other as IRelativeUrlConfigurationKey); diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs index d3151e50c..09999e627 100644 --- a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -63,6 +63,7 @@ async ValueTask IAsyncDisposable.DisposeAsync() #region IRelativeUrlConfigurationKey implementation Uri IRelativeUrlConfigurationKey.Url => AbsoluteUri; + bool IRelativeUrlConfigurationKey.ExactMatch => false; bool IEquatable.Equals(ICounterConfigurationKey other) => this.EqualsWith(other as IRelativeUrlConfigurationKey); diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index 11d15eae0..24177c42c 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -175,7 +175,7 @@ public class OrchardCoreUITestExecutorConfiguration /// /// Gets or sets configuration for performance counting and monitoring. /// - public CounterConfiguration CounterConfiguration { get; set; } = new(); + public CounterConfigurations CounterConfiguration { get; set; } = new(); public async Task AssertAppLogsMaybeAsync(IWebApplicationInstance instance, Action log) { diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index bade74602..b1bed3d84 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -109,7 +109,7 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) _context.CounterDataCollector.Reset(); _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.Running); _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Running.AssertCounterData - ?? CounterConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Running); + ?? CounterConfigurations.DefaultAssertCounterData(_configuration.CounterConfiguration.Running); failureDumpContainer = _context.FailureDumpContainer; _context.SetDefaultBrowserSize(); @@ -482,7 +482,7 @@ private async Task SetupAsync() _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.Setup); _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Setup.AssertCounterData - ?? CounterConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Setup); + ?? CounterConfigurations.DefaultAssertCounterData(_configuration.CounterConfiguration.Setup); SetupSqlServerSnapshot(); SetupAzureBlobStorageSnapshot(); From 406e6eddc078e86f0aab8f8540a39a4ff5c30056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 27 Aug 2023 10:12:30 +0200 Subject: [PATCH 22/44] Fixing analyzer violations --- Lombiq.Tests.UI/Services/CounterDataCollector.cs | 2 +- .../Services/Counters/Data/DbCommandCounterKey.cs | 6 ++++-- Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs | 4 ++-- .../OrchardCoreHosting/OrchardApplicationFactory.cs | 2 -- Lombiq.Tests.UI/Services/UITestExecutionSession.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index 6266d89d8..2911958fd 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -54,5 +54,5 @@ public override IEnumerable Dump() public void AssertCounter() => AssertCounter(this); private void ProbeCaptureCompleted(ICounterProbe probe) => - probe.Dump().ForEach(line => _testOutputHelper.WriteLine(line)); + probe.Dump().ForEach(_testOutputHelper.WriteLine); } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs index b8eafb6d0..360fdc066 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Lombiq.Tests.UI.Services.Counters.Data; @@ -40,8 +41,9 @@ public override IEnumerable Dump() if (Parameters.Any()) { var commandParams = Parameters.Select((parameter, index) => - FormattableString.Invariant( - $"[{index.ToTechnicalString()}]{parameter.Key ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) + string.Create( + CultureInfo.InvariantCulture, + $"[{index}]{parameter.Key ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) .Join(", "); lines.Add($"\t\t{commandParams}"); } diff --git a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs index e7ec48570..407c593f4 100644 --- a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Globalization; namespace Lombiq.Tests.UI.Services.Counters.Value; @@ -9,5 +9,5 @@ public abstract class CounterValue : ICounterValue public TValue Value { get; set; } public virtual IEnumerable Dump() => - new[] { FormattableString.Invariant($"{GetType().Name} value: {Value}") }; + new[] { string.Create(CultureInfo.InvariantCulture, $"{GetType().Name} value: {Value}") }; } diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index c2560c75b..108f0139c 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -1,7 +1,5 @@ using Lombiq.Tests.Integration.Services; using Lombiq.Tests.UI.Services.Counters; -using Lombiq.Tests.UI.Services.Counters.Middlewares; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index dd3eff9ca..db1b4ecf6 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -117,7 +117,7 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) await _testManifest.TestAsync(_context); await _context.AssertLogsAsync(); - _context.CounterDataCollector.Dump().ForEach(line => _testOutputHelper.WriteLine(line)); + _context.CounterDataCollector.Dump().ForEach(_testOutputHelper.WriteLine); _context.CounterDataCollector.AssertCounter(); return true; From 6bf29178192ad83427dfa0bb5ea4b5ae32f2aafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 27 Aug 2023 22:34:29 +0200 Subject: [PATCH 23/44] Adding ProbedSqliteConnection --- .../Counters/Data/ProbedSqliteConnection.cs | 36 +++++++++++++++++++ .../Services/ProbedConnectionFactory.cs | 13 +++++-- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/ProbedSqliteConnection.cs diff --git a/Lombiq.Tests.UI/Services/Counters/Data/ProbedSqliteConnection.cs b/Lombiq.Tests.UI/Services/Counters/Data/ProbedSqliteConnection.cs new file mode 100644 index 000000000..c6db6a64d --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/ProbedSqliteConnection.cs @@ -0,0 +1,36 @@ +using Microsoft.Data.Sqlite; +using System; +using System.ComponentModel; +using System.Data.Common; + +namespace Lombiq.Tests.UI.Services.Counters.Data; + +// This is required to avoid the component being visible in VS Toolbox. +[DesignerCategory("")] +public class ProbedSqliteConnection : SqliteConnection +{ + private readonly ICounterDataCollector _counterDataCollector; + + internal SqliteConnection ProbedConnection { get; private set; } + + public ProbedSqliteConnection(SqliteConnection connection, ICounterDataCollector counterDataCollector) + : base(connection.ConnectionString) => + _counterDataCollector = counterDataCollector ?? throw new ArgumentNullException(nameof(counterDataCollector)); + + protected virtual DbCommand CreateDbCommand(DbCommand original) => + new ProbedDbCommand(original, this, _counterDataCollector); + + protected override DbCommand CreateDbCommand() => + CreateDbCommand(CreateCommand()); + + protected override void Dispose(bool disposing) + { + if (disposing && ProbedConnection is not null) + { + ProbedConnection.Dispose(); + } + + base.Dispose(disposing); + ProbedConnection = null; + } +} diff --git a/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs index ed8a164c8..61032278e 100644 --- a/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs +++ b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs @@ -1,5 +1,6 @@ using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.Counters.Data; +using Microsoft.Data.Sqlite; using System; using System.Data.Common; using YesSql; @@ -19,6 +20,14 @@ public ProbedConnectionFactory(IConnectionFactory connectionFactory, ICounterDat _counterDataCollector = counterDataCollector; } - public DbConnection CreateConnection() => - new ProbedDbConnection(_connectionFactory.CreateConnection(), _counterDataCollector); + public DbConnection CreateConnection() + { + var connection = _connectionFactory.CreateConnection(); + + // This consition and the ProbedSqliteConnection can be removed once + // https://github.com/OrchardCMS/OrchardCore/issues/14217 get fixed. + return connection is SqliteConnection sqliteConnection + ? new ProbedSqliteConnection(sqliteConnection, _counterDataCollector) + : new ProbedDbConnection(connection, _counterDataCollector); + } } From 126c97beaae971b00f49da5ea666d52ed1dd45ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 10 Sep 2023 22:11:40 +0200 Subject: [PATCH 24/44] Improving log and exception message --- Lombiq.Tests.UI/Services/CounterDataCollector.cs | 4 ++-- Lombiq.Tests.UI/Services/Counters/CounterKey.cs | 1 + Lombiq.Tests.UI/Services/Counters/CounterProbe.cs | 2 +- Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs | 6 +++--- .../Services/Counters/Data/DbCommandCounterKey.cs | 6 +++--- .../Services/Counters/Data/DbCommandExecuteCounterKey.cs | 2 ++ .../Counters/Data/DbCommandTextExecuteCounterKey.cs | 2 ++ .../Services/Counters/Data/DbReaderReadCounterKey.cs | 2 ++ Lombiq.Tests.UI/Services/Counters/ICounterKey.cs | 5 +++++ Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs | 4 ++-- Lombiq.Tests.UI/Services/Counters/ICounterValue.cs | 5 +++++ Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs | 2 ++ .../Services/Counters/Value/IntegerCounterValue.cs | 6 ++++++ 13 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index 2911958fd..1e1b771f5 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -11,7 +11,7 @@ public sealed class CounterDataCollector : CounterProbeBase, ICounterDataCollect { private readonly ITestOutputHelper _testOutputHelper; private readonly ConcurrentBag _probes = new(); - public override bool IsRunning => true; + public override bool IsAttached => true; public Action AssertCounterData { get; set; } public string Phase { get; set; } @@ -32,7 +32,7 @@ public void Reset() public override void Increment(ICounterKey counter) { - _probes.Where(probe => probe.IsRunning) + _probes.Where(probe => probe.IsAttached) .ForEach(probe => probe.Increment(counter)); base.Increment(counter); } diff --git a/Lombiq.Tests.UI/Services/Counters/CounterKey.cs b/Lombiq.Tests.UI/Services/Counters/CounterKey.cs index 368e533d0..97e89484a 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterKey.cs @@ -7,6 +7,7 @@ namespace Lombiq.Tests.UI.Services.Counters; public abstract class CounterKey : ICounterKey #pragma warning restore S4035 // Classes implementing "IEquatable" should be sealed { + public abstract string DisplayName { get; } public abstract bool Equals(ICounterKey other); protected abstract int HashCode(); public override bool Equals(object obj) => Equals(obj as ICounterKey); diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs index 563ac68ba..f75207816 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbe.cs @@ -8,7 +8,7 @@ public abstract class CounterProbe : CounterProbeBase, IDisposable { private bool _disposed; - public override bool IsRunning => !_disposed; + public override bool IsAttached => !_disposed; public ICounterDataCollector CounterDataCollector { get; init; } public override IEnumerable Dump() diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs index 3f9501e59..a3b23dbd3 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs @@ -10,7 +10,7 @@ public abstract class CounterProbeBase : ICounterProbe { private readonly ConcurrentDictionary _counters = new(); - public abstract bool IsRunning { get; } + public abstract bool IsAttached { get; } public Action CaptureCompleted { get; set; } public IDictionary Counters => _counters; @@ -39,14 +39,14 @@ public virtual IEnumerable DumpSummary() { var keyGroupLines = new List { - keyGroup.Key.Name, + keyGroup.First().Key.DisplayName, }; var integerCounter = keyGroup.Select(entry => entry.Value) .OfType() .Select(value => value.Value) .Max(); - keyGroupLines.Add($"\t{nameof(IntegerCounterValue)} max: {integerCounter.ToTechnicalString()}"); + keyGroupLines.Add($"\tmaximum: {integerCounter.ToTechnicalString()}"); return keyGroupLines.Select(line => $"\t{line}"); })); diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs index 360fdc066..939810d14 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs @@ -34,8 +34,8 @@ public override IEnumerable Dump() { var lines = new List { - GetType().Name, - $"\t{CommandText}", + DisplayName, + $"\tQuery: {CommandText}", }; if (Parameters.Any()) @@ -45,7 +45,7 @@ public override IEnumerable Dump() CultureInfo.InvariantCulture, $"[{index}]{parameter.Key ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) .Join(", "); - lines.Add($"\t\t{commandParams}"); + lines.Add($"\t\tParameters: {commandParams}"); } return lines; diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs index 80edb9c39..0fa70652e 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs @@ -6,6 +6,8 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public sealed class DbCommandExecuteCounterKey : DbCommandCounterKey { + public override string DisplayName => "Database command with parameters execute counter"; + public DbCommandExecuteCounterKey(string commandText, IEnumerable> parameters) : base(commandText, parameters) { diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs index db221af22..27f3cca3f 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs @@ -7,6 +7,8 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public sealed class DbCommandTextExecuteCounterKey : DbCommandCounterKey { + public override string DisplayName => "Database command execute counter"; + public DbCommandTextExecuteCounterKey(string commandText) : base(commandText, Enumerable.Empty>()) { diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs index a95e6549d..115a41721 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs @@ -6,6 +6,8 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public class DbReaderReadCounterKey : DbCommandCounterKey { + public override string DisplayName => "Database read counter"; + public DbReaderReadCounterKey(string commandText, IEnumerable> parameters) : base(commandText, parameters) { diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs index 759bf293a..7bc7294af 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs @@ -8,6 +8,11 @@ namespace Lombiq.Tests.UI.Services.Counters; /// public interface ICounterKey : IEquatable { + /// + /// Gets the display name of the key. + /// + string DisplayName { get; } + /// /// Dumps the key content to a human-readable format. /// diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs index a64d44c59..52c6e49e2 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs @@ -9,9 +9,9 @@ namespace Lombiq.Tests.UI.Services.Counters; public interface ICounterProbe { /// - /// Gets a value indicating whether the instance is running. + /// Gets a value indicating whether the instance is attached. /// - bool IsRunning { get; } + bool IsAttached { get; } /// /// Gets or sets a callback which is called when the probe completed capturing the data. diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs index 82059dca3..0a1186da9 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterValue.cs @@ -7,6 +7,11 @@ namespace Lombiq.Tests.UI.Services.Counters; /// public interface ICounterValue { + /// + /// Gets the display name of the value. + /// + string DisplayName { get; } + /// /// Dumps the value content to a human-readable format. /// diff --git a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs index 407c593f4..bbf0f07ee 100644 --- a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs @@ -6,6 +6,8 @@ namespace Lombiq.Tests.UI.Services.Counters.Value; public abstract class CounterValue : ICounterValue where TValue : struct { + public virtual string DisplayName => "Count"; + public TValue Value { get; set; } public virtual IEnumerable Dump() => diff --git a/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs index 5a55355df..baac5fb8e 100644 --- a/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs @@ -1,8 +1,14 @@ using System; +using System.Collections.Generic; namespace Lombiq.Tests.UI.Services.Counters.Value; public class IntegerCounterValue : CounterValue { + public override IEnumerable Dump() => new[] + { + $"{DisplayName}: {this}", + }; + public override string ToString() => Value.ToTechnicalString(); } From 82c9090be5667438a3301651fb226bce999e668d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 10 Sep 2023 22:32:56 +0200 Subject: [PATCH 25/44] Improving logs Excluding unnecessary counts --- Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs | 5 +++++ .../Services/Counters/Data/DbCommandCounterKey.cs | 1 + 2 files changed, 6 insertions(+) diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs index a3b23dbd3..6b83c96e6 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs @@ -28,6 +28,11 @@ public virtual void Increment(ICounterKey counter) => public virtual IEnumerable DumpSummary() { + if (!Counters.Any()) + { + return Enumerable.Empty(); + } + var lines = new List { "Summary:", diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs index 939810d14..6033e3c70 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs @@ -25,6 +25,7 @@ public override bool Equals(ICounterKey other) && other.GetType() == GetType() && GetType() == otherKey.GetType() && string.Equals(CommandText, otherKey.CommandText, StringComparison.OrdinalIgnoreCase) + && Parameters.Any() && Parameters .Select(param => (param.Key, param.Value)) .SequenceEqual(otherKey.Parameters.Select(param => (param.Key, param.Value))); From f50c13c07705f290afc59e2072d0d8b06ed1aa11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sat, 23 Sep 2023 20:37:44 +0200 Subject: [PATCH 26/44] Throwing session probe exception in test context --- Lombiq.Tests.UI.Samples/Readme.md | 1 + .../Tests/DuplicatedSqlQueryDetectorTests.cs | 52 +++++++++++++++++++ .../Tests/InteractiveModeTests.cs | 1 + .../Services/CounterDataCollector.cs | 22 ++++++-- .../Configuration/CounterConfiguration.cs | 2 +- .../Configuration/CounterConfigurations.cs | 42 +++++++++------ .../Counters/Data/DbReaderReadCounterKey.cs | 2 +- .../Counters/ICounterDataCollector.cs | 7 +++ .../Counters/IOutOfTestContextCounterProbe.cs | 9 ++++ .../Services/Counters/SessionProbe.cs | 2 +- 10 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs create mode 100644 Lombiq.Tests.UI/Services/Counters/IOutOfTestContextCounterProbe.cs diff --git a/Lombiq.Tests.UI.Samples/Readme.md b/Lombiq.Tests.UI.Samples/Readme.md index 15a4ac422..0663c384b 100644 --- a/Lombiq.Tests.UI.Samples/Readme.md +++ b/Lombiq.Tests.UI.Samples/Readme.md @@ -27,6 +27,7 @@ For general details about and on using the Toolbox see the [root Readme](../Read - [Basic visual verification tests](Tests/BasicVisualVerificationTests.cs) - [Testing in tenants](Tests/TenantTests.cs) - [Interactive mode](Tests/InteractiveModeTests.cs) +- [Duplicated SQL query detector](Tests/DuplicatedSqlQueryDetectorTests.cs) ## Adding new tutorials diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs new file mode 100644 index 000000000..331a52c5c --- /dev/null +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -0,0 +1,52 @@ +using Lombiq.Tests.UI.Attributes; +using Lombiq.Tests.UI.Extensions; +using Lombiq.Tests.UI.Services; +using Lombiq.Tests.UI.Services.Counters.Configuration; +using Shouldly; +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Lombiq.Tests.UI.Samples.Tests; + +public class DuplicatedSqlQueryDetectorTests : UITestBase +{ + public DuplicatedSqlQueryDetectorTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + } + + [Theory, Chrome] + public Task RepeatedSqlQueryDuringRunningPhaseShouldThrow(Browser browser) => + Should.ThrowAsync(() => + ExecuteTestAfterSetupAsync( + context => context.SignInDirectlyAndGoToDashboardAsync(), + browser, + ConfigureAsync)); + + [Theory, Chrome] + public Task DbReaderReadDuringRunningPhaseShouldThrow(Browser browser) => + Should.ThrowAsync(() => + ExecuteTestAfterSetupAsync( + async context => await context.GoToHomePageAsync(onlyIfNotAlreadyThere: false), + browser, + ConfigureAsync)); + + private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration configuration) + { + // The test is guaranteed to fail so we don't want to retry it needlessly. + configuration.MaxRetryCount = 0; + + var adminCounterConfiguration = new CounterConfiguration(); + adminCounterConfiguration.SessionThreshold.Disable = false; + adminCounterConfiguration.SessionThreshold.DbReaderReadThreshold = 0; + configuration.CounterConfiguration.Running.Add( + new RelativeUrlConfigurationKey(new Uri("/Admin", UriKind.Relative), exactMatch: false), + adminCounterConfiguration); + + return Task.CompletedTask; + } +} + +// END OF TRAINING SECTION: Duplicated SQL query detector. diff --git a/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs b/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs index bba0b7399..98e027598 100644 --- a/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs @@ -74,3 +74,4 @@ await Task.WhenAll( } // END OF TRAINING SECTION: Interactive mode. +// NEXT STATION: Head over to DuplicatedSqlQueryDetectorTests.cs. diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index 1e1b771f5..37772d586 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -11,8 +11,9 @@ public sealed class CounterDataCollector : CounterProbeBase, ICounterDataCollect { private readonly ITestOutputHelper _testOutputHelper; private readonly ConcurrentBag _probes = new(); + private readonly ConcurrentBag _postponedCounterExceptions = new(); public override bool IsAttached => true; - public Action AssertCounterData { get; set; } + public Action AssertCounterData { get; set; } public string Phase { get; set; } public CounterDataCollector(ITestOutputHelper testOutputHelper) => @@ -27,6 +28,7 @@ public void AttachProbe(ICounterProbe probe) public void Reset() { _probes.Clear(); + _postponedCounterExceptions.Clear(); Clear(); } @@ -38,6 +40,7 @@ public override void Increment(ICounterKey counter) } public override string DumpHeadline() => $"{nameof(CounterDataCollector)}, Phase = {Phase}"; + public override IEnumerable Dump() { var lines = new List @@ -50,8 +53,21 @@ public override IEnumerable Dump() return lines; } - public void AssertCounter(ICounterProbe probe) => AssertCounterData?.Invoke(probe); - public void AssertCounter() => AssertCounter(this); + public void AssertCounter(ICounterProbe probe) => AssertCounterData?.Invoke(this, probe); + + public void AssertCounter() + { + if (_postponedCounterExceptions.Any()) + { + throw new AggregateException( + "There were exceptions out of the test execution context.", + _postponedCounterExceptions); + } + + AssertCounter(this); + } + + public void PostponeCounterException(Exception exception) => _postponedCounterExceptions.Add(exception); private void ProbeCaptureCompleted(ICounterProbe probe) => probe.Dump().ForEach(_testOutputHelper.WriteLine); diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs index 7786682c7..ff12cdadc 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs @@ -10,7 +10,7 @@ public class CounterConfiguration /// /// Gets or sets the counter assertion method. /// - public Action AssertCounterData { get; set; } + public Action AssertCounterData { get; set; } /// /// Gets or sets the exclude filter. Can be used to exclude counted values before assertion. diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs index cefea1c73..8bf5bfe53 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs @@ -20,8 +20,9 @@ public class CounterConfigurations /// public RunningPhaseCounterConfiguration Running { get; } = new(); - public static Action DefaultAssertCounterData(PhaseCounterConfiguration configuration) => - probe => + public static Action DefaultAssertCounterData( + PhaseCounterConfiguration configuration) => + (collector, probe) => { var counterConfiguration = configuration as CounterConfiguration; if (counterConfiguration is RunningPhaseCounterConfiguration runningPhaseCounterConfiguration @@ -46,21 +47,28 @@ public static Action DefaultAssertCounterData(PhaseCounterConfigu if (threshold is { } settings && settings.Settings.Disable is not true) { - AssertIntegerCounterValue( - probe, - counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandExecutionThreshold)}", - settings.Settings.DbCommandExecutionThreshold); - AssertIntegerCounterValue( - probe, - counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandTextExecutionThreshold)}", - settings.Settings.DbCommandTextExecutionThreshold); - AssertIntegerCounterValue( - probe, - counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", - settings.Settings.DbReaderReadThreshold); + try + { + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandExecutionThreshold)}", + settings.Settings.DbCommandExecutionThreshold); + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandTextExecutionThreshold)}", + settings.Settings.DbCommandTextExecutionThreshold); + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", + settings.Settings.DbReaderReadThreshold); + } + catch (CounterThresholdException exception) when (probe is IOutOfTestContextCounterProbe) + { + collector.PostponeCounterException(exception); + } } }; diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs index 115a41721..0d8ef3a22 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs @@ -6,7 +6,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public class DbReaderReadCounterKey : DbCommandCounterKey { - public override string DisplayName => "Database read counter"; + public override string DisplayName => "Database reader read counter"; public DbReaderReadCounterKey(string commandText, IEnumerable> parameters) : base(commandText, parameters) diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs b/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs index cad73948e..57ec30d41 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs @@ -1,3 +1,5 @@ +using System; + namespace Lombiq.Tests.UI.Services.Counters; /// @@ -26,4 +28,9 @@ public interface ICounterDataCollector : ICounterProbe /// Asserts the data collected by the instance. /// void AssertCounter(); + + /// + /// Postpones exception thrown by a counter in case when the exception was thrown out of the test context. + /// + void PostponeCounterException(Exception exception); } diff --git a/Lombiq.Tests.UI/Services/Counters/IOutOfTestContextCounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/IOutOfTestContextCounterProbe.cs new file mode 100644 index 000000000..a97654845 --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/IOutOfTestContextCounterProbe.cs @@ -0,0 +1,9 @@ +namespace Lombiq.Tests.UI.Services.Counters; + +/// +/// Represents a probe that is out of the test context, i.e. it's not executed in the test context, but in the web +/// application context. +/// +public interface IOutOfTestContextCounterProbe : ICounterProbe +{ +} diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs index 09999e627..a2b5399bf 100644 --- a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -9,7 +9,7 @@ namespace Lombiq.Tests.UI.Services.Counters; -public sealed class SessionProbe : CounterProbe, ISession, IRelativeUrlConfigurationKey +public sealed class SessionProbe : CounterProbe, IOutOfTestContextCounterProbe, ISession, IRelativeUrlConfigurationKey { private readonly ISession _session; public string RequestMethod { get; init; } From 8561fac3b1c64a69d6dc97bf7387a2457e65df94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Mon, 23 Oct 2023 23:50:11 +0200 Subject: [PATCH 27/44] Fixing test Adding docs --- .../Tests/DuplicatedSqlQueryDetectorTests.cs | 14 ++++++++--- .../Configuration/CounterConfiguration.cs | 25 ++++++++----------- .../Configuration/CounterConfigurations.cs | 2 +- .../RelativeUrlConfigurationKeyExtensions.cs | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs index 331a52c5c..b21cf2289 100644 --- a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -10,6 +10,8 @@ namespace Lombiq.Tests.UI.Samples.Tests; +// Some times you may want to detect duplicated SQL queries. This can be useful if you want to make sure that your code +// does not execute the same query multiple times. public class DuplicatedSqlQueryDetectorTests : UITestBase { public DuplicatedSqlQueryDetectorTests(ITestOutputHelper testOutputHelper) @@ -17,30 +19,36 @@ public DuplicatedSqlQueryDetectorTests(ITestOutputHelper testOutputHelper) { } + // This test will fail because the app will read the the same command result more times then the configured threshold + // during the Admin page rendering. [Theory, Chrome] - public Task RepeatedSqlQueryDuringRunningPhaseShouldThrow(Browser browser) => + public Task DbReaderReadDuringRunningPhaseShouldThrow(Browser browser) => Should.ThrowAsync(() => ExecuteTestAfterSetupAsync( context => context.SignInDirectlyAndGoToDashboardAsync(), browser, ConfigureAsync)); + // This test will pass because not the Admin page was loaded. [Theory, Chrome] - public Task DbReaderReadDuringRunningPhaseShouldThrow(Browser browser) => - Should.ThrowAsync(() => + public Task DbReaderReadDuringRunningPhaseShouldNotThrow(Browser browser) => + Should.NotThrowAsync(() => ExecuteTestAfterSetupAsync( async context => await context.GoToHomePageAsync(onlyIfNotAlreadyThere: false), browser, ConfigureAsync)); + // We configure the test to throw an exception if a certain counter threshold is exceeded, but only in case of Admin page. private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration configuration) { // The test is guaranteed to fail so we don't want to retry it needlessly. configuration.MaxRetryCount = 0; var adminCounterConfiguration = new CounterConfiguration(); + // Let's enable and configure the counter threshold for ORM sessions. adminCounterConfiguration.SessionThreshold.Disable = false; adminCounterConfiguration.SessionThreshold.DbReaderReadThreshold = 0; + // Apply the configuration to the Admin page only. configuration.CounterConfiguration.Running.Add( new RelativeUrlConfigurationKey(new Uri("/Admin", UriKind.Relative), exactMatch: false), adminCounterConfiguration); diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs index ff12cdadc..744d6d446 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs @@ -7,6 +7,13 @@ namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class CounterConfiguration { + private const string WorkflowTypeStartActivitiesQuery = + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))"; + /// /// Gets or sets the counter assertion method. /// @@ -53,33 +60,21 @@ public class CounterConfiguration public static IEnumerable DefaultExcludeList { get; } = new List { new DbCommandExecuteCounterKey( - "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" - + " AS [WorkflowTypeStartActivitiesIndex_a1]" - + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" - + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" - + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + WorkflowTypeStartActivitiesQuery, new List> { new("p0", "ContentCreatedEvent"), new("p1", value: true), }), new DbCommandExecuteCounterKey( - "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" - + " AS [WorkflowTypeStartActivitiesIndex_a1]" - + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" - + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" - + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + WorkflowTypeStartActivitiesQuery, new List> { new("p0", "ContentPublishedEvent"), new("p1", value: true), }), new DbCommandExecuteCounterKey( - "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" - + " AS [WorkflowTypeStartActivitiesIndex_a1]" - + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" - + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" - + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))", + WorkflowTypeStartActivitiesQuery, new List> { new("p0", "ContentUpdatedEvent"), diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs index 8bf5bfe53..76f3fe0ac 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs @@ -45,7 +45,7 @@ public static Action DefaultAssertCounterD _ => null, }; - if (threshold is { } settings && settings.Settings.Disable is not true) + if (threshold is { } settings && !settings.Settings.Disable) { try { diff --git a/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs index dce8d90a3..4ff27c4d7 100644 --- a/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/RelativeUrlConfigurationKeyExtensions.cs @@ -15,6 +15,6 @@ public static bool EqualsWith(this IRelativeUrlConfigurationKey left, IRelativeU return (left.ExactMatch || right.ExactMatch) ? string.Equals(leftUrl, rightUrl, StringComparison.OrdinalIgnoreCase) - : leftUrl.StartsWithOrdinalIgnoreCase(rightUrl) || rightUrl.StartsWithOrdinalIgnoreCase(leftUrl); + : leftUrl.EqualsOrdinalIgnoreCase(rightUrl) || rightUrl.EqualsOrdinalIgnoreCase(leftUrl); } } From fb9be54cf25d096a0397307c4844b89ae9d11f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 24 Oct 2023 00:08:59 +0200 Subject: [PATCH 28/44] Adding comment --- Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs index 52c6e49e2..39cc22a41 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterProbe.cs @@ -4,7 +4,8 @@ namespace Lombiq.Tests.UI.Services.Counters; /// -/// Represents a probe for the counter infrastructure. +/// Represents a probe for the counter infrastructure to collect data while it is attached to the component under +/// monitoring. /// public interface ICounterProbe { From 4935a11b37fcb8e979db66f90ffa864b11c30cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 24 Oct 2023 00:16:44 +0200 Subject: [PATCH 29/44] Fix spelling --- Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs index 6b83c96e6..d84e96fa8 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs @@ -51,7 +51,7 @@ public virtual IEnumerable DumpSummary() .OfType() .Select(value => value.Value) .Max(); - keyGroupLines.Add($"\tmaximum: {integerCounter.ToTechnicalString()}"); + keyGroupLines.Add($"\tmaximum: {integerCounter.ToTechnicalString()}"); // #spell-check-ignore-line return keyGroupLines.Select(line => $"\t{line}"); })); From c2906c789e8b7d8f7ff4e59a13c6e1701dbcf22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 5 Nov 2023 20:50:57 +0100 Subject: [PATCH 30/44] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../Tests/BasicOrchardFeaturesTests.cs | 9 +++++---- .../Tests/DuplicatedSqlQueryDetectorTests.cs | 13 +++++++------ .../Configuration/PhaseCounterConfiguration.cs | 2 +- .../Services/Counters/ICounterDataCollector.cs | 2 +- Lombiq.Tests.UI/Services/Counters/ICounterKey.cs | 2 +- .../Counters/IRelativeUrlConfigurationKey.cs | 2 +- Lombiq.Tests.UI/Services/Counters/SessionProbe.cs | 4 ++-- Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs | 4 ++-- 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs index 7255fbcc3..0123a555b 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs @@ -28,10 +28,11 @@ public Task BasicOrchardFeaturesShouldWork(Browser browser) => browser, configuration => { - // The UI Testing Toolbox includes a DbCommand execution counter. After the end of the test, it checks the - // number of executed commands with the same SQL command and parameter set against the threshold value - // in its configuration. If the executed command count is greater than the threshold, it raises a - // CounterThresholdException. So here we set the minimum required value to avoid it. + // The UI Testing Toolbox includes a DbCommand execution counter to check for duplicated SQL queries.. + // After the end of the test, it checks the number of executed commands with the same SQL command text + // and parameter set against the threshold value in its configuration. If the executed command count is + // greater than the threshold, it raises a CounterThresholdException. So here we set the minimum + // required value to avoid it. configuration.CounterConfiguration.Running.PhaseThreshold.DbCommandExecutionThreshold = 26; return Task.CompletedTask; diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs index b21cf2289..b46d8ebae 100644 --- a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -11,7 +11,7 @@ namespace Lombiq.Tests.UI.Samples.Tests; // Some times you may want to detect duplicated SQL queries. This can be useful if you want to make sure that your code -// does not execute the same query multiple times. +// does not execute the same query multiple times, wasting time and computing resources. public class DuplicatedSqlQueryDetectorTests : UITestBase { public DuplicatedSqlQueryDetectorTests(ITestOutputHelper testOutputHelper) @@ -19,10 +19,10 @@ public DuplicatedSqlQueryDetectorTests(ITestOutputHelper testOutputHelper) { } - // This test will fail because the app will read the the same command result more times then the configured threshold + // This test will fail because the app will read the same command result more times than the configured threshold // during the Admin page rendering. [Theory, Chrome] - public Task DbReaderReadDuringRunningPhaseShouldThrow(Browser browser) => + public Task PageWithTooManyDuplicatedSqlQueriesShouldThrow(Browser browser) => Should.ThrowAsync(() => ExecuteTestAfterSetupAsync( context => context.SignInDirectlyAndGoToDashboardAsync(), @@ -31,14 +31,15 @@ public Task DbReaderReadDuringRunningPhaseShouldThrow(Browser browser) => // This test will pass because not the Admin page was loaded. [Theory, Chrome] - public Task DbReaderReadDuringRunningPhaseShouldNotThrow(Browser browser) => + public Task PageWithoutDuplicatedSqlQueriesShouldPass(Browser browser) => Should.NotThrowAsync(() => ExecuteTestAfterSetupAsync( async context => await context.GoToHomePageAsync(onlyIfNotAlreadyThere: false), browser, ConfigureAsync)); - // We configure the test to throw an exception if a certain counter threshold is exceeded, but only in case of Admin page. + // We configure the test to throw an exception if a certain counter threshold is exceeded, but only in case of Admin + // pages. private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration configuration) { // The test is guaranteed to fail so we don't want to retry it needlessly. @@ -48,7 +49,7 @@ private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration config // Let's enable and configure the counter threshold for ORM sessions. adminCounterConfiguration.SessionThreshold.Disable = false; adminCounterConfiguration.SessionThreshold.DbReaderReadThreshold = 0; - // Apply the configuration to the Admin page only. + // Apply the configuration to the Admin pages only. configuration.CounterConfiguration.Running.Add( new RelativeUrlConfigurationKey(new Uri("/Admin", UriKind.Relative), exactMatch: false), adminCounterConfiguration); diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs index 62b903018..cf35d7dec 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs @@ -3,7 +3,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class PhaseCounterConfiguration : CounterConfiguration { /// - /// Gets or sets threshold configuration used under app phase(setup, running) lifetime. + /// Gets or sets threshold configuration used under the app phase (setup, running) lifetime. /// public CounterThresholdConfiguration PhaseThreshold { get; set; } = new CounterThresholdConfiguration { diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs b/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs index 57ec30d41..1a51c6ea9 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterDataCollector.cs @@ -30,7 +30,7 @@ public interface ICounterDataCollector : ICounterProbe void AssertCounter(); /// - /// Postpones exception thrown by a counter in case when the exception was thrown out of the test context. + /// Postpones exception thrown by a counter when the exception was thrown from the test context. /// void PostponeCounterException(Exception exception); } diff --git a/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs index 7bc7294af..1a241d3e7 100644 --- a/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/ICounterKey.cs @@ -16,6 +16,6 @@ public interface ICounterKey : IEquatable /// /// Dumps the key content to a human-readable format. /// - /// A human-readable representation of instance. + /// A human-readable representation of the instance. IEnumerable Dump(); } diff --git a/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs b/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs index 2b8f34ee0..26f272f60 100644 --- a/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/IRelativeUrlConfigurationKey.cs @@ -13,7 +13,7 @@ public interface IRelativeUrlConfigurationKey : ICounterConfigurationKey Uri Url { get; } /// - /// Gets a value indicating whether the URL should be matched exactly or substring match enough. + /// Gets a value indicating whether the URL should be matched exactly or substring match is enough. /// bool ExactMatch { get; } } diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs index a2b5399bf..01072375b 100644 --- a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -55,8 +55,8 @@ ISession ISession.RegisterIndexes(IIndexProvider[] indexProviders, string collec async ValueTask IAsyncDisposable.DisposeAsync() { await _session.DisposeAsync(); - // Should be at the end because, the Session implementation calls CommitOrRollbackTransactionAsync in DisposeAsync - // and we should count the executed db commands in it. + // Should be at the end because the Session implementation calls CommitOrRollbackTransactionAsync in + // DisposeAsync and we should count the executed DB commands in it. Dispose(); } diff --git a/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs index 61032278e..7d29135e7 100644 --- a/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs +++ b/Lombiq.Tests.UI/Services/ProbedConnectionFactory.cs @@ -24,8 +24,8 @@ public DbConnection CreateConnection() { var connection = _connectionFactory.CreateConnection(); - // This consition and the ProbedSqliteConnection can be removed once - // https://github.com/OrchardCMS/OrchardCore/issues/14217 get fixed. + // This condition and the ProbedSqliteConnection can be removed once + // https://github.com/OrchardCMS/OrchardCore/issues/14217 gets fixed. return connection is SqliteConnection sqliteConnection ? new ProbedSqliteConnection(sqliteConnection, _counterDataCollector) : new ProbedDbConnection(connection, _counterDataCollector); From 8c58730c437ce540374802ff32dce9c991f0015e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 12 Nov 2023 12:41:12 +0100 Subject: [PATCH 31/44] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoltán Lehóczky --- .../CounterThresholdConfiguration.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs index 75a56d454..72acfcbae 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs @@ -8,17 +8,20 @@ public class CounterThresholdConfiguration public bool Disable { get; set; } = true; /// - /// Gets or sets the threshold of executed s. Uses - /// and - /// for counting. + /// Gets or sets the threshold for the count of executions, with the + /// query only counted as a duplicate if both its text () and + /// parameters () match. See + /// for counting using only the command text. /// - public int DbCommandExecutionThreshold { get; set; } = 11; + public int DbCommandIncludingParametersExecutionCountThreshold { get; set; } = 11; /// - /// Gets or sets the threshold of executed s. Uses - /// for counting. + /// Gets or sets the threshold for the count of executions, with the + /// query counted as a duplicate if its text () matches. + /// Parameters () are not taken into account. See + /// for counting using also the parameters. /// - public int DbCommandTextExecutionThreshold { get; set; } = 11; + public int DbCommandExcludingParametersExecutionThreshold { get; set; } = 11; /// /// Gets or sets the threshold of readings of s. Uses From 4f334fe69f311310e20e744493e3e4471ed8033d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 9 Apr 2024 20:16:23 +0200 Subject: [PATCH 32/44] Fixes after merge --- .../Tests/BasicOrchardFeaturesTests.cs | 5 ++--- Lombiq.Tests.UI.Samples/Tests/RemoteTests.cs | 1 + Lombiq.Tests.UI/OrchardCoreUITestBase.cs | 8 ++++++-- Lombiq.Tests.UI/RemoteUITestBase.cs | 2 +- .../Counters/Configuration/CounterConfigurations.cs | 10 +++++----- .../Configuration/PhaseCounterConfiguration.cs | 4 ++-- Lombiq.Tests.UI/Services/Counters/SessionProbe.cs | 5 +++++ Lombiq.Tests.UI/Services/OrchardCoreInstance.cs | 5 +++-- .../Services/OrchardCoreUITestExecutorConfiguration.cs | 2 +- Lombiq.Tests.UI/Services/UITestExecutionSession.cs | 4 +++- Lombiq.Tests.UI/Services/UITestExecutor.cs | 4 +++- 11 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs index ab515974b..f5cd9364a 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs @@ -20,10 +20,9 @@ public BasicOrchardFeaturesTests(ITestOutputHelper testOutputHelper) // We could reuse the previously specified SetupHelpers.RecipeId const here but it's actually a different recipe for // this test. [Fact] - public Task BasicOrchardFeaturesShouldWork(Browser browser) => + public Task BasicOrchardFeaturesShouldWork() => ExecuteTestAsync( context => context.TestBasicOrchardFeaturesAsync(RecipeIds.BasicOrchardFeaturesTests), - configuration => { // The UI Testing Toolbox includes a DbCommand execution counter to check for duplicated SQL queries.. @@ -31,7 +30,7 @@ public Task BasicOrchardFeaturesShouldWork(Browser browser) => // and parameter set against the threshold value in its configuration. If the executed command count is // greater than the threshold, it raises a CounterThresholdException. So here we set the minimum // required value to avoid it. - configuration.CounterConfiguration.Running.PhaseThreshold.DbCommandExecutionThreshold = 26; + configuration.CounterConfiguration.Running.PhaseThreshold.DbCommandIncludingParametersExecutionCountThreshold = 26; return Task.CompletedTask; }); diff --git a/Lombiq.Tests.UI.Samples/Tests/RemoteTests.cs b/Lombiq.Tests.UI.Samples/Tests/RemoteTests.cs index 85673c3e2..ee8c3f7e6 100644 --- a/Lombiq.Tests.UI.Samples/Tests/RemoteTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/RemoteTests.cs @@ -43,3 +43,4 @@ public Task ExampleDotComShouldWork() => } // END OF TRAINING SECTION: Remote tests. +// NEXT STATION: Head over to DuplicatedSqlQueryDetectorTests.cs. diff --git a/Lombiq.Tests.UI/OrchardCoreUITestBase.cs b/Lombiq.Tests.UI/OrchardCoreUITestBase.cs index 778ad6744..1de79cbf0 100644 --- a/Lombiq.Tests.UI/OrchardCoreUITestBase.cs +++ b/Lombiq.Tests.UI/OrchardCoreUITestBase.cs @@ -334,8 +334,12 @@ protected virtual async Task ExecuteTestAsync( if (changeConfigurationAsync != null) await changeConfigurationAsync(configuration); await ExecuteOrchardCoreTestAsync( - (configuration, contextId) => - new OrchardCoreInstance(configuration.OrchardCoreConfiguration, contextId, configuration.TestOutputHelper), + (configuration, contextId, counterDataCollector) => + new OrchardCoreInstance( + configuration.OrchardCoreConfiguration, + contextId, + configuration.TestOutputHelper, + counterDataCollector), testManifest, configuration); } diff --git a/Lombiq.Tests.UI/RemoteUITestBase.cs b/Lombiq.Tests.UI/RemoteUITestBase.cs index e4984f0d9..4f4d17a77 100644 --- a/Lombiq.Tests.UI/RemoteUITestBase.cs +++ b/Lombiq.Tests.UI/RemoteUITestBase.cs @@ -71,6 +71,6 @@ async Task BaseUriVisitingTest(UITestContext context) if (changeConfigurationAsync != null) await changeConfigurationAsync(configuration); - await ExecuteOrchardCoreTestAsync((_, _) => new RemoteInstance(baseUri), testManifest, configuration); + await ExecuteOrchardCoreTestAsync((_, _, _) => new RemoteInstance(baseUri), testManifest, configuration); } } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs index 76f3fe0ac..9f42dfba3 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs @@ -18,7 +18,7 @@ public class CounterConfigurations /// /// Gets the counter configuration used in the running phase of the web application. /// - public RunningPhaseCounterConfiguration Running { get; } = new(); + public RunningPhaseCounterConfiguration Running { get; } = []; public static Action DefaultAssertCounterData( PhaseCounterConfiguration configuration) => @@ -52,13 +52,13 @@ public static Action DefaultAssertCounterD AssertIntegerCounterValue( probe, counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandExecutionThreshold)}", - settings.Settings.DbCommandExecutionThreshold); + $"{settings.Name}.{nameof(settings.Settings.DbCommandIncludingParametersExecutionCountThreshold)}", + settings.Settings.DbCommandIncludingParametersExecutionCountThreshold); AssertIntegerCounterValue( probe, counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandTextExecutionThreshold)}", - settings.Settings.DbCommandTextExecutionThreshold); + $"{settings.Name}.{nameof(settings.Settings.DbCommandExcludingParametersExecutionThreshold)}", + settings.Settings.DbCommandExcludingParametersExecutionThreshold); AssertIntegerCounterValue( probe, counterConfiguration.ExcludeFilter ?? (key => false), diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs index cf35d7dec..35e5a8da0 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/PhaseCounterConfiguration.cs @@ -7,8 +7,8 @@ public class PhaseCounterConfiguration : CounterConfiguration /// public CounterThresholdConfiguration PhaseThreshold { get; set; } = new CounterThresholdConfiguration { - DbCommandExecutionThreshold = 22, - DbCommandTextExecutionThreshold = 44, + DbCommandIncludingParametersExecutionCountThreshold = 22, + DbCommandExcludingParametersExecutionThreshold = 44, DbReaderReadThreshold = 11, }; } diff --git a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs index 01072375b..1df6ae253 100644 --- a/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/SessionProbe.cs @@ -29,8 +29,13 @@ public SessionProbe(ICounterDataCollector counterDataCollector, string requestMe protected override void OnDisposing() => _session.Dispose(); + // Needs to be implemented on mock class. +#pragma warning disable CS0618 // Type or member is obsolete void ISession.Save(object obj, bool checkConcurrency, string collection) => _session.Save(obj, checkConcurrency, collection); +#pragma warning restore CS0618 // Type or member is obsolete + Task ISession.SaveAsync(object obj, bool checkConcurrency, string collection) => + _session.SaveAsync(obj, checkConcurrency, collection); void ISession.Delete(object item, string collection) => _session.Delete(item, collection); bool ISession.Import(object item, long id, long version, string collection) => diff --git a/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs b/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs index ae52bba2a..2285a8afd 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreInstance.cs @@ -2,6 +2,7 @@ using Lombiq.Tests.UI.Constants; using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Models; +using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.OrchardCoreHosting; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -51,7 +52,7 @@ public sealed class OrchardCoreInstance : IWebApplicationInstance private readonly OrchardCoreConfiguration _configuration; private readonly string _contextId; private readonly ITestOutputHelper _testOutputHelper; - private readonly CounterDataCollector _counterDataCollector; + private readonly ICounterDataCollector _counterDataCollector; private string _contentRootPath; private bool _isDisposed; private OrchardApplicationFactory _orchardApplication; @@ -64,7 +65,7 @@ public OrchardCoreInstance( OrchardCoreConfiguration configuration, string contextId, ITestOutputHelper testOutputHelper, - CounterDataCollector counterDataCollector) + ICounterDataCollector counterDataCollector) { _configuration = configuration; _contextId = contextId; diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index af3342ffa..0ae823d76 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -1,6 +1,6 @@ using Lombiq.Tests.UI.Extensions; -using Lombiq.Tests.UI.Services.Counters.Configuration; using Lombiq.Tests.UI.SecurityScanning; +using Lombiq.Tests.UI.Services.Counters.Configuration; using Lombiq.Tests.UI.Services.GitHub; using Lombiq.Tests.UI.Shortcuts.Controllers; using OpenQA.Selenium; diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 30fab5986..85240c04d 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -643,7 +643,9 @@ Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommand _configuration.OrchardCoreConfiguration.BeforeAppStart.RemoveAll(UITestingBeforeAppStartHandlerAsync); _configuration.OrchardCoreConfiguration.BeforeAppStart += UITestingBeforeAppStartHandlerAsync; - _applicationInstance = _webApplicationInstanceFactory(_configuration, contextId); + var counterDataCollector = new CounterDataCollector(_testOutputHelper); + + _applicationInstance = _webApplicationInstanceFactory(_configuration, contextId, counterDataCollector); var uri = await _applicationInstance.StartUpAsync(); _configuration.SetUpEvents(); diff --git a/Lombiq.Tests.UI/Services/UITestExecutor.cs b/Lombiq.Tests.UI/Services/UITestExecutor.cs index 41170c021..ab4e1458e 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutor.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutor.cs @@ -1,6 +1,7 @@ using Lombiq.HelpfulLibraries.Common.Utilities; using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Models; +using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.GitHub; using System; using System.IO; @@ -12,7 +13,8 @@ namespace Lombiq.Tests.UI.Services; public delegate IWebApplicationInstance WebApplicationInstanceFactory( OrchardCoreUITestExecutorConfiguration configuration, - string contextId); + string contextId, + ICounterDataCollector counterDataCollector); public static class UITestExecutor { From 027486d21d39b277a00deb1bb2ccb646cb66aa4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 9 Apr 2024 22:09:51 +0200 Subject: [PATCH 33/44] Fixing analyzer violations --- .../Models/UITestContextParameters.cs | 16 ++++++++++ .../Services/CounterDataCollector.cs | 6 ++-- .../Counters/Data/DbCommandCounterKey.cs | 2 +- .../OrchardApplicationFactory.cs | 2 +- Lombiq.Tests.UI/Services/UITestContext.cs | 30 +++++++------------ .../Services/UITestExecutionSession.cs | 19 +++++++----- 6 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 Lombiq.Tests.UI/Models/UITestContextParameters.cs diff --git a/Lombiq.Tests.UI/Models/UITestContextParameters.cs b/Lombiq.Tests.UI/Models/UITestContextParameters.cs new file mode 100644 index 000000000..1fbfbc027 --- /dev/null +++ b/Lombiq.Tests.UI/Models/UITestContextParameters.cs @@ -0,0 +1,16 @@ +using Lombiq.Tests.UI.SecurityScanning; +using Lombiq.Tests.UI.Services; + +namespace Lombiq.Tests.UI.Models; + +internal record UITestContextParameters +{ + public string Id { get; init; } + public UITestManifest TestManifest { get; init; } + public OrchardCoreUITestExecutorConfiguration Configuration { get; init; } + public IWebApplicationInstance Application { get; init; } + public AtataScope Scope { get; init; } + public RunningContextContainer RunningContextContainer { get; init; } + public ZapManager ZapManager { get; init; } + public CounterDataCollector CounterDataCollector { get; init; } +} diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index 37772d586..c91f3ab98 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -10,8 +10,8 @@ namespace Lombiq.Tests.UI.Services; public sealed class CounterDataCollector : CounterProbeBase, ICounterDataCollector { private readonly ITestOutputHelper _testOutputHelper; - private readonly ConcurrentBag _probes = new(); - private readonly ConcurrentBag _postponedCounterExceptions = new(); + private readonly ConcurrentBag _probes = []; + private readonly ConcurrentBag _postponedCounterExceptions = []; public override bool IsAttached => true; public Action AssertCounterData { get; set; } public string Phase { get; set; } @@ -57,7 +57,7 @@ public override IEnumerable Dump() public void AssertCounter() { - if (_postponedCounterExceptions.Any()) + if (!_postponedCounterExceptions.IsEmpty) { throw new AggregateException( "There were exceptions out of the test execution context.", diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs index 6033e3c70..4e1a2e12a 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs @@ -7,7 +7,7 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public abstract class DbCommandCounterKey : CounterKey { - private readonly List> _parameters = new(); + private readonly List> _parameters = []; public string CommandText { get; private set; } public IEnumerable> Parameters => _parameters; diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index de36903bf..3a0aa80c9 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -33,7 +33,7 @@ public sealed class OrchardApplicationFactory : WebApplicationFactory< private readonly Action _configureHost; private readonly Action _configuration; private readonly Action _configureOrchard; - private readonly ConcurrentBag _createdStores = new(); + private readonly ConcurrentBag _createdStores = []; public OrchardApplicationFactory( ICounterDataCollector counterDataCollector, diff --git a/Lombiq.Tests.UI/Services/UITestContext.cs b/Lombiq.Tests.UI/Services/UITestContext.cs index 35c58201d..d2bcb3e5e 100644 --- a/Lombiq.Tests.UI/Services/UITestContext.cs +++ b/Lombiq.Tests.UI/Services/UITestContext.cs @@ -112,26 +112,18 @@ public class UITestContext /// public CounterDataCollector CounterDataCollector { get; init; } - public UITestContext( - string id, - UITestManifest testManifest, - OrchardCoreUITestExecutorConfiguration configuration, - IWebApplicationInstance application, - AtataScope scope, - RunningContextContainer runningContextContainer, - ZapManager zapManager, - CounterDataCollector counterDataCollector) + internal UITestContext(UITestContextParameters parameters) { - Id = id; - TestManifest = testManifest; - Configuration = configuration; - SqlServerRunningContext = runningContextContainer.SqlServerRunningContext; - Application = application; - Scope = scope; - SmtpServiceRunningContext = runningContextContainer.SmtpServiceRunningContext; - AzureBlobStorageRunningContext = runningContextContainer.AzureBlobStorageRunningContext; - ZapManager = zapManager; - CounterDataCollector = counterDataCollector; + Id = parameters.Id; + TestManifest = parameters.TestManifest; + Configuration = parameters.Configuration; + SqlServerRunningContext = parameters.RunningContextContainer.SqlServerRunningContext; + Application = parameters.Application; + Scope = parameters.Scope; + SmtpServiceRunningContext = parameters.RunningContextContainer.SmtpServiceRunningContext; + AzureBlobStorageRunningContext = parameters.RunningContextContainer.AzureBlobStorageRunningContext; + ZapManager = parameters.ZapManager; + CounterDataCollector = parameters.CounterDataCollector; } /// diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 85240c04d..5bc18831f 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -675,14 +675,17 @@ Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommand var atataScope = await AtataFactory.StartAtataScopeAsync(_testOutputHelper, uri, _configuration); return new UITestContext( - contextId, - _testManifest, - _configuration, - _applicationInstance, - atataScope, - new RunningContextContainer(sqlServerContext, smtpContext, azureBlobStorageContext), - _zapManager, - counterDataCollector); + new() + { + Id = contextId, + TestManifest = _testManifest, + Configuration = _configuration, + Application = _applicationInstance, + Scope = atataScope, + RunningContextContainer = new RunningContextContainer(sqlServerContext, smtpContext, azureBlobStorageContext), + ZapManager = _zapManager, + CounterDataCollector = counterDataCollector, + }); } private string GetSetupHashCode() => From fa3a95c741353fa0962cb684f86e5113f4c81368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 9 Apr 2024 22:20:07 +0200 Subject: [PATCH 34/44] Fixing analyzer violations again --- Lombiq.Tests.UI/Models/UITestContextParameters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Models/UITestContextParameters.cs b/Lombiq.Tests.UI/Models/UITestContextParameters.cs index 1fbfbc027..ca4ca43c5 100644 --- a/Lombiq.Tests.UI/Models/UITestContextParameters.cs +++ b/Lombiq.Tests.UI/Models/UITestContextParameters.cs @@ -3,7 +3,7 @@ namespace Lombiq.Tests.UI.Models; -internal record UITestContextParameters +internal sealed record UITestContextParameters { public string Id { get; init; } public UITestManifest TestManifest { get; init; } From 0e4f177e2e870b7e5b8586483206201611cd565c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 7 May 2024 00:00:40 +0200 Subject: [PATCH 35/44] Adding custom type for storing db command parameters in configuration instead of KeyValuePair<> Setting default configuration more generic Moving OSOCE default configuration to OrchardCoreUITestExecutorConfiguration --- .../Tests/BasicOrchardFeaturesTests.cs | 2 +- .../Tests/DuplicatedSqlQueryDetectorTests.cs | 21 +++- .../Configuration/CounterConfiguration.cs | 60 +--------- .../Configuration/CounterConfigurations.cs | 84 +------------- .../CounterThresholdConfiguration.cs | 11 +- .../Data/CounterDbCommandParameter.cs | 7 ++ .../Counters/Data/DbCommandCounterKey.cs | 12 +- .../Data/DbCommandExecuteCounterKey.cs | 9 +- .../Data/DbCommandTextExecuteCounterKey.cs | 3 +- .../Counters/Data/DbReaderReadCounterKey.cs | 9 +- .../CounterDataCollectorExtensions.cs | 3 + .../OrchardCoreUITestExecutorConfiguration.cs | 104 ++++++++++++++++++ .../Services/UITestExecutionSession.cs | 9 +- Readme.md | 1 + 14 files changed, 167 insertions(+), 168 deletions(-) create mode 100644 Lombiq.Tests.UI/Services/Counters/Data/CounterDbCommandParameter.cs diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs index f5cd9364a..6e756f2ba 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs @@ -30,7 +30,7 @@ public Task BasicOrchardFeaturesShouldWork() => // and parameter set against the threshold value in its configuration. If the executed command count is // greater than the threshold, it raises a CounterThresholdException. So here we set the minimum // required value to avoid it. - configuration.CounterConfiguration.Running.PhaseThreshold.DbCommandIncludingParametersExecutionCountThreshold = 26; + configuration.CounterConfiguration.AfterSetup.PhaseThreshold.DbCommandIncludingParametersExecutionCountThreshold = 26; return Task.CompletedTask; }); diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs index b46d8ebae..ad2bfb63d 100644 --- a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -29,7 +29,7 @@ public Task PageWithTooManyDuplicatedSqlQueriesShouldThrow(Browser browser) => browser, ConfigureAsync)); - // This test will pass because not the Admin page was loaded. + // This test will pass because not any of the Admin page was loaded. [Theory, Chrome] public Task PageWithoutDuplicatedSqlQueriesShouldPass(Browser browser) => Should.NotThrowAsync(() => @@ -45,12 +45,21 @@ private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration config // The test is guaranteed to fail so we don't want to retry it needlessly. configuration.MaxRetryCount = 0; - var adminCounterConfiguration = new CounterConfiguration(); - // Let's enable and configure the counter threshold for ORM sessions. - adminCounterConfiguration.SessionThreshold.Disable = false; - adminCounterConfiguration.SessionThreshold.DbReaderReadThreshold = 0; + var adminCounterConfiguration = new CounterConfiguration + { + ExcludeFilter = OrchardCoreUITestExecutorConfiguration.DefaultCounterExcludeFilter, + SessionThreshold = + { + // Let's enable and configure the counter threshold for ORM sessions. + IsEnabled = true, + DbCommandExcludingParametersExecutionThreshold = 5, + DbCommandIncludingParametersExecutionCountThreshold = 15, + DbReaderReadThreshold = 0, + }, + }; + // Apply the configuration to the Admin pages only. - configuration.CounterConfiguration.Running.Add( + configuration.CounterConfiguration.AfterSetup.Add( new RelativeUrlConfigurationKey(new Uri("/Admin", UriKind.Relative), exactMatch: false), adminCounterConfiguration); diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs index 3b81553d0..f33f7de86 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs @@ -1,19 +1,9 @@ -using Lombiq.Tests.UI.Services.Counters.Data; using System; -using System.Collections.Generic; -using System.Linq; namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class CounterConfiguration { - private const string WorkflowTypeStartActivitiesQuery = - "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" - + " AS [WorkflowTypeStartActivitiesIndex_a1]" - + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" - + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" - + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))"; - /// /// Gets or sets the counter assertion method. /// @@ -22,65 +12,23 @@ public class CounterConfiguration /// /// Gets or sets the exclude filter. Can be used to exclude counted values before assertion. /// - public Func ExcludeFilter { get; set; } = DefaultExcludeFilter; + public Func ExcludeFilter { get; set; } /// /// Gets or sets threshold configuration used under navigation requests. See: /// . /// See: . /// - public CounterThresholdConfiguration NavigationThreshold { get; set; } = new CounterThresholdConfiguration - { - DbCommandIncludingParametersExecutionCountThreshold = 11, - DbCommandExcludingParametersExecutionThreshold = 22, - DbReaderReadThreshold = 11, - }; + public CounterThresholdConfiguration NavigationThreshold { get; set; } = new(); /// /// Gets or sets threshold configuration used per lifetime. See: /// . /// - public CounterThresholdConfiguration SessionThreshold { get; set; } = new CounterThresholdConfiguration - { - DbCommandIncludingParametersExecutionCountThreshold = 22, - DbCommandExcludingParametersExecutionThreshold = 44, - DbReaderReadThreshold = 11, - }; + public CounterThresholdConfiguration SessionThreshold { get; set; } = new(); /// /// Gets or sets threshold configuration used per page load. See: . /// - public CounterThresholdConfiguration PageLoadThreshold { get; set; } = new CounterThresholdConfiguration - { - DbCommandIncludingParametersExecutionCountThreshold = 22, - DbCommandExcludingParametersExecutionThreshold = 44, - DbReaderReadThreshold = 11, - }; - - public static IEnumerable DefaultExcludeList { get; } = new List - { - new DbCommandExecuteCounterKey( - WorkflowTypeStartActivitiesQuery, - new List> - { - new("p0", "ContentCreatedEvent"), - new("p1", value: true), - }), - new DbCommandExecuteCounterKey( - WorkflowTypeStartActivitiesQuery, - new List> - { - new("p0", "ContentPublishedEvent"), - new("p1", value: true), - }), - new DbCommandExecuteCounterKey( - WorkflowTypeStartActivitiesQuery, - new List> - { - new("p0", "ContentUpdatedEvent"), - new("p1", value: true), - }), - }; - - public static bool DefaultExcludeFilter(ICounterKey key) => DefaultExcludeList.Contains(key); + public CounterThresholdConfiguration PageLoadThreshold { get; set; } = new(); } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs index 9f42dfba3..63f736c6a 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs @@ -1,11 +1,3 @@ -using Lombiq.Tests.UI.Exceptions; -using Lombiq.Tests.UI.Services.Counters.Data; -using Lombiq.Tests.UI.Services.Counters.Extensions; -using Lombiq.Tests.UI.Services.Counters.Value; -using System; -using System.Collections.Generic; -using System.Linq; - namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class CounterConfigurations @@ -18,79 +10,5 @@ public class CounterConfigurations /// /// Gets the counter configuration used in the running phase of the web application. /// - public RunningPhaseCounterConfiguration Running { get; } = []; - - public static Action DefaultAssertCounterData( - PhaseCounterConfiguration configuration) => - (collector, probe) => - { - var counterConfiguration = configuration as CounterConfiguration; - if (counterConfiguration is RunningPhaseCounterConfiguration runningPhaseCounterConfiguration - && probe is ICounterConfigurationKey counterConfigurationKey) - { - counterConfiguration = runningPhaseCounterConfiguration.GetMaybeByKey(counterConfigurationKey) - ?? configuration; - } - - (CounterThresholdConfiguration Settings, string Name)? threshold = probe switch - { - NavigationProbe => - (Settings: counterConfiguration.NavigationThreshold, Name: nameof(counterConfiguration.NavigationThreshold)), - PageLoadProbe => - (Settings: counterConfiguration.PageLoadThreshold, Name: nameof(counterConfiguration.PageLoadThreshold)), - SessionProbe => - (Settings: counterConfiguration.SessionThreshold, Name: nameof(counterConfiguration.SessionThreshold)), - CounterDataCollector when counterConfiguration is PhaseCounterConfiguration phaseCounterConfiguration => - (Settings: phaseCounterConfiguration.PhaseThreshold, Name: nameof(phaseCounterConfiguration.PhaseThreshold)), - _ => null, - }; - - if (threshold is { } settings && !settings.Settings.Disable) - { - try - { - AssertIntegerCounterValue( - probe, - counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandIncludingParametersExecutionCountThreshold)}", - settings.Settings.DbCommandIncludingParametersExecutionCountThreshold); - AssertIntegerCounterValue( - probe, - counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbCommandExcludingParametersExecutionThreshold)}", - settings.Settings.DbCommandExcludingParametersExecutionThreshold); - AssertIntegerCounterValue( - probe, - counterConfiguration.ExcludeFilter ?? (key => false), - $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", - settings.Settings.DbReaderReadThreshold); - } - catch (CounterThresholdException exception) when (probe is IOutOfTestContextCounterProbe) - { - collector.PostponeCounterException(exception); - } - } - }; - - public static void AssertIntegerCounterValue( - ICounterProbe probe, - Func excludeFilter, - string thresholdName, - int threshold) - where TKey : ICounterKey => - probe.Counters.Keys - .OfType() - .Where(key => !excludeFilter(key)) - .ForEach(key => - { - if (probe.Counters[key] is IntegerCounterValue counterValue - && counterValue.Value > threshold) - { - throw new CounterThresholdException( - probe, - key, - counterValue, - $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); - } - }); + public RunningPhaseCounterConfiguration AfterSetup { get; } = []; } diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs index 46f52609a..8d7ccea02 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs @@ -3,9 +3,9 @@ namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class CounterThresholdConfiguration { /// - /// Gets or sets a value indicating whether the current threshold configuration and checking is disabled. + /// Gets or sets a value indicating whether the current threshold configuration and checking is enabled. /// - public bool Disable { get; set; } = true; + public bool IsEnabled { get; set; } /// /// Gets or sets the threshold for the count of executions, with the @@ -13,7 +13,7 @@ public class CounterThresholdConfiguration /// parameters () match. See /// for counting using only the command text. /// - public int DbCommandIncludingParametersExecutionCountThreshold { get; set; } = 11; + public int DbCommandIncludingParametersExecutionCountThreshold { get; set; } = 1; /// /// Gets or sets the threshold for the count of executions, with the @@ -21,12 +21,13 @@ public class CounterThresholdConfiguration /// Parameters () are not taken into account. See /// for counting using also the parameters. /// - public int DbCommandExcludingParametersExecutionThreshold { get; set; } = 11; + public int DbCommandExcludingParametersExecutionThreshold { get; set; } = 1; /// - /// Gets or sets the threshold of readings of s. Uses + /// Gets or sets the threshold of readings of s. Uses /// and /// for counting. /// + // TODO: Human readable comments. public int DbReaderReadThreshold { get; set; } = 11; } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/CounterDbCommandParameter.cs b/Lombiq.Tests.UI/Services/Counters/Data/CounterDbCommandParameter.cs new file mode 100644 index 000000000..f9c7a5e9a --- /dev/null +++ b/Lombiq.Tests.UI/Services/Counters/Data/CounterDbCommandParameter.cs @@ -0,0 +1,7 @@ +namespace Lombiq.Tests.UI.Services.Counters.Data; + +public class CounterDbCommandParameter(string name, object value) +{ + public string Name { get; set; } = name; + public object Value { get; set; } = value; +} diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs index 4e1a2e12a..e07ec02cd 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandCounterKey.cs @@ -7,11 +7,11 @@ namespace Lombiq.Tests.UI.Services.Counters.Data; public abstract class DbCommandCounterKey : CounterKey { - private readonly List> _parameters = []; + private readonly List _parameters = []; public string CommandText { get; private set; } - public IEnumerable> Parameters => _parameters; + public IEnumerable Parameters => _parameters; - protected DbCommandCounterKey(string commandText, IEnumerable> parameters) + protected DbCommandCounterKey(string commandText, IEnumerable parameters) { _parameters.AddRange(parameters); CommandText = commandText; @@ -27,8 +27,8 @@ public override bool Equals(ICounterKey other) && string.Equals(CommandText, otherKey.CommandText, StringComparison.OrdinalIgnoreCase) && Parameters.Any() && Parameters - .Select(param => (param.Key, param.Value)) - .SequenceEqual(otherKey.Parameters.Select(param => (param.Key, param.Value))); + .Select(param => (param.Name, param.Value)) + .SequenceEqual(otherKey.Parameters.Select(param => (param.Name, param.Value))); } public override IEnumerable Dump() @@ -44,7 +44,7 @@ public override IEnumerable Dump() var commandParams = Parameters.Select((parameter, index) => string.Create( CultureInfo.InvariantCulture, - $"[{index}]{parameter.Key ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) + $"[{index}]{parameter.Name ?? string.Empty} = {parameter.Value?.ToString() ?? "(null)"}")) .Join(", "); lines.Add($"\t\tParameters: {commandParams}"); } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs index 0fa70652e..68f3e4602 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandExecuteCounterKey.cs @@ -8,15 +8,20 @@ public sealed class DbCommandExecuteCounterKey : DbCommandCounterKey { public override string DisplayName => "Database command with parameters execute counter"; - public DbCommandExecuteCounterKey(string commandText, IEnumerable> parameters) + public DbCommandExecuteCounterKey(string commandText, IEnumerable parameters) : base(commandText, parameters) { } + public DbCommandExecuteCounterKey(string commandText, params CounterDbCommandParameter[] parameters) + : this(commandText, parameters.AsEnumerable()) + { + } + public static DbCommandExecuteCounterKey CreateFrom(DbCommand dbCommand) => new( dbCommand.CommandText, dbCommand.Parameters .OfType() - .Select(parameter => new KeyValuePair(parameter.ParameterName, parameter.Value))); + .Select(parameter => new CounterDbCommandParameter(parameter.ParameterName, parameter.Value))); } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs index 27f3cca3f..52b2b8224 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Data.Common; using System.Linq; @@ -10,7 +9,7 @@ public sealed class DbCommandTextExecuteCounterKey : DbCommandCounterKey public override string DisplayName => "Database command execute counter"; public DbCommandTextExecuteCounterKey(string commandText) - : base(commandText, Enumerable.Empty>()) + : base(commandText, Enumerable.Empty()) { } diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs index 0d8ef3a22..fd4996360 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbReaderReadCounterKey.cs @@ -8,15 +8,20 @@ public class DbReaderReadCounterKey : DbCommandCounterKey { public override string DisplayName => "Database reader read counter"; - public DbReaderReadCounterKey(string commandText, IEnumerable> parameters) + public DbReaderReadCounterKey(string commandText, IEnumerable parameters) : base(commandText, parameters) { } + public DbReaderReadCounterKey(string commandText, params CounterDbCommandParameter[] parameters) + : this(commandText, parameters.AsEnumerable()) + { + } + public static DbReaderReadCounterKey CreateFrom(DbCommand dbCommand) => new( dbCommand.CommandText, dbCommand.Parameters .OfType() - .Select(parameter => new KeyValuePair(parameter.ParameterName, parameter.Value))); + .Select(parameter => new CounterDbCommandParameter(parameter.ParameterName, parameter.Value))); } diff --git a/Lombiq.Tests.UI/Services/Counters/Extensions/CounterDataCollectorExtensions.cs b/Lombiq.Tests.UI/Services/Counters/Extensions/CounterDataCollectorExtensions.cs index f2516cdba..29f3820d1 100644 --- a/Lombiq.Tests.UI/Services/Counters/Extensions/CounterDataCollectorExtensions.cs +++ b/Lombiq.Tests.UI/Services/Counters/Extensions/CounterDataCollectorExtensions.cs @@ -39,6 +39,7 @@ public static Task DbCommandExecuteScalarAsync( { collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteScalarAsync(cancellationToken); } @@ -49,6 +50,7 @@ public static DbDataReader DbCommandExecuteDbDataReader( { collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteReader(behavior); } @@ -60,6 +62,7 @@ public static Task DbCommandExecuteDbDataReaderAsync( { collector.Increment(DbCommandExecuteCounterKey.CreateFrom(dbCommand)); collector.Increment(DbCommandTextExecuteCounterKey.CreateFrom(dbCommand)); + return dbCommand.ExecuteReaderAsync(behavior, cancellationToken); } } diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index 0ae823d76..1849b294b 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -1,6 +1,11 @@ +using Lombiq.Tests.UI.Exceptions; using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.SecurityScanning; +using Lombiq.Tests.UI.Services.Counters; using Lombiq.Tests.UI.Services.Counters.Configuration; +using Lombiq.Tests.UI.Services.Counters.Data; +using Lombiq.Tests.UI.Services.Counters.Extensions; +using Lombiq.Tests.UI.Services.Counters.Value; using Lombiq.Tests.UI.Services.GitHub; using Lombiq.Tests.UI.Shortcuts.Controllers; using OpenQA.Selenium; @@ -25,6 +30,13 @@ public enum Browser public class OrchardCoreUITestExecutorConfiguration { + private const string WorkflowTypeStartActivitiesQuery = + "SELECT DISTINCT [Document].* FROM [Document] INNER JOIN [WorkflowTypeStartActivitiesIndex]" + + " AS [WorkflowTypeStartActivitiesIndex_a1]" + + " ON [WorkflowTypeStartActivitiesIndex_a1].[DocumentId] = [Document].[Id]" + + " WHERE (([WorkflowTypeStartActivitiesIndex_a1].[StartActivityName] = @p0)" + + " and ([WorkflowTypeStartActivitiesIndex_a1].[IsEnabled] = @p1))"; + public static readonly Func AssertAppLogsAreEmptyAsync = app => app.LogsShouldBeEmptyAsync(); @@ -46,6 +58,22 @@ public class OrchardCoreUITestExecutorConfiguration // under a different URL. !logEntry.IsNotFoundLogEntry("/favicon.ico"); + public static readonly IEnumerable DefaultCounterExcludeList = new List + { + new DbCommandExecuteCounterKey( + WorkflowTypeStartActivitiesQuery, + new("p0", "ContentCreatedEvent"), + new("p1", value: true)), + new DbCommandExecuteCounterKey( + WorkflowTypeStartActivitiesQuery, + new("p0", "ContentPublishedEvent"), + new("p1", value: true)), + new DbCommandExecuteCounterKey( + WorkflowTypeStartActivitiesQuery, + new("p0", "ContentUpdatedEvent"), + new("p1", value: true)), + }; + /// /// Gets the global events available during UI test execution. /// @@ -262,4 +290,80 @@ public static Func CreateAppLogAssertionForSecuri return app => app.LogsShouldBeEmptyAsync(canContainWarnings: true, permittedErrorLines); } + + public static Action DefaultAssertCounterData( + PhaseCounterConfiguration configuration) => + (collector, probe) => + { + var counterConfiguration = configuration as CounterConfiguration; + if (counterConfiguration is RunningPhaseCounterConfiguration runningPhaseCounterConfiguration + && probe is ICounterConfigurationKey counterConfigurationKey) + { + counterConfiguration = runningPhaseCounterConfiguration.GetMaybeByKey(counterConfigurationKey) + ?? configuration; + } + + (CounterThresholdConfiguration Settings, string Name)? threshold = probe switch + { + NavigationProbe => + (Settings: counterConfiguration.NavigationThreshold, Name: nameof(counterConfiguration.NavigationThreshold)), + PageLoadProbe => + (Settings: counterConfiguration.PageLoadThreshold, Name: nameof(counterConfiguration.PageLoadThreshold)), + SessionProbe => + (Settings: counterConfiguration.SessionThreshold, Name: nameof(counterConfiguration.SessionThreshold)), + CounterDataCollector when counterConfiguration is PhaseCounterConfiguration phaseCounterConfiguration => + (Settings: phaseCounterConfiguration.PhaseThreshold, Name: nameof(phaseCounterConfiguration.PhaseThreshold)), + _ => null, + }; + + if (threshold is { } settings && settings.Settings.IsEnabled) + { + try + { + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandIncludingParametersExecutionCountThreshold)}", + settings.Settings.DbCommandIncludingParametersExecutionCountThreshold); + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbCommandExcludingParametersExecutionThreshold)}", + settings.Settings.DbCommandExcludingParametersExecutionThreshold); + AssertIntegerCounterValue( + probe, + counterConfiguration.ExcludeFilter ?? (key => false), + $"{settings.Name}.{nameof(settings.Settings.DbReaderReadThreshold)}", + settings.Settings.DbReaderReadThreshold); + } + catch (CounterThresholdException exception) when (probe is IOutOfTestContextCounterProbe) + { + collector.PostponeCounterException(exception); + } + } + }; + + public static void AssertIntegerCounterValue( + ICounterProbe probe, + Func excludeFilter, + string thresholdName, + int threshold) + where TKey : ICounterKey => + probe.Counters.Keys + .OfType() + .Where(key => !excludeFilter(key)) + .ForEach(key => + { + if (probe.Counters[key] is IntegerCounterValue counterValue + && counterValue.Value > threshold) + { + throw new CounterThresholdException( + probe, + key, + counterValue, + $"Counter value is greater then {thresholdName}, threshold: {threshold.ToTechnicalString()}."); + } + }); + + public static bool DefaultCounterExcludeFilter(ICounterKey key) => DefaultCounterExcludeList.Contains(key); } diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 5bc18831f..80658077d 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -7,7 +7,6 @@ using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Models; using Lombiq.Tests.UI.SecurityScanning; -using Lombiq.Tests.UI.Services.Counters.Configuration; using Lombiq.Tests.UI.Services.GitHub; using Microsoft.VisualBasic.FileIO; using Mono.Unix; @@ -112,9 +111,9 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) _context.FailureDumpContainer.Clear(); _context.CounterDataCollector.Reset(); - _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.Running); - _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Running.AssertCounterData - ?? CounterConfigurations.DefaultAssertCounterData(_configuration.CounterConfiguration.Running); + _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.AfterSetup); + _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.AfterSetup.AssertCounterData + ?? OrchardCoreUITestExecutorConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.AfterSetup); failureDumpContainer = _context.FailureDumpContainer; _context.SetDefaultBrowserSize(); @@ -507,7 +506,7 @@ private async Task SetupAsync() _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.Setup); _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Setup.AssertCounterData - ?? CounterConfigurations.DefaultAssertCounterData(_configuration.CounterConfiguration.Setup); + ?? OrchardCoreUITestExecutorConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Setup); SetupSqlServerSnapshot(); SetupAzureBlobStorageSnapshot(); diff --git a/Readme.md b/Readme.md index bad9741d7..842195d62 100644 --- a/Readme.md +++ b/Readme.md @@ -27,6 +27,7 @@ Highlights: - If your app uses a camera, a fake video capture source in Chrome is supported. [Here's a demo video of the feature](https://www.youtube.com/watch?v=sGcD0eJ2ytc), and check out the docs [here](Lombiq.Tests.UI/Docs/FakeVideoCaptureSource.md). - Interactive mode for debugging the app while the test is paused. [Here's a demo of the feature](Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs), and a [demo video here](https://www.youtube.com/watch?v=ItNltaruWTY). - Security scanning with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/), the world's most widely used web app security scanner, right from UI tests. See a demo video [here](https://www.youtube.com/watch?v=iUYivLkFbY4). +- See a demo video of the project [here](https://www.youtube.com/watch?v=mEUg6-pad-E), and the Orchard Harvest 2023 conference talk about automated QA in Orchard Core [here](https://youtu.be/CHdhwD2NHBU). Also, see our [Testing Toolbox](https://github.com/Lombiq/Testing-Toolbox) for similar features for lower-level tests. From 849553f73c2fd13ba970b94a4d3e967120279a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 7 May 2024 22:33:55 +0200 Subject: [PATCH 36/44] Allowing to enable/disable counter subsystem by configuration Fine-tuning thresholds in tests --- .../Tests/DuplicatedSqlQueryDetectorTests.cs | 7 +++- .../Services/CounterDataCollector.cs | 16 ++++++++ .../Configuration/CounterConfigurations.cs | 5 +++ .../Services/UITestExecutionSession.cs | 40 ++++++++++++++----- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs index ad2bfb63d..965f2fc67 100644 --- a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -50,10 +50,10 @@ private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration config ExcludeFilter = OrchardCoreUITestExecutorConfiguration.DefaultCounterExcludeFilter, SessionThreshold = { - // Let's enable and configure the counter threshold for ORM sessions. + // Let's enable and configure the counter thresholds for ORM sessions. IsEnabled = true, DbCommandExcludingParametersExecutionThreshold = 5, - DbCommandIncludingParametersExecutionCountThreshold = 15, + DbCommandIncludingParametersExecutionCountThreshold = 2, DbReaderReadThreshold = 0, }, }; @@ -63,6 +63,9 @@ private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration config new RelativeUrlConfigurationKey(new Uri("/Admin", UriKind.Relative), exactMatch: false), adminCounterConfiguration); + // Enable the counter subsystem. + configuration.CounterConfiguration.IsEnabled = true; + return Task.CompletedTask; } } diff --git a/Lombiq.Tests.UI/Services/CounterDataCollector.cs b/Lombiq.Tests.UI/Services/CounterDataCollector.cs index c91f3ab98..6eef50df0 100644 --- a/Lombiq.Tests.UI/Services/CounterDataCollector.cs +++ b/Lombiq.Tests.UI/Services/CounterDataCollector.cs @@ -15,12 +15,18 @@ public sealed class CounterDataCollector : CounterProbeBase, ICounterDataCollect public override bool IsAttached => true; public Action AssertCounterData { get; set; } public string Phase { get; set; } + public bool IsEnabled { get; set; } public CounterDataCollector(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper; public void AttachProbe(ICounterProbe probe) { + if (!IsEnabled) + { + return; + } + probe.CaptureCompleted = ProbeCaptureCompleted; _probes.Add(probe); } @@ -34,6 +40,11 @@ public void Reset() public override void Increment(ICounterKey counter) { + if (!IsEnabled) + { + return; + } + _probes.Where(probe => probe.IsAttached) .ForEach(probe => probe.Increment(counter)); base.Increment(counter); @@ -57,6 +68,11 @@ public override IEnumerable Dump() public void AssertCounter() { + if (!IsEnabled) + { + return; + } + if (!_postponedCounterExceptions.IsEmpty) { throw new AggregateException( diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs index 63f736c6a..bf9beee5c 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfigurations.cs @@ -2,6 +2,11 @@ namespace Lombiq.Tests.UI.Services.Counters.Configuration; public class CounterConfigurations { + /// + /// Gets or sets a value indicating whether the whole counter infrastructure is enabled. + /// + public bool IsEnabled { get; set; } + /// /// Gets the counter configuration used in the setup phase of the web application. /// diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 80658077d..72323c00a 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -7,6 +7,7 @@ using Lombiq.Tests.UI.Helpers; using Lombiq.Tests.UI.Models; using Lombiq.Tests.UI.SecurityScanning; +using Lombiq.Tests.UI.Services.Counters.Configuration; using Lombiq.Tests.UI.Services.GitHub; using Microsoft.VisualBasic.FileIO; using Mono.Unix; @@ -110,10 +111,11 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) _context ??= await CreateContextAsync(); _context.FailureDumpContainer.Clear(); - _context.CounterDataCollector.Reset(); - _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.AfterSetup); - _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.AfterSetup.AssertCounterData - ?? OrchardCoreUITestExecutorConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.AfterSetup); + + BeginDataCollection( + _configuration.CounterConfiguration.AfterSetup, + nameof(_configuration.CounterConfiguration.AfterSetup)); + failureDumpContainer = _context.FailureDumpContainer; _context.SetDefaultBrowserSize(); @@ -121,8 +123,8 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) await _testManifest.TestAsync(_context); await _context.AssertLogsAsync(); - _context.CounterDataCollector.Dump().ForEach(_testOutputHelper.WriteLine); - _context.CounterDataCollector.AssertCounter(); + + EndAssertDataCollection(); return true; } @@ -162,6 +164,21 @@ public async Task ExecuteAsync(int retryCount, string dumpRootPath) return false; } + private void BeginDataCollection(PhaseCounterConfiguration counterConfiguration, string phase) + { + _context.CounterDataCollector.Reset(); + _context.CounterDataCollector.Phase = phase; + _context.CounterDataCollector.AssertCounterData = counterConfiguration.AssertCounterData + ?? OrchardCoreUITestExecutorConfiguration.DefaultAssertCounterData(counterConfiguration); + _context.CounterDataCollector.IsEnabled = _configuration.CounterConfiguration.IsEnabled; + } + + private void EndAssertDataCollection() + { + _context.CounterDataCollector.Dump().ForEach(_testOutputHelper.WriteLine); + _context.CounterDataCollector.AssertCounter(); + } + private async ValueTask ShutdownAsync() { if (_configuration.RunAssertLogsOnAllPageChanges) @@ -504,9 +521,9 @@ private async Task SetupAsync() // config to be available at startup too. _context = await CreateContextAsync(); - _context.CounterDataCollector.Phase = nameof(_configuration.CounterConfiguration.Setup); - _context.CounterDataCollector.AssertCounterData = _configuration.CounterConfiguration.Setup.AssertCounterData - ?? OrchardCoreUITestExecutorConfiguration.DefaultAssertCounterData(_configuration.CounterConfiguration.Setup); + BeginDataCollection( + _configuration.CounterConfiguration.Setup, + nameof(_configuration.CounterConfiguration.Setup)); SetupSqlServerSnapshot(); SetupAzureBlobStorageSnapshot(); @@ -516,8 +533,9 @@ private async Task SetupAsync() var result = (_context, await setupConfiguration.SetupOperation(_context)); await _context.AssertLogsAsync(); - _context.CounterDataCollector.Dump().ForEach(line => _testOutputHelper.WriteLine(line)); - _context.CounterDataCollector.AssertCounter(); + + EndAssertDataCollection(); + _testOutputHelper.WriteLineTimestampedAndDebug("Finished setup operation."); return result; From 9203e36bbb7ddbb7d7c5b8353705591849cd3656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 7 May 2024 23:40:56 +0200 Subject: [PATCH 37/44] Adding a bit more information to CounterThresholdException message --- Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs index 59d1dbb03..c838b0502 100644 --- a/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs +++ b/Lombiq.Tests.UI/Exceptions/CounterThresholdException.cs @@ -54,7 +54,10 @@ private static string FormatMessage( ICounterValue value, string message) { - var builder = new StringBuilder(); + var builder = new StringBuilder() + .AppendLine() + .AppendLine("A counter value has crossed the configured threshold level. Details:"); + if (probe is not null) builder.AppendLine(probe.DumpHeadline()); counter?.Dump().ForEach(line => builder.AppendLine(line)); value?.Dump().ForEach(line => builder.AppendLine(line)); From a52cff109d6b6e87fd886e9ad1e0a6b80525c60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 7 May 2024 23:44:07 +0200 Subject: [PATCH 38/44] Removing unnecessary Should.NotThrowAsync() --- .../Tests/DuplicatedSqlQueryDetectorTests.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs index 965f2fc67..e757b2b4c 100644 --- a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -32,11 +32,10 @@ public Task PageWithTooManyDuplicatedSqlQueriesShouldThrow(Browser browser) => // This test will pass because not any of the Admin page was loaded. [Theory, Chrome] public Task PageWithoutDuplicatedSqlQueriesShouldPass(Browser browser) => - Should.NotThrowAsync(() => - ExecuteTestAfterSetupAsync( - async context => await context.GoToHomePageAsync(onlyIfNotAlreadyThere: false), - browser, - ConfigureAsync)); + ExecuteTestAfterSetupAsync( + async context => await context.GoToHomePageAsync(onlyIfNotAlreadyThere: false), + browser, + ConfigureAsync); // We configure the test to throw an exception if a certain counter threshold is exceeded, but only in case of Admin // pages. From 30b4997d1ba28f7dca1b6aacfabba767e61a9d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Wed, 8 May 2024 00:13:15 +0200 Subject: [PATCH 39/44] Adding test to demonstrate the scenario when the counter thresholds are exactly matching with the counter values captured during navigation. --- .../Tests/DuplicatedSqlQueryDetectorTests.cs | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs index e757b2b4c..d69405916 100644 --- a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -1,4 +1,3 @@ -using Lombiq.Tests.UI.Attributes; using Lombiq.Tests.UI.Extensions; using Lombiq.Tests.UI.Services; using Lombiq.Tests.UI.Services.Counters.Configuration; @@ -21,25 +20,43 @@ public DuplicatedSqlQueryDetectorTests(ITestOutputHelper testOutputHelper) // This test will fail because the app will read the same command result more times than the configured threshold // during the Admin page rendering. - [Theory, Chrome] - public Task PageWithTooManyDuplicatedSqlQueriesShouldThrow(Browser browser) => + [Fact] + public Task PageWithTooManyDuplicatedSqlQueriesShouldThrow() => Should.ThrowAsync(() => ExecuteTestAfterSetupAsync( context => context.SignInDirectlyAndGoToDashboardAsync(), - browser, - ConfigureAsync)); + configuration => ConfigureAsync( + configuration, + commandExcludingParametersThreshold: 3, + commandIncludingParametersThreshold: 2, + readerReadThreshold: 0))); - // This test will pass because not any of the Admin page was loaded. - [Theory, Chrome] - public Task PageWithoutDuplicatedSqlQueriesShouldPass(Browser browser) => + // This test will pass because not any of the Admin page was loaded where the SQL queries are under monitoring. + [Fact] + public Task PageWithoutDuplicatedSqlQueriesShouldPass() => ExecuteTestAfterSetupAsync( - async context => await context.GoToHomePageAsync(onlyIfNotAlreadyThere: false), - browser, - ConfigureAsync); + context => context.GoToHomePageAsync(onlyIfNotAlreadyThere: false), + configuration => ConfigureAsync(configuration)); + + // This test will pass because counter thresholds are exactly matching with the counter values captured during + // navigating to the Admin dashboard page. + [Fact] + public Task PageWithMatchingCounterThresholdsShouldPass() => + ExecuteTestAfterSetupAsync( + context => context.SignInDirectlyAndGoToDashboardAsync(), + configuration => ConfigureAsync( + configuration, + commandExcludingParametersThreshold: 3, + commandIncludingParametersThreshold: 2, + readerReadThreshold: 2)); // We configure the test to throw an exception if a certain counter threshold is exceeded, but only in case of Admin // pages. - private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration configuration) + private static Task ConfigureAsync( + OrchardCoreUITestExecutorConfiguration configuration, + int commandExcludingParametersThreshold = 0, + int commandIncludingParametersThreshold = 0, + int readerReadThreshold = 0) { // The test is guaranteed to fail so we don't want to retry it needlessly. configuration.MaxRetryCount = 0; @@ -51,9 +68,9 @@ private static Task ConfigureAsync(OrchardCoreUITestExecutorConfiguration config { // Let's enable and configure the counter thresholds for ORM sessions. IsEnabled = true, - DbCommandExcludingParametersExecutionThreshold = 5, - DbCommandIncludingParametersExecutionCountThreshold = 2, - DbReaderReadThreshold = 0, + DbCommandExcludingParametersExecutionThreshold = commandExcludingParametersThreshold, + DbCommandIncludingParametersExecutionCountThreshold = commandIncludingParametersThreshold, + DbReaderReadThreshold = readerReadThreshold, }, }; From 1b500d83658c8a85229566a5bd34623b0c74f169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 12 May 2024 22:34:47 +0200 Subject: [PATCH 40/44] Removing unnecessary configuration --- .../Tests/BasicOrchardFeaturesTests.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs index 6e756f2ba..d72f425df 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicOrchardFeaturesTests.cs @@ -21,19 +21,7 @@ public BasicOrchardFeaturesTests(ITestOutputHelper testOutputHelper) // this test. [Fact] public Task BasicOrchardFeaturesShouldWork() => - ExecuteTestAsync( - context => context.TestBasicOrchardFeaturesAsync(RecipeIds.BasicOrchardFeaturesTests), - configuration => - { - // The UI Testing Toolbox includes a DbCommand execution counter to check for duplicated SQL queries.. - // After the end of the test, it checks the number of executed commands with the same SQL command text - // and parameter set against the threshold value in its configuration. If the executed command count is - // greater than the threshold, it raises a CounterThresholdException. So here we set the minimum - // required value to avoid it. - configuration.CounterConfiguration.AfterSetup.PhaseThreshold.DbCommandIncludingParametersExecutionCountThreshold = 26; - - return Task.CompletedTask; - }); + ExecuteTestAsync(context => context.TestBasicOrchardFeaturesAsync(RecipeIds.BasicOrchardFeaturesTests)); } // END OF TRAINING SECTION: Basic Orchard features tests. From bca3add00afad3ebb8b2a31c4cdc99e54534b39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 12 May 2024 22:54:24 +0200 Subject: [PATCH 41/44] Renaming `PageLoadProbe` -> `RequestProbe` and `PageLoadProbeMiddleware` -> `RequestProbeMiddleware` --- .../Services/Counters/Configuration/CounterConfiguration.cs | 2 +- ...PageLoadProbeMiddleware.cs => RequestProbeMiddleware.cs} | 6 +++--- .../Services/Counters/{PageLoadProbe.cs => RequestProbe.cs} | 6 +++--- .../OrchardCoreHosting/OrchardApplicationFactory.cs | 2 +- .../Services/OrchardCoreUITestExecutorConfiguration.cs | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename Lombiq.Tests.UI/Services/Counters/Middlewares/{PageLoadProbeMiddleware.cs => RequestProbeMiddleware.cs} (65%) rename Lombiq.Tests.UI/Services/Counters/{PageLoadProbe.cs => RequestProbe.cs} (69%) diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs index f33f7de86..23520089c 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterConfiguration.cs @@ -28,7 +28,7 @@ public class CounterConfiguration public CounterThresholdConfiguration SessionThreshold { get; set; } = new(); /// - /// Gets or sets threshold configuration used per page load. See: . + /// Gets or sets threshold configuration used per page load. See: . /// public CounterThresholdConfiguration PageLoadThreshold { get; set; } = new(); } diff --git a/Lombiq.Tests.UI/Services/Counters/Middlewares/PageLoadProbeMiddleware.cs b/Lombiq.Tests.UI/Services/Counters/Middlewares/RequestProbeMiddleware.cs similarity index 65% rename from Lombiq.Tests.UI/Services/Counters/Middlewares/PageLoadProbeMiddleware.cs rename to Lombiq.Tests.UI/Services/Counters/Middlewares/RequestProbeMiddleware.cs index 3259fbc66..8da8f2add 100644 --- a/Lombiq.Tests.UI/Services/Counters/Middlewares/PageLoadProbeMiddleware.cs +++ b/Lombiq.Tests.UI/Services/Counters/Middlewares/RequestProbeMiddleware.cs @@ -5,12 +5,12 @@ namespace Lombiq.Tests.UI.Services.Counters.Middlewares; -public class PageLoadProbeMiddleware +public class RequestProbeMiddleware { private readonly RequestDelegate _next; private readonly ICounterDataCollector _counterDataCollector; - public PageLoadProbeMiddleware(RequestDelegate next, ICounterDataCollector counterDataCollector) + public RequestProbeMiddleware(RequestDelegate next, ICounterDataCollector counterDataCollector) { _next = next; _counterDataCollector = counterDataCollector; @@ -18,7 +18,7 @@ public PageLoadProbeMiddleware(RequestDelegate next, ICounterDataCollector count public async Task InvokeAsync(HttpContext context) { - using (new PageLoadProbe(_counterDataCollector, context.Request.Method, new Uri(context.Request.GetEncodedUrl()))) + using (new RequestProbe(_counterDataCollector, context.Request.Method, new Uri(context.Request.GetEncodedUrl()))) await _next.Invoke(context); } } diff --git a/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs b/Lombiq.Tests.UI/Services/Counters/RequestProbe.cs similarity index 69% rename from Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs rename to Lombiq.Tests.UI/Services/Counters/RequestProbe.cs index ef50b522f..f3683e177 100644 --- a/Lombiq.Tests.UI/Services/Counters/PageLoadProbe.cs +++ b/Lombiq.Tests.UI/Services/Counters/RequestProbe.cs @@ -3,19 +3,19 @@ namespace Lombiq.Tests.UI.Services.Counters; -public sealed class PageLoadProbe : CounterProbe, IRelativeUrlConfigurationKey +public sealed class RequestProbe : CounterProbe, IRelativeUrlConfigurationKey { public string RequestMethod { get; init; } public Uri AbsoluteUri { get; init; } - public PageLoadProbe(ICounterDataCollector counterDataCollector, string requestMethod, Uri absoluteUri) + public RequestProbe(ICounterDataCollector counterDataCollector, string requestMethod, Uri absoluteUri) : base(counterDataCollector) { RequestMethod = requestMethod; AbsoluteUri = absoluteUri; } - public override string DumpHeadline() => $"{nameof(PageLoadProbe)}, [{RequestMethod}]{AbsoluteUri}"; + public override string DumpHeadline() => $"{nameof(RequestProbe)}, [{RequestMethod}]{AbsoluteUri}"; #region IRelativeUrlConfigurationKey implementation diff --git a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs index 3a0aa80c9..3d73274dd 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreHosting/OrchardApplicationFactory.cs @@ -126,7 +126,7 @@ .ImplementationInstance as ConfigurationManager int.MaxValue); builder.Configure( - app => app.UseMiddleware(), + app => app.UseMiddleware(), int.MaxValue); } diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index 1849b294b..25c39a706 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -307,7 +307,7 @@ public static Action DefaultAssertCounterD { NavigationProbe => (Settings: counterConfiguration.NavigationThreshold, Name: nameof(counterConfiguration.NavigationThreshold)), - PageLoadProbe => + RequestProbe => (Settings: counterConfiguration.PageLoadThreshold, Name: nameof(counterConfiguration.PageLoadThreshold)), SessionProbe => (Settings: counterConfiguration.SessionThreshold, Name: nameof(counterConfiguration.SessionThreshold)), From 71663b9b5a947993b93b8e5c95633dc6264da1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Mon, 13 May 2024 20:02:43 +0200 Subject: [PATCH 42/44] Adding more comments Adding more sample tests --- .../Tests/DuplicatedSqlQueryDetectorTests.cs | 28 ++++++++++++++++++- .../CounterThresholdConfiguration.cs | 13 +++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs index d69405916..6e92cdb14 100644 --- a/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/DuplicatedSqlQueryDetectorTests.cs @@ -21,7 +21,7 @@ public DuplicatedSqlQueryDetectorTests(ITestOutputHelper testOutputHelper) // This test will fail because the app will read the same command result more times than the configured threshold // during the Admin page rendering. [Fact] - public Task PageWithTooManyDuplicatedSqlQueriesShouldThrow() => + public Task PageWithTooManyReadsOnDbDataReaderShouldThrow() => Should.ThrowAsync(() => ExecuteTestAfterSetupAsync( context => context.SignInDirectlyAndGoToDashboardAsync(), @@ -31,6 +31,32 @@ public Task PageWithTooManyDuplicatedSqlQueriesShouldThrow() => commandIncludingParametersThreshold: 2, readerReadThreshold: 0))); + // This test will fail because the app will execute the same SQL query having same parameter set more times than + // expected during the Admin page rendering. + [Fact] + public Task PageWithTooManySqlQueriesExecutedHavingTheSameParameterSetShouldThrow() => + Should.ThrowAsync(() => + ExecuteTestAfterSetupAsync( + context => context.SignInDirectlyAndGoToDashboardAsync(), + configuration => ConfigureAsync( + configuration, + commandExcludingParametersThreshold: 3, + commandIncludingParametersThreshold: 0, + readerReadThreshold: 2))); + + // This test will fail because the app will execute the same SQL query more times than expected during the Admin + // page rendering. + [Fact] + public Task PageWithTooManySqlQueriesExecutedShouldThrow() => + Should.ThrowAsync(() => + ExecuteTestAfterSetupAsync( + context => context.SignInDirectlyAndGoToDashboardAsync(), + configuration => ConfigureAsync( + configuration, + commandExcludingParametersThreshold: 0, + commandIncludingParametersThreshold: 2, + readerReadThreshold: 2))); + // This test will pass because not any of the Admin page was loaded where the SQL queries are under monitoring. [Fact] public Task PageWithoutDuplicatedSqlQueriesShouldPass() => diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs index 8d7ccea02..6a89eb24b 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs @@ -28,6 +28,15 @@ public class CounterThresholdConfiguration /// and /// for counting. /// - // TODO: Human readable comments. - public int DbReaderReadThreshold { get; set; } = 11; + /// + /// + /// Use this to set the maximum number of reads allowed on a instace. + /// The counter infrastructure counts the and + /// calls, also the + /// calls are counted on + /// instance returned by the + /// . + /// + /// + public int DbReaderReadThreshold { get; set; } } From 2c7e741589baae68b36d3c2444f546262cb5b974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 14 Jul 2024 19:00:06 +0200 Subject: [PATCH 43/44] Spelling --- .../Counters/Configuration/CounterThresholdConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs index 6a89eb24b..d4230a4f2 100644 --- a/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs +++ b/Lombiq.Tests.UI/Services/Counters/Configuration/CounterThresholdConfiguration.cs @@ -30,7 +30,7 @@ public class CounterThresholdConfiguration /// /// /// - /// Use this to set the maximum number of reads allowed on a instace. + /// Use this to set the maximum number of reads allowed on a instance. /// The counter infrastructure counts the and /// calls, also the /// calls are counted on From a6f685cfcd793ee871f476533249a1c56fd28e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Sun, 14 Jul 2024 19:20:57 +0200 Subject: [PATCH 44/44] Fixing analyzer warnings --- Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs | 2 +- .../Counters/Data/DbCommandTextExecuteCounterKey.cs | 3 +-- Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs | 2 +- .../Services/Counters/Value/IntegerCounterValue.cs | 6 +++--- .../Services/OrchardCoreUITestExecutorConfiguration.cs | 6 +++--- Readme.md | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs index d84e96fa8..3ca8091a6 100644 --- a/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs +++ b/Lombiq.Tests.UI/Services/Counters/CounterProbeBase.cs @@ -30,7 +30,7 @@ public virtual IEnumerable DumpSummary() { if (!Counters.Any()) { - return Enumerable.Empty(); + return []; } var lines = new List diff --git a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs index 52b2b8224..e8993b6e5 100644 --- a/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs +++ b/Lombiq.Tests.UI/Services/Counters/Data/DbCommandTextExecuteCounterKey.cs @@ -1,6 +1,5 @@ using System; using System.Data.Common; -using System.Linq; namespace Lombiq.Tests.UI.Services.Counters.Data; @@ -9,7 +8,7 @@ public sealed class DbCommandTextExecuteCounterKey : DbCommandCounterKey public override string DisplayName => "Database command execute counter"; public DbCommandTextExecuteCounterKey(string commandText) - : base(commandText, Enumerable.Empty()) + : base(commandText, []) { } diff --git a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs index bbf0f07ee..fd161a152 100644 --- a/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/Value/CounterValue.cs @@ -11,5 +11,5 @@ public abstract class CounterValue : ICounterValue public TValue Value { get; set; } public virtual IEnumerable Dump() => - new[] { string.Create(CultureInfo.InvariantCulture, $"{GetType().Name} value: {Value}") }; + [string.Create(CultureInfo.InvariantCulture, $"{GetType().Name} value: {Value}")]; } diff --git a/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs b/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs index baac5fb8e..a17696f8d 100644 --- a/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs +++ b/Lombiq.Tests.UI/Services/Counters/Value/IntegerCounterValue.cs @@ -5,10 +5,10 @@ namespace Lombiq.Tests.UI.Services.Counters.Value; public class IntegerCounterValue : CounterValue { - public override IEnumerable Dump() => new[] - { + public override IEnumerable Dump() => + [ $"{DisplayName}: {this}", - }; + ]; public override string ToString() => Value.ToTechnicalString(); } diff --git a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs index 25c39a706..b433a2307 100644 --- a/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs +++ b/Lombiq.Tests.UI/Services/OrchardCoreUITestExecutorConfiguration.cs @@ -58,8 +58,8 @@ public class OrchardCoreUITestExecutorConfiguration // under a different URL. !logEntry.IsNotFoundLogEntry("/favicon.ico"); - public static readonly IEnumerable DefaultCounterExcludeList = new List - { + public static readonly IEnumerable DefaultCounterExcludeList = + [ new DbCommandExecuteCounterKey( WorkflowTypeStartActivitiesQuery, new("p0", "ContentCreatedEvent"), @@ -72,7 +72,7 @@ public class OrchardCoreUITestExecutorConfiguration WorkflowTypeStartActivitiesQuery, new("p0", "ContentUpdatedEvent"), new("p1", value: true)), - }; + ]; /// /// Gets the global events available during UI test execution. diff --git a/Readme.md b/Readme.md index 842195d62..b918aadfe 100644 --- a/Readme.md +++ b/Readme.md @@ -27,7 +27,7 @@ Highlights: - If your app uses a camera, a fake video capture source in Chrome is supported. [Here's a demo video of the feature](https://www.youtube.com/watch?v=sGcD0eJ2ytc), and check out the docs [here](Lombiq.Tests.UI/Docs/FakeVideoCaptureSource.md). - Interactive mode for debugging the app while the test is paused. [Here's a demo of the feature](Lombiq.Tests.UI.Samples/Tests/InteractiveModeTests.cs), and a [demo video here](https://www.youtube.com/watch?v=ItNltaruWTY). - Security scanning with [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/), the world's most widely used web app security scanner, right from UI tests. See a demo video [here](https://www.youtube.com/watch?v=iUYivLkFbY4). -- +- Duplicate SQL query detector: Built-in infrastructure to detect N+1 query issue. See a demo video of the project [here](https://www.youtube.com/watch?v=mEUg6-pad-E), and the Orchard Harvest 2023 conference talk about automated QA in Orchard Core [here](https://youtu.be/CHdhwD2NHBU). Also, see our [Testing Toolbox](https://github.com/Lombiq/Testing-Toolbox) for similar features for lower-level tests.