Skip to content

Commit 6ca427c

Browse files
authored
chore: increase the amount of unit testing (#5)
* chore: increase the amount of unit testing * Fix stack overflow in tests * remove tests from code coverage by exclusion * Update claude file * Further adding tests * Further testing * Fix complete names of tests Improve coverage tests
1 parent 010024f commit 6ca427c

581 files changed

Lines changed: 47091 additions & 1424 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,40 @@ The `--treenode-filter` follows the pattern: `/{AssemblyName}/{Namespace}/{Class
9898
3. Disable `VerifierSettings.AutoVerify()` after accepting
9999
4. Re-run tests to confirm they pass without AutoVerify
100100

101+
### Code Coverage
102+
103+
Code coverage uses **Microsoft.Testing.Extensions.CodeCoverage** configured in `src/testconfig.json`. Coverage is collected for production assemblies only (test projects and TestModels are excluded).
104+
105+
```powershell
106+
# Run tests with code coverage (from src/ folder)
107+
dotnet test --solution ReactiveUI.Binding.SourceGenerators.slnx -c Release -- --coverage --coverage-output-format cobertura
108+
109+
# Generate HTML report using ReportGenerator (install if needed: dotnet tool install -g dotnet-reportgenerator-globaltool)
110+
# Find all cobertura files and generate report to /tmp/<folder>
111+
reportgenerator \
112+
-reports:"tests/**/TestResults/**/*.cobertura.xml" \
113+
-targetdir:/tmp/code_coverage \
114+
-reporttypes:"Html;TextSummary"
115+
116+
# View the text summary
117+
cat /tmp/code_coverage/Summary.txt
118+
119+
# Open HTML report in browser
120+
xdg-open /tmp/code_coverage/index.html # Linux
121+
open /tmp/code_coverage/index.html # macOS
122+
```
123+
124+
**Key configuration** (`src/testconfig.json`):
125+
- `modulePaths.include`: `ReactiveUI\\.Binding\\..*` — covers all production assemblies
126+
- `modulePaths.exclude`: `.*Tests.*`, `.*TestRunner.*`, `.*TestModels.*` — excludes test/runner/model assemblies
127+
- `skipAutoProperties: true` — auto-properties excluded from coverage metrics
128+
129+
**Tips:**
130+
- Always clean `bin/` and `obj/` folders before coverage runs to avoid stale results
131+
- The `ReactiveUI.Binding.GeneratedCode.TestModels` assembly has `[assembly: ExcludeFromCodeCoverage]` so it won't appear in reports even though its module path matches the include pattern
132+
- `DiagnosticWarnings.cs` coverage appears as 0% in `ReactiveUI.Binding.SourceGenerators` — this is a linked-file artifact; the code is actually tested via the `ReactiveUI.Binding.Analyzer` assembly
133+
- Put coverage reports in `/tmp/` to avoid accidentally committing them
134+
101135
## Architecture Overview
102136

103137
### What This Project Does

src/ReactiveUI.Binding.Reactive/ReactiveSchedulerExtensions.cs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace ReactiveUI.Binding;
1313
/// Extension methods for property binding with IScheduler support.
1414
/// Requires the ReactiveUI.Binding.Reactive package for System.Reactive interop.
1515
/// </summary>
16+
[ExcludeFromCodeCoverage]
1617
public static class ReactiveSchedulerExtensions
1718
{
1819
private const string NoGeneratedBindingMessage =
@@ -270,6 +271,7 @@ public static IDisposable BindTwoWay<TSource, TSourceProp, TTarget, TTargetProp>
270271
throw new InvalidOperationException(NoGeneratedBindingMessage);
271272
}
272273

274+
#if NET8_0_OR_GREATER
273275
/// <summary>
274276
/// Creates a one-way binding from a view model property to a view property with a specified selector and scheduler.
275277
/// </summary>
@@ -283,6 +285,8 @@ public static IDisposable BindTwoWay<TSource, TSourceProp, TTarget, TTargetProp>
283285
/// <param name="viewProperty">An expression that selects the view property to update.</param>
284286
/// <param name="selector">A function that converts the view model property value to the view property type.</param>
285287
/// <param name="scheduler">The scheduler to use for the binding.</param>
288+
/// <param name="vmPropertyExpression">The caller argument expression for <paramref name="vmProperty"/>. Auto-populated by the compiler.</param>
289+
/// <param name="viewPropertyExpression">The caller argument expression for <paramref name="viewProperty"/>. Auto-populated by the compiler.</param>
286290
/// <param name="callerFilePath">The source file path of the caller. Auto-populated by the compiler.</param>
287291
/// <param name="callerLineNumber">The source line number of the caller. Auto-populated by the compiler.</param>
288292
/// <returns>A reactive binding that can be disposed to disconnect the binding.</returns>
@@ -293,12 +297,46 @@ public static IReactiveBinding<TView, TOut> OneWayBind<TViewModel, TView, TProp,
293297
Expression<Func<TView, TOut>> viewProperty,
294298
Func<TProp, TOut> selector,
295299
IScheduler? scheduler,
300+
[CallerArgumentExpression("vmProperty")] string vmPropertyExpression = "",
301+
[CallerArgumentExpression("viewProperty")] string viewPropertyExpression = "",
296302
[CallerFilePath] string callerFilePath = "",
297303
[CallerLineNumber] int callerLineNumber = 0)
298304
where TViewModel : class
299305
where TView : class, IViewFor
300-
=> throw new InvalidOperationException(NoGeneratedBindingMessage);
306+
#else
307+
/// <summary>
308+
/// Creates a one-way binding from a view model property to a view property with a specified selector and scheduler.
309+
/// </summary>
310+
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
311+
/// <typeparam name="TView">The type of the view.</typeparam>
312+
/// <typeparam name="TProp">The type of the view model property.</typeparam>
313+
/// <typeparam name="TOut">The type of the view property.</typeparam>
314+
/// <param name="view">The view to bind to.</param>
315+
/// <param name="viewModel">The view model to observe.</param>
316+
/// <param name="vmProperty">An expression that selects the view model property to observe.</param>
317+
/// <param name="viewProperty">An expression that selects the view property to update.</param>
318+
/// <param name="selector">A function that converts the view model property value to the view property type.</param>
319+
/// <param name="scheduler">The scheduler to use for the binding.</param>
320+
/// <param name="callerFilePath">The source file path of the caller. Auto-populated by the compiler.</param>
321+
/// <param name="callerLineNumber">The source line number of the caller. Auto-populated by the compiler.</param>
322+
/// <returns>A reactive binding that can be disposed to disconnect the binding.</returns>
323+
public static IReactiveBinding<TView, TOut> OneWayBind<TViewModel, TView, TProp, TOut>(
324+
this TView view,
325+
TViewModel viewModel,
326+
Expression<Func<TViewModel, TProp>> vmProperty,
327+
Expression<Func<TView, TOut>> viewProperty,
328+
Func<TProp, TOut> selector,
329+
IScheduler? scheduler,
330+
[CallerFilePath] string callerFilePath = "",
331+
[CallerLineNumber] int callerLineNumber = 0)
332+
where TViewModel : class
333+
where TView : class, IViewFor
334+
#endif
335+
{
336+
throw new InvalidOperationException(NoGeneratedBindingMessage);
337+
}
301338

339+
#if NET8_0_OR_GREATER
302340
/// <summary>
303341
/// Creates a two-way binding between a view model property and a view property with conversion functions and a specified scheduler.
304342
/// </summary>
@@ -313,6 +351,8 @@ public static IReactiveBinding<TView, TOut> OneWayBind<TViewModel, TView, TProp,
313351
/// <param name="vmToViewConverter">A function that converts the view model property value to the view property type.</param>
314352
/// <param name="viewToVmConverter">A function that converts the view property value back to the view model property type.</param>
315353
/// <param name="scheduler">The scheduler to use for the binding.</param>
354+
/// <param name="vmPropertyExpression">The caller argument expression for <paramref name="vmProperty"/>. Auto-populated by the compiler.</param>
355+
/// <param name="viewPropertyExpression">The caller argument expression for <paramref name="viewProperty"/>. Auto-populated by the compiler.</param>
316356
/// <param name="callerFilePath">The source file path of the caller. Auto-populated by the compiler.</param>
317357
/// <param name="callerLineNumber">The source line number of the caller. Auto-populated by the compiler.</param>
318358
/// <returns>A reactive binding that can be disposed to disconnect the binding.</returns>
@@ -324,11 +364,46 @@ public static IReactiveBinding<TView, TOut> OneWayBind<TViewModel, TView, TProp,
324364
Func<TVMProp, TVProp> vmToViewConverter,
325365
Func<TVProp, TVMProp> viewToVmConverter,
326366
IScheduler? scheduler,
367+
[CallerArgumentExpression("vmProperty")] string vmPropertyExpression = "",
368+
[CallerArgumentExpression("viewProperty")] string viewPropertyExpression = "",
327369
[CallerFilePath] string callerFilePath = "",
328370
[CallerLineNumber] int callerLineNumber = 0)
329371
where TViewModel : class
330372
where TView : class, IViewFor
331-
=> throw new InvalidOperationException(NoGeneratedBindingMessage);
373+
#else
374+
/// <summary>
375+
/// Creates a two-way binding between a view model property and a view property with conversion functions and a specified scheduler.
376+
/// </summary>
377+
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
378+
/// <typeparam name="TView">The type of the view.</typeparam>
379+
/// <typeparam name="TVMProp">The type of the view model property.</typeparam>
380+
/// <typeparam name="TVProp">The type of the view property.</typeparam>
381+
/// <param name="view">The view to bind to.</param>
382+
/// <param name="viewModel">The view model to observe.</param>
383+
/// <param name="vmProperty">An expression that selects the view model property to observe.</param>
384+
/// <param name="viewProperty">An expression that selects the view property to update.</param>
385+
/// <param name="vmToViewConverter">A function that converts the view model property value to the view property type.</param>
386+
/// <param name="viewToVmConverter">A function that converts the view property value back to the view model property type.</param>
387+
/// <param name="scheduler">The scheduler to use for the binding.</param>
388+
/// <param name="callerFilePath">The source file path of the caller. Auto-populated by the compiler.</param>
389+
/// <param name="callerLineNumber">The source line number of the caller. Auto-populated by the compiler.</param>
390+
/// <returns>A reactive binding that can be disposed to disconnect the binding.</returns>
391+
public static IReactiveBinding<TView, (object? view, bool isViewModel)> Bind<TViewModel, TView, TVMProp, TVProp>(
392+
this TView view,
393+
TViewModel viewModel,
394+
Expression<Func<TViewModel, TVMProp>> vmProperty,
395+
Expression<Func<TView, TVProp>> viewProperty,
396+
Func<TVMProp, TVProp> vmToViewConverter,
397+
Func<TVProp, TVMProp> viewToVmConverter,
398+
IScheduler? scheduler,
399+
[CallerFilePath] string callerFilePath = "",
400+
[CallerLineNumber] int callerLineNumber = 0)
401+
where TViewModel : class
402+
where TView : class, IViewFor
403+
#endif
404+
{
405+
throw new InvalidOperationException(NoGeneratedBindingMessage);
406+
}
332407

333408
/// <summary>
334409
/// Creates a one-way binding from a source property to a target property using an explicit <see cref="IBindingTypeConverter"/>.

src/ReactiveUI.Binding.SourceGenerators/BindingGenerator.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ public class BindingGenerator : IIncrementalGenerator
2222
/// <inheritdoc/>
2323
public void Initialize(IncrementalGeneratorInitializationContext context)
2424
{
25+
// Emit the [ExcludeFromCodeCoverage] attribute on the partial class once,
26+
// so individual dispatch files don't duplicate it.
27+
context.RegisterPostInitializationOutput(static ctx =>
28+
{
29+
const string source = """
30+
// <auto-generated/>
31+
#pragma warning disable
32+
#nullable enable
33+
34+
namespace ReactiveUI.Binding
35+
{
36+
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
37+
internal static partial class __ReactiveUIGeneratedBindings
38+
{
39+
}
40+
}
41+
""";
42+
ctx.AddSource("GeneratedBindingsAttributes.g.cs", source);
43+
});
44+
2545
// Detect whether the consumer project supports CallerArgumentExpression (C# 10+ AND attribute available).
2646
// When supported, invocation generators use expression-text dispatch; otherwise file/line dispatch.
2747
var supportsCallerArgExpr = context.ParseOptionsProvider

src/ReactiveUI.Binding.SourceGenerators/CodeGeneration/BindCodeGenerator.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,8 @@ internal static void GenerateBindMethod(
264264
string vmNext = inv.HasScheduler ? "__vmSelected" : "vmBind";
265265
string viewNext = inv.HasScheduler ? "__viewSelected" : "viewBind";
266266
sb.AppendLine($$"""
267-
var {{vmNext}} = global::ReactiveUI.Binding.Observables.ObservableExtensions.Select({{vmVar}}, vmToViewConverter);
268-
var {{viewNext}} = global::ReactiveUI.Binding.Observables.ObservableExtensions.Select({{viewVar}}, viewToVmConverter);
267+
var {{vmNext}} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({{vmVar}}, vmToViewConverter);
268+
var {{viewNext}} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({{viewVar}}, viewToVmConverter);
269269
""");
270270
vmVar = vmNext;
271271
viewVar = viewNext;
@@ -283,20 +283,20 @@ internal static void GenerateBindMethod(
283283

284284
sb.AppendLine($$"""
285285
286-
var d1 = global::ReactiveUI.Binding.Observables.ObservableExtensions.Subscribe({{vmVar}}, value =>
286+
var d1 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe({{vmVar}}, value =>
287287
{
288288
{{viewPropertyAccess}} = value;
289289
});
290290
291-
var __viewSkipped = global::ReactiveUI.Binding.Observables.ObservableExtensions.Skip({{viewVar}}, 1);
292-
var d2 = global::ReactiveUI.Binding.Observables.ObservableExtensions.Subscribe(__viewSkipped, value =>
291+
var __viewSkipped = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Skip({{viewVar}}, 1);
292+
var d2 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe(__viewSkipped, value =>
293293
{
294294
{{vmSetAccess}} = value;
295295
});
296296
297-
var __vmTagged = global::ReactiveUI.Binding.Observables.ObservableExtensions.Select({{vmVar}}, v => ((object?)v, true));
298-
var __viewTagged = global::ReactiveUI.Binding.Observables.ObservableExtensions.Select(__viewSkipped, v => ((object?)v, false));
299-
var changed = global::ReactiveUI.Binding.Observables.ObservableExtensions.Merge(__vmTagged, __viewTagged);
297+
var __vmTagged = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({{vmVar}}, v => ((object?)v, true));
298+
var __viewTagged = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select(__viewSkipped, v => ((object?)v, false));
299+
var changed = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Merge(__vmTagged, __viewTagged);
300300
301301
var disposable = new global::ReactiveUI.Binding.Observables.CompositeDisposable2(d1, d2);
302302
@@ -313,20 +313,20 @@ internal static void GenerateBindMethod(
313313
{
314314
sb.AppendLine($$"""
315315
316-
var d1 = global::ReactiveUI.Binding.Observables.ObservableExtensions.Subscribe(vmObs, value =>
316+
var d1 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe(vmObs, value =>
317317
{
318318
{{viewPropertyAccess}} = value;
319319
});
320320
321-
var __viewSkipped = global::ReactiveUI.Binding.Observables.ObservableExtensions.Skip(viewObs, 1);
322-
var d2 = global::ReactiveUI.Binding.Observables.ObservableExtensions.Subscribe(__viewSkipped, value =>
321+
var __viewSkipped = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Skip(viewObs, 1);
322+
var d2 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe(__viewSkipped, value =>
323323
{
324324
{{vmSetAccess}} = value;
325325
});
326326
327-
var __vmTagged = global::ReactiveUI.Binding.Observables.ObservableExtensions.Select(vmObs, v => ((object?)v, true));
328-
var __viewTagged = global::ReactiveUI.Binding.Observables.ObservableExtensions.Select(__viewSkipped, v => ((object?)v, false));
329-
var changed = global::ReactiveUI.Binding.Observables.ObservableExtensions.Merge(__vmTagged, __viewTagged);
327+
var __vmTagged = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select(vmObs, v => ((object?)v, true));
328+
var __viewTagged = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select(__viewSkipped, v => ((object?)v, false));
329+
var changed = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Merge(__vmTagged, __viewTagged);
330330
331331
var disposable = new global::ReactiveUI.Binding.Observables.CompositeDisposable2(d1, d2);
332332

src/ReactiveUI.Binding.SourceGenerators/CodeGeneration/BindOneWayCodeGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ internal static void GenerateBindOneWayMethod(
255255
{
256256
string nextVar = inv.HasScheduler ? "__selected" : "bindObs";
257257
sb.AppendLine($$"""
258-
var {{nextVar}} = global::ReactiveUI.Binding.Observables.ObservableExtensions.Select({{currentVar}}, conversionFunc);
258+
var {{nextVar}} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({{currentVar}}, conversionFunc);
259259
""");
260260
currentVar = nextVar;
261261
}
@@ -270,7 +270,7 @@ internal static void GenerateBindOneWayMethod(
270270

271271
sb.AppendLine($$"""
272272
273-
return global::ReactiveUI.Binding.Observables.ObservableExtensions.Subscribe({{currentVar}}, value =>
273+
return global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe({{currentVar}}, value =>
274274
{
275275
{{targetAccess}} = value;
276276
});
@@ -282,7 +282,7 @@ internal static void GenerateBindOneWayMethod(
282282
{
283283
sb.AppendLine($$"""
284284
285-
return global::ReactiveUI.Binding.Observables.ObservableExtensions.Subscribe(sourceObs, value =>
285+
return global::ReactiveUI.Binding.Observables.RxBindingExtensions.Subscribe(sourceObs, value =>
286286
{
287287
{{targetAccess}} = value;
288288
});

0 commit comments

Comments
 (0)