diff --git a/.skipped-tests b/.skipped-tests index 91a20a0a5b629..61c07d731a010 100644 --- a/.skipped-tests +++ b/.skipped-tests @@ -1,5 +1,6 @@ -//dotnet/test/common:NetworkInterceptionTests-chrome -//dotnet/test/common:NetworkInterceptionTests-edge +-//dotnet/test/firefox:FirefoxDriverTest-firefox -//java/test/org/openqa/selenium/chrome:ChromeDriverFunctionalTest -//java/test/org/openqa/selenium/chrome:ChromeDriverFunctionalTest-remote -//java/test/org/openqa/selenium/edge:EdgeDriverFunctionalTest diff --git a/dotnet/src/webdriver/DriverService.cs b/dotnet/src/webdriver/DriverService.cs index 86477f98c8c46..efa81509956cb 100644 --- a/dotnet/src/webdriver/DriverService.cs +++ b/dotnet/src/webdriver/DriverService.cs @@ -284,7 +284,7 @@ protected virtual void Dispose(bool disposing) /// Raises the event. /// /// A that contains the event data. - protected void OnDriverProcessStarting(DriverProcessStartingEventArgs eventArgs) + protected virtual void OnDriverProcessStarting(DriverProcessStartingEventArgs eventArgs) { if (eventArgs == null) { @@ -298,7 +298,7 @@ protected void OnDriverProcessStarting(DriverProcessStartingEventArgs eventArgs) /// Raises the event. /// /// A that contains the event data. - protected void OnDriverProcessStarted(DriverProcessStartedEventArgs eventArgs) + protected virtual void OnDriverProcessStarted(DriverProcessStartedEventArgs eventArgs) { if (eventArgs == null) { diff --git a/dotnet/src/webdriver/Firefox/FirefoxDriverService.cs b/dotnet/src/webdriver/Firefox/FirefoxDriverService.cs index 223f5b58156a0..37c99b4927618 100644 --- a/dotnet/src/webdriver/Firefox/FirefoxDriverService.cs +++ b/dotnet/src/webdriver/Firefox/FirefoxDriverService.cs @@ -22,6 +22,7 @@ using System.Globalization; using System.IO; using System.Text; +using System.Threading.Tasks; namespace OpenQA.Selenium.Firefox; @@ -32,6 +33,11 @@ public sealed class FirefoxDriverService : DriverService { private const string DefaultFirefoxDriverServiceFileName = "geckodriver"; + /// + /// Process management fields for the log writer. + /// + private StreamWriter? logWriter; + /// /// Initializes a new instance of the class. /// @@ -87,6 +93,16 @@ protected override DriverOptions GetDefaultDriverOptions() /// public bool OpenBrowserToolbox { get; set; } + /// + /// Gets or sets the file path where log output should be written. + /// + /// + /// A or value indicates no log file to specify. + /// This approach takes the process output and redirects it to a file because GeckoDriver does not + /// offer a way to specify a log file path directly. + /// + public string? LogPath { get; set; } + /// /// Gets or sets the level at which log output is displayed. /// @@ -177,6 +193,75 @@ protected override string CommandLineArguments } } + /// + /// Handles the event when the driver service process is starting. + /// + /// The event arguments containing information about the driver service process. + /// + /// This method initializes a log writer if a log path is specified and redirects output streams to capture logs. + /// + protected override void OnDriverProcessStarting(DriverProcessStartingEventArgs eventArgs) + { + if (!string.IsNullOrEmpty(this.LogPath)) + { + string? directory = Path.GetDirectoryName(this.LogPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // Initialize the log writer + logWriter = new StreamWriter(this.LogPath, append: true) { AutoFlush = true }; + + // Configure process to redirect output + eventArgs.DriverServiceProcessStartInfo.RedirectStandardOutput = true; + eventArgs.DriverServiceProcessStartInfo.RedirectStandardError = true; + } + + base.OnDriverProcessStarting(eventArgs); + } + + /// + /// Handles the event when the driver process has started. + /// + /// The event arguments containing information about the started driver process. + /// + /// This method reads the output and error streams asynchronously and writes them to the log file if available. + /// + protected override void OnDriverProcessStarted(DriverProcessStartedEventArgs eventArgs) + { + if (logWriter == null) return; + if (eventArgs.StandardOutputStreamReader != null) + { + _ = Task.Run(() => ReadStreamAsync(eventArgs.StandardOutputStreamReader)); + } + + if (eventArgs.StandardErrorStreamReader != null) + { + _ = Task.Run(() => ReadStreamAsync(eventArgs.StandardErrorStreamReader)); + } + + base.OnDriverProcessStarted(eventArgs); + } + + /// + /// Disposes of the resources used by the instance. + /// + /// A value indicating whether the method is being called from Dispose. + /// + /// If disposing is true, it disposes of the log writer if it exists. + /// + protected override void Dispose(bool disposing) + { + if (logWriter != null && disposing) + { + logWriter.Dispose(); + logWriter = null; + } + + base.Dispose(disposing); + } + /// /// Creates a default instance of the FirefoxDriverService. /// @@ -258,4 +343,24 @@ private static string FirefoxDriverServiceFileName() return fileName; } + + private async Task ReadStreamAsync(StreamReader reader) + { + try + { + string? line; + while ((line = await reader.ReadLineAsync()) != null) + { + if (logWriter != null) + { + logWriter.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {line}"); + } + } + } + catch (Exception ex) + { + // Log or handle the exception appropriately + System.Diagnostics.Debug.WriteLine($"Error reading stream: {ex.Message}"); + } + } } diff --git a/dotnet/test/firefox/BUILD.bazel b/dotnet/test/firefox/BUILD.bazel new file mode 100644 index 0000000000000..398b528554122 --- /dev/null +++ b/dotnet/test/firefox/BUILD.bazel @@ -0,0 +1,27 @@ +load("//dotnet:defs.bzl", "dotnet_nunit_test_suite", "framework") + +dotnet_nunit_test_suite( + name = "LargeTests", + size = "large", + srcs = glob( + [ + "**/*Test.cs", + "**/*Tests.cs", + ], + ) + [ + "//dotnet/test/common:assembly-fixtures", + ], + browsers = [ + "firefox", + ], + data = [ + "//dotnet/test/common:test-data", + ], + target_frameworks = ["net8.0"], + deps = [ + "//dotnet/src/support", + "//dotnet/src/webdriver:webdriver-net8.0", + "//dotnet/test/common:fixtures", + framework("nuget", "NUnit"), + ], +) diff --git a/dotnet/test/firefox/FirefoxDriverServiceTest.cs b/dotnet/test/firefox/FirefoxDriverServiceTest.cs new file mode 100644 index 0000000000000..1b617316e65a3 --- /dev/null +++ b/dotnet/test/firefox/FirefoxDriverServiceTest.cs @@ -0,0 +1,53 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using NUnit.Framework; +using System.IO; + +namespace OpenQA.Selenium.Firefox; + +[TestFixture] +public class FirefoxDriverServiceTest : DriverTestFixture +{ + [Test] + public void ShouldRedirectGeckoDriverLogsToFile() + { + FirefoxOptions options = new FirefoxOptions(); + string logPath = Path.GetTempFileName(); + options.LogLevel = FirefoxDriverLogLevel.Trace; + + FirefoxDriverService service = FirefoxDriverService.CreateDefaultService(); + service.LogPath = logPath; + + IWebDriver driver2 = new FirefoxDriver(service, options); + + try + { + Assert.That(File.Exists(logPath), Is.True); + string logContent = File.ReadAllText(logPath); + Assert.That(logContent, Does.Contain("geckodriver")); + } + finally + { + driver2.Quit(); + File.Delete(logPath); + } + } + +}