From b949dcaf4e16f0c60fdffbca91fbde1f452233e1 Mon Sep 17 00:00:00 2001
From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com>
Date: Thu, 7 Dec 2023 23:33:13 +0300
Subject: [PATCH 1/5] [dotnet] Possibility to output internal log messages to
file (#13249)
* INitial implementation
* With context
* Update HttpCommandExecutor.cs
* Nullable handlers
* Don't capture logger
* Log message issuer
* Simplify things
* Continue
* Update nunit adapter to work with dotnet 7 and be more friendly with windows
* Rename to LogEventLevel
* Typo
* Introduce LogContextManager
* Typo again
* Rename to Timestamp
* Make ILogger as static field
* Support hierarchical contexts
* Rename to EmitMessage
* Do not emit message to parent context
* Deep copy of loggers and handlers per context
* Make fields private
* Static works with current log context
* Create context with minimum level
* Set minimum level for context
* Rename to WithHandler
* Set minimum level per issuer
* Simplify getting internal logger
* Use DateTimeOffset
* Docs for log event level
* Docs for ILogger
* Docs for Logger
* Docs for others
* Make ILogger interface as internal
* Revert "Make ILogger interface as internal"
This reverts commit 3cf6e489dc5aaaa7dc78a54d22d97230ff6b0b11.
* First test
* Update LogTest.cs
* Fix build error
* Info minimum log level by default
* Remove unnecessary log call in ChromeDriver
* Adjust log levels in console output
* Make it length fixed
* Make webdriver assembly internals visible to tests
* Make ILogger hidden from user
* More tests for log context
* Init
* Rename back to AddHandler
* Make format script happy?
* Make format script happy?
* Rename back to SetLevel
* Console handler by default
* Output logs to stderr
* New api to mange log handlers
* Use logging in DriverFactory
* Revert "Use logging in DriverFactory"
This reverts commit e3255a6217851882ef772165fb01bff1df8c244f.
* Verbose driver creation in tests
* Search driver type in loaded assemblies
* Decalare internals visible to in csproj to not conflict with bazel
* Clean specific assembly name for driver type
* Old school using to make bazel happy
* Fix targeting packs for test targets
* It works
* Small clean up
* Lock in ctor
* Dispose at process exit
* Remove redundant Clone for log handlers
* Dispose log handlers when context finishes
* Lock writing to the disk globally
* Fix new list of log handlers for context
* Don't lock in ctor
* Add docs
* Change format of datetime in file log
* Thread safe disposing
* Add finilizer
* Docs for finilizer
* Add tests
* Recreating missing dirs
---
.../Internal/Logging/ConsoleLogHandler.cs | 9 --
.../Internal/Logging/FileLogHandler.cs | 94 +++++++++++++++++++
.../webdriver/Internal/Logging/ILogHandler.cs | 6 --
.../webdriver/Internal/Logging/LogContext.cs | 26 +++--
.../Internal/Logging/FileLogHandlerTest.cs | 43 +++++++++
5 files changed, 155 insertions(+), 23 deletions(-)
create mode 100644 dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs
create mode 100644 dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs
diff --git a/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs b/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs
index 83759ac09dee6..30f63dd625673 100644
--- a/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs
+++ b/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs
@@ -36,14 +36,5 @@ public void Handle(LogEvent logEvent)
{
Console.Error.WriteLine($"{logEvent.Timestamp:HH:mm:ss.fff} {_levels[(int)logEvent.Level]} {logEvent.IssuedBy.Name}: {logEvent.Message}");
}
-
- ///
- /// Creates a new instance of the class.
- ///
- /// A new instance of the class.
- public ILogHandler Clone()
- {
- return this;
- }
}
}
diff --git a/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs b/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs
new file mode 100644
index 0000000000000..915ea43870863
--- /dev/null
+++ b/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+
+namespace OpenQA.Selenium.Internal.Logging
+{
+ ///
+ /// Represents a log handler that writes log events to a file.
+ ///
+ public class FileLogHandler : ILogHandler, IDisposable
+ {
+ // performance trick to avoid expensive Enum.ToString() with fixed length
+ private static readonly string[] _levels = { "TRACE", "DEBUG", " INFO", " WARN", "ERROR" };
+
+ private FileStream _fileStream;
+ private StreamWriter _streamWriter;
+
+ private readonly object _lockObj = new object();
+ private bool _isDisposed;
+
+ ///
+ /// Initializes a new instance of the class with the specified file path.
+ ///
+ /// The path of the log file.
+ public FileLogHandler(string path)
+ {
+ if (string.IsNullOrEmpty(path)) throw new ArgumentException("File log path cannot be null or empty.", nameof(path));
+
+ var directory = Path.GetDirectoryName(path);
+ if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ _fileStream = File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
+ _fileStream.Seek(0, SeekOrigin.End);
+ _streamWriter = new StreamWriter(_fileStream, System.Text.Encoding.UTF8)
+ {
+ AutoFlush = true
+ };
+ }
+
+ ///
+ /// Handles a log event by writing it to the log file.
+ ///
+ /// The log event to handle.
+ public void Handle(LogEvent logEvent)
+ {
+ lock (_lockObj)
+ {
+ _streamWriter.WriteLine($"{logEvent.Timestamp:r} {_levels[(int)logEvent.Level]} {logEvent.IssuedBy.Name}: {logEvent.Message}");
+ }
+ }
+
+ ///
+ /// Disposes the file log handler and releases any resources used.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Finalizes the file log handler instance.
+ ///
+ ~FileLogHandler()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Disposes the file log handler and releases any resources used.
+ ///
+ /// A flag indicating whether to dispose managed resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ lock (_lockObj)
+ {
+ if (!_isDisposed)
+ {
+ if (disposing)
+ {
+ _streamWriter?.Dispose();
+ _streamWriter = null;
+ _fileStream?.Dispose();
+ _fileStream = null;
+ }
+
+ _isDisposed = true;
+ }
+ }
+ }
+ }
+}
diff --git a/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs b/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs
index 44c3ae67f6377..9c0365e0881e4 100644
--- a/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs
+++ b/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs
@@ -28,11 +28,5 @@ public interface ILogHandler
///
/// The log event to handle.
void Handle(LogEvent logEvent);
-
- ///
- /// Creates a clone of the log handler.
- ///
- /// A clone of the log handler.
- ILogHandler Clone();
}
}
diff --git a/dotnet/src/webdriver/Internal/Logging/LogContext.cs b/dotnet/src/webdriver/Internal/Logging/LogContext.cs
index 6b19059b5cd5a..57713c23edc53 100644
--- a/dotnet/src/webdriver/Internal/Logging/LogContext.cs
+++ b/dotnet/src/webdriver/Internal/Logging/LogContext.cs
@@ -60,19 +60,16 @@ public ILogContext CreateContext(LogEventLevel minimumLevel)
loggers = new ConcurrentDictionary(_loggers.Select(l => new KeyValuePair(l.Key, new Logger(l.Value.Issuer, minimumLevel))));
}
- IList handlers = null;
+ var context = new LogContext(minimumLevel, this, loggers, null);
if (Handlers != null)
{
- handlers = new List(Handlers.Select(h => h.Clone()));
- }
- else
- {
- handlers = new List();
+ foreach (var handler in Handlers)
+ {
+ context.Handlers.Add(handler);
+ }
}
- var context = new LogContext(minimumLevel, this, loggers, Handlers);
-
Log.CurrentContext = context;
return context;
@@ -137,6 +134,19 @@ public ILogContext SetLevel(Type issuer, LogEventLevel level)
public void Dispose()
{
+ // Dispose log handlers associated with this context
+ // if they are hot handled by parent context
+ if (Handlers != null && _parentLogContext != null && _parentLogContext.Handlers != null)
+ {
+ foreach (var logHandler in Handlers)
+ {
+ if (!_parentLogContext.Handlers.Contains(logHandler))
+ {
+ (logHandler as IDisposable)?.Dispose();
+ }
+ }
+ }
+
Log.CurrentContext = _parentLogContext;
}
}
diff --git a/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs b/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs
new file mode 100644
index 0000000000000..072582723b154
--- /dev/null
+++ b/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs
@@ -0,0 +1,43 @@
+using NUnit.Framework;
+using System;
+using System.IO;
+
+namespace OpenQA.Selenium.Internal.Logging
+{
+ public class FileLogHandlerTest
+ {
+ [Test]
+ [TestCase(null)]
+ [TestCase("")]
+ public void ShouldNotAcceptIncorrectPath(string path)
+ {
+ var act = () => new FileLogHandler(path);
+
+ Assert.That(act, Throws.ArgumentException);
+ }
+
+ [Test]
+ public void ShouldHandleLogEvent()
+ {
+ var tempFile = Path.GetTempFileName();
+
+ try
+ {
+ using (var fileLogHandler = new FileLogHandler(tempFile))
+ {
+ fileLogHandler.Handle(new LogEvent(typeof(FileLogHandlerTest), DateTimeOffset.Now, LogEventLevel.Info, "test message"));
+ }
+
+ Assert.That(File.ReadAllText(tempFile), Does.Contain("test message"));
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+ }
+}
From 8e75d5dfc42be1ebfc9ed30d0e3103efeed1f969 Mon Sep 17 00:00:00 2001
From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com>
Date: Thu, 7 Dec 2023 23:36:57 +0300
Subject: [PATCH 2/5] [dotnet] Change a list of downloadable files to
IReadOnlyList (#13265)
---
dotnet/src/webdriver/IHasDownloads.cs | 4 ++--
dotnet/src/webdriver/Remote/RemoteWebDriver.cs | 6 +++---
dotnet/test/common/DownloadsTest.cs | 6 +++---
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/dotnet/src/webdriver/IHasDownloads.cs b/dotnet/src/webdriver/IHasDownloads.cs
index 19a2f9982dfb6..e857621495462 100644
--- a/dotnet/src/webdriver/IHasDownloads.cs
+++ b/dotnet/src/webdriver/IHasDownloads.cs
@@ -28,8 +28,8 @@ public interface IHasDownloads
///
/// Retrieves the downloadable files.
///
- /// A list of file names available for download.
- List GetDownloadableFiles();
+ /// A read-only list of file names available for download.
+ IReadOnlyList GetDownloadableFiles();
///
/// Downloads a file with the specified file name and returns a dictionary containing the downloaded file's data.
diff --git a/dotnet/src/webdriver/Remote/RemoteWebDriver.cs b/dotnet/src/webdriver/Remote/RemoteWebDriver.cs
index cc8ae20bdaca7..35d0b62ebf3cd 100644
--- a/dotnet/src/webdriver/Remote/RemoteWebDriver.cs
+++ b/dotnet/src/webdriver/Remote/RemoteWebDriver.cs
@@ -471,10 +471,10 @@ public DevToolsSession GetDevToolsSession(int protocolVersion)
}
///
- /// Retrieves the downloadable files as a map of file names and their corresponding URLs.
+ /// Retrieves the downloadable files.
///
- /// A list containing file names as keys and URLs as values.
- public List GetDownloadableFiles()
+ /// A read-only list of file names available for download.
+ public IReadOnlyList GetDownloadableFiles()
{
var enableDownloads = this.Capabilities.GetCapability(CapabilityType.EnableDownloads);
if (enableDownloads == null || !(bool) enableDownloads) {
diff --git a/dotnet/test/common/DownloadsTest.cs b/dotnet/test/common/DownloadsTest.cs
index 5acecdd7d434e..2597e0a65e3fe 100644
--- a/dotnet/test/common/DownloadsTest.cs
+++ b/dotnet/test/common/DownloadsTest.cs
@@ -41,7 +41,7 @@ public void CanListDownloadableFiles()
{
DownloadWithBrowser();
- List names = ((RemoteWebDriver) driver).GetDownloadableFiles();
+ IReadOnlyList names = ((RemoteWebDriver) driver).GetDownloadableFiles();
Assert.That(names, Contains.Item("file_1.txt"));
Assert.That(names, Contains.Item("file_2.jpg"));
}
@@ -52,7 +52,7 @@ public void CanDownloadFile()
{
DownloadWithBrowser();
- List names = ((RemoteWebDriver) driver).GetDownloadableFiles();
+ IReadOnlyList names = ((RemoteWebDriver) driver).GetDownloadableFiles();
string fileName = names[0];
string targetDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
@@ -72,7 +72,7 @@ public void CanDeleteFiles()
((RemoteWebDriver)driver).DeleteDownloadableFiles();
- List names = ((RemoteWebDriver) driver).GetDownloadableFiles();
+ IReadOnlyList names = ((RemoteWebDriver) driver).GetDownloadableFiles();
Assert.IsEmpty(names, "The names list should be empty.");
}
From 7647b5f343330b24bb29092bbbd07cb964014ea3 Mon Sep 17 00:00:00 2001
From: titusfortner
Date: Thu, 7 Dec 2023 15:32:43 -0600
Subject: [PATCH 3/5] [build] create prepare_release task that builds packages
for all languages
---
Rakefile | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/Rakefile b/Rakefile
index 182d064b25478..073a8e1984b7f 100644
--- a/Rakefile
+++ b/Rakefile
@@ -133,7 +133,7 @@ task all: [
:"selenium-java",
'//java/test/org/openqa/selenium/environment:webserver'
]
-task all_zip: [:'java-release-zip']
+task all_zip: [:'java-release-zip', :'dotnet-release-zip']
task tests: [
'//java/test/org/openqa/selenium/htmlunit:htmlunit',
'//java/test/org/openqa/selenium/firefox:test-synthesized',
@@ -403,6 +403,26 @@ def read_m2_user_pass
return [user, pass]
end
+task :prepare_release do
+ RELEASE_TARGETS = [
+ '//java/src/org/openqa/selenium:client-zip',
+ '//java/src/org/openqa/selenium/grid:server-zip',
+ '//java/src/org/openqa/selenium/grid:executable-grid',
+ '//dotnet/src/webdriver:webdriver-pack',
+ '//dotnet/src/webdriver:webdriver-strongnamed-pack',
+ '//dotnet/src/support:support-pack',
+ '//dotnet/src/support:support-strongnamed-pack',
+ '//javascript/node/selenium-webdriver:selenium-webdriver',
+ '//py:selenium-wheel',
+ '//py:selenium-sdist',
+ ]
+
+ RELEASE_TARGETS.each do |target|
+ Bazel::execute('build', ['--config', 'release'], target)
+ end
+ Bazel::execute('build', ['--stamp'], '//rb:selenium-webdriver')
+end
+
task 'publish-maven': JAVA_RELEASE_TARGETS do
creds = read_m2_user_pass
JAVA_RELEASE_TARGETS.each do |p|
From 150625680769af147f0d69e7dc06bf1dd0010280 Mon Sep 17 00:00:00 2001
From: titusfortner
Date: Thu, 7 Dec 2023 15:58:24 -0600
Subject: [PATCH 4/5] [build] create publish-nuget task
---
Rakefile | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/Rakefile b/Rakefile
index 073a8e1984b7f..7fb18c0ed65a7 100644
--- a/Rakefile
+++ b/Rakefile
@@ -430,6 +430,17 @@ task 'publish-maven': JAVA_RELEASE_TARGETS do
end
end
+NUGET_RELEASE_ASSETS = [
+ "./bazel-bin/dotnet/src/webdriver/Selenium.WebDriver.#{dotnet_version}.nupkg",
+ "./bazel-bin/dotnet/src/webdriver/Selenium.Support.#{dotnet_version}.nupkg"
+]
+
+task 'publish-nuget' do
+ NUGET_RELEASE_ASSETS.each do |asset|
+ sh "dotnet nuget push #{asset} --api-key #{ENV[:NUGET_API_KEY]} --source https://api.nuget.org/v3/index.json"
+ end
+end
+
task 'publish-maven-snapshot': JAVA_RELEASE_TARGETS do
creds = read_m2_user_pass
if java_version.end_with?('-SNAPSHOT')
From 6620bce4e8e9da1fee3ec5a5547afa7dece3f80e Mon Sep 17 00:00:00 2001
From: titusfortner
Date: Thu, 7 Dec 2023 16:09:07 -0600
Subject: [PATCH 5/5] [build] create publish-pypi task
---
Rakefile | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/Rakefile b/Rakefile
index 7fb18c0ed65a7..0867c2c1de1cb 100644
--- a/Rakefile
+++ b/Rakefile
@@ -62,6 +62,12 @@ def dotnet_version
end
end
+def python_version
+ File.foreach('py/BUILD.bazel') do |line|
+ return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION')
+ end
+end
+
# The build system used by webdriver is layered on top of rake, and we call it
# "crazy fun" for no readily apparent reason.
@@ -423,6 +429,17 @@ task :prepare_release do
Bazel::execute('build', ['--stamp'], '//rb:selenium-webdriver')
end
+PYPI_ASSETS = [
+ "bazel-bin/py/selenium-#{python_version}-py3-none-any.whl",
+ "bazel-bin/py/selenium-#{python_version}.tar.gz"
+]
+
+task 'publish-pypi' do
+ PYPI_ASSETS.each do |asset|
+ sh "python3 -m twine upload #{asset}"
+ end
+end
+
task 'publish-maven': JAVA_RELEASE_TARGETS do
creds = read_m2_user_pass
JAVA_RELEASE_TARGETS.each do |p|