Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions WeatherExtension.Tests/ProgramLifecycleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright (c) Bald Bearded Builder LLC
// Bald Bearded Builder LLC licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Ext.Weather.UnitTests;

[TestClass]
public class ProgramLifecycleTests
{
[TestMethod]
public void WaitHandle_WaitAnyReturnsImmediatelyWhenFirstHandleSignaled()
{
// Arrange
using var handle1 = new ManualResetEvent(true); // Pre-signaled
using var handle2 = new ManualResetEvent(false);

// Act
int result = WaitHandle.WaitAny(new[] { handle1, handle2 }, timeout: 100);

// Assert
Assert.AreEqual(0, result, "WaitAny should return index 0 when first handle is signaled");
}

[TestMethod]
public void WaitHandle_WaitAnyReturnsSecondHandleIndex()
{
// Arrange
using var handle1 = new ManualResetEvent(false);
using var handle2 = new ManualResetEvent(true); // Pre-signaled

// Act
int result = WaitHandle.WaitAny(new[] { handle1, handle2 }, timeout: 100);

// Assert
Assert.AreEqual(1, result, "WaitAny should return index 1 when second handle is signaled");
}

[TestMethod]
public void WaitHandle_WaitAnyTimesOutWhenNeitherSignaled()
{
// Arrange
using var handle1 = new ManualResetEvent(false);
using var handle2 = new ManualResetEvent(false);

// Act
int result = WaitHandle.WaitAny(new[] { handle1, handle2 }, timeout: 100);

// Assert
Assert.AreEqual(WaitHandle.WaitTimeout, result, "WaitAny should timeout when neither handle is signaled");
}

[TestMethod]
public void CoordinatorAndDisposalEventCanBeWaitedTogether()
{
// Arrange
using var disposalEvent = new ManualResetEvent(false);
using var coordinator = new WeatherExtension.ShutdownCoordinator();
var waitHandles = new[] { disposalEvent, coordinator.ShutdownHandle };

// Act - Signal the disposal event
disposalEvent.Set();
int result = WaitHandle.WaitAny(waitHandles, timeout: 1000);

// Assert
Assert.AreEqual(0, result, "WaitAny should return 0 when disposal event is signaled first");
}

[TestMethod]
public void TimerWatchdogCanBeDisabledAfterShutdownCompletes()
{
// Arrange
var watchdogFired = false;
Timer? watchdog = null;

// Act
watchdog = new Timer(
_ => watchdogFired = true,
state: null,
dueTime: TimeSpan.FromMilliseconds(100),
period: Timeout.InfiniteTimeSpan);

// Simulate shutdown completing before timeout
Thread.Sleep(50);
watchdog.Change(Timeout.Infinite, Timeout.Infinite);
Thread.Sleep(100);

// Assert
Assert.IsFalse(watchdogFired, "Watchdog should not fire if disabled before timeout");

watchdog.Dispose();
}

[TestMethod]
public void TimerWatchdogFiresIfShutdownSlow()
{
// Arrange
var watchdogFired = false;
Timer? watchdog = null;

// Act
watchdog = new Timer(
_ => watchdogFired = true,
state: null,
dueTime: TimeSpan.FromMilliseconds(100),
period: Timeout.InfiniteTimeSpan);

// Simulate slow shutdown - don't disable the watchdog
Thread.Sleep(200);

// Assert
Assert.IsTrue(watchdogFired, "Watchdog should fire if shutdown takes too long");

watchdog.Dispose();
}

[TestMethod]
public void ExtensionDisposeEventCanBeTriggeredManually()
{
// Arrange
using var disposalEvent = new ManualResetEvent(false);
var extension = new WeatherExtension.WeatherExtension(disposalEvent);

// Act - Simulate external shutdown signal triggering manual Dispose
extension.Dispose();

// Assert
Assert.IsTrue(disposalEvent.WaitOne(timeout: 100), "Manual Dispose should signal the event");
}

[TestMethod]
public void MultipleHandlesAreThreadSafeInWaitAny()
{
// Arrange
using var handle1 = new ManualResetEvent(false);
using var handle2 = new ManualResetEvent(false);
var results = new List<int>();

// Act - Multiple threads waiting on same handles
var threads = Enumerable.Range(0, 5).Select(_ => new Thread(() =>
{
int result = WaitHandle.WaitAny(new[] { handle1, handle2 }, timeout: 500);
lock (results)
{
results.Add(result);
}
})).ToList();

threads.ForEach(t => t.Start());
Thread.Sleep(100);

// Signal handle1
handle1.Set();

threads.ForEach(t => t.Join(timeout: 2000));

// Assert
Assert.IsTrue(results.All(r => r == 0), "All waiting threads should return handle1's index");
}
}
148 changes: 148 additions & 0 deletions WeatherExtension.Tests/ResourceDisposalVerificationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Bald Bearded Builder LLC
// Bald Bearded Builder LLC licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Ext.Weather.UnitTests;

[TestClass]
public class ResourceDisposalVerificationTests
{
[TestMethod]
public void WeatherCommandsProvider_DisposesAllBands()
{
// Arrange
using var provider = new Microsoft.CmdPal.Ext.Weather.WeatherCommandsProvider();

// Act
provider.Dispose();

// Assert - Should not throw and should complete
provider.Dispose(); // Second dispose should be safe
}

[TestMethod]
public void OpenMeteoService_DisposesHttpClient()
{
// Arrange
using var handler = new HttpClientHandler();
var service = new Microsoft.CmdPal.Ext.Weather.Services.OpenMeteoService(handler);

// Act
service.Dispose();

// Assert - Service should dispose without error
Assert.IsTrue(true, "OpenMeteoService disposal completed");
}

[TestMethod]
public void GeocodingService_DisposesHttpClient()
{
// Arrange
using var handler = new HttpClientHandler();
var service = new Microsoft.CmdPal.Ext.Weather.Services.GeocodingService(handler);

// Act
service.Dispose();

// Assert - Service should dispose without error
Assert.IsTrue(true, "GeocodingService disposal completed");
}

[TestMethod]
public void WeatherListPage_CancelsCancellationTokens()
{
// Arrange
var weatherService = new Mock<Microsoft.CmdPal.Ext.Weather.Services.IWeatherService>();
var geocodingService = new Mock<Microsoft.CmdPal.Ext.Weather.Services.IGeocodingService>();
var settingsManager = new Microsoft.CmdPal.Ext.Weather.Services.WeatherSettingsManager();
var favoritesManager = new Microsoft.CmdPal.Ext.Weather.Services.FavoritesManager();

var page = new Microsoft.CmdPal.Ext.Weather.Pages.WeatherListPage(
weatherService.Object,
geocodingService.Object,
settingsManager,
favoritesManager);

// Act
page.Dispose();

// Assert - Should not throw
Assert.IsTrue(true, "WeatherListPage disposal completed");
}

[TestMethod]
public void PinnedWeatherBand_StopsTimerAndCancelsCts()
{
// Arrange
var location = new Microsoft.CmdPal.Ext.Weather.Models.GeocodingResult
{
Latitude = 0,
Longitude = 0,
DisplayName = "Test",
};

var weatherService = new Mock<Microsoft.CmdPal.Ext.Weather.Services.IWeatherService>();
var settings = new Microsoft.CmdPal.Ext.Weather.Services.WeatherSettingsManager();
var card = new Mock<Microsoft.CmdPal.Ext.Weather.Pages.WeatherBandCard>();

var band = new Microsoft.CmdPal.Ext.Weather.DockBands.PinnedWeatherBand(
location,
weatherService.Object as Microsoft.CmdPal.Ext.Weather.Services.OpenMeteoService,
settings,
card.Object);

// Act
band.Dispose();

// Assert - Should not throw
Assert.IsTrue(true, "PinnedWeatherBand disposal completed");
}

[TestMethod]
public void DisposalEventHandlers_AreProperlyUnsubscribed()
{
// Arrange - Create provider and retrieve disposal event handler counts
var provider = new Microsoft.CmdPal.Ext.Weather.WeatherCommandsProvider();

// Act
provider.Dispose();

// Assert - Multiple disposes should not duplicate handlers
provider.Dispose();
Assert.IsTrue(true, "Event handlers properly cleaned up");
}

[TestMethod]
public void ShutdownCoordinator_ReleasesAllResources()
{
// Arrange
var coordinator = new WeatherExtension.ShutdownCoordinator();
var initialThreadCount = System.Diagnostics.Process.GetCurrentProcess().ThreadCount;

// Act
coordinator.Dispose();
Thread.Sleep(100); // Give listener thread time to exit

var finalThreadCount = System.Diagnostics.Process.GetCurrentProcess().ThreadCount;

// Assert
Assert.LessOrEqual(finalThreadCount, initialThreadCount, "Shutdown coordinator should release its listener thread");
}

[TestMethod]
public void FullDisposalChain_CompletesWithoutDeadlock()
{
// Arrange
using var disposedEvent = new ManualResetEvent(false);
using var coordinator = new WeatherExtension.ShutdownCoordinator();
var extension = new WeatherExtension.WeatherExtension(disposedEvent);

// Act & Assert - Should complete without hanging
extension.Dispose();
bool completed = disposedEvent.WaitOne(timeout: TimeSpan.FromSeconds(2));

coordinator.Dispose();

Assert.IsTrue(completed, "Disposal chain should complete promptly");
}
}
Loading
Loading