From e6b0a7757b11c8fc409997284e88f5433b40c763 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 18:55:54 +0000
Subject: [PATCH 1/8] Initial plan
From 5af7142c1527ddff3f74f1776ddc712e1f737e34 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 19:01:38 +0000
Subject: [PATCH 2/8] Add SimulatedTiVo device implementation with interfaces
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
---
.../Host/ITestEnvironment.cs | 29 ++++
.../Host/TestEnvironment.cs | 86 ++++++++++
.../SimulatedTiVo/ITestDevice.cs | 29 ++++
.../SimulatedTiVo/ITestDeviceBuilder.cs | 20 +++
.../SimulatedTiVo/RecordedMessage.cs | 22 +++
.../SimulatedTiVo/SimulatedTiVoDevice.cs | 148 ++++++++++++++++++
.../SimulatedTiVoDeviceBuilder.cs | 40 +++++
7 files changed, 374 insertions(+)
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ITestEnvironment.cs
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDevice.cs
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDeviceBuilder.cs
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/RecordedMessage.cs
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ITestEnvironment.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ITestEnvironment.cs
new file mode 100644
index 0000000..d1446ea
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ITestEnvironment.cs
@@ -0,0 +1,29 @@
+namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+
+///
+/// Manages simulated test devices for a test run.
+///
+public interface ITestEnvironment : IDisposable
+{
+ ///
+ /// Registers a device builder with the given name.
+ ///
+ /// The unique name for the device.
+ /// The device builder to register.
+ void RegisterDevice(string name, ITestDeviceBuilder builder);
+
+ ///
+ /// Starts a registered device and returns the running device instance.
+ ///
+ /// The name of the device to start.
+ /// The running test device.
+ ITestDevice StartDevice(string name);
+
+ ///
+ /// Attempts to retrieve a running device by name.
+ ///
+ /// The name of the device to retrieve.
+ /// The running device, if found.
+ /// True if the device was found; otherwise, false.
+ bool TryGetDevice(string name, out ITestDevice? device);
+}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs
new file mode 100644
index 0000000..b72a1d5
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs
@@ -0,0 +1,86 @@
+using AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+
+namespace AdaptiveRemote.EndtoEndTests.Host;
+
+///
+/// Default implementation of .
+///
+public sealed class TestEnvironment : ITestEnvironment
+{
+ private readonly Dictionary _builders = new();
+ private readonly Dictionary _devices = new();
+ private bool _disposed;
+
+ ///
+ public void RegisterDevice(string name, ITestDeviceBuilder builder)
+ {
+ if (_builders.ContainsKey(name))
+ {
+ throw new InvalidOperationException($"Device with name '{name}' is already registered.");
+ }
+
+ _builders[name] = builder;
+ }
+
+ ///
+ public ITestDevice StartDevice(string name)
+ {
+ if (!_builders.TryGetValue(name, out ITestDeviceBuilder? builder))
+ {
+ throw new InvalidOperationException($"No device builder registered with name '{name}'.");
+ }
+
+ if (_devices.ContainsKey(name))
+ {
+ throw new InvalidOperationException($"Device with name '{name}' is already started.");
+ }
+
+ ITestDevice device = builder.Start();
+ _devices[name] = device;
+ return device;
+ }
+
+ ///
+ public bool TryGetDevice(string name, out ITestDevice? device)
+ {
+ return _devices.TryGetValue(name, out device);
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ foreach (ITestDevice device in _devices.Values)
+ {
+ try
+ {
+ device.Dispose();
+ }
+ catch
+ {
+ // Ignore disposal errors
+ }
+ }
+
+ _devices.Clear();
+
+ foreach (ITestDeviceBuilder builder in _builders.Values)
+ {
+ try
+ {
+ builder.Dispose();
+ }
+ catch
+ {
+ // Ignore disposal errors
+ }
+ }
+
+ _builders.Clear();
+ _disposed = true;
+ }
+}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDevice.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDevice.cs
new file mode 100644
index 0000000..f77dab7
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDevice.cs
@@ -0,0 +1,29 @@
+namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+
+///
+/// Represents a running simulated test device.
+/// All methods are synchronous for ease of use in test scenarios.
+///
+public interface ITestDevice : IDisposable
+{
+ ///
+ /// Stops the device and releases resources. Safe to call multiple times.
+ ///
+ void Stop();
+
+ ///
+ /// Gets the TCP port the device is listening on. Valid while device is running.
+ ///
+ int Port { get; }
+
+ ///
+ /// Returns a copy of messages recorded by the device.
+ ///
+ /// A read-only list of recorded messages.
+ IReadOnlyList GetRecordedMessages();
+
+ ///
+ /// Clears all recorded messages to prepare for the next test.
+ ///
+ void ClearRecordedMessages();
+}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDeviceBuilder.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDeviceBuilder.cs
new file mode 100644
index 0000000..983b384
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDeviceBuilder.cs
@@ -0,0 +1,20 @@
+namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+
+///
+/// Builder pattern for configuring and starting a simulated test device.
+///
+public interface ITestDeviceBuilder : IDisposable
+{
+ ///
+ /// Configures the TCP port for the device. Use 0 for an ephemeral port.
+ ///
+ /// The port number to use.
+ /// This builder instance for fluent configuration.
+ ITestDeviceBuilder WithPort(int port);
+
+ ///
+ /// Starts the device synchronously and returns the running device.
+ ///
+ /// A running test device instance.
+ ITestDevice Start();
+}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/RecordedMessage.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/RecordedMessage.cs
new file mode 100644
index 0000000..64f7a28
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/RecordedMessage.cs
@@ -0,0 +1,22 @@
+namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+
+///
+/// Represents a message recorded by a simulated test device.
+///
+public sealed record RecordedMessage
+{
+ ///
+ /// Gets the timestamp when the message was recorded.
+ ///
+ public DateTimeOffset Timestamp { get; init; }
+
+ ///
+ /// Gets the raw ASCII payload of the message.
+ ///
+ public string Payload { get; init; } = string.Empty;
+
+ ///
+ /// Gets a value indicating whether this message was received from the application (true) or sent to it (false).
+ ///
+ public bool Incoming { get; init; }
+}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
new file mode 100644
index 0000000..d22af7b
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
@@ -0,0 +1,148 @@
+using System.Collections.Concurrent;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using Microsoft.Extensions.Logging;
+
+namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+
+///
+/// Simulates a TiVo device for E2E testing.
+/// Accepts TCP connections and records messages according to the TiVo protocol.
+///
+public sealed class SimulatedTiVoDevice : ITestDevice
+{
+ private readonly ILogger _logger;
+ private readonly TcpListener _listener;
+ private readonly CancellationTokenSource _cancellationTokenSource = new();
+ private readonly ConcurrentBag _recordedMessages = new();
+ private readonly Task _listenerTask;
+ private bool _disposed;
+
+ internal SimulatedTiVoDevice(int port, ILogger logger)
+ {
+ _logger = logger;
+ _listener = new TcpListener(IPAddress.Loopback, port);
+ _listener.Start();
+ Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
+
+ _logger.LogInformation("SimulatedTiVoDevice started on port {Port}", Port);
+
+ _listenerTask = Task.Run(() => AcceptConnectionsAsync(_cancellationTokenSource.Token));
+ }
+
+ ///
+ public int Port { get; }
+
+ ///
+ public IReadOnlyList GetRecordedMessages()
+ {
+ return _recordedMessages.ToList();
+ }
+
+ ///
+ public void ClearRecordedMessages()
+ {
+ _recordedMessages.Clear();
+ _logger.LogInformation("Cleared recorded messages");
+ }
+
+ ///
+ public void Stop()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _logger.LogInformation("Stopping SimulatedTiVoDevice on port {Port}", Port);
+
+ _cancellationTokenSource.Cancel();
+ _listener.Stop();
+
+ try
+ {
+ WaitHelpers.WaitForAsyncTask(_ => _listenerTask, TimeSpan.FromSeconds(5));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error waiting for listener task to complete");
+ }
+
+ _disposed = true;
+ }
+
+ ///
+ public void Dispose()
+ {
+ Stop();
+ _cancellationTokenSource.Dispose();
+ }
+
+ private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ TcpClient client = await _listener.AcceptTcpClientAsync(cancellationToken);
+ _logger.LogInformation("Accepted connection from {RemoteEndPoint}", client.Client.RemoteEndPoint);
+
+ // Handle client in background
+ _ = Task.Run(() => HandleClientAsync(client, cancellationToken), cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error accepting connection");
+ break;
+ }
+ }
+ }
+
+ private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
+ {
+ try
+ {
+ using (client)
+ using (NetworkStream stream = client.GetStream())
+ using (StreamReader reader = new(stream, Encoding.ASCII))
+ {
+ while (!cancellationToken.IsCancellationRequested && client.Connected)
+ {
+ string? line = await reader.ReadLineAsync(cancellationToken);
+
+ if (line is null)
+ {
+ break;
+ }
+
+ RecordedMessage message = new RecordedMessage
+ {
+ Timestamp = DateTimeOffset.UtcNow,
+ Payload = line,
+ Incoming = true
+ };
+
+ _recordedMessages.Add(message);
+ _logger.LogInformation("Received message: {Payload}", line);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected on shutdown
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error handling client connection");
+ }
+ finally
+ {
+ _logger.LogInformation("Client disconnected");
+ }
+ }
+}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs
new file mode 100644
index 0000000..17f8172
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs
@@ -0,0 +1,40 @@
+using Microsoft.Extensions.Logging;
+
+namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+
+///
+/// Builder for creating and starting a SimulatedTiVoDevice.
+///
+public sealed class SimulatedTiVoDeviceBuilder : ITestDeviceBuilder
+{
+ private readonly ILogger _logger;
+ private int _port = 0; // Default to ephemeral port
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Logger for the simulated device.
+ public SimulatedTiVoDeviceBuilder(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ public ITestDeviceBuilder WithPort(int port)
+ {
+ _port = port;
+ return this;
+ }
+
+ ///
+ public ITestDevice Start()
+ {
+ return new SimulatedTiVoDevice(_port, _logger);
+ }
+
+ ///
+ public void Dispose()
+ {
+ // No resources to dispose in builder
+ }
+}
From bfada98156f1a8dcdbfb67d68e0c8eaabfddeac1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 19:05:13 +0000
Subject: [PATCH 3/8] Add test steps and host integration for simulated TiVo
device
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
---
.../HostSteps.cs | 24 +++++-
.../StepsBase.cs | 20 ++++-
.../TiVoSteps.cs | 81 +++++++++++++++++++
.../Logging/TestContextLoggerExtensions.cs | 10 +++
4 files changed, 133 insertions(+), 2 deletions(-)
create mode 100644 test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
index 79cc181..8adc509 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
@@ -1,6 +1,7 @@
using AdaptiveRemote.EndtoEndTests;
using AdaptiveRemote.EndtoEndTests.Host;
using AdaptiveRemote.EndtoEndTests.Logging;
+using AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Reqnroll;
@@ -13,6 +14,13 @@ public class HostSteps : StepsBase
private string LogFilePath => Path.Combine(TestContext.TestResultsDirectory!, TestContext.TestName + ".log");
[BeforeScenario]
+ public void OnBeforeScenario_SetUpTestEnvironment()
+ {
+ ITestEnvironment testEnvironment = new TestEnvironment();
+ ProvideContainerObject(testEnvironment);
+ }
+
+ [BeforeScenario(Order = 200)]
public void OnBeforeScenario_SetUpHostFactory(AdaptiveRemoteHostSettings hostSettings)
{
if (!File.Exists(hostSettings.ExePath))
@@ -25,7 +33,17 @@ public void OnBeforeScenario_SetUpHostFactory(AdaptiveRemoteHostSettings hostSet
Assert.Inconclusive($"Working directory not found: {hostSettings.WorkingDirectory}");
}
- hostSettings = hostSettings.AddCommandLineArgs($"--tivo:Fake=True --broadlink:Fake=True --log:FilePath=\"{LogFilePath}\"");
+ // Check if we have a simulated TiVo device running
+ ITestEnvironment testEnvironment = GetContainerObject();
+ string tivoArgs = "--tivo:Fake=True";
+
+ if (testEnvironment.TryGetDevice("TiVo", out ITestDevice? tivoDevice) && tivoDevice != null)
+ {
+ // Use the simulated device instead of fake
+ tivoArgs = $"--tivo:IP=127.0.0.1:{tivoDevice.Port}";
+ }
+
+ hostSettings = hostSettings.AddCommandLineArgs($"{tivoArgs} --broadlink:Fake=True --log:FilePath=\"{LogFilePath}\"");
ProvideContainerObjectFactory(() => AdaptiveRemoteHost.CreateBuilder(hostSettings)
.ConfigureLogging(builder =>
@@ -89,5 +107,9 @@ public void OnAfterScenario_AttachLogsToTestContextAndStopHost()
{
Host.Stop();
}
+
+ // Clean up test environment
+ ITestEnvironment? testEnvironment = GetContainerObjectOrDefault();
+ testEnvironment?.Dispose();
}
}
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs b/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
index e6e7cf1..85a66ed 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
@@ -22,13 +22,31 @@ public abstract class StepsBase : IContainerDependentObject
public ILogger Logger => _logger ??= Host.CreateLogger(GetType().Name);
- private ObjectType GetContainerObject()
+ protected ObjectType GetContainerObject()
where ObjectType : notnull
{
Assert.IsNotNull(_container, "Attempting to access container object before IContainerDependentObject.SetObjectContainer has been called");
return _container.Resolve();
}
+ protected ObjectType? GetContainerObjectOrDefault()
+ where ObjectType : class
+ {
+ if (_container == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return _container.Resolve();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
protected void ProvideContainerObject(ObjectType instance)
where ObjectType : class
{
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
new file mode 100644
index 0000000..ab6227b
--- /dev/null
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
@@ -0,0 +1,81 @@
+using AdaptiveRemote.EndtoEndTests;
+using AdaptiveRemote.EndtoEndTests.Logging;
+using AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Reqnroll;
+
+namespace AdaptiveRemote.EndToEndTests.Steps;
+
+[Binding]
+public class TiVoSteps : StepsBase
+{
+ private const string TiVoDeviceName = "TiVo";
+
+ [BeforeScenario("@tivo", Order = 100)]
+ public void OnBeforeScenario_StartSimulatedTiVoDevice()
+ {
+ ITestEnvironment testEnvironment = GetContainerObject();
+ Microsoft.Extensions.Logging.ILogger logger = TestContext.GetLogger("SimulatedTiVoDevice");
+
+ ITestDeviceBuilder builder = new SimulatedTiVoDeviceBuilder(logger);
+ testEnvironment.RegisterDevice(TiVoDeviceName, builder);
+
+ ITestDevice device = testEnvironment.StartDevice(TiVoDeviceName);
+
+ TestContext.WriteLine($"Simulated TiVo device started on port {device.Port}");
+ }
+
+ [Given(@"there is a simulated TiVo device")]
+ public void GivenThereIsASimulatedTiVoDevice()
+ {
+ ITestEnvironment testEnvironment = GetContainerObject();
+
+ if (!testEnvironment.TryGetDevice(TiVoDeviceName, out ITestDevice? _))
+ {
+ Assert.Fail("Simulated TiVo device is not running. Ensure the test scenario is tagged with @tivo.");
+ }
+
+ TestContext.WriteLine("Simulated TiVo device is running");
+ }
+
+ [Given(@"the application is in the Ready state")]
+ public void GivenTheApplicationIsInTheReadyState()
+ {
+ if (!IsHostRunning)
+ {
+ Assert.Fail("Cannot check application state. The application is not started.");
+ }
+
+ // Wait for the application to be in Ready state
+ Host.Application.WaitForPhase(LifecyclePhase.Ready, timeout: TimeSpan.FromSeconds(60));
+ }
+
+ [Then(@"I should see the TiVo receives a {string} message")]
+ public void ThenIShouldSeeTheTiVoReceivesAMessage(string expectedCommand)
+ {
+ ITestEnvironment testEnvironment = GetContainerObject();
+
+ if (!testEnvironment.TryGetDevice(TiVoDeviceName, out ITestDevice? device))
+ {
+ Assert.Fail("TiVo device is not running");
+ }
+
+ // Poll for the message with a timeout of 3 seconds
+ bool found = WaitHelpers.ExecuteWithRetries(
+ () =>
+ {
+ IReadOnlyList messages = device!.GetRecordedMessages();
+ return messages.Any(m => m.Incoming && m.Payload.Contains(expectedCommand, StringComparison.OrdinalIgnoreCase));
+ },
+ timeoutInSeconds: 3);
+
+ if (!found)
+ {
+ IReadOnlyList messages = device!.GetRecordedMessages();
+ string recordedMessages = string.Join(", ", messages.Select(m => $"[{m.Timestamp:HH:mm:ss.fff}] {(m.Incoming ? "←" : "→")} {m.Payload}"));
+ Assert.Fail($"Expected TiVo to receive a message containing '{expectedCommand}', but it was not found. Recorded messages: {recordedMessages}");
+ }
+
+ TestContext.WriteLine($"Successfully verified TiVo received message containing: {expectedCommand}");
+ }
+}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs
index 7a73b92..0bf10e9 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs
@@ -7,4 +7,14 @@ public static class TestContextLoggerExtensions
{
public static ILoggingBuilder AddTestContext(this ILoggingBuilder builder, TestContext testContext)
=> builder.AddProvider(new TestContextLoggerProvider(testContext));
+
+ public static ILogger GetLogger(this TestContext testContext, string categoryName)
+ {
+ ILoggerFactory factory = LoggerFactory.Create(builder =>
+ {
+ builder.AddTestContext(testContext);
+ });
+
+ return factory.CreateLogger(categoryName);
+ }
}
From 85f83ca0c30d626bc11a5611eb47e726085fd93b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 19:11:21 +0000
Subject: [PATCH 4/8] Fix SimulatedTiVoDevice to handle \r line terminator and
add TiVo E2E test
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
---
.../TiVoDevice.feature | 17 +++++++++++++
.../HostSteps.cs | 2 +-
.../TiVoSteps.cs | 4 ++--
.../SimulatedTiVo/SimulatedTiVoDevice.cs | 24 ++++++++++++++++++-
4 files changed, 43 insertions(+), 4 deletions(-)
create mode 100644 test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature
diff --git a/test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature b/test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature
new file mode 100644
index 0000000..4e09f94
--- /dev/null
+++ b/test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature
@@ -0,0 +1,17 @@
+@tivo
+Feature: TiVo Device Integration
+ As a user
+ I want the application to communicate with TiVo devices
+ So that I can control my TiVo using the adaptive remote
+
+Scenario: TiVo receives Play command
+ Given there is a simulated TiVo device
+ And the application is not running
+ When I start the application
+ Then I should see the application in the Ready phase
+ And I should not see any warning or error messages in the logs
+ When I click on the 'Play' button
+ Then I should see the TiVo receives a "PLAY" message
+ When I click on the 'Exit' button
+ And I wait for the application to shut down
+ Then I should not see any warning or error messages in the logs
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
index 8adc509..af5c607 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
@@ -13,7 +13,7 @@ public class HostSteps : StepsBase
{
private string LogFilePath => Path.Combine(TestContext.TestResultsDirectory!, TestContext.TestName + ".log");
- [BeforeScenario]
+ [BeforeScenario(Order = 50)]
public void OnBeforeScenario_SetUpTestEnvironment()
{
ITestEnvironment testEnvironment = new TestEnvironment();
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
index ab6227b..47ba8c1 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
@@ -60,14 +60,14 @@ public void ThenIShouldSeeTheTiVoReceivesAMessage(string expectedCommand)
Assert.Fail("TiVo device is not running");
}
- // Poll for the message with a timeout of 3 seconds
+ // Poll for the message with a timeout of 5 seconds
bool found = WaitHelpers.ExecuteWithRetries(
() =>
{
IReadOnlyList messages = device!.GetRecordedMessages();
return messages.Any(m => m.Incoming && m.Payload.Contains(expectedCommand, StringComparison.OrdinalIgnoreCase));
},
- timeoutInSeconds: 3);
+ timeoutInSeconds: 5);
if (!found)
{
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
index d22af7b..6c45f37 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
@@ -113,7 +113,7 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken cancell
{
while (!cancellationToken.IsCancellationRequested && client.Connected)
{
- string? line = await reader.ReadLineAsync(cancellationToken);
+ string? line = await ReadLineAsync(reader, cancellationToken);
if (line is null)
{
@@ -145,4 +145,26 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken cancell
_logger.LogInformation("Client disconnected");
}
}
+
+ private static async Task ReadLineAsync(StreamReader reader, CancellationToken cancellationToken)
+ {
+ StringBuilder line = new();
+ int value;
+
+ while ((value = await Task.Run(reader.Read, cancellationToken)) >= 0)
+ {
+ char c = (char)value;
+
+ if (c == '\r' || c == '\n')
+ {
+ // End of line reached
+ return line.ToString();
+ }
+
+ line.Append(c);
+ }
+
+ // End of stream reached
+ return line.Length > 0 ? line.ToString() : null;
+ }
}
From 468f7a2ee8896f4a5eeec564ead76eddc48ba2f0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 19:12:07 +0000
Subject: [PATCH 5/8] Replace spec with documentation for SimulatedTiVoDevice
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
---
.../_doc_SimulatedTiVoDevice.md | 216 ++++++++++++++++++
.../_spec_SimulatedTiVoDevice.md | 126 ----------
2 files changed, 216 insertions(+), 126 deletions(-)
create mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/_doc_SimulatedTiVoDevice.md
delete mode 100644 test/AdaptiveRemote.EndtoEndTests.TestServices/_spec_SimulatedTiVoDevice.md
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/_doc_SimulatedTiVoDevice.md b/test/AdaptiveRemote.EndtoEndTests.TestServices/_doc_SimulatedTiVoDevice.md
new file mode 100644
index 0000000..3903b4b
--- /dev/null
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/_doc_SimulatedTiVoDevice.md
@@ -0,0 +1,216 @@
+# Simulated TiVo Device for Testing
+
+Issue: ADR-120 — Simulated TiVo device for testing
+
+## Overview
+
+The simulated TiVo device provides a locally runnable test double that implements the TiVo TCP-based protocol, enabling end-to-end tests to validate TiVo device interactions without requiring physical hardware.
+
+## Architecture
+
+### Components
+
+#### SimulatedTiVoDevice
+The core TCP server that listens for connections and records incoming messages.
+
+**Location:** `test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs`
+
+**Key Features:**
+- Binds to loopback (127.0.0.1) by default for security
+- Supports ephemeral ports (port 0) for parallel test execution
+- Records all incoming messages with timestamps
+- Implements the TiVo line-based ASCII protocol (messages terminated with `\r`)
+- Thread-safe message recording using `ConcurrentBag`
+- Async TCP connection handling
+- Clean shutdown support
+
+#### TestEnvironment
+Manages the lifecycle of simulated devices during test runs.
+
+**Location:** `test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs`
+
+**Key Features:**
+- Device registration and lifecycle management
+- Name-based device lookup
+- Automatic cleanup on disposal
+- Builder pattern support
+
+#### Test Steps
+Integration with Reqnroll test framework via step definitions.
+
+**Location:** `test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs`
+
+**Key Features:**
+- `@tivo` tag support for scenarios requiring simulated device
+- Automatic device startup before host initialization
+- Message verification with configurable timeout
+- Clear error messages with recorded message history
+
+### Interfaces
+
+```csharp
+// Base recorded message structure
+public sealed record RecordedMessage
+{
+ public DateTimeOffset Timestamp { get; init; }
+ public string Payload { get; init; }
+ public bool Incoming { get; init; }
+}
+
+// Running device interface
+public interface ITestDevice : IDisposable
+{
+ void Stop();
+ int Port { get; }
+ IReadOnlyList GetRecordedMessages();
+ void ClearRecordedMessages();
+}
+
+// Device builder interface
+public interface ITestDeviceBuilder : IDisposable
+{
+ ITestDeviceBuilder WithPort(int port);
+ ITestDevice Start();
+}
+
+// Test environment interface
+public interface ITestEnvironment : IDisposable
+{
+ void RegisterDevice(string name, ITestDeviceBuilder builder);
+ ITestDevice StartDevice(string name);
+ bool TryGetDevice(string name, out ITestDevice? device);
+}
+```
+
+## Protocol Implementation
+
+### TiVo TCP Protocol
+- **Port:** Configurable (default 31339 for real devices; ephemeral for tests)
+- **Transport:** TCP over loopback
+- **Message Format:** Line-based ASCII with `\r` (carriage return) terminator
+- **Command Format:** `IRCODE {command}\r` (e.g., `IRCODE PLAY\r`)
+
+### Key Implementation Details
+
+**Line Terminator Handling:**
+The TiVo protocol uses `\r` (carriage return) as the line terminator, not the standard `\n` (line feed) or `\r\n` (carriage return + line feed). This required a custom `ReadLineAsync` implementation to properly detect end-of-line.
+
+**Message Recording:**
+All incoming messages are recorded immediately upon receipt with:
+- UTC timestamp
+- Raw payload (without line terminator)
+- Direction flag (incoming vs. outgoing)
+
+## Usage
+
+### Basic Test Scenario
+
+```gherkin
+@tivo
+Feature: TiVo Device Integration
+ Scenario: TiVo receives Play command
+ Given there is a simulated TiVo device
+ And the application is not running
+ When I start the application
+ Then I should see the application in the Ready phase
+ When I click on the 'Play' button
+ Then I should see the TiVo receives a "PLAY" message
+```
+
+### Test Step Definitions
+
+```csharp
+[Given(@"there is a simulated TiVo device")]
+public void GivenThereIsASimulatedTiVoDevice()
+{
+ // Device is automatically started via BeforeScenario hook
+ // This step just verifies it's running
+}
+
+[Then(@"I should see the TiVo receives a {string} message")]
+public void ThenIShouldSeeTheTiVoReceivesAMessage(string expectedCommand)
+{
+ // Polls for message with 5-second timeout
+ // Returns detailed error with all recorded messages on failure
+}
+```
+
+### Configuration
+
+The simulated device is automatically configured when a test scenario is tagged with `@tivo`:
+
+1. **BeforeScenario (Order=50):** TestEnvironment is created
+2. **BeforeScenario (Order=100, @tivo):** Simulated TiVo device is started on ephemeral port
+3. **BeforeScenario (Order=200):** Host is started with `--tivo:IP=127.0.0.1:{port}` argument
+4. **AfterScenario:** Cleanup of devices and host
+
+## Testing
+
+### Running E2E Tests
+
+```bash
+# Build headless host for Linux
+dotnet build src/AdaptiveRemote.Headless/AdaptiveRemote.Headless.csproj -r linux-x64
+
+# Install Playwright browsers (one-time)
+pwsh src/AdaptiveRemote.Headless/bin/Debug/net8.0/playwright.ps1 install chromium
+
+# Run all headless E2E tests
+dotnet test test/AdaptiveRemote.EndToEndTests.HeadlessHost/AdaptiveRemote.EndToEndTests.HeadlessHost.csproj
+```
+
+### Test Results
+
+As of ADR-120 implementation:
+- ✅ Application startup and shutdown without errors
+- ✅ TiVo receives Play command
+
+Both tests pass consistently on Linux (Ubuntu) with Playwright headless browser.
+
+## Design Decisions
+
+### Why In-Process?
+Running the simulated device in the test process (rather than as a separate service) provides:
+- Simpler lifecycle management
+- No inter-process communication overhead
+- Direct access to recorded messages
+- Easier debugging
+
+### Why Loopback Only?
+Binding to loopback (127.0.0.1) by default ensures:
+- No firewall configuration required
+- No security risks from external connections
+- Consistent behavior across environments
+
+### Why Ephemeral Ports?
+Using port 0 (ephemeral) by default enables:
+- Parallel test execution without port conflicts
+- No need for port coordination between tests
+- CI/CD pipeline compatibility
+
+### Why ConcurrentBag for Message Recording?
+`ConcurrentBag` provides:
+- Thread-safe message recording
+- Low contention for add operations
+- Simple API for test verification
+
+## Known Limitations
+
+1. **No Response Simulation:** The current implementation only records incoming messages. It does not send responses back to the client. This is sufficient for testing command transmission but not for testing response handling.
+
+2. **Single Connection:** While the device accepts multiple connections sequentially, it does not handle multiple simultaneous connections. This is not a limitation for current test scenarios.
+
+3. **No Message Replay:** Messages are only recorded, not replayed. Tests must poll for messages within the assertion timeout.
+
+## Future Enhancements
+
+- **Response Simulation:** Add support for scripted responses to enable testing of bidirectional communication
+- **Message Filtering:** Add query methods for filtering recorded messages by timestamp, content, or pattern
+- **Connection Metrics:** Track connection count, duration, and bandwidth for performance testing
+- **Multi-Device Support:** Extend TestEnvironment to support multiple device types (e.g., Broadlink)
+
+## References
+
+- TiVo Protocol: Uses I8Beef.TiVo library implementation as reference
+- Test Framework: Reqnroll (SpecFlow successor) with MSTest
+- Original Specification: `test/AdaptiveRemote.EndtoEndTests.TestServices/_spec_SimulatedTiVoDevice.md` (superseded by this document)
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/_spec_SimulatedTiVoDevice.md b/test/AdaptiveRemote.EndtoEndTests.TestServices/_spec_SimulatedTiVoDevice.md
deleted file mode 100644
index 101b36f..0000000
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/_spec_SimulatedTiVoDevice.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Simulated devices for testing
-
-Issue: ADR-120 — Simulated TiVo device for testing
-
---
-
-**Scope:**
-- Provide a locally runnable simulated TiVo device that speaks the same TCP-based protocol used by the real TiVo device so end-to-end tests can validate messages and behaviors.
-
-
-**Goal:**
-- Replace the current `NullCommandService`-based tests with a deterministic, inspectable test device that records and validates messages from the application under test.
-
-**Background:**
-- Current tests use `NullCommandService` (via the `--tivo:Fake=True` command line argument) which cannot validate real protocol-level interactions. A simulated device will allow higher-fidelity E2E tests for device-specific flows.
-- Related issues: ADR-120 (this doc).
-
-**Assumptions & Constraints:**
- - Runs on Windows and Linux test hosts; should run in-process in the test process.
- - Communication with the application is TCP-based and should accept/emit the same messages as the real device.
- - Record messages via the project's logging pipeline (`ILogger`); CI captures test logs so no custom JSON persistence is required.
- - Accessibility, security, and platform compatibility follow existing test-host constraints from the project.
- - Concrete constraints:
- - Bind TCP listener to loopback only by default.
- - No admin/elevated privileges required to run.
- - Do not write artifacts outside of test-controlled directories.
- - Support Windows and Linux test runners used by CI; avoid native-only dependencies.
-
-**Functional Requirements:**
-1. The simulator exposes a configurable TCP endpoint (port selectable per-test).
-2. It accepts connections and parses incoming messages according to the TiVo protocol used by the app.
-3. It records all inbound and outbound messages with timestamps for later inspection.
-4. Scripted responses are out of scope for the initial implementation and may be added later if needed.
-5. It provides a synchronous in-process .NET control API for tests to query recorded messages.
-6. It runs embedded inside the test process (in-process only). Step/BeforeScenario bindings will be responsible for starting and stopping devices.
-
-**Non‑Functional Requirements:**
-- Deterministic behavior under tests (configurable timeouts and delays).
-- Lightweight; start/stop quickly to keep test run-time low.
-- Secure by default: bind to loopback only unless explicitly configured.
-- Logging must integrate with existing test logs and be easy to attach on CI failures.
-
-**Acceptance Criteria:**
-- A test can start the simulator, perform an application flow, and then assert that a specific message was received within a timeout.
-- Recorded message payloads are accessible to the test and match expected protocol fields.
-- Simulator supports injecting a scripted response and the application under test reacts as if a real device responded.
-
-**Design / Architecture (high-level):**
-- Component: `SimulatedTiVoDevice`
- - TCP listener on configured port
- - Message parser/serializer matching the TiVo protocol (derive behavior from `AdaptiveRemote.App/Services/TiVo/TiVoService.cs`).
- - Recorder store (in-memory) and surface messages via `ILogger` so existing test logs capture them.
- - Control API: in-process .NET API (synchronous) for test orchestration
-- Component: `TestEnvironment`
- - A high-level manager for test device builders and running devices.
- - Implementation will live in the Host folder of `AdaptiveRemote.EndToEndTests.TestServices` so the test host builder can compose devices and the Step/BeforeScenario bindings can start/stop them.
-
-The following is a C# API that follows a DeviceBuilder pattern (preferred). Builders are responsible for construction and starting; started devices expose only synchronous methods for test use. Long-running or async work may be wrapped by `WaitHelpers.WaitForAsyncTask` in the implementation.
-
-```csharp
-// Minimal representation of a recorded message (line-based ASCII payload)
-public sealed record RecordedMessage
-{
- public DateTimeOffset Timestamp { get; init; }
- public string Payload { get; init; } = string.Empty; // raw ASCII line
- public bool Incoming { get; init; } // true if received from the app
-}
-
-// Represents a started, running test device. Methods are synchronous.
-public interface ITestDevice : IDisposable
-{
- // Stop the device and release resources. Safe to call multiple times.
- void Stop();
-
- // The TCP port the device is listening on. Valid while device is running.
- int Port { get; }
-
- // Return a copy of recorded messages observed by the device.
- IReadOnlyList GetRecordedMessages();
-
- // Clear the messages that have been recorded so far, to prepare for the next test
- void ClearRecordedMessages();
-}
-
-// Builder pattern: configure and start a device. Start returns a running ITestDevice.
-public interface ITestDeviceBuilder : IDisposable
-{
- // Optionally configure a preferred port (0 for ephemeral).
- ITestDeviceBuilder WithPort(int port);
-
- // Start the device synchronously and return the running device.
- ITestDevice Start();
-}
-
-// Simple TestEnvironment manager to hold builders/devices for a test run
-public interface ITestEnvironment : IDisposable
-{
- // Register device builder
- void RegisterDevice(string name, ITestDeviceBuilder builder);
-
- // Start a named device and return the running device
- ITestDevice StartDevice(string name);
-
- // Retrieve a running device
- bool TryGetDevice(string name, out ITestDevice? device);
-}
-```
-
-**Implementation Plan (proposed tasks):**
-1. Create `SimulatedTiVoDevice` in a new folder within the TestServices project: `SimulatedTiVo`
-2. Implement a minimal TCP listener and a simple message parser that accepts TiVo command messages (derive behavior from the client code in `AdaptiveRemote.App/Services/TiVo/TiVoService.cs`).
-3. Add recorder and control API (`ITestDevice`) that exposes recorded messages for verification and logs messages via `ILogger`.
-4. Add `TestEnvironment` to the Host folder of `AdaptiveRemote.EndToEndTests.TestServices` to manage simulated devices; Step/BeforeScenario bindings will call into it to start/stop devices.
-5. Add step bindings and test hooks to `AdaptiveRemote.EndToEndTests.Steps` to register/start/stop devices.
-6. Add Feature files to `AdaptiveRemote.EndToEndTests.Features` for new TiVo scenarios (Gherkin steps already proposed in the test suite).
-
-**Proposed Gherkin test steps:**
- Given there is a simulated TiVo device #Ensure the TiVo simulator is running
- And the application is in the Ready state #Ensure the application is started and ready; shut down and restart if it's in an error state
- When I click on the Play button #Step already exists
- Then I should see the TiVo receives a Play message #Check the TiVo simulator for the expected message
-
-**Risks & Mitigations:**
-- Risk: Flaky timing interactions. Mitigation: Expose configurable delays/timeouts and use WaitHelpers.ExecuteWithRetries to poll for assertions. See `IApplicationTestServiceExtensions.WaitForPhase` as an example.
-
-
From 7f9c377500d728ca4dab8673b776d819afc06615 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Jan 2026 19:15:05 +0000
Subject: [PATCH 6/8] Address code review feedback: improve thread safety,
async patterns, and message matching
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
---
.../TiVoSteps.cs | 7 +++--
.../SimulatedTiVo/SimulatedTiVoDevice.cs | 29 ++++++++++++++-----
2 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
index 47ba8c1..37674c4 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
@@ -61,11 +61,12 @@ public void ThenIShouldSeeTheTiVoReceivesAMessage(string expectedCommand)
}
// Poll for the message with a timeout of 5 seconds
+ // Look for the full IRCODE command format for more precise matching
bool found = WaitHelpers.ExecuteWithRetries(
() =>
{
IReadOnlyList messages = device!.GetRecordedMessages();
- return messages.Any(m => m.Incoming && m.Payload.Contains(expectedCommand, StringComparison.OrdinalIgnoreCase));
+ return messages.Any(m => m.Incoming && m.Payload.Equals($"IRCODE {expectedCommand}", StringComparison.OrdinalIgnoreCase));
},
timeoutInSeconds: 5);
@@ -73,9 +74,9 @@ public void ThenIShouldSeeTheTiVoReceivesAMessage(string expectedCommand)
{
IReadOnlyList messages = device!.GetRecordedMessages();
string recordedMessages = string.Join(", ", messages.Select(m => $"[{m.Timestamp:HH:mm:ss.fff}] {(m.Incoming ? "←" : "→")} {m.Payload}"));
- Assert.Fail($"Expected TiVo to receive a message containing '{expectedCommand}', but it was not found. Recorded messages: {recordedMessages}");
+ Assert.Fail($"Expected TiVo to receive message 'IRCODE {expectedCommand}', but it was not found. Recorded messages: {recordedMessages}");
}
- TestContext.WriteLine($"Successfully verified TiVo received message containing: {expectedCommand}");
+ TestContext.WriteLine($"Successfully verified TiVo received message: IRCODE {expectedCommand}");
}
}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
index 6c45f37..89a812b 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
@@ -43,7 +43,12 @@ public IReadOnlyList GetRecordedMessages()
///
public void ClearRecordedMessages()
{
- _recordedMessages.Clear();
+ // Create a new bag instead of clearing the existing one for thread safety
+ while (_recordedMessages.TryTake(out _))
+ {
+ // Remove all items
+ }
+
_logger.LogInformation("Cleared recorded messages");
}
@@ -85,7 +90,8 @@ private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
{
try
{
- TcpClient client = await _listener.AcceptTcpClientAsync(cancellationToken);
+ // Use CancellationToken.None to explicitly indicate we handle cancellation via the while loop condition
+ TcpClient client = await _listener.AcceptTcpClientAsync(CancellationToken.None);
_logger.LogInformation("Accepted connection from {RemoteEndPoint}", client.Client.RemoteEndPoint);
// Handle client in background
@@ -149,11 +155,20 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken cancell
private static async Task ReadLineAsync(StreamReader reader, CancellationToken cancellationToken)
{
StringBuilder line = new();
- int value;
- while ((value = await Task.Run(reader.Read, cancellationToken)) >= 0)
+ while (!cancellationToken.IsCancellationRequested)
{
- char c = (char)value;
+ // Use ReadAsync with a buffer instead of Task.Run with synchronous Read
+ char[] buffer = new char[1];
+ int bytesRead = await reader.ReadAsync(buffer, cancellationToken);
+
+ if (bytesRead == 0)
+ {
+ // End of stream reached
+ return line.Length > 0 ? line.ToString() : null;
+ }
+
+ char c = buffer[0];
if (c == '\r' || c == '\n')
{
@@ -164,7 +179,7 @@ private async Task HandleClientAsync(TcpClient client, CancellationToken cancell
line.Append(c);
}
- // End of stream reached
- return line.Length > 0 ? line.ToString() : null;
+ // Cancelled
+ return null;
}
}
From 66a718dbd1d4f361c56a5a13f22e097e611b1108 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 28 Jan 2026 07:16:00 +0000
Subject: [PATCH 7/8] Rename ITest* to ISimulated*, remove
GetContainerObjectOrDefault, move Ready state step to HostSteps, make
simulated TiVo available for all tests
Co-authored-by: jodavis <6740581+jodavis@users.noreply.github.com>
---
.../TiVoDevice.feature | 4 +-
.../HostSteps.cs | 45 ++++++++++++++-----
.../StepsBase.cs | 22 ++-------
.../TiVoSteps.cs | 44 +-----------------
...nvironment.cs => ISimulatedEnvironment.cs} | 12 ++---
...Environment.cs => SimulatedEnvironment.cs} | 22 ++++-----
.../Logging/TestContextLoggerExtensions.cs | 10 -----
.../{ITestDevice.cs => ISimulatedDevice.cs} | 4 +-
...eBuilder.cs => ISimulatedDeviceBuilder.cs} | 10 ++---
.../SimulatedTiVo/SimulatedTiVoDevice.cs | 2 +-
.../SimulatedTiVoDeviceBuilder.cs | 6 +--
11 files changed, 68 insertions(+), 113 deletions(-)
rename test/AdaptiveRemote.EndtoEndTests.TestServices/Host/{ITestEnvironment.cs => ISimulatedEnvironment.cs} (70%)
rename test/AdaptiveRemote.EndtoEndTests.TestServices/Host/{TestEnvironment.cs => SimulatedEnvironment.cs} (65%)
rename test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/{ITestDevice.cs => ISimulatedDevice.cs} (89%)
rename test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/{ITestDeviceBuilder.cs => ISimulatedDeviceBuilder.cs} (62%)
diff --git a/test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature b/test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature
index 4e09f94..2314215 100644
--- a/test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature
+++ b/test/AdaptiveRemote.EndToEndTests.Features/TiVoDevice.feature
@@ -1,12 +1,10 @@
-@tivo
Feature: TiVo Device Integration
As a user
I want the application to communicate with TiVo devices
So that I can control my TiVo using the adaptive remote
Scenario: TiVo receives Play command
- Given there is a simulated TiVo device
- And the application is not running
+ Given the application is not running
When I start the application
Then I should see the application in the Ready phase
And I should not see any warning or error messages in the logs
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
index af5c607..ceb9d9f 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
@@ -11,13 +11,28 @@ namespace AdaptiveRemote.EndToEndTests.Steps;
[Binding]
public class HostSteps : StepsBase
{
+ private const string TiVoDeviceName = "TiVo";
private string LogFilePath => Path.Combine(TestContext.TestResultsDirectory!, TestContext.TestName + ".log");
[BeforeScenario(Order = 50)]
- public void OnBeforeScenario_SetUpTestEnvironment()
+ public void OnBeforeScenario_SetUpSimulatedEnvironment()
{
- ITestEnvironment testEnvironment = new TestEnvironment();
- ProvideContainerObject(testEnvironment);
+ SimulatedEnvironment simulatedEnvironment = new SimulatedEnvironment();
+ ProvideContainerObject(simulatedEnvironment);
+
+ // Register and start the simulated TiVo device for all tests
+ // Create a simple logger that writes to TestContext
+ ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddProvider(new TestContextLoggerProvider(TestContext));
+ });
+ ILogger logger = loggerFactory.CreateLogger("SimulatedTiVoDevice");
+
+ ISimulatedDeviceBuilder builder = new SimulatedTiVoDeviceBuilder(logger);
+ simulatedEnvironment.RegisterDevice(TiVoDeviceName, builder);
+ ISimulatedDevice device = simulatedEnvironment.StartDevice(TiVoDeviceName);
+
+ TestContext.WriteLine($"Simulated TiVo device started on port {device.Port}");
}
[BeforeScenario(Order = 200)]
@@ -33,13 +48,10 @@ public void OnBeforeScenario_SetUpHostFactory(AdaptiveRemoteHostSettings hostSet
Assert.Inconclusive($"Working directory not found: {hostSettings.WorkingDirectory}");
}
- // Check if we have a simulated TiVo device running
- ITestEnvironment testEnvironment = GetContainerObject();
+ // Use the simulated TiVo device
string tivoArgs = "--tivo:Fake=True";
-
- if (testEnvironment.TryGetDevice("TiVo", out ITestDevice? tivoDevice) && tivoDevice != null)
+ if (SimulatedEnvironment.TryGetDevice(TiVoDeviceName, out ISimulatedDevice? tivoDevice) && tivoDevice != null)
{
- // Use the simulated device instead of fake
tivoArgs = $"--tivo:IP=127.0.0.1:{tivoDevice.Port}";
}
@@ -69,6 +81,18 @@ public void GivenTheApplicationIsNotRunning()
}
}
+ [Given(@"the application is in the Ready state")]
+ public void GivenTheApplicationIsInTheReadyState()
+ {
+ if (!IsHostRunning)
+ {
+ Assert.Fail("Cannot check application state. The application is not started.");
+ }
+
+ // Wait for the application to be in Ready state
+ Host.Application.WaitForPhase(LifecyclePhase.Ready, timeout: TimeSpan.FromSeconds(60));
+ }
+
[When(@"I start the application")]
public void WhenIStartTheApplication()
{
@@ -108,8 +132,7 @@ public void OnAfterScenario_AttachLogsToTestContextAndStopHost()
Host.Stop();
}
- // Clean up test environment
- ITestEnvironment? testEnvironment = GetContainerObjectOrDefault();
- testEnvironment?.Dispose();
+ // Clean up simulated environment
+ SimulatedEnvironment?.Dispose();
}
}
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs b/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
index 85a66ed..d4385ed 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
@@ -1,4 +1,5 @@
using AdaptiveRemote.EndtoEndTests.Host;
+using AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Reqnroll.BoDi;
@@ -10,6 +11,7 @@ public abstract class StepsBase : IContainerDependentObject
{
private IObjectContainer? _container;
private AdaptiveRemoteHost? _host;
+ private ISimulatedEnvironment? _simulatedEnvironment;
private ILogger? _logger;
public void SetObjectContainer(IObjectContainer container) => _container = container;
@@ -20,6 +22,8 @@ public abstract class StepsBase : IContainerDependentObject
public TestContext TestContext => GetContainerObject();
+ public ISimulatedEnvironment SimulatedEnvironment => _simulatedEnvironment ??= GetContainerObject();
+
public ILogger Logger => _logger ??= Host.CreateLogger(GetType().Name);
protected ObjectType GetContainerObject()
@@ -29,24 +33,6 @@ protected ObjectType GetContainerObject()
return _container.Resolve();
}
- protected ObjectType? GetContainerObjectOrDefault()
- where ObjectType : class
- {
- if (_container == null)
- {
- return null;
- }
-
- try
- {
- return _container.Resolve();
- }
- catch
- {
- return null;
- }
- }
-
protected void ProvideContainerObject(ObjectType instance)
where ObjectType : class
{
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
index 37674c4..7104dc4 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
@@ -1,5 +1,4 @@
using AdaptiveRemote.EndtoEndTests;
-using AdaptiveRemote.EndtoEndTests.Logging;
using AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Reqnroll;
@@ -11,51 +10,10 @@ public class TiVoSteps : StepsBase
{
private const string TiVoDeviceName = "TiVo";
- [BeforeScenario("@tivo", Order = 100)]
- public void OnBeforeScenario_StartSimulatedTiVoDevice()
- {
- ITestEnvironment testEnvironment = GetContainerObject();
- Microsoft.Extensions.Logging.ILogger logger = TestContext.GetLogger("SimulatedTiVoDevice");
-
- ITestDeviceBuilder builder = new SimulatedTiVoDeviceBuilder(logger);
- testEnvironment.RegisterDevice(TiVoDeviceName, builder);
-
- ITestDevice device = testEnvironment.StartDevice(TiVoDeviceName);
-
- TestContext.WriteLine($"Simulated TiVo device started on port {device.Port}");
- }
-
- [Given(@"there is a simulated TiVo device")]
- public void GivenThereIsASimulatedTiVoDevice()
- {
- ITestEnvironment testEnvironment = GetContainerObject();
-
- if (!testEnvironment.TryGetDevice(TiVoDeviceName, out ITestDevice? _))
- {
- Assert.Fail("Simulated TiVo device is not running. Ensure the test scenario is tagged with @tivo.");
- }
-
- TestContext.WriteLine("Simulated TiVo device is running");
- }
-
- [Given(@"the application is in the Ready state")]
- public void GivenTheApplicationIsInTheReadyState()
- {
- if (!IsHostRunning)
- {
- Assert.Fail("Cannot check application state. The application is not started.");
- }
-
- // Wait for the application to be in Ready state
- Host.Application.WaitForPhase(LifecyclePhase.Ready, timeout: TimeSpan.FromSeconds(60));
- }
-
[Then(@"I should see the TiVo receives a {string} message")]
public void ThenIShouldSeeTheTiVoReceivesAMessage(string expectedCommand)
{
- ITestEnvironment testEnvironment = GetContainerObject();
-
- if (!testEnvironment.TryGetDevice(TiVoDeviceName, out ITestDevice? device))
+ if (!SimulatedEnvironment.TryGetDevice(TiVoDeviceName, out ISimulatedDevice? device))
{
Assert.Fail("TiVo device is not running");
}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ITestEnvironment.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ISimulatedEnvironment.cs
similarity index 70%
rename from test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ITestEnvironment.cs
rename to test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ISimulatedEnvironment.cs
index d1446ea..e182f10 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ITestEnvironment.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/ISimulatedEnvironment.cs
@@ -1,23 +1,23 @@
namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
///
-/// Manages simulated test devices for a test run.
+/// Manages simulated devices for a test run.
///
-public interface ITestEnvironment : IDisposable
+public interface ISimulatedEnvironment : IDisposable
{
///
/// Registers a device builder with the given name.
///
/// The unique name for the device.
/// The device builder to register.
- void RegisterDevice(string name, ITestDeviceBuilder builder);
+ void RegisterDevice(string name, ISimulatedDeviceBuilder builder);
///
/// Starts a registered device and returns the running device instance.
///
/// The name of the device to start.
- /// The running test device.
- ITestDevice StartDevice(string name);
+ /// The running simulated device.
+ ISimulatedDevice StartDevice(string name);
///
/// Attempts to retrieve a running device by name.
@@ -25,5 +25,5 @@ public interface ITestEnvironment : IDisposable
/// The name of the device to retrieve.
/// The running device, if found.
/// True if the device was found; otherwise, false.
- bool TryGetDevice(string name, out ITestDevice? device);
+ bool TryGetDevice(string name, out ISimulatedDevice? device);
}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/SimulatedEnvironment.cs
similarity index 65%
rename from test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs
rename to test/AdaptiveRemote.EndtoEndTests.TestServices/Host/SimulatedEnvironment.cs
index b72a1d5..674f270 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/TestEnvironment.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/Host/SimulatedEnvironment.cs
@@ -3,16 +3,16 @@
namespace AdaptiveRemote.EndtoEndTests.Host;
///
-/// Default implementation of .
+/// Default implementation of .
///
-public sealed class TestEnvironment : ITestEnvironment
+public sealed class SimulatedEnvironment : ISimulatedEnvironment
{
- private readonly Dictionary _builders = new();
- private readonly Dictionary _devices = new();
+ private readonly Dictionary _builders = new();
+ private readonly Dictionary _devices = new();
private bool _disposed;
///
- public void RegisterDevice(string name, ITestDeviceBuilder builder)
+ public void RegisterDevice(string name, ISimulatedDeviceBuilder builder)
{
if (_builders.ContainsKey(name))
{
@@ -23,9 +23,9 @@ public void RegisterDevice(string name, ITestDeviceBuilder builder)
}
///
- public ITestDevice StartDevice(string name)
+ public ISimulatedDevice StartDevice(string name)
{
- if (!_builders.TryGetValue(name, out ITestDeviceBuilder? builder))
+ if (!_builders.TryGetValue(name, out ISimulatedDeviceBuilder? builder))
{
throw new InvalidOperationException($"No device builder registered with name '{name}'.");
}
@@ -35,13 +35,13 @@ public ITestDevice StartDevice(string name)
throw new InvalidOperationException($"Device with name '{name}' is already started.");
}
- ITestDevice device = builder.Start();
+ ISimulatedDevice device = builder.Start();
_devices[name] = device;
return device;
}
///
- public bool TryGetDevice(string name, out ITestDevice? device)
+ public bool TryGetDevice(string name, out ISimulatedDevice? device)
{
return _devices.TryGetValue(name, out device);
}
@@ -54,7 +54,7 @@ public void Dispose()
return;
}
- foreach (ITestDevice device in _devices.Values)
+ foreach (ISimulatedDevice device in _devices.Values)
{
try
{
@@ -68,7 +68,7 @@ public void Dispose()
_devices.Clear();
- foreach (ITestDeviceBuilder builder in _builders.Values)
+ foreach (ISimulatedDeviceBuilder builder in _builders.Values)
{
try
{
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs
index 0bf10e9..7a73b92 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/Logging/TestContextLoggerExtensions.cs
@@ -7,14 +7,4 @@ public static class TestContextLoggerExtensions
{
public static ILoggingBuilder AddTestContext(this ILoggingBuilder builder, TestContext testContext)
=> builder.AddProvider(new TestContextLoggerProvider(testContext));
-
- public static ILogger GetLogger(this TestContext testContext, string categoryName)
- {
- ILoggerFactory factory = LoggerFactory.Create(builder =>
- {
- builder.AddTestContext(testContext);
- });
-
- return factory.CreateLogger(categoryName);
- }
}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDevice.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ISimulatedDevice.cs
similarity index 89%
rename from test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDevice.cs
rename to test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ISimulatedDevice.cs
index f77dab7..907d3c6 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDevice.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ISimulatedDevice.cs
@@ -1,10 +1,10 @@
namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
///
-/// Represents a running simulated test device.
+/// Represents a running simulated device.
/// All methods are synchronous for ease of use in test scenarios.
///
-public interface ITestDevice : IDisposable
+public interface ISimulatedDevice : IDisposable
{
///
/// Stops the device and releases resources. Safe to call multiple times.
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDeviceBuilder.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ISimulatedDeviceBuilder.cs
similarity index 62%
rename from test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDeviceBuilder.cs
rename to test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ISimulatedDeviceBuilder.cs
index 983b384..8c9c354 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ITestDeviceBuilder.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/ISimulatedDeviceBuilder.cs
@@ -1,20 +1,20 @@
namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
///
-/// Builder pattern for configuring and starting a simulated test device.
+/// Builder pattern for configuring and starting a simulated device.
///
-public interface ITestDeviceBuilder : IDisposable
+public interface ISimulatedDeviceBuilder : IDisposable
{
///
/// Configures the TCP port for the device. Use 0 for an ephemeral port.
///
/// The port number to use.
/// This builder instance for fluent configuration.
- ITestDeviceBuilder WithPort(int port);
+ ISimulatedDeviceBuilder WithPort(int port);
///
/// Starts the device synchronously and returns the running device.
///
- /// A running test device instance.
- ITestDevice Start();
+ /// A running simulated device instance.
+ ISimulatedDevice Start();
}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
index 89a812b..af15b29 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDevice.cs
@@ -10,7 +10,7 @@ namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
/// Simulates a TiVo device for E2E testing.
/// Accepts TCP connections and records messages according to the TiVo protocol.
///
-public sealed class SimulatedTiVoDevice : ITestDevice
+public sealed class SimulatedTiVoDevice : ISimulatedDevice
{
private readonly ILogger _logger;
private readonly TcpListener _listener;
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs
index 17f8172..c435c31 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/SimulatedTiVo/SimulatedTiVoDeviceBuilder.cs
@@ -5,7 +5,7 @@ namespace AdaptiveRemote.EndtoEndTests.SimulatedTiVo;
///
/// Builder for creating and starting a SimulatedTiVoDevice.
///
-public sealed class SimulatedTiVoDeviceBuilder : ITestDeviceBuilder
+public sealed class SimulatedTiVoDeviceBuilder : ISimulatedDeviceBuilder
{
private readonly ILogger _logger;
private int _port = 0; // Default to ephemeral port
@@ -20,14 +20,14 @@ public SimulatedTiVoDeviceBuilder(ILogger logger)
}
///
- public ITestDeviceBuilder WithPort(int port)
+ public ISimulatedDeviceBuilder WithPort(int port)
{
_port = port;
return this;
}
///
- public ITestDevice Start()
+ public ISimulatedDevice Start()
{
return new SimulatedTiVoDevice(_port, _logger);
}
From 61d38c9c32886cd2ad8c9348c1b6f0849c4c3158 Mon Sep 17 00:00:00 2001
From: Joe Davis
Date: Wed, 28 Jan 2026 10:02:57 -0800
Subject: [PATCH 8/8] Some minor name changes and timing fixes
---
.../AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs | 4 ++--
.../AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs | 4 ++--
.../AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs | 2 +-
.../IUITestServiceExtensions.cs | 13 ++++++++++---
4 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
index ceb9d9f..beac06a 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/HostSteps.cs
@@ -50,7 +50,7 @@ public void OnBeforeScenario_SetUpHostFactory(AdaptiveRemoteHostSettings hostSet
// Use the simulated TiVo device
string tivoArgs = "--tivo:Fake=True";
- if (SimulatedEnvironment.TryGetDevice(TiVoDeviceName, out ISimulatedDevice? tivoDevice) && tivoDevice != null)
+ if (Environment.TryGetDevice(TiVoDeviceName, out ISimulatedDevice? tivoDevice) && tivoDevice != null)
{
tivoArgs = $"--tivo:IP=127.0.0.1:{tivoDevice.Port}";
}
@@ -133,6 +133,6 @@ public void OnAfterScenario_AttachLogsToTestContextAndStopHost()
}
// Clean up simulated environment
- SimulatedEnvironment?.Dispose();
+ Environment?.Dispose();
}
}
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs b/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
index d4385ed..b841f37 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/StepsBase.cs
@@ -22,11 +22,11 @@ public abstract class StepsBase : IContainerDependentObject
public TestContext TestContext => GetContainerObject();
- public ISimulatedEnvironment SimulatedEnvironment => _simulatedEnvironment ??= GetContainerObject();
+ public ISimulatedEnvironment Environment => _simulatedEnvironment ??= GetContainerObject();
public ILogger Logger => _logger ??= Host.CreateLogger(GetType().Name);
- protected ObjectType GetContainerObject()
+ private ObjectType GetContainerObject()
where ObjectType : notnull
{
Assert.IsNotNull(_container, "Attempting to access container object before IContainerDependentObject.SetObjectContainer has been called");
diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
index 7104dc4..fc860e0 100644
--- a/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
+++ b/test/AdaptiveRemote.EndToEndTests.Steps/TiVoSteps.cs
@@ -13,7 +13,7 @@ public class TiVoSteps : StepsBase
[Then(@"I should see the TiVo receives a {string} message")]
public void ThenIShouldSeeTheTiVoReceivesAMessage(string expectedCommand)
{
- if (!SimulatedEnvironment.TryGetDevice(TiVoDeviceName, out ISimulatedDevice? device))
+ if (!Environment.TryGetDevice(TiVoDeviceName, out ISimulatedDevice? device))
{
Assert.Fail("TiVo device is not running");
}
diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs
index 9fba0d9..ccc92e1 100644
--- a/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs
+++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs
@@ -68,10 +68,17 @@ public static void ClickButton(this IUITestService service, string label, int ti
/// Thrown when the operation times out.
public static void ClickButton(this IUITestService service, string label, TimeSpan timeout)
{
- bool succeeded = WaitHelpers.WaitForAsyncTask(ct => service.ClickButtonAsync(label, ct));
- if (!succeeded)
+ try
{
- throw new TimeoutException($"Clicking button '{label}' did not complete within timeout.");
+ bool succeeded = WaitHelpers.WaitForAsyncTask(ct => service.ClickButtonAsync(label, ct), timeout);
+ if (!succeeded)
+ {
+ throw new TimeoutException($"Clicking button '{label}' did not complete within timeout.");
+ }
+ }
+ catch (AggregateException ex) when (label.Equals("Exit", StringComparison.OrdinalIgnoreCase) && ex.InnerException is StreamJsonRpc.ConnectionLostException)
+ {
+ // This exception occurs sometimes when clicking the "Exit" button if the application shuts down to fast
}
}
}