A few fixes following merge from master

This commit is contained in:
Nicolas Musset
2026-06-29 21:04:35 +02:00
parent 0b26daa159
commit 4bd02aae72
7 changed files with 89 additions and 109 deletions
+8 -8
View File
@@ -51,10 +51,10 @@ Global
{78695DE1-E621-45DF-975D-CFD407081E23}.Debug|Any CPU.Build.0 = Debug|Any CPU
{78695DE1-E621-45DF-975D-CFD407081E23}.Release|Any CPU.ActiveCfg = Release|Any CPU
{78695DE1-E621-45DF-975D-CFD407081E23}.Release|Any CPU.Build.0 = Release|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Release|Any CPU.Build.0 = Release|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Release|Any CPU.Build.0 = Release|Any CPU
{BAC8FB10-95ED-4FBB-9925-F6F0E15BD936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BAC8FB10-95ED-4FBB-9925-F6F0E15BD936}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BAC8FB10-95ED-4FBB-9925-F6F0E15BD936}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -107,10 +107,10 @@ Global
{3B613E66-671E-4049-8EF5-43CDAA549210}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B613E66-671E-4049-8EF5-43CDAA549210}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B613E66-671E-4049-8EF5-43CDAA549210}.Release|Any CPU.Build.0 = Release|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{835D46B3-1E85-4D28-9F25-A1BD7A2C30F7}.Release|Any CPU.Build.0 = Release|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A321E09-24D6-4446-8E95-2C29404C8B76}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -16,7 +16,6 @@
<PackageReference Include="System.Drawing.Common" />
<ProjectReference Include="..\..\tests\xunit.runner.stride\xunit.runner.stride.csproj" />
<ProjectReference Include="..\Stride.Core.Yaml\Stride.Core.Yaml.csproj" />
<PackageReference Include="System.Drawing.Common" Condition="'$(TargetFramework)' == '$(StrideFramework)'" />
</ItemGroup>
<Import Project="$(StrideRoot)sources/sdk/Stride.Build.Sdk.Tests/Sdk/Sdk.targets" />
</Project>
@@ -27,6 +27,9 @@ internal sealed class FakeDialogService : IDialogService
public Task<CheckedMessageBoxResult> CheckedMessageBoxAsync(string message, bool? isChecked, string checkboxMessage, IReadOnlyCollection<DialogButtonInfo> buttons, MessageBoxImage image = MessageBoxImage.None)
=> throw new NotImplementedException();
public Task<int> CheckedMessageBoxAsync(string message, IReadOnlyCollection<DialogCheckBoxInfo> checkBoxes, IReadOnlyCollection<DialogButtonInfo> buttons, MessageBoxImage image = MessageBoxImage.None)
=> throw new NotImplementedException();
public Task<UFile?> OpenFilePickerAsync(UDirectory? initialPath = null, IReadOnlyList<FilePickerFilter>? filters = null)
=> throw new NotImplementedException();
@@ -30,13 +30,12 @@ public class ImageComparisonViewModel : ViewModelBase
public string? StatsSummary { get; }
public string Label { get; }
Bitmap? currentBitmap;
public Bitmap? CurrentBitmap
{
get => currentBitmap;
get;
set
{
if (SetProperty(ref currentBitmap, value))
if (SetValue(ref field, value))
{
OnPropertyChanged(nameof(ShowCurrentImage));
OnPropertyChanged(nameof(ShowCurrentPlaceholder));
@@ -44,13 +43,12 @@ public class ImageComparisonViewModel : ViewModelBase
}
}
Bitmap? referenceBitmap;
public Bitmap? ReferenceBitmap
{
get => referenceBitmap;
get;
set
{
if (SetProperty(ref referenceBitmap, value))
if (SetValue(ref field, value))
{
OnPropertyChanged(nameof(ShowReferenceImage));
OnPropertyChanged(nameof(ShowReferencePlaceholder));
@@ -58,13 +56,12 @@ public class ImageComparisonViewModel : ViewModelBase
}
}
WriteableBitmap? diffBitmap;
public WriteableBitmap? DiffBitmap
{
get => diffBitmap;
get;
set
{
if (SetProperty(ref diffBitmap, value))
if (SetValue(ref field, value))
{
OnPropertyChanged(nameof(ShowDiffImage));
OnPropertyChanged(nameof(ShowDiffPassed));
@@ -73,11 +70,11 @@ public class ImageComparisonViewModel : ViewModelBase
}
}
public bool ShowCurrentImage => currentBitmap is not null;
public bool ShowCurrentPlaceholder => currentBitmap is null && Passed;
public bool ShowReferenceImage => referenceBitmap is not null;
public bool ShowReferencePlaceholder => referenceBitmap is null;
public bool ShowDiffImage => diffBitmap is not null;
public bool ShowDiffPassed => diffBitmap is null && Passed;
public bool ShowDiffPlaceholder => diffBitmap is null && !Passed;
public bool ShowCurrentImage => CurrentBitmap is not null;
public bool ShowCurrentPlaceholder => CurrentBitmap is null && Passed;
public bool ShowReferenceImage => ReferenceBitmap is not null;
public bool ShowReferencePlaceholder => ReferenceBitmap is null;
public bool ShowDiffImage => DiffBitmap is not null;
public bool ShowDiffPassed => DiffBitmap is null && Passed;
public bool ShowDiffPlaceholder => DiffBitmap is null && !Passed;
}
@@ -50,35 +50,32 @@ public class TestCaseViewModel : TestNodeViewModel
}
}
string? failureMessage;
/// <summary>Combined exception messages from the most recent run (newline-joined). Empty if the test passed or hasn't run.</summary>
public string? FailureMessage
{
get => failureMessage;
set => SetProperty(ref failureMessage, value);
get;
set => SetValue(ref field, value);
}
string? failureStackTrace;
/// <summary>Combined stack traces from the most recent run. Empty if the test passed or hasn't run.</summary>
public string? FailureStackTrace
{
get => failureStackTrace;
set => SetProperty(ref failureStackTrace, value);
get;
set => SetValue(ref field, value);
}
string? output;
/// <summary>Captured stdout/stderr produced during the most recent run.</summary>
public string? Output
{
get => output;
get;
set
{
SetProperty(ref output, value);
SetValue(ref field, value);
OnPropertyChanged(nameof(HasOutput));
}
}
public bool HasOutput => !string.IsNullOrEmpty(output);
public bool HasOutput => !string.IsNullOrEmpty(Output);
// === Image comparison (populated from ImageComparisonCompleted event in GameTestBase) ===
@@ -11,89 +11,81 @@ public abstract class TestNodeViewModel : ViewModelBase
public abstract TestCaseViewModel? LocateTestCase(ITestCase testCase);
bool running;
public bool Running
{
get => running;
set { if (SetProperty(ref running, value)) OnPropertyChanged(nameof(HasResult)); }
get;
set { if (SetValue(ref field, value)) OnPropertyChanged(nameof(HasResult)); }
}
bool failed;
public bool Failed
{
get => failed;
set { if (SetProperty(ref failed, value)) OnPropertyChanged(nameof(HasResult)); }
get;
set { if (SetValue(ref field, value)) OnPropertyChanged(nameof(HasResult)); }
}
bool succeeded;
public bool Succeeded
{
get => succeeded;
set { if (SetProperty(ref succeeded, value)) OnPropertyChanged(nameof(HasResult)); }
get;
set { if (SetValue(ref field, value)) OnPropertyChanged(nameof(HasResult)); }
}
bool skipped;
public bool Skipped
{
get => skipped;
set { if (SetProperty(ref skipped, value)) OnPropertyChanged(nameof(HasResult)); }
get;
set { if (SetValue(ref field, value)) OnPropertyChanged(nameof(HasResult)); }
}
bool pending;
/// <summary>True after the test was queued for a run but before it actually starts;
/// drives the "…" icon so multi-test runs show what's still waiting.</summary>
public bool Pending
{
get => pending;
set { if (SetProperty(ref pending, value)) OnPropertyChanged(nameof(HasResult)); }
get;
set { if (SetValue(ref field, value)) OnPropertyChanged(nameof(HasResult)); }
}
/// <summary>True once the test has any kind of result (running, queued, or finished).
/// The detail view shows a big "Run" call-to-action when this is false.</summary>
public bool HasResult => running || failed || succeeded || skipped || pending || elapsedSeconds.HasValue;
public bool HasResult => Running || Failed || Succeeded || Skipped || Pending || ElapsedSeconds.HasValue;
decimal? elapsedSeconds;
/// <summary>Last execution time in seconds. <see langword="null"/> if not yet run.</summary>
public decimal? ElapsedSeconds
{
get => elapsedSeconds;
get;
set
{
SetProperty(ref elapsedSeconds, value);
SetValue(ref field, value);
OnPropertyChanged(nameof(ElapsedDisplay));
OnPropertyChanged(nameof(HasElapsed));
OnPropertyChanged(nameof(HasResult));
}
}
public bool HasElapsed => elapsedSeconds.HasValue;
public bool HasElapsed => ElapsedSeconds.HasValue;
public string ElapsedDisplay
{
get
{
if (!elapsedSeconds.HasValue) return string.Empty;
var ms = (double)elapsedSeconds.Value * 1000.0;
if (!ElapsedSeconds.HasValue) return string.Empty;
var ms = (double)ElapsedSeconds.Value * 1000.0;
return ms < 1000.0 ? $"{ms:F0} ms" : $"{ms / 1000.0:F2} s";
}
}
bool isVisible = true;
/// <summary>Whether this node passes the current filter. Group nodes are visible if any descendant is visible.</summary>
public bool IsVisible
{
get => isVisible;
set => SetProperty(ref isVisible, value);
}
get;
set => SetValue(ref field, value);
} = true;
bool isPrimarySelection;
/// <summary>True for the focused row in a multi-selection — the one shown in the inspect
/// pane. Drives a small left accent stripe so the primary stands out from the rest of
/// the selection.</summary>
public bool IsPrimarySelection
{
get => isPrimarySelection;
set => SetProperty(ref isPrimarySelection, value);
get;
set => SetValue(ref field, value);
}
public abstract string DisplayName { get; }
@@ -375,24 +375,22 @@ public class TestsViewModel : ViewModelBase
// === Filter ===
string filterText = string.Empty;
public string FilterText
{
get => filterText;
get;
set
{
if (SetProperty(ref filterText, value ?? string.Empty))
if (SetValue(ref field, value ?? string.Empty))
ApplyFilter();
}
}
} = string.Empty;
StatusFilter statusFilter = StatusFilter.All;
public StatusFilter StatusFilter
{
get => statusFilter;
get;
set
{
if (SetProperty(ref statusFilter, value))
if (SetValue(ref field, value))
{
OnPropertyChanged(nameof(StatusFilterIsAll));
OnPropertyChanged(nameof(StatusFilterIsPassed));
@@ -402,38 +400,38 @@ public class TestsViewModel : ViewModelBase
ApplyFilter();
}
}
}
} = StatusFilter.All;
// ToggleButton bindings (one-way set true; user can't un-toggle the active one)
public bool StatusFilterIsAll
{
get => statusFilter == StatusFilter.All;
get => StatusFilter == StatusFilter.All;
set { if (value) StatusFilter = StatusFilter.All; else OnPropertyChanged(nameof(StatusFilterIsAll)); }
}
public bool StatusFilterIsPassed
{
get => statusFilter == StatusFilter.Passed;
get => StatusFilter == StatusFilter.Passed;
set { if (value) StatusFilter = StatusFilter.Passed; else OnPropertyChanged(nameof(StatusFilterIsPassed)); }
}
public bool StatusFilterIsFailed
{
get => statusFilter == StatusFilter.Failed;
get => StatusFilter == StatusFilter.Failed;
set { if (value) StatusFilter = StatusFilter.Failed; else OnPropertyChanged(nameof(StatusFilterIsFailed)); }
}
public bool StatusFilterIsSkipped
{
get => statusFilter == StatusFilter.Skipped;
get => StatusFilter == StatusFilter.Skipped;
set { if (value) StatusFilter = StatusFilter.Skipped; else OnPropertyChanged(nameof(StatusFilterIsSkipped)); }
}
public bool StatusFilterIsNotRun
{
get => statusFilter == StatusFilter.NotRun;
get => StatusFilter == StatusFilter.NotRun;
set { if (value) StatusFilter = StatusFilter.NotRun; else OnPropertyChanged(nameof(StatusFilterIsNotRun)); }
}
public void ApplyFilter()
{
var needle = filterText.Trim();
var needle = FilterText.Trim();
foreach (var root in TestCases)
ApplyFilter(root, needle);
}
@@ -451,7 +449,7 @@ public class TestsViewModel : ViewModelBase
else if (node is TestCaseViewModel testCase)
{
bool matchesText = needle.Length == 0 || testCase.DisplayName.Contains(needle, StringComparison.OrdinalIgnoreCase);
bool matchesStatus = statusFilter switch
bool matchesStatus = StatusFilter switch
{
StatusFilter.All => true,
StatusFilter.Passed => testCase.Succeeded,
@@ -468,19 +466,18 @@ public class TestsViewModel : ViewModelBase
// === Counts / summary ===
int passedCount, failedCount, skippedCount, notRunCount, totalCount;
decimal totalElapsedSeconds;
public int PassedCount { get => passedCount; private set => SetProperty(ref passedCount, value); }
public int FailedCount { get => failedCount; private set => SetProperty(ref failedCount, value); }
public int SkippedCount { get => skippedCount; private set => SetProperty(ref skippedCount, value); }
public int NotRunCount { get => notRunCount; private set => SetProperty(ref notRunCount, value); }
public int TotalCount { get => totalCount; private set => SetProperty(ref totalCount, value); }
public int PassedCount { get; private set => SetValue(ref field, value); }
public int FailedCount { get; private set => SetValue(ref field, value); }
public int SkippedCount { get; private set => SetValue(ref field, value); }
public int NotRunCount { get; private set => SetValue(ref field, value); }
public int TotalCount { get; private set => SetValue(ref field, value); }
public decimal TotalElapsedSeconds
{
get => totalElapsedSeconds;
get;
private set
{
if (SetProperty(ref totalElapsedSeconds, value))
if (SetValue(ref field, value))
OnPropertyChanged(nameof(TotalElapsedDisplay));
}
}
@@ -489,15 +486,15 @@ public class TestsViewModel : ViewModelBase
{
get
{
var s = (double)totalElapsedSeconds;
var s = (double)TotalElapsedSeconds;
return s < 60 ? $"{s:F2}s" : $"{(int)(s / 60)}m {s % 60:F1}s";
}
}
public bool HasFailures => failedCount > 0;
public bool HasFailures => FailedCount > 0;
/// <summary>True when there's at least one failed test AND no run in progress; drives the "Re-run failed" button.</summary>
public bool CanRunFailed => failedCount > 0 && !runningTests;
public bool CanRunFailed => FailedCount > 0 && !RunningTests;
void RecomputeCounts()
{
@@ -545,20 +542,18 @@ public class TestsViewModel : ViewModelBase
// === State ===
double testCompletion;
public double TestCompletion
{
get => testCompletion;
set => SetProperty(ref testCompletion, value);
get;
set => SetValue(ref field, value);
}
bool runningTests;
public bool RunningTests
{
get => runningTests;
get;
set
{
if (SetProperty(ref runningTests, value))
if (SetValue(ref field, value))
OnPropertyChanged(nameof(CanRunFailed));
}
}
@@ -571,15 +566,14 @@ public class TestsViewModel : ViewModelBase
/// (typically <c>GameTestBase.RenderDocMode</c>). Accepts <c>null</c> ("never"), "error", "always".</summary>
public Action<string?>? SetRenderDocMode { get; set; }
bool isForceSaveImage;
/// <summary>Persistent toggle: when on, every run sets <c>ForceSaveImageOnSuccess</c> so
/// the rendered output is written to disk even when the gold comparison passes.</summary>
public bool IsForceSaveImage
{
get => isForceSaveImage;
get;
set
{
if (SetProperty(ref isForceSaveImage, value))
if (SetValue(ref field, value))
SetForceSaveImage?.Invoke(value);
}
}
@@ -592,7 +586,7 @@ public class TestsViewModel : ViewModelBase
get => renderDocMode;
set
{
if (SetProperty(ref renderDocMode, value))
if (SetValue(ref renderDocMode, value))
{
var s = value switch
{
@@ -617,15 +611,14 @@ public class TestsViewModel : ViewModelBase
// === Mobile / narrow-viewport state ===
bool isNarrowMode;
/// <summary>True when the main split is in stacked (phone/portrait) mode. Drives the
/// drill-down nav (tap-to-detail / back) and hides per-row run buttons.</summary>
public bool IsNarrowMode
{
get => isNarrowMode;
get;
set
{
if (SetProperty(ref isNarrowMode, value))
if (SetValue(ref field, value))
{
OnPropertyChanged(nameof(IsWideMode));
OnPropertyChanged(nameof(RowMinHeight));
@@ -637,18 +630,17 @@ public class TestsViewModel : ViewModelBase
/// <summary>Inverse of <see cref="IsNarrowMode"/>; convenience for per-row "show on wide
/// only" visibility bindings.</summary>
public bool IsWideMode => !isNarrowMode;
public bool IsWideMode => !IsNarrowMode;
/// <summary>Minimum tree-row height; bumped in narrow mode to give touch targets enough
/// surface to be reliably tappable.</summary>
public double RowMinHeight => isNarrowMode ? 44 : 0;
public double RowMinHeight => IsNarrowMode ? 44 : 0;
bool isDetailPageActive;
/// <summary>In narrow mode, true means the detail pane is showing instead of the list.
/// Ignored in wide mode.</summary>
public bool IsDetailPageActive
{
get => isDetailPageActive;
set => SetProperty(ref isDetailPageActive, value);
get;
set => SetValue(ref field, value);
}
}