Skip to content

Commit f79ab7b

Browse files
Add Tests to ensure BindableOptions are consistent with Options (#2875)
1 parent c32be36 commit f79ab7b

File tree

8 files changed

+252
-146
lines changed

8 files changed

+252
-146
lines changed

src/Sentry/SentryOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ public Dictionary<string, string> DefaultTags {
734734
internal set => _defaultTags = value;
735735
}
736736

737-
/// <summary>
737+
/// <summary>
738738
/// Indicates whether the performance feature is enabled, via any combination of
739739
/// <see cref="EnableTracing"/>, <see cref="TracesSampleRate"/>, or <see cref="TracesSampler"/>.
740740
/// </summary>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#if !NETFRAMEWORK
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace Sentry.AspNetCore.Tests;
5+
6+
public class BindableSentryAspNetCoreOptionsTests: BindableTests<SentryAspNetCoreOptions>
7+
{
8+
[Fact]
9+
public void BindableProperties_MatchOptionsProperties()
10+
{
11+
var actual = GetPropertyNames<BindableSentryAspNetCoreOptions>();
12+
AssertContainsAllOptionsProperties(actual);
13+
}
14+
15+
[Fact]
16+
public void ApplyTo_SetsOptionsFromConfig()
17+
{
18+
// Arrange
19+
var actual = new SentryAspNetCoreOptions();
20+
var bindable = new BindableSentryAspNetCoreOptions();
21+
22+
// Act
23+
Fixture.Config.Bind(bindable);
24+
bindable.ApplyTo(actual);
25+
26+
// Assert
27+
AssertContainsExpectedPropertyValues(actual);
28+
}
29+
}
30+
#endif
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#if !NETFRAMEWORK
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace Sentry.Extensions.Logging.Tests;
5+
6+
public class SentryLoggingOptionsTests : BindableTests<SentryLoggingOptions>
7+
{
8+
[Fact]
9+
public void BindableProperties_MatchOptionsProperties()
10+
{
11+
var propertyNames = GetPropertyNames<BindableSentryLoggingOptions>();
12+
AssertContainsAllOptionsProperties(propertyNames);
13+
}
14+
15+
[Fact]
16+
public void ApplyTo_SetsOptionsFromConfig()
17+
{
18+
// Arrange
19+
var actual = new SentryLoggingOptions();
20+
var bindable = new BindableSentryLoggingOptions();
21+
22+
// Act
23+
Fixture.Config.Bind(bindable);
24+
bindable.ApplyTo(actual);
25+
26+
// Assert
27+
AssertContainsExpectedPropertyValues(actual);
28+
}
29+
}
30+
#endif
31+
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#if !NETFRAMEWORK
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace Sentry.Testing;
5+
6+
public abstract class BindableTests<TOptions>(params string[] skipProperties)
7+
{
8+
public class TextFixture
9+
{
10+
public IEnumerable<string> ExpectedPropertyNames { get; }
11+
public List<KeyValuePair<PropertyInfo, object>> ExpectedPropertyValues { get; }
12+
13+
public IConfigurationRoot Config { get; }
14+
15+
public TextFixture(params string[] skipProperties)
16+
{
17+
ExpectedPropertyNames = GetBindableProperties(skipProperties).Select(x => x.Name);
18+
ExpectedPropertyValues = GetBindableProperties(skipProperties).Select(GetDummyBindableValue).ToList();
19+
Config = new ConfigurationBuilder()
20+
.AddInMemoryCollection(ExpectedPropertyValues.SelectMany(ToConfigValues))
21+
.Build();
22+
}
23+
}
24+
25+
protected TextFixture Fixture { get; } = new(skipProperties);
26+
27+
private static IEnumerable<PropertyInfo> GetBindableProperties(IEnumerable<string> skipProperties)
28+
{
29+
return typeof(TOptions).GetProperties()
30+
.Where(p =>
31+
!p.PropertyType.IsSubclassOf(typeof(Delegate)) // Exclude delegate properties
32+
&& !p.PropertyType.IsInterface // Exclude interface properties
33+
&& !skipProperties.Contains(p.Name) // Exclude any properties explicitly excluded by derived classes
34+
#if ANDROID
35+
&& !(p.PropertyType == typeof(SentryOptions.AndroidOptions)) // Exclude the Mobile sub-property
36+
#elif __IOS__
37+
&& !(p.PropertyType == typeof(SentryOptions.IosOptions)) // Exclude the Mobile sub-property
38+
#endif
39+
);
40+
}
41+
42+
protected IEnumerable<string> GetPropertyNames<T>() => typeof(T).GetProperties().Select(x => x.Name).ToList();
43+
44+
private static KeyValuePair<PropertyInfo,object> GetDummyBindableValue(PropertyInfo propertyInfo)
45+
{
46+
var propertyType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
47+
var value = propertyType switch
48+
{
49+
not null when propertyType == typeof(bool) => true,
50+
not null when propertyType == typeof(string) => $"fake {propertyInfo.Name}",
51+
not null when propertyType == typeof(int) => 7,
52+
not null when propertyType == typeof(long) => 7,
53+
not null when propertyType == typeof(float) => 0.3f,
54+
not null when propertyType == typeof(double) => 0.6,
55+
not null when propertyType == typeof(TimeSpan) => TimeSpan.FromSeconds(3),
56+
not null when propertyType.IsEnum => GetNonDefaultEnumValue(propertyType),
57+
not null when propertyType == typeof(Dictionary<string, string>) =>
58+
new Dictionary<string, string>
59+
{
60+
{$"key1", $"{propertyInfo.Name}value1"},
61+
{$"key2", $"{propertyInfo.Name}value2"}
62+
},
63+
_ => throw new NotSupportedException($"Unsupported property type on property {propertyInfo.Name}")
64+
};
65+
return new KeyValuePair<PropertyInfo,object>(propertyInfo, value);
66+
}
67+
68+
private static IEnumerable<KeyValuePair<string, string>> ToConfigValues(KeyValuePair<PropertyInfo, object> item)
69+
{
70+
var (prop, value) = item;
71+
var propertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
72+
if (propertyType == typeof(Dictionary<string, string>))
73+
{
74+
foreach (var kvp in (Dictionary<string, string>)value)
75+
{
76+
yield return new KeyValuePair<string, string>($"{prop.Name}:{kvp.Key}", kvp.Value);
77+
}
78+
}
79+
else
80+
{
81+
yield return new KeyValuePair<string, string>(prop.Name, value.ToString());
82+
}
83+
}
84+
85+
private static object GetNonDefaultEnumValue(Type enumType)
86+
{
87+
var enumValues = Enum.GetValues(enumType);
88+
if (enumValues.Length > 1)
89+
{
90+
return enumValues.GetValue(1); // return second value
91+
}
92+
throw new InvalidOperationException("Enum has no non-default values");
93+
}
94+
95+
protected void AssertContainsAllOptionsProperties(IEnumerable<string> actual)
96+
{
97+
var missing = Fixture.ExpectedPropertyNames.Where(x => !actual.Contains(x));
98+
99+
missing.Should().BeEmpty();
100+
}
101+
102+
protected void AssertContainsExpectedPropertyValues(TOptions actual)
103+
{
104+
using (new AssertionScope())
105+
{
106+
foreach (var (prop, expectedValue) in Fixture.ExpectedPropertyValues)
107+
{
108+
var actualValue = actual.GetProperty(prop.Name);
109+
if (prop.PropertyType == typeof(Dictionary<string, string>))
110+
{
111+
actualValue.Should().BeEquivalentTo(expectedValue);
112+
}
113+
else
114+
{
115+
actualValue.Should().Be(expectedValue);
116+
}
117+
}
118+
}
119+
}
120+
}
121+
#endif

test/Sentry.Testing/Sentry.Testing.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@
2222
<InternalsVisibleTo Include="Sentry.Tests" PublicKey="$(SentryPublicKey)" />
2323
</ItemGroup>
2424

25+
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
26+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.0" />
27+
</ItemGroup>
28+
2529
</Project>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#if !NETFRAMEWORK
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace Sentry.Tests;
5+
6+
public class BindableSentryOptionsTests : BindableTests<SentryOptions>
7+
{
8+
[Fact]
9+
public void BindableProperties_MatchOptionsProperties()
10+
{
11+
var actual = GetPropertyNames<BindableSentryOptions>();
12+
AssertContainsAllOptionsProperties(actual);
13+
}
14+
15+
[Fact]
16+
public void ApplyTo_SetsOptionsFromConfig()
17+
{
18+
// Arrange
19+
var actual = new SentryOptions();
20+
var bindable = new BindableSentryOptions();
21+
22+
// Act
23+
Fixture.Config.Bind(bindable);
24+
bindable.ApplyTo(actual);
25+
26+
// Assert
27+
AssertContainsExpectedPropertyValues(actual);
28+
}
29+
}
30+
#endif
Lines changed: 15 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,30 @@
1+
#if ANDROID
12
using Microsoft.Extensions.Configuration;
23

34
namespace Sentry.Tests.Platforms.Android;
45

5-
public class BindableSentryOptionsTests
6+
public class BindableSentryOptionsTests : BindableTests<SentryOptions.AndroidOptions>
67
{
7-
#if ANDROID
8-
[SkippableFact]
9-
public void ApplyTo_SetsAndroidOptionsFromConfig()
8+
[Fact]
9+
public void BindableProperties_MatchOptionsProperties()
1010
{
11-
var expected = new SentryOptions.AndroidOptions(new SentryOptions())
12-
{
13-
AnrEnabled = true,
14-
AnrReportInDebug = true,
15-
AnrTimeoutInterval = TimeSpan.FromSeconds(3),
16-
AttachScreenshot = true,
17-
EnableActivityLifecycleBreadcrumbs = true,
18-
EnableAppComponentBreadcrumbs = true,
19-
EnableAppLifecycleBreadcrumbs = true,
20-
EnableRootCheck = true,
21-
EnableSystemEventBreadcrumbs = true,
22-
EnableUserInteractionBreadcrumbs = true,
23-
EnableAutoActivityLifecycleTracing = true,
24-
EnableActivityLifecycleTracingAutoFinish = true,
25-
EnableUserInteractionTracing = true,
26-
AttachThreads = true,
27-
ConnectionTimeout = TimeSpan.FromSeconds(7),
28-
EnableNdk = true,
29-
EnableShutdownHook = true,
30-
EnableUncaughtExceptionHandler = true,
31-
PrintUncaughtStackTrace = true,
32-
ReadTimeout = TimeSpan.FromSeconds(13),
33-
EnableAndroidSdkTracing = true,
34-
EnableAndroidSdkBeforeSend = true,
35-
};
11+
var actual = GetPropertyNames<BindableSentryOptions.AndroidOptions>();
12+
AssertContainsAllOptionsProperties(actual);
13+
}
3614

37-
var config = new ConfigurationBuilder()
38-
.AddInMemoryCollection(new Dictionary<string, string>
39-
{
40-
["AnrEnabled"] = expected.AnrEnabled.ToString(),
41-
["AnrReportInDebug"] = expected.AnrReportInDebug.ToString(),
42-
["AnrTimeoutInterval"] = expected.AnrTimeoutInterval.ToString(),
43-
["AttachScreenshot"] = expected.AttachScreenshot.ToString(),
44-
["EnableActivityLifecycleBreadcrumbs"] = expected.EnableActivityLifecycleBreadcrumbs.ToString(),
45-
["EnableAppComponentBreadcrumbs"] = expected.EnableAppComponentBreadcrumbs.ToString(),
46-
["EnableAppLifecycleBreadcrumbs"] = expected.EnableAppLifecycleBreadcrumbs.ToString(),
47-
["EnableRootCheck"] = expected.EnableRootCheck.ToString(),
48-
["EnableSystemEventBreadcrumbs"] = expected.EnableSystemEventBreadcrumbs.ToString(),
49-
["EnableUserInteractionBreadcrumbs"] = expected.EnableUserInteractionBreadcrumbs.ToString(),
50-
["EnableAutoActivityLifecycleTracing"] = expected.EnableAutoActivityLifecycleTracing.ToString(),
51-
["EnableActivityLifecycleTracingAutoFinish"] = expected.EnableActivityLifecycleTracingAutoFinish.ToString(),
52-
["EnableUserInteractionTracing"] = expected.EnableUserInteractionTracing.ToString(),
53-
["AttachThreads"] = expected.AttachThreads.ToString(),
54-
["ConnectionTimeout"] = expected.ConnectionTimeout.ToString(),
55-
["EnableNdk"] = expected.EnableNdk.ToString(),
56-
["EnableShutdownHook"] = expected.EnableShutdownHook.ToString(),
57-
["EnableUncaughtExceptionHandler"] = expected.EnableUncaughtExceptionHandler.ToString(),
58-
["PrintUncaughtStackTrace"] = expected.PrintUncaughtStackTrace.ToString(),
59-
["ReadTimeout"] = expected.ReadTimeout.ToString(),
60-
["EnableAndroidSdkTracing"] = expected.EnableAndroidSdkTracing.ToString(),
61-
["EnableAndroidSdkBeforeSend"] = expected.EnableAndroidSdkBeforeSend.ToString()
62-
}).Build();
63-
var bindable = new BindableSentryOptions.AndroidOptions();
15+
[Fact]
16+
public void ApplyTo_SetsOptionsFromConfig()
17+
{
18+
// Arrange
6419
var actual = new SentryOptions.AndroidOptions(new SentryOptions());
20+
var bindable = new BindableSentryOptions.AndroidOptions();
6521

6622
// Act
67-
config.Bind(bindable);
23+
Fixture.Config.Bind(bindable);
6824
bindable.ApplyTo(actual);
6925

7026
// Assert
71-
using (new AssertionScope())
72-
{
73-
actual.AnrEnabled.Should().Be(expected.AnrEnabled);
74-
actual.AnrReportInDebug.Should().Be(expected.AnrReportInDebug);
75-
actual.AnrTimeoutInterval.Should().Be(expected.AnrTimeoutInterval);
76-
actual.AttachScreenshot.Should().Be(expected.AttachScreenshot);
77-
actual.EnableActivityLifecycleBreadcrumbs.Should().Be(expected.EnableActivityLifecycleBreadcrumbs);
78-
actual.EnableAppComponentBreadcrumbs.Should().Be(expected.EnableAppComponentBreadcrumbs);
79-
actual.EnableAppLifecycleBreadcrumbs.Should().Be(expected.EnableAppLifecycleBreadcrumbs);
80-
actual.EnableRootCheck.Should().Be(expected.EnableRootCheck);
81-
actual.EnableSystemEventBreadcrumbs.Should().Be(expected.EnableSystemEventBreadcrumbs);
82-
actual.EnableUserInteractionBreadcrumbs.Should().Be(expected.EnableUserInteractionBreadcrumbs);
83-
actual.EnableAutoActivityLifecycleTracing.Should().Be(expected.EnableAutoActivityLifecycleTracing);
84-
actual.EnableActivityLifecycleTracingAutoFinish.Should().Be(expected.EnableActivityLifecycleTracingAutoFinish);
85-
actual.EnableUserInteractionTracing.Should().Be(expected.EnableUserInteractionTracing);
86-
actual.AttachThreads.Should().Be(expected.AttachThreads);
87-
actual.ConnectionTimeout.Should().Be(expected.ConnectionTimeout);
88-
actual.EnableNdk.Should().Be(expected.EnableNdk);
89-
actual.EnableShutdownHook.Should().Be(expected.EnableShutdownHook);
90-
actual.EnableUncaughtExceptionHandler.Should().Be(expected.EnableUncaughtExceptionHandler);
91-
actual.PrintUncaughtStackTrace.Should().Be(expected.PrintUncaughtStackTrace);
92-
actual.ReadTimeout.Should().Be(expected.ReadTimeout);
93-
actual.EnableAndroidSdkTracing.Should().Be(expected.EnableAndroidSdkTracing);
94-
actual.EnableAndroidSdkBeforeSend.Should().Be(expected.EnableAndroidSdkBeforeSend);
95-
}
27+
AssertContainsExpectedPropertyValues(actual);
9628
}
97-
#endif
9829
}
30+
#endif

0 commit comments

Comments
 (0)