Skip to content

Commit 8f6c60f

Browse files
committed
Work on unit tests
1 parent 293a516 commit 8f6c60f

11 files changed

Lines changed: 1027 additions & 10 deletions

File tree

EDDI/App.xaml.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@
77
using System.Diagnostics;
88
using System.Globalization;
99
using System.Net.Http;
10+
using System.Runtime.CompilerServices;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using System.Windows;
1314
using Utilities;
1415

16+
[assembly: InternalsVisibleTo( "Tests" )]
1517
namespace Eddi
1618
{
1719
/// <summary>
1820
/// Interaction logic for App.xaml
1921
/// </summary>
2022
public partial class App : Application
2123
{
22-
public static Mutex eddiMutex { get; private set; }
24+
public static Mutex eddiMutex { get; internal set; }
2325

2426
// True if we have been started by VoiceAttack and the VaProxy object has been set
2527
public static System.Version VoiceAttackVersion { get; set; }

EddiCore/EDDI.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class EDDI: INotifyPropertyChanged
4040
private static bool started;
4141
public static bool running = true;
4242

43-
public bool inTelepresence { get; private set; }
43+
public bool inTelepresence { get; internal set; }
4444

4545
public bool inHorizons
4646
{
@@ -79,7 +79,7 @@ private string gameVersion
7979

8080
private readonly StarSystemSignalSourceManager signalSourceManager = new StarSystemSignalSourceManager();
8181

82-
public System.Version GameVersion { get; private set; }
82+
public System.Version GameVersion { get; internal set; }
8383

8484
// EDDI uses APIs which only return data for the "live" galaxy, game version 4.0 or later.
8585
private readonly System.Version minGameVersion = new System.Version(4, 0);

EddiUI/EddiUI.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
1010
<OutputPath>..\bin\Release\</OutputPath>
1111
</PropertyGroup>
12+
<ItemGroup>
13+
<None Remove="logo-with-alpha.png" />
14+
</ItemGroup>
15+
<ItemGroup>
16+
<EmbeddedResource Include="logo-with-alpha.png" />
17+
</ItemGroup>
1218
<ItemGroup>
1319
<PackageReference Include="CommonMark.NET" />
1420
<PackageReference Include="Microsoft.CSharp" />
@@ -61,6 +67,9 @@
6167
<PackagePath>\</PackagePath>
6268
</None>
6369
</ItemGroup>
70+
<!--<ItemGroup>
71+
<Resource Include="logo-with-alpha.png" />
72+
</ItemGroup>-->
6473
<ItemGroup>
6574
<SplashScreen Include="..\graphics\logo-with-alpha.png" />
6675
</ItemGroup>

EddiUI/MainWindow.xaml.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using System.IO;
1212
using System.IO.Compression;
1313
using System.Linq;
14+
using System.Reflection;
15+
using System.Runtime.CompilerServices;
1416
using System.Text.RegularExpressions;
1517
using System.Threading.Tasks;
1618
using System.Windows;
@@ -21,6 +23,7 @@
2123
using System.Windows.Media;
2224
using Utilities;
2325

26+
[assembly: InternalsVisibleTo( "Tests" )]
2427
namespace EddiUI
2528
{
2629
/// <summary>
@@ -30,7 +33,7 @@ public partial class MainWindow : Window
3033
{
3134
public System.Windows.Controls.TabControl MainTabControl => tabControl;
3235

33-
struct LanguageDef : IComparable<LanguageDef>
36+
internal struct LanguageDef : IComparable<LanguageDef>
3437
{
3538
public CultureInfo ci;
3639
public string displayName { get; set; }
@@ -167,7 +170,7 @@ public MainWindow()
167170
{
168171
if (!EDDI.FromVA)
169172
{
170-
var splashScreen = new SplashScreen("logo-with-alpha.png");
173+
var splashScreen = new SplashScreen( Assembly.GetExecutingAssembly(), "logo-with-alpha.png");
171174
splashScreen.Show(true);
172175
}
173176

@@ -234,7 +237,7 @@ private void OnUnloaded ( object sender, RoutedEventArgs e )
234237
EDDI.Instance.HotkeyManager.UnregisterAllHotkeys();
235238
}
236239

237-
class TabItemComparer : Comparer<TabItem>
240+
internal class TabItemComparer : Comparer<TabItem>
238241
{
239242
public StringComparer stringComparer { get; }
240243

@@ -274,7 +277,7 @@ private void LoadAndSortTabs(EDDIConfiguration eddiConfiguration)
274277
}
275278
}
276279

277-
private List<LanguageDef> GetAvailableLangs()
280+
internal List<LanguageDef> GetAvailableLangs()
278281
{
279282
var cultures = new List<LanguageDef>
280283
{
@@ -531,7 +534,7 @@ protected override void OnClosing(CancelEventArgs e)
531534
}
532535
}
533536

534-
private void EnsureValidDecimal(object sender, TextCompositionEventArgs e)
537+
internal void EnsureValidDecimal(object sender, TextCompositionEventArgs e)
535538
{
536539
// Match valid characters
537540
Regex regex = new Regex(@"[0-9\.]");

InaraResponder/InaraResponder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@
1010
using System.Collections.Generic;
1111
using System.Globalization;
1212
using System.Linq;
13+
using System.Runtime.CompilerServices;
1314
using System.Threading.Tasks;
1415
using System.Windows.Controls;
1516
using Utilities;
1617

18+
[assembly: InternalsVisibleTo( "Tests" )]
1719
namespace EddiInaraResponder
1820
{
1921
// Documentation: https://inara.cz/inara-api-docs/
2022

2123
[UsedImplicitly]
2224
public class InaraResponder : IEddiResponder
2325
{
24-
private readonly IInaraService inaraService = new InaraService();
26+
public IInaraService inaraService { get; internal set; } = new InaraService();
2527

2628
// This responder currently requires game version 4.0 or later.
2729
private static readonly System.Version minGameVersion = new System.Version(4, 0);
@@ -716,7 +718,7 @@ private void handleShipLoadoutEvent(ShipLoadoutEvent @event)
716718
}));
717719
}
718720

719-
private static Dictionary<string, object> GetModuleData(string slotName, Module module)
721+
internal static Dictionary<string, object> GetModuleData(string slotName, Module module)
720722
{
721723
Dictionary<string, object> moduleData;
722724
if (module is null)

Tests/AppTests.cs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
using Eddi;
2+
using EddiConfigService.Configurations;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using System;
5+
using System.Globalization;
6+
using System.IO;
7+
using System.Net.Http;
8+
using System.Reflection;
9+
using System.Threading;
10+
using Utilities;
11+
12+
namespace Tests
13+
{
14+
[TestClass, TestCategory( "UnitTests" )]
15+
public class AppTests : TestBase
16+
{
17+
[TestInitialize]
18+
public void Init ()
19+
{
20+
// Make test environment safe (disable telemetry, unit testing flags)
21+
MakeSafe();
22+
23+
// Ensure log directory exists so Logging won't fail when tests inspect files
24+
Directory.CreateDirectory( Constants.DATA_DIR );
25+
}
26+
27+
[TestCleanup]
28+
public void Cleanup ()
29+
{
30+
// Try to clean up any mutex left behind by tests
31+
try
32+
{
33+
App.eddiMutex?.ReleaseMutex();
34+
}
35+
catch
36+
{
37+
// ignore if current thread doesn't own it or already released
38+
}
39+
finally
40+
{
41+
App.eddiMutex?.Dispose();
42+
App.eddiMutex = null;
43+
}
44+
}
45+
46+
[TestMethod]
47+
public void AlreadyRunning_ReturnsTrueWhenExternalMutexHeld ()
48+
{
49+
// Ensure starting state - dispose any existing App mutex
50+
App.eddiMutex?.Dispose();
51+
App.eddiMutex = null;
52+
53+
Mutex external = null;
54+
try
55+
{
56+
// Create an external (simulated other process) mutex and take ownership
57+
external = new Mutex( true, Constants.EDDI_SYSTEM_MUTEX_NAME, out bool externalOwner );
58+
Assert.IsTrue( externalOwner, "Test setup failed to obtain external mutex ownership" );
59+
60+
// Now call AlreadyRunning which will create its own mutex; it should detect an existing owner
61+
var already = App.AlreadyRunning();
62+
Assert.IsTrue( already, "AlreadyRunning should return true when an external mutex exists" );
63+
}
64+
finally
65+
{
66+
// Release & dispose external mutex
67+
try
68+
{ external?.ReleaseMutex(); }
69+
catch
70+
{
71+
// ignored
72+
}
73+
74+
external?.Dispose();
75+
76+
// Dispose the mutex created by AlreadyRunning (may not be owned by this thread)
77+
App.eddiMutex?.Dispose();
78+
App.eddiMutex = null;
79+
}
80+
}
81+
82+
[TestMethod]
83+
public void ApplyAnyOverrideCulture_AppliesValidCulture ()
84+
{
85+
var config = new EDDIConfiguration
86+
{
87+
OverrideCulture = "fr-FR"
88+
};
89+
90+
// Apply override
91+
App.ApplyAnyOverrideCulture( config );
92+
93+
// Default thread cultures and current thread culture should reflect override
94+
Assert.IsNotNull( CultureInfo.DefaultThreadCurrentCulture );
95+
Assert.AreEqual( "fr-FR", CultureInfo.DefaultThreadCurrentCulture.Name, "DefaultThreadCurrentCulture should be set to 'fr-FR'" );
96+
Assert.AreEqual( "fr-FR", Thread.CurrentThread.CurrentCulture.Name, "Current thread culture should be set to 'fr-FR'" );
97+
}
98+
99+
[TestMethod]
100+
public void ApplyAnyOverrideCulture_InvalidCultureDoesNotThrow_AndSetsDefaultToNull ()
101+
{
102+
// Start with a known default so we can observe the fallback behavior
103+
CultureInfo backupDefault = CultureInfo.DefaultThreadCurrentCulture;
104+
105+
var config = new EDDIConfiguration
106+
{
107+
OverrideCulture = "this-is-not-a-culture"
108+
};
109+
110+
// Should not throw
111+
App.ApplyAnyOverrideCulture( config );
112+
113+
// Per implementation, invalid culture triggers ApplyCulture(null) -> DefaultThreadCurrentCulture becomes null
114+
Assert.IsNull( CultureInfo.DefaultThreadCurrentCulture, "DefaultThreadCurrentCulture should have been set to null when invalid override culture is provided" );
115+
116+
// Restore original default to avoid affecting other tests
117+
CultureInfo.DefaultThreadCurrentCulture = backupDefault;
118+
CultureInfo.DefaultThreadCurrentUICulture = backupDefault;
119+
}
120+
121+
[TestMethod]
122+
public void CrashLogger_SuppressesRollbarInternalHttpExceptions ()
123+
{
124+
// Build an HttpRequestException with a stack trace containing "Rollbar" by throwing from a helper method
125+
HttpRequestException httpEx = null;
126+
try
127+
{
128+
ThrowHttpRequestExceptionWithRollbarInStack();
129+
}
130+
catch ( HttpRequestException hre )
131+
{
132+
httpEx = hre;
133+
}
134+
Assert.IsNotNull( httpEx );
135+
136+
// Wrap in AggregateException as the CrashLogger expects
137+
var agg = new AggregateException(httpEx);
138+
139+
// Invoke private static CrashLogger via reflection and ensure it doesn't throw
140+
var crashLogger = typeof(App).GetMethod("CrashLogger", BindingFlags.NonPublic | BindingFlags.Static);
141+
Assert.IsNotNull( crashLogger, "CrashLogger method not found via reflection" );
142+
143+
// Should not throw
144+
crashLogger.Invoke( null, new object[] { agg } );
145+
}
146+
147+
[TestMethod]
148+
public void CrashLogger_LogsUnhandledException ()
149+
{
150+
// Ensure we have a fresh log file to inspect
151+
var logFile = Path.Combine(Constants.DATA_DIR, "eddi.log");
152+
if ( File.Exists( logFile ) )
153+
{
154+
File.Delete( logFile );
155+
}
156+
157+
var crashLogger = typeof(App).GetMethod("CrashLogger", BindingFlags.NonPublic | BindingFlags.Static);
158+
Assert.IsNotNull( crashLogger );
159+
160+
var ex = new Exception("TestCrashLogger");
161+
162+
// Invoke CrashLogger - it will call Logging.Error which writes asynchronously
163+
crashLogger.Invoke( null, new object[] { ex } );
164+
165+
// Wait for the background logging task to complete and write to file (polling with timeout)
166+
var sw = System.Diagnostics.Stopwatch.StartNew();
167+
bool found = false;
168+
while ( sw.Elapsed < TimeSpan.FromSeconds( 5 ) )
169+
{
170+
if ( File.Exists( logFile ) )
171+
{
172+
var contents = File.ReadAllText(logFile);
173+
if ( contents.Contains( "Unhandled exception: TestCrashLogger." ) )
174+
{
175+
found = true;
176+
break;
177+
}
178+
}
179+
Thread.Sleep( 100 );
180+
}
181+
182+
Assert.IsTrue( found, "CrashLogger did not write the expected message to the log file within the timeout" );
183+
}
184+
185+
// Helper used to create an HttpRequestException whose stack trace contains the substring "Rollbar"
186+
private void ThrowHttpRequestExceptionWithRollbarInStack ()
187+
{
188+
// Method name intentionally contains "Rollbar" so that the resulting stack trace includes that token.
189+
RollbarMarker();
190+
}
191+
192+
private void RollbarMarker ()
193+
{
194+
// Throw an HttpRequestException so its stack trace contains "RollbarMarker" (and thus "Rollbar")
195+
throw new HttpRequestException( "simulated http exception" );
196+
}
197+
}
198+
}

0 commit comments

Comments
 (0)