diff --git a/src/SizeBench.GUI.Tests/Converters/TwoStringsToDiffViewerUIConverterTests.cs b/src/SizeBench.GUI.Tests/Converters/TwoStringsToDiffViewerUIConverterTests.cs index 8857eb9..2ed2b52 100644 --- a/src/SizeBench.GUI.Tests/Converters/TwoStringsToDiffViewerUIConverterTests.cs +++ b/src/SizeBench.GUI.Tests/Converters/TwoStringsToDiffViewerUIConverterTests.cs @@ -1,4 +1,5 @@ using System.Windows; +using SizeBench.GUI.Controls; namespace SizeBench.GUI.Converters.Tests; #nullable disable // WPF's IValueConverter is not correctly nullable-annotated, so we disable nullable for the source and tests of the value converters. @@ -11,10 +12,10 @@ public void TargetTypeMustBeObject() => Assert.ThrowsExactly(() => TwoStringsToDiffViewerUIConverter.Instance.Convert(new object[] { 5 }, typeof(int), null /* ConverterParameter */, null /* CultureInfo */)); [TestMethod] - public void MoreThanTwoInputsNotAccepted() + public void MoreThanThreeInputsNotAccepted() { - Assert.ThrowsExactly(() => TwoStringsToDiffViewerUIConverter.Instance.Convert(new object[] { "disasm 1", "disasm 2", "disasm 3" }, typeof(object), null /* ConverterParameter */, null /* CultureInfo */)); Assert.ThrowsExactly(() => TwoStringsToDiffViewerUIConverter.Instance.Convert(new object[] { "disasm 1", "disasm 2", "disasm 3", "disasm 4" }, typeof(object), null /* ConverterParameter */, null /* CultureInfo */)); + Assert.ThrowsExactly(() => TwoStringsToDiffViewerUIConverter.Instance.Convert(new object[] { "disasm 1", "disasm 2", "disasm 3", "disasm 4", "disasm 5" }, typeof(object), null /* ConverterParameter */, null /* CultureInfo */)); } [TestMethod] @@ -34,6 +35,10 @@ public void IfEitherInputIsNotAStringThrow() Assert.ThrowsExactly(() => TwoStringsToDiffViewerUIConverter.Instance.Convert(new object[] { 0.8f, "disasm 2" }, typeof(object), null /* ConverterParameter */, null /* CultureInfo */)); } + [TestMethod] + public void ThirdArgumentMustBeZoomPercentInt() + => Assert.ThrowsExactly(() => TwoStringsToDiffViewerUIConverter.Instance.Convert(new object[] { "disasm 1", "disasm 2", "100" }, typeof(object), null /* ConverterParameter */, null /* CultureInfo */)); + [TestMethod] public void IfBothInputsAreStringsReturnsSomething() { @@ -45,4 +50,11 @@ public void IfBothInputsAreStringsReturnsSomething() [TestMethod] public void ConvertBackThrows() => Assert.ThrowsExactly(() => TwoStringsToDiffViewerUIConverter.Instance.ConvertBack(new FrameworkElement(), new Type[] { typeof(string), typeof(string) }, null /* ConverterParameter */, null /* CultureInfo */)); + + [TestMethod] + public void ThirdArgumentControlsFontSize() + { + var diffViewer = (SelectableDiffViewer)TwoStringsToDiffViewerUIConverter.Instance.Convert(new object[] { "disasm 1", "disasm 2", 120 }, typeof(object), null /* ConverterParameter */, null /* CultureInfo */); + Assert.AreEqual(19.2, diffViewer.EffectiveFontSize, 0.001); + } } diff --git a/src/SizeBench.GUI.Tests/DisassemblySettingsStoreTests.cs b/src/SizeBench.GUI.Tests/DisassemblySettingsStoreTests.cs new file mode 100644 index 0000000..48cfd2c --- /dev/null +++ b/src/SizeBench.GUI.Tests/DisassemblySettingsStoreTests.cs @@ -0,0 +1,33 @@ +using System.IO; +using SizeBench.TestInfrastructure; + +namespace SizeBench.GUI.Tests; + +[TestClass] +public sealed class DisassemblySettingsStoreTests +{ + [TestMethod] + public void ZoomPercentPersistsAcrossStoreInstances() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + try + { + var storagePath = Path.Combine(tempDirectory, "DisassemblySettings.json"); + using var logger = new TestNoOpApplicationLogger(); + var store = new DisassemblySettingsStore(logger, storagePath); + + Assert.AreEqual(DisassemblySettingsStore.DefaultZoomPercent, store.TemplateFoldabilityDisassemblyZoomPercent); + + store.TemplateFoldabilityDisassemblyZoomPercent = 140; + + var reloadedStore = new DisassemblySettingsStore(logger, storagePath); + Assert.AreEqual(140, reloadedStore.TemplateFoldabilityDisassemblyZoomPercent); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } +} diff --git a/src/SizeBench.GUI.Tests/MainWindowViewModelTests.cs b/src/SizeBench.GUI.Tests/MainWindowViewModelTests.cs index ac805a5..999f75c 100644 --- a/src/SizeBench.GUI.Tests/MainWindowViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/MainWindowViewModelTests.cs @@ -1,4 +1,5 @@ -using Castle.Windsor; +using System.IO; +using Castle.Windsor; using SizeBench.AnalysisEngine; using SizeBench.Logging; using SizeBench.TestInfrastructure; @@ -13,12 +14,19 @@ public sealed class MainWindowViewModelTests : IDisposable public IWindsorContainer WindsorContainer = new WindsorContainer(); public Mock MockSession = new Mock(); public Mock MockSessionFactory = new Mock(); + private Mock MockRecentSessionStore = new Mock(); + private Mock MockRecentSessionLauncher = new Mock(); [TestInitialize] public void TestInitialize() { this.MockSession = new Mock(); this.MockSessionFactory = new Mock(); + this.MockRecentSessionStore = new Mock(); + this.MockRecentSessionLauncher = new Mock(); + this.MockRecentSessionStore.Setup(store => store.GetRecentSessions()).Returns(Array.Empty()); + this.MockRecentSessionStore.Setup(store => store.RecordSession(It.IsAny())) + .Returns(recentSession => recentSession); this.WindsorContainer = new WindsorContainer(); } @@ -32,7 +40,9 @@ public void DeeplinkWithOnlyBinaryPathDoesNothing() using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); this.MockSessionFactory.Verify(sf => sf.CreateSession(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } @@ -46,7 +56,9 @@ public void DeeplinkWithOnlyPDBPathDoesNothing() using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); this.MockSessionFactory.Verify(sf => sf.CreateSession(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } @@ -63,9 +75,16 @@ public async Task DeeplinkWithBothPathsOpensSession() using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); await vm.TryResolveDeeplink(new Uri($"sizebench://2.0/Test?BinaryPath={Uri.EscapeDataString(expectedBinaryPath)}&PDBPath={Uri.EscapeDataString(expectedPDBPath)}")); this.MockSessionFactory.Verify(sf => sf.CreateSession(expectedBinaryPath, expectedPDBPath, It.IsAny(), It.IsAny()), Times.Exactly(1)); + this.MockRecentSessionStore.Verify(store => store.RecordSession(It.Is(recentSession => + recentSession.Kind == RecentSessionKind.SingleBinary && + recentSession.BinaryPath == expectedBinaryPath && + recentSession.PDBPath == expectedPDBPath)), + Times.Exactly(1)); } [TestMethod] @@ -83,7 +102,9 @@ public async Task DeeplinkWithBothPathsAndInAppPageGoesDeep() using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); var deeplinkResolutionTask = vm.TryResolveDeeplink(new Uri($"sizebench://2.0/{Uri.EscapeDataString(expectedInAppPage)}?BinaryPath={Uri.EscapeDataString(expectedBinaryPath)}&PDBPath={Uri.EscapeDataString(expectedPDBPath)}")); Assert.IsEmpty(vm.OpenTabs); @@ -110,7 +131,9 @@ public void DeeplinkToDiffWithOnlyBeforeAndAfterBinaryPathsDoesNothing() using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); this.MockSessionFactory.Verify(sf => sf.CreateSession(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); this.MockSessionFactory.Verify(sf => sf.CreateDiffSession(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } @@ -125,7 +148,9 @@ public void DeeplinkToDiffWithOneBinaryPathAndBeforeAndAfterPDBPathsDoesNothing( using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); this.MockSessionFactory.Verify(sf => sf.CreateSession(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); this.MockSessionFactory.Verify(sf => sf.CreateDiffSession(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } @@ -149,13 +174,22 @@ public async Task DeeplinkToDiffWithAllFourPathsOpensDiffSession() using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); await vm.TryResolveDeeplink(new Uri($"sizebench://2.0/Test?BeforeBinaryPath={Uri.EscapeDataString(expectedBeforeBinaryPath)}" + $"&BeforePDBPath={Uri.EscapeDataString(expectedBeforePDBPath)}" + $"&AfterBinaryPath={Uri.EscapeDataString(expectedAfterBinaryPath)}" + $"&AfterPDBPath={Uri.EscapeDataString(expectedAfterPDBPath)}")); this.MockSessionFactory.Verify(sf => sf.CreateSession(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); this.MockSessionFactory.Verify(sf => sf.CreateDiffSession(expectedBeforeBinaryPath, expectedBeforePDBPath, expectedAfterBinaryPath, expectedAfterPDBPath, It.IsAny()), Times.Exactly(1)); + this.MockRecentSessionStore.Verify(store => store.RecordSession(It.Is(recentSession => + recentSession.Kind == RecentSessionKind.BinaryDiff && + recentSession.BeforeBinaryPath == expectedBeforeBinaryPath && + recentSession.BeforePdbPath == expectedBeforePDBPath && + recentSession.AfterBinaryPath == expectedAfterBinaryPath && + recentSession.AfterPdbPath == expectedAfterPDBPath)), + Times.Exactly(1)); } [TestMethod] @@ -179,7 +213,9 @@ public async Task DeeplinkToDiffWithAllFourPathsAndInAppPageGoesDeep() using var appLogger = new TestNoOpApplicationLogger(); var vm = new MainWindowViewModel(container, appLogger, - this.MockSessionFactory.Object); + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); var deeplinkResolutionTask = vm.TryResolveDeeplink(new Uri($"sizebench://2.0/{Uri.EscapeDataString(expectedInAppPage)}?" + $"BeforeBinaryPath={Uri.EscapeDataString(expectedBeforeBinaryPath)}" + @@ -202,5 +238,136 @@ public async Task DeeplinkToDiffWithAllFourPathsAndInAppPageGoesDeep() Assert.AreEqual(new Uri(expectedInAppPage, UriKind.Relative), vm.SelectedTab!.CurrentPage); } + [TestMethod] + public void ConstructorLoadsRecentSessionsFromStore() + { + var expectedRecentSessions = new[] + { + RecentSession.CreateSingle(@"c:\dev\foo.dll", @"c:\dev\foo.pdb", new SessionOptions()), + RecentSession.CreateDiff(@"c:\dev\before.dll", @"c:\dev\before.pdb", @"c:\dev\after.dll", @"c:\dev\after.pdb") + }; + + this.MockRecentSessionStore.Setup(store => store.GetRecentSessions()).Returns(expectedRecentSessions); + + using var container = new WindsorContainer(); + using var appLogger = new TestNoOpApplicationLogger(); + var vm = new MainWindowViewModel(container, + appLogger, + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); + + CollectionAssert.AreEqual(expectedRecentSessions, vm.RecentSessions.ToArray()); + Assert.IsTrue(vm.HasRecentSessions); + } + + [TestMethod] + public async Task OpenRecentSingleBinarySessionCreatesTab() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + try + { + var binaryPath = Path.Combine(tempDirectory, "foo.dll"); + var pdbPath = Path.Combine(tempDirectory, "foo.pdb"); + await File.WriteAllTextAsync(binaryPath, String.Empty); + await File.WriteAllTextAsync(pdbPath, String.Empty); + + var recentSession = RecentSession.CreateSingle(binaryPath, + pdbPath, + new SessionOptions() { SymbolSourcesSupported = SymbolSourcesSupported.Code }); + + this.MockSessionFactory.Setup(sf => sf.CreateSession(binaryPath, pdbPath, It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Mock().Object)); + + using var container = new WindsorContainer(); + using var appLogger = new TestNoOpApplicationLogger(); + var vm = new MainWindowViewModel(container, + appLogger, + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); + + await vm.OpenRecentSession(recentSession); + + this.MockSessionFactory.Verify(sf => sf.CreateSession(binaryPath, + pdbPath, + It.Is(options => options.SymbolSourcesSupported == SymbolSourcesSupported.Code), + It.IsAny()), + Times.Exactly(1)); + Assert.AreEqual(1, vm.OpenTabs.Count); + Assert.IsInstanceOfType(vm.OpenTabs[0], typeof(SingleBinaryTab)); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + [TestMethod] + public async Task DeeplinkWithBothPathsAndSymbolSourcesSupportedUsesProvidedSessionOptions() + { + var expectedBinaryPath = @"c:\dev\blah.dll"; + var expectedPDBPath = @"c:\dev\other\blah.pdb"; + + this.MockSessionFactory.Setup(sf => sf.CreateSession(expectedBinaryPath, expectedPDBPath, It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Mock().Object)); + + using var container = new WindsorContainer(); + using var appLogger = new TestNoOpApplicationLogger(); + var vm = new MainWindowViewModel(container, + appLogger, + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); + + await vm.TryResolveDeeplink(new Uri($"sizebench://2.0/Test?BinaryPath={Uri.EscapeDataString(expectedBinaryPath)}&PDBPath={Uri.EscapeDataString(expectedPDBPath)}&SymbolSourcesSupported={(int)SymbolSourcesSupported.Code}")); + + this.MockSessionFactory.Verify(sf => sf.CreateSession(expectedBinaryPath, + expectedPDBPath, + It.Is(options => options.SymbolSourcesSupported == SymbolSourcesSupported.Code), + It.IsAny()), + Times.Exactly(1)); + } + + [TestMethod] + public void OpenRecentSessionInNewWindowUsesLauncher() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + try + { + var binaryPath = Path.Combine(tempDirectory, "foo.dll"); + var pdbPath = Path.Combine(tempDirectory, "foo.pdb"); + File.WriteAllText(binaryPath, String.Empty); + File.WriteAllText(pdbPath, String.Empty); + + var recentSession = RecentSession.CreateSingle(binaryPath, + pdbPath, + new SessionOptions() { SymbolSourcesSupported = SymbolSourcesSupported.Code }); + + using var container = new WindsorContainer(); + using var appLogger = new TestNoOpApplicationLogger(); + var vm = new MainWindowViewModel(container, + appLogger, + this.MockSessionFactory.Object, + this.MockRecentSessionStore.Object, + this.MockRecentSessionLauncher.Object); + + vm.OpenRecentSessionInNewWindowCommand.Execute(recentSession); + + this.MockRecentSessionLauncher.Verify(launcher => launcher.LaunchRecentSession(It.Is(session => session.Matches(recentSession))), + Times.Exactly(1)); + this.MockSessionFactory.Verify(sf => sf.CreateSession(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + this.MockSessionFactory.Verify(sf => sf.CreateDiffSession(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + public void Dispose() => this.WindsorContainer.Dispose(); } diff --git a/src/SizeBench.GUI.Tests/Pages/BinarySectionDiffPageViewModelTests.cs b/src/SizeBench.GUI.Tests/Pages/BinarySectionDiffPageViewModelTests.cs index b6bbfa8..8fc69fa 100644 --- a/src/SizeBench.GUI.Tests/Pages/BinarySectionDiffPageViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Pages/BinarySectionDiffPageViewModelTests.cs @@ -237,6 +237,7 @@ public async Task SymbolsInitializeWhenTabSelected() await tcsTestResultsComplete.Task; Assert.AreSequenceEqual(allSymbolDiffsInBinarySection, viewmodel.Symbols, Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InAnyOrder); + Assert.AreEqual(allSymbolDiffsInBinarySection.Count, viewmodel.FilteredSymbolDiffs!.Cast().Count()); // We should have started 2 long-running tasks, one for the binary section load and one for the symbols this.MockUITaskScheduler.Verify(uits => uits.StartLongRunningUITask(It.IsAny(), It.IsAny>()), Times.Exactly(2)); @@ -336,7 +337,10 @@ public async Task ExcelExportWorksForEveryTab() viewmodel.SelectedTab = (int)BinarySectionPageViewModel.BinarySectionPageTabIndex.SymbolsTab; Assert.IsTrue(viewmodel.ExportSymbolsToExcelCommand.CanExecute()); viewmodel.ExportSymbolsToExcelCommand.Execute(); - this.MockUITaskScheduler.Verify(uits => uits.StartExcelExport(this.MockExcelExporter.Object, allSymbolDiffsInBinarySection), Times.Exactly(1)); + this.MockUITaskScheduler.Verify(uits => uits.StartExcelExport(this.MockExcelExporter.Object, + It.Is>(symbols => symbols.Count == allSymbolDiffsInBinarySection.Count && + !symbols.Except(allSymbolDiffsInBinarySection).Any())), + Times.Exactly(1)); } public void Dispose() => this.TestDataGenerator.Dispose(); diff --git a/src/SizeBench.GUI.Tests/Pages/COFFGroupDiffPageViewModelTests.cs b/src/SizeBench.GUI.Tests/Pages/COFFGroupDiffPageViewModelTests.cs index 1121455..56a7bc3 100644 --- a/src/SizeBench.GUI.Tests/Pages/COFFGroupDiffPageViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Pages/COFFGroupDiffPageViewModelTests.cs @@ -96,7 +96,7 @@ private async Task AssertInitialStateThenFillInAsyncLoads(COFFGroupDiffPageViewM sawCompilandsPropertyChange && sawLibsPropertyChange) { - tcsTestResultsComplete.SetResult(new object()); + tcsTestResultsComplete.TrySetResult(new object()); } }; @@ -153,6 +153,7 @@ await AssertInitialStateThenFillInAsyncLoads(viewmodel, Assert.AreEqual("COFFGroupContributionDiffsByName[.text$mn].SizeDiff", viewmodel.ContributionSizeSortMemberPath); Assert.AreEqual("COFFGroupContributionDiffsByName[.text$mn].VirtualSizeDiff", viewmodel.ContributionVirtualSizeSortMemberPath); Assert.IsTrue(ReferenceEquals(symbolList, viewmodel.SymbolDiffs)); + Assert.AreEqual(symbolList.Count, viewmodel.FilteredSymbolDiffs!.Cast().Count()); // Verify libs are filtered to the CG Assert.HasCount(4, this.TestDataGenerator.LibDiffs); @@ -168,6 +169,66 @@ await AssertInitialStateThenFillInAsyncLoads(viewmodel, [Timeout(5 * 1000, CooperativeCancellation = true)] [TestMethod] + public async Task SymbolFiltersCanShowBaselineOnlyUpdateOnlyAndModified() + { + var textMnCGDiff = this.TestDataGenerator.TextMnCGDiff; + var viewmodel = CreateViewmodelForTesting(textMnCGDiff, + out var tcsTestResultsComplete, + out var symbolList, + out var tcsCOFFGroupReady, + out var tcsSymbolsReady, + out var tcsLibsReady, + out var tcsCompilandsReady); + + viewmodel.SetQueryString(new Dictionary() + { + { "COFFGroup", ".text$mn" } + }); + + var initTask = viewmodel.InitializeAsync(); + + await AssertInitialStateThenFillInAsyncLoads(viewmodel, + tcsTestResultsComplete, + symbolList, + tcsCOFFGroupReady, + tcsSymbolsReady, + tcsLibsReady, + tcsCompilandsReady); + + await initTask; + + var baselineOnlyCount = symbolList.Count(sd => sd.BeforeSymbol != null && sd.AfterSymbol is null); + var updateOnlyCount = symbolList.Count(sd => sd.BeforeSymbol is null && sd.AfterSymbol != null); + var modifiedCount = symbolList.Count(sd => sd.BeforeSymbol != null && sd.AfterSymbol != null && (sd.SizeDiff != 0 || sd.VirtualSizeDiff != 0)); + var identicalCount = symbolList.Count(sd => sd.BeforeSymbol != null && sd.AfterSymbol != null && sd.SizeDiff == 0 && sd.VirtualSizeDiff == 0); + + viewmodel.IncludeModifiedSymbols = false; + viewmodel.IncludeSymbolsOnlyInUpdate = false; + CollectionAssert.AreEquivalent(symbolList.Where(sd => sd.BeforeSymbol != null && sd.AfterSymbol is null).ToList(), + viewmodel.FilteredSymbolDiffs!.Cast().ToList()); + Assert.AreEqual(baselineOnlyCount, viewmodel.FilteredSymbolDiffs!.Cast().Count()); + + viewmodel.IncludeSymbolsOnlyInBaseline = false; + viewmodel.IncludeModifiedSymbols = true; + CollectionAssert.AreEquivalent(symbolList.Where(sd => sd.BeforeSymbol != null && sd.AfterSymbol != null).ToList(), + viewmodel.FilteredSymbolDiffs.Cast().ToList()); + Assert.AreEqual(modifiedCount, viewmodel.FilteredSymbolDiffs!.Cast().Count()); + + viewmodel.IncludeModifiedSymbols = false; + viewmodel.IncludeIdenticalSymbols = true; + CollectionAssert.AreEquivalent(symbolList.Where(sd => sd.BeforeSymbol != null && sd.AfterSymbol != null && sd.SizeDiff == 0 && sd.VirtualSizeDiff == 0).ToList(), + viewmodel.FilteredSymbolDiffs.Cast().ToList()); + Assert.AreEqual(identicalCount, viewmodel.FilteredSymbolDiffs.Cast().Count()); + + viewmodel.IncludeIdenticalSymbols = false; + viewmodel.IncludeSymbolsOnlyInUpdate = true; + CollectionAssert.AreEquivalent(symbolList.Where(sd => sd.BeforeSymbol is null && sd.AfterSymbol != null).ToList(), + viewmodel.FilteredSymbolDiffs.Cast().ToList()); + Assert.AreEqual(updateOnlyCount, viewmodel.FilteredSymbolDiffs.Cast().Count()); + } + + [Timeout(5 * 1000)] + [TestMethod] public async Task CancelingSymbolLoadingIsFine() { var textMnCGDiff = this.TestDataGenerator.TextMnCGDiff; diff --git a/src/SizeBench.GUI.Tests/Pages/CompilandDiffPageViewModelTests.cs b/src/SizeBench.GUI.Tests/Pages/CompilandDiffPageViewModelTests.cs index 4572949..193f044 100644 --- a/src/SizeBench.GUI.Tests/Pages/CompilandDiffPageViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Pages/CompilandDiffPageViewModelTests.cs @@ -49,20 +49,24 @@ public async Task SymbolsInitializeAsync() }); var initTask = viewmodel.InitializeAsync(); - var propertyNameResult = String.Empty; + var sawSymbolDiffsPropertyChange = false; viewmodel.PropertyChanged += (s, e) => { - propertyNameResult = e.PropertyName; - tcsTestResultsComplete.SetResult(new object()); + if (e.PropertyName == nameof(CompilandDiffPageViewModel.SymbolDiffs)) + { + sawSymbolDiffsPropertyChange = true; + tcsTestResultsComplete.TrySetResult(new object()); + } }; tcsSymbolsReady.SetResult(allSymbolsInCompilandDiff); await tcsTestResultsComplete.Task; await initTask; - Assert.AreEqual(nameof(CompilandDiffPageViewModel.SymbolDiffs), propertyNameResult); + Assert.IsTrue(sawSymbolDiffsPropertyChange); Assert.AreSequenceEqual(allSymbolsInCompilandDiff, viewmodel.SymbolDiffs!.ToList()); + Assert.AreEqual(allSymbolsInCompilandDiff.Count, viewmodel.FilteredSymbolDiffs!.Cast().Count()); } [Timeout(1000 * 5, CooperativeCancellation = true)] // 5s @@ -93,7 +97,10 @@ public async Task CanExportSymbolsToExcel() viewmodel.ExportSymbolsToExcelCommand.Execute(); - this.MockUITaskScheduler.Verify(uits => uits.StartExcelExport(this.MockExcelExporter.Object, allSymbolsInCompilandDiff), Times.Exactly(1)); + this.MockUITaskScheduler.Verify(uits => uits.StartExcelExport(this.MockExcelExporter.Object, + It.Is>(symbols => symbols.Count == allSymbolsInCompilandDiff.Count && + !symbols.Except(allSymbolsInCompilandDiff).Any())), + Times.Exactly(1)); } public void Dispose() => this.TestDataGenerator.Dispose(); diff --git a/src/SizeBench.GUI.Tests/Pages/LibDiffPageViewModelTests.cs b/src/SizeBench.GUI.Tests/Pages/LibDiffPageViewModelTests.cs index df82fd5..46a6537 100644 --- a/src/SizeBench.GUI.Tests/Pages/LibDiffPageViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Pages/LibDiffPageViewModelTests.cs @@ -47,20 +47,24 @@ public async Task SymbolsInitializeAsyncFromConstruction() }); var initTask = viewmodel.InitializeAsync(); - var propertyNameResult = String.Empty; + var sawSymbolDiffsPropertyChange = false; viewmodel.PropertyChanged += (s, e) => { - propertyNameResult = e.PropertyName; - tcsTestResultsComplete.SetResult(new object()); + if (e.PropertyName == nameof(LibDiffPageViewModel.SymbolDiffs)) + { + sawSymbolDiffsPropertyChange = true; + tcsTestResultsComplete.TrySetResult(new object()); + } }; tcsSymbolsReady.SetResult(allSymbolDiffsInLibDiff); await tcsTestResultsComplete.Task; await initTask; - Assert.AreEqual(nameof(LibDiffPageViewModel.SymbolDiffs), propertyNameResult); + Assert.IsTrue(sawSymbolDiffsPropertyChange); Assert.AreSequenceEqual(allSymbolDiffsInLibDiff, viewmodel.SymbolDiffs!.ToList()); + Assert.AreEqual(allSymbolDiffsInLibDiff.Count, viewmodel.FilteredSymbolDiffs!.Cast().Count()); } [Timeout(1000 * 5, CooperativeCancellation = true)] // 5s @@ -90,7 +94,10 @@ public async Task CanExportSymbolsToExcel() viewmodel.ExportSymbolsToExcelCommand.Execute(); - this.MockUITaskScheduler.Verify(uits => uits.StartExcelExport(this.MockExcelExporter.Object, allSymbolsInLibDiff), Times.Exactly(1)); + this.MockUITaskScheduler.Verify(uits => uits.StartExcelExport(this.MockExcelExporter.Object, + It.Is>(symbols => symbols.Count == allSymbolsInLibDiff.Count && + !symbols.Except(allSymbolsInLibDiff).Any())), + Times.Exactly(1)); } public void Dispose() => this.TestDataGenerator.Dispose(); diff --git a/src/SizeBench.GUI.Tests/Pages/Symbols/CodeBlockSymbolDiffPageViewModelTests.cs b/src/SizeBench.GUI.Tests/Pages/Symbols/CodeBlockSymbolDiffPageViewModelTests.cs index 1385a8b..db3805d 100644 --- a/src/SizeBench.GUI.Tests/Pages/Symbols/CodeBlockSymbolDiffPageViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Pages/Symbols/CodeBlockSymbolDiffPageViewModelTests.cs @@ -14,6 +14,7 @@ public sealed class CodeBlockSymbolDiffPageViewModelTests : IDisposable { private DiffTestDataGenerator Generator = new DiffTestDataGenerator(); private Mock MockUITaskScheduler = new Mock(); + private Mock MockDisassemblySettings = new Mock(); [TestInitialize] public void TestInitialize() @@ -21,6 +22,8 @@ public void TestInitialize() this.Generator = new DiffTestDataGenerator(); this.MockUITaskScheduler = new Mock(); this.MockUITaskScheduler.SetupForSynchronousCompletionOfLongRunningUITasks(); + this.MockDisassemblySettings = new Mock(); + this.MockDisassemblySettings.SetupProperty(settings => settings.TemplateFoldabilityDisassemblyZoomPercent, 100); } [TestMethod] @@ -36,11 +39,14 @@ public async Task BlockLoadsEvenIfPlacementsAreCanceled() tcsPlacement.SetCanceled(this.TestContext.CancellationToken); this.Generator.MockBeforeSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.BeforeSymbol!, It.IsAny())).Returns(tcsPlacement.Task); this.Generator.MockBeforeSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.BeforeSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockBeforeSession.Setup(s => s.DisassembleFunction(expectedDiff.BeforeSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("before")); this.Generator.MockAfterSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.AfterSymbol!, It.IsAny())).Returns(tcsPlacement.Task); this.Generator.MockAfterSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.AfterSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockAfterSession.Setup(s => s.DisassembleFunction(expectedDiff.AfterSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("after")); var viewmodel = new CodeBlockSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -63,6 +69,8 @@ public async Task BlockLoadsEvenIfPlacementsAreCanceled() Assert.IsTrue(ReferenceEquals(expectedDiff, viewmodel.ParentFunctionSymbolDiff)); Assert.IsFalse(viewmodel.IsBeforeParentFunctionComplex); Assert.IsFalse(viewmodel.IsAfterParentFunctionComplex); + Assert.AreEqual("before", viewmodel.Disassembly1); + Assert.AreEqual("after", viewmodel.Disassembly2); } [TestMethod] @@ -81,11 +89,14 @@ public async Task BlockPlacementLookupWorks() .Returns(Task.FromResult(blockDiff)); this.Generator.MockBeforeSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.BeforeSymbol!, It.IsAny())).Returns(Task.FromResult(beforePlacement)); this.Generator.MockBeforeSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.BeforeSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockBeforeSession.Setup(s => s.DisassembleFunction(expectedDiff.BeforeSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("before")); this.Generator.MockAfterSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.AfterSymbol!, It.IsAny())).Returns(Task.FromResult(afterPlacement)); this.Generator.MockAfterSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.AfterSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockAfterSession.Setup(s => s.DisassembleFunction(expectedDiff.AfterSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("after")); var viewmodel = new CodeBlockSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -108,13 +119,16 @@ public async Task BlockPlacementLookupWorks() Assert.IsTrue(ReferenceEquals(expectedDiff, viewmodel.ParentFunctionSymbolDiff)); Assert.IsFalse(viewmodel.IsBeforeParentFunctionComplex); Assert.IsFalse(viewmodel.IsAfterParentFunctionComplex); + Assert.AreEqual("before", viewmodel.Disassembly1); + Assert.AreEqual("after", viewmodel.Disassembly2); } [TestMethod] public async Task NonexistentCodeBlockDoesItsBest() { var viewmodel = new CodeBlockSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -136,6 +150,8 @@ public async Task NonexistentCodeBlockDoesItsBest() Assert.IsNull(viewmodel.ParentFunctionSymbolDiff); Assert.IsFalse(viewmodel.IsBeforeParentFunctionComplex); Assert.IsFalse(viewmodel.IsAfterParentFunctionComplex); + Assert.IsNull(viewmodel.Disassembly1); + Assert.IsNull(viewmodel.Disassembly2); } [TestMethod] @@ -190,11 +206,14 @@ public async Task BlockThatIsFoldedLoadsAllBlocksAtRVA() .Returns(Task.FromResult(primaryBlockDiff)); this.Generator.MockBeforeSession.Setup(s => s.LookupSymbolPlacementInBinary(primaryBlockDiff.BeforeSymbol, It.IsAny())).Returns(Task.FromResult(beforePlacement)); this.Generator.MockBeforeSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(primaryBlockDiff.BeforeSymbol.RVA, It.IsAny())).Returns(Task.FromResult>(allBeforeFoldedFunctions)); + this.Generator.MockBeforeSession.Setup(s => s.DisassembleFunction(expectedDiff.BeforeSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("before")); this.Generator.MockAfterSession.Setup(s => s.LookupSymbolPlacementInBinary(primaryBlockDiff.AfterSymbol, It.IsAny())).Returns(Task.FromResult(afterPlacement)); this.Generator.MockAfterSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(primaryBlockDiff.AfterSymbol.RVA, It.IsAny())).Returns(Task.FromResult>(allAfterFoldedFunctions)); + this.Generator.MockAfterSession.Setup(s => s.DisassembleFunction(expectedDiff.AfterSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("after")); var viewmodel = new CodeBlockSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -224,6 +243,8 @@ public async Task BlockThatIsFoldedLoadsAllBlocksAtRVA() Assert.IsTrue(ReferenceEquals(expectedDiff, viewmodel.ParentFunctionSymbolDiff)); Assert.IsTrue(viewmodel.IsBeforeParentFunctionComplex); Assert.IsTrue(viewmodel.IsAfterParentFunctionComplex); + Assert.AreEqual("before", viewmodel.Disassembly1); + Assert.AreEqual("after", viewmodel.Disassembly2); } [TestMethod] @@ -244,11 +265,14 @@ public async Task WriteTestForBlocksOfDifferentTypeWarningText() .Returns(Task.FromResult(blockDiff)); this.Generator.MockBeforeSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.BeforeSymbol!, It.IsAny())).Returns(Task.FromResult(beforePlacement)); this.Generator.MockBeforeSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.BeforeSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockBeforeSession.Setup(s => s.DisassembleFunction(expectedDiff.BeforeSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("before")); this.Generator.MockAfterSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.AfterSymbol!, It.IsAny())).Returns(Task.FromResult(afterPlacement)); this.Generator.MockAfterSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.AfterSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockAfterSession.Setup(s => s.DisassembleFunction(expectedDiff.AfterSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("after")); var viewmodel = new CodeBlockSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { diff --git a/src/SizeBench.GUI.Tests/Pages/Symbols/FunctionCodeSymbolDiffPageViewModelTests.cs b/src/SizeBench.GUI.Tests/Pages/Symbols/FunctionCodeSymbolDiffPageViewModelTests.cs index a74a6ed..c9ca7fc 100644 --- a/src/SizeBench.GUI.Tests/Pages/Symbols/FunctionCodeSymbolDiffPageViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Pages/Symbols/FunctionCodeSymbolDiffPageViewModelTests.cs @@ -14,6 +14,7 @@ public sealed class FunctionCodeSymbolDiffPageViewModelTests : IDisposable { private DiffTestDataGenerator Generator = new DiffTestDataGenerator(); private Mock MockUITaskScheduler = new Mock(); + private Mock MockDisassemblySettings = new Mock(); [TestInitialize] public void TestInitialize() @@ -21,6 +22,8 @@ public void TestInitialize() this.Generator = new DiffTestDataGenerator(); this.MockUITaskScheduler = new Mock(); this.MockUITaskScheduler.SetupForSynchronousCompletionOfLongRunningUITasks(); + this.MockDisassemblySettings = new Mock(); + this.MockDisassemblySettings.SetupProperty(settings => settings.TemplateFoldabilityDisassemblyZoomPercent, 100); } [TestMethod] @@ -36,11 +39,14 @@ public async Task FunctionLoadsEvenIfPlacementsAreCanceled() tcsPlacement.SetCanceled(this.TestContext.CancellationToken); this.Generator.MockBeforeSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.BeforeSymbol!, It.IsAny())).Returns(tcsPlacement.Task); this.Generator.MockBeforeSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.BeforeSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockBeforeSession.Setup(s => s.DisassembleFunction(expectedDiff.BeforeSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("before")); this.Generator.MockAfterSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.AfterSymbol!, It.IsAny())).Returns(tcsPlacement.Task); this.Generator.MockAfterSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.AfterSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockAfterSession.Setup(s => s.DisassembleFunction(expectedDiff.AfterSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("after")); var viewmodel = new FunctionCodeSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -63,6 +69,9 @@ public async Task FunctionLoadsEvenIfPlacementsAreCanceled() Assert.IsTrue(ReferenceEquals(expectedDiff, viewmodel.FunctionDiff)); Assert.AreEqual(String.Empty, viewmodel.BeforeAttributes); Assert.AreEqual(String.Empty, viewmodel.AfterAttributes); + Assert.AreEqual("before", viewmodel.Disassembly1); + Assert.AreEqual("after", viewmodel.Disassembly2); + Assert.AreEqual(100, viewmodel.DisassemblyZoomPercent); } [TestMethod] @@ -81,11 +90,14 @@ public async Task FunctionPlacementLookupWorks() .Returns(Task.FromResult(blockDiff)); this.Generator.MockBeforeSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.BeforeSymbol!, It.IsAny())).Returns(Task.FromResult(beforePlacement)); this.Generator.MockBeforeSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.BeforeSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockBeforeSession.Setup(s => s.DisassembleFunction(expectedDiff.BeforeSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("before")); this.Generator.MockAfterSession.Setup(s => s.LookupSymbolPlacementInBinary(blockDiff.AfterSymbol!, It.IsAny())).Returns(Task.FromResult(afterPlacement)); this.Generator.MockAfterSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(blockDiff.AfterSymbol!.RVA, It.IsAny())).Returns(Task.FromResult>(new List())); + this.Generator.MockAfterSession.Setup(s => s.DisassembleFunction(expectedDiff.AfterSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("after")); var viewmodel = new FunctionCodeSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -112,13 +124,16 @@ public async Task FunctionPlacementLookupWorks() Assert.IsTrue(ReferenceEquals(expectedDiff, viewmodel.FunctionDiff)); Assert.AreEqual(String.Empty, viewmodel.BeforeAttributes); Assert.AreEqual(String.Empty, viewmodel.AfterAttributes); + Assert.AreEqual("before", viewmodel.Disassembly1); + Assert.AreEqual("after", viewmodel.Disassembly2); } [TestMethod] public async Task NonexistentFunctionDoesItsBest() { var viewmodel = new FunctionCodeSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -140,6 +155,8 @@ public async Task NonexistentFunctionDoesItsBest() Assert.IsNull(viewmodel.FunctionDiff); Assert.AreEqual(String.Empty, viewmodel.BeforeAttributes); Assert.AreEqual(String.Empty, viewmodel.AfterAttributes); + Assert.IsNull(viewmodel.Disassembly1); + Assert.IsNull(viewmodel.Disassembly2); } [TestMethod] @@ -194,11 +211,14 @@ public async Task FunctionThatIsFoldedLoadsAllFunctionsAtRVA() .Returns(Task.FromResult(primaryBlockDiff)); this.Generator.MockBeforeSession.Setup(s => s.LookupSymbolPlacementInBinary(primaryBlockDiff.BeforeSymbol, It.IsAny())).Returns(Task.FromResult(beforePlacement)); this.Generator.MockBeforeSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(primaryBlockDiff.BeforeSymbol.RVA, It.IsAny())).Returns(Task.FromResult>(allBeforeFoldedFunctions)); + this.Generator.MockBeforeSession.Setup(s => s.DisassembleFunction(expectedDiff.BeforeSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("before")); this.Generator.MockAfterSession.Setup(s => s.LookupSymbolPlacementInBinary(primaryBlockDiff.AfterSymbol, It.IsAny())).Returns(Task.FromResult(afterPlacement)); this.Generator.MockAfterSession.Setup(s => s.EnumerateAllSymbolsFoldedAtRVA(primaryBlockDiff.AfterSymbol.RVA, It.IsAny())).Returns(Task.FromResult>(allAfterFoldedFunctions)); + this.Generator.MockAfterSession.Setup(s => s.DisassembleFunction(expectedDiff.AfterSymbol!, It.IsAny(), It.IsAny())).Returns(Task.FromResult("after")); var viewmodel = new FunctionCodeSymbolDiffPageViewModel(this.MockUITaskScheduler.Object, - this.Generator.MockDiffSession.Object); + this.Generator.MockDiffSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { @@ -227,6 +247,8 @@ public async Task FunctionThatIsFoldedLoadsAllFunctionsAtRVA() Assert.HasCount(2, viewmodel.AfterBlockPlacements); Assert.AreEqual("Attributes: has been PGO'd, has been optimized for speed", viewmodel.BeforeAttributes); Assert.AreEqual("Attributes: has been PGO'd, has been optimized for speed", viewmodel.AfterAttributes); + Assert.AreEqual("before", viewmodel.Disassembly1); + Assert.AreEqual("after", viewmodel.Disassembly2); } public void Dispose() => this.Generator.Dispose(); diff --git a/src/SizeBench.GUI.Tests/Pages/TemplateFoldabilityItemPageViewModelTests.cs b/src/SizeBench.GUI.Tests/Pages/TemplateFoldabilityItemPageViewModelTests.cs index db4ce46..801e757 100644 --- a/src/SizeBench.GUI.Tests/Pages/TemplateFoldabilityItemPageViewModelTests.cs +++ b/src/SizeBench.GUI.Tests/Pages/TemplateFoldabilityItemPageViewModelTests.cs @@ -11,6 +11,7 @@ public sealed class TemplateFoldabilityPageViewModelTests : IDisposable { private Mock MockSession = new Mock(); private Mock MockUITaskScheduler = new Mock(); + private Mock MockDisassemblySettings = new Mock(); private SessionDataCache DataCache = new SessionDataCache(); private TestDIAAdapter TestDIAAdapter = new TestDIAAdapter(); private List TemplateFoldabilityItems = new List(); @@ -24,6 +25,8 @@ public void TestInitialize() this.MockUITaskScheduler = new Mock(); this.MockUITaskScheduler.SetupForSynchronousCompletionOfLongRunningUITasks(); + this.MockDisassemblySettings = new Mock(); + this.MockDisassemblySettings.SetupProperty(settings => settings.TemplateFoldabilityDisassemblyZoomPercent, 100); this.DataCache = new SessionDataCache() { @@ -42,7 +45,8 @@ public void TestInitialize() public async Task SettingBothDisassemblySymbolsKicksOffDisassemblyProcess() { var viewmodel = new TemplateFoldabilityItemPageViewModel(this.MockUITaskScheduler.Object, - this.MockSession.Object); + this.MockSession.Object, + this.MockDisassemblySettings.Object); viewmodel.SetQueryString(new Dictionary() { { "TemplateName", "SomeNamespace::MyType::FoldableFunction(T2, T1)" } @@ -112,5 +116,25 @@ public async Task SettingBothDisassemblySymbolsKicksOffDisassemblyProcess() Assert.IsNull(viewmodel.Disassembly2); } + [TestMethod] + public void ZoomPreferenceLoadsAndSavesThroughSettings() + { + this.MockDisassemblySettings.Object.TemplateFoldabilityDisassemblyZoomPercent = 140; + + var viewmodel = new TemplateFoldabilityItemPageViewModel(this.MockUITaskScheduler.Object, + this.MockSession.Object, + this.MockDisassemblySettings.Object); + + Assert.AreEqual(140, viewmodel.DisassemblyZoomPercent); + + viewmodel.IncreaseDisassemblyZoom(); + Assert.AreEqual(160, viewmodel.DisassemblyZoomPercent); + Assert.AreEqual(160, this.MockDisassemblySettings.Object.TemplateFoldabilityDisassemblyZoomPercent); + + viewmodel.DecreaseDisassemblyZoom(); + Assert.AreEqual(140, viewmodel.DisassemblyZoomPercent); + Assert.AreEqual(140, this.MockDisassemblySettings.Object.TemplateFoldabilityDisassemblyZoomPercent); + } + public void Dispose() => this.DataCache.Dispose(); } diff --git a/src/SizeBench.GUI.Tests/RecentSessionStoreTests.cs b/src/SizeBench.GUI.Tests/RecentSessionStoreTests.cs new file mode 100644 index 0000000..fe181ad --- /dev/null +++ b/src/SizeBench.GUI.Tests/RecentSessionStoreTests.cs @@ -0,0 +1,108 @@ +using System.IO; +using SizeBench.AnalysisEngine; +using SizeBench.TestInfrastructure; + +namespace SizeBench.GUI.Tests; + +[TestClass] +public sealed class RecentSessionStoreTests +{ + [TestMethod] + public void RecordSessionPersistsAndReloadsSingleAndDiffSessions() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + try + { + var storagePath = Path.Combine(tempDirectory, "RecentSessions.json"); + using var logger = new TestNoOpApplicationLogger(); + var store = new RecentSessionStore(logger, storagePath); + + var singleSession = RecentSession.CreateSingle(@"c:\dev\foo.dll", + @"c:\dev\foo.pdb", + new SessionOptions() { SymbolSourcesSupported = SymbolSourcesSupported.Code | SymbolSourcesSupported.DataSymbols }); + var diffSession = RecentSession.CreateDiff(@"c:\dev\before.dll", + @"c:\dev\before.pdb", + @"c:\dev\after.dll", + @"c:\dev\after.pdb"); + + store.RecordSession(singleSession); + store.RecordSession(diffSession); + + var reloadedStore = new RecentSessionStore(logger, storagePath); + var recentSessions = reloadedStore.GetRecentSessions(); + + Assert.AreEqual(2, recentSessions.Count); + Assert.AreEqual(RecentSessionKind.BinaryDiff, recentSessions[0].Kind); + Assert.AreEqual(@"c:\dev\before.dll", recentSessions[0].BeforeBinaryPath); + Assert.AreEqual(RecentSessionKind.SingleBinary, recentSessions[1].Kind); + Assert.AreEqual(SymbolSourcesSupported.Code | SymbolSourcesSupported.DataSymbols, recentSessions[1].SymbolSourcesSupported); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + [TestMethod] + public void RecordSessionDedupesAndKeepsNewestCopy() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + try + { + var storagePath = Path.Combine(tempDirectory, "RecentSessions.json"); + using var logger = new TestNoOpApplicationLogger(); + var store = new RecentSessionStore(logger, storagePath); + + var originalSession = RecentSession.CreateSingle(@"c:\dev\foo.dll", @"c:\dev\foo.pdb", new SessionOptions(), DateTimeOffset.UtcNow.AddDays(-2)); + var newerSession = RecentSession.CreateSingle(@"c:\dev\foo.dll", @"c:\dev\foo.pdb", new SessionOptions(), DateTimeOffset.UtcNow.AddDays(-1)); + + store.RecordSession(originalSession); + store.RecordSession(newerSession); + + var recentSessions = store.GetRecentSessions(); + + Assert.AreEqual(1, recentSessions.Count); + Assert.AreEqual(@"c:\dev\foo.dll", recentSessions[0].BinaryPath); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + [TestMethod] + public void RecordSessionCapsHistoryAtTenEntries() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + try + { + var storagePath = Path.Combine(tempDirectory, "RecentSessions.json"); + using var logger = new TestNoOpApplicationLogger(); + var store = new RecentSessionStore(logger, storagePath); + + for (var i = 0; i < RecentSessionStore.MaximumStoredSessions + 2; i++) + { + store.RecordSession(RecentSession.CreateSingle($@"c:\dev\foo{i}.dll", + $@"c:\dev\foo{i}.pdb", + new SessionOptions(), + DateTimeOffset.UtcNow.AddMinutes(-i))); + } + + var recentSessions = store.GetRecentSessions(); + + Assert.AreEqual(RecentSessionStore.MaximumStoredSessions, recentSessions.Count); + Assert.AreEqual(@"c:\dev\foo11.dll", recentSessions[0].BinaryPath); + Assert.AreEqual(@"c:\dev\foo2.dll", recentSessions[^1].BinaryPath); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } +} diff --git a/src/SizeBench.GUI/Controls/SelectableDiffViewer/SelectableDiffViewer.cs b/src/SizeBench.GUI/Controls/SelectableDiffViewer/SelectableDiffViewer.cs index bc563c1..0ba6622 100644 --- a/src/SizeBench.GUI/Controls/SelectableDiffViewer/SelectableDiffViewer.cs +++ b/src/SizeBench.GUI/Controls/SelectableDiffViewer/SelectableDiffViewer.cs @@ -33,19 +33,24 @@ public sealed class SelectableDiffViewer : UserControl private const double DiffFontSize = 16; + // Exposed so the zoom-percent behavior (see TwoStringsToDiffViewerUIConverter) can be unit tested. + public double EffectiveFontSize { get; } + private readonly FlowDocumentScrollViewer _textViewer; private readonly ScrollViewer _gutterScrollViewer; private readonly string _longestLine; private ScrollViewer? _textInnerScrollViewer; private bool _pageWidthSet; - public SelectableDiffViewer(string oldText, string newText) + public SelectableDiffViewer(string oldText, string newText, int zoomPercent = 100) { ArgumentNullException.ThrowIfNull(oldText); ArgumentNullException.ThrowIfNull(newText); + this.EffectiveFontSize = DiffFontSize * zoomPercent / 100.0; + var fontFamily = new FontFamily("Consolas"); - var lineHeight = Math.Ceiling(DiffFontSize * fontFamily.LineSpacing); + var lineHeight = Math.Ceiling(this.EffectiveFontSize * fontFamily.LineSpacing); var diff = InlineDiffBuilder.Diff(oldText, newText); var rows = DiffRowBuilder.BuildRows(diff); @@ -53,7 +58,7 @@ public SelectableDiffViewer(string oldText, string newText) var document = new FlowDocument { FontFamily = fontFamily, - FontSize = DiffFontSize, + FontSize = this.EffectiveFontSize, PagePadding = new Thickness(0), }; @@ -97,7 +102,7 @@ public SelectableDiffViewer(string oldText, string newText) Text = row.LineNumber, Foreground = LineNumberForeground, FontFamily = fontFamily, - FontSize = DiffFontSize, + FontSize = this.EffectiveFontSize, TextAlignment = TextAlignment.Right, VerticalAlignment = VerticalAlignment.Center, Padding = new Thickness(6, 0, 6, 0), @@ -192,7 +197,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(document.FontFamily, document.FontStyle, document.FontWeight, document.FontStretch), - DiffFontSize, + this.EffectiveFontSize, Brushes.Black, pixelsPerDip); diff --git a/src/SizeBench.GUI/Converters/TwoStringsToDiffViewerUIConverter.cs b/src/SizeBench.GUI/Converters/TwoStringsToDiffViewerUIConverter.cs index 6f3ed7c..07f93c1 100644 --- a/src/SizeBench.GUI/Converters/TwoStringsToDiffViewerUIConverter.cs +++ b/src/SizeBench.GUI/Converters/TwoStringsToDiffViewerUIConverter.cs @@ -18,9 +18,9 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur throw new ArgumentException("targetType must be object - this is really meant to go into ContentPresenter.Content"); } - if (values?.Length > 2) + if (values?.Length > 3) { - throw new ArgumentException("This converter expects exactly two inputs - the two strings to diff"); + throw new ArgumentException("This converter expects two or three inputs - the two strings to diff, and optionally a zoom percent."); } if (values is null || values.Length < 2 || values[0] is null || values[0] == DependencyProperty.UnsetValue || values[1] is null || values[1] == DependencyProperty.UnsetValue) @@ -40,7 +40,18 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur } #pragma warning restore CA1508 - return new SelectableDiffViewer(leftString, rightString); + var zoomPercent = 100; + if (values.Length == 3 && values[2] != DependencyProperty.UnsetValue && values[2] != null) + { + if (values[2] is not int requestedZoomPercent) + { + throw new ArgumentException("values[2] must be an int zoom percent"); + } + + zoomPercent = requestedZoomPercent; + } + + return new SelectableDiffViewer(leftString, rightString, zoomPercent); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) diff --git a/src/SizeBench.GUI/DisassemblySettingsStore.cs b/src/SizeBench.GUI/DisassemblySettingsStore.cs new file mode 100644 index 0000000..3edbff2 --- /dev/null +++ b/src/SizeBench.GUI/DisassemblySettingsStore.cs @@ -0,0 +1,120 @@ +using System.IO; +using System.Text.Json; +using SizeBench.Logging; + +namespace SizeBench.GUI; + +internal interface IDisassemblySettings +{ + int TemplateFoldabilityDisassemblyZoomPercent { get; set; } +} + +internal sealed class DisassemblySettingsStore : IDisassemblySettings +{ + internal const int DefaultZoomPercent = 100; + + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions() + { + WriteIndented = true + }; + + private readonly IApplicationLogger _applicationLogger; + private readonly string _storagePath; + private readonly PersistedDisassemblySettings _settings; + + public int TemplateFoldabilityDisassemblyZoomPercent + { + get => this._settings.TemplateFoldabilityDisassemblyZoomPercent; + set + { + var normalizedValue = NormalizeZoomPercent(value); + if (this._settings.TemplateFoldabilityDisassemblyZoomPercent != normalizedValue) + { + this._settings.TemplateFoldabilityDisassemblyZoomPercent = normalizedValue; + PersistSettings(); + } + } + } + + public DisassemblySettingsStore(IApplicationLogger applicationLogger) + : this(applicationLogger, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SizeBench", "DisassemblySettings.json")) + { + } + + internal DisassemblySettingsStore(IApplicationLogger applicationLogger, string storagePath) + { + this._applicationLogger = applicationLogger ?? throw new ArgumentNullException(nameof(applicationLogger)); + this._storagePath = storagePath ?? throw new ArgumentNullException(nameof(storagePath)); + this._settings = LoadSettings(); + } + + private PersistedDisassemblySettings LoadSettings() + { + if (File.Exists(this._storagePath) == false) + { + return new PersistedDisassemblySettings(); + } + + try + { + using var stream = File.OpenRead(this._storagePath); + var settings = JsonSerializer.Deserialize(stream, SerializerOptions); + if (settings is null) + { + return new PersistedDisassemblySettings(); + } + + settings.TemplateFoldabilityDisassemblyZoomPercent = NormalizeZoomPercent(settings.TemplateFoldabilityDisassemblyZoomPercent); + return settings; + } + catch (JsonException ex) + { + this._applicationLogger.LogException("Unable to deserialize disassembly settings.", ex); + return new PersistedDisassemblySettings(); + } + catch (IOException ex) + { + this._applicationLogger.LogException("Unable to read disassembly settings.", ex); + return new PersistedDisassemblySettings(); + } + catch (UnauthorizedAccessException ex) + { + this._applicationLogger.LogException("Unable to read disassembly settings.", ex); + return new PersistedDisassemblySettings(); + } + } + + private void PersistSettings() + { + try + { + var directory = Path.GetDirectoryName(this._storagePath); + if (String.IsNullOrEmpty(directory) == false) + { + Directory.CreateDirectory(directory); + } + + using var stream = File.Create(this._storagePath); + JsonSerializer.Serialize(stream, this._settings, SerializerOptions); + } + catch (IOException ex) + { + this._applicationLogger.LogException("Unable to persist disassembly settings.", ex); + } + catch (UnauthorizedAccessException ex) + { + this._applicationLogger.LogException("Unable to persist disassembly settings.", ex); + } + } + + private static int NormalizeZoomPercent(int zoomPercent) + { + var clamped = Math.Clamp(zoomPercent, 0, 200); + return clamped % 20 == 0 ? clamped : DefaultZoomPercent; + } + + private sealed class PersistedDisassemblySettings + { + public int TemplateFoldabilityDisassemblyZoomPercent { get; set; } = DefaultZoomPercent; + } +} diff --git a/src/SizeBench.GUI/MainWindow.xaml b/src/SizeBench.GUI/MainWindow.xaml index 5de0dcf..5060310 100644 --- a/src/SizeBench.GUI/MainWindow.xaml +++ b/src/SizeBench.GUI/MainWindow.xaml @@ -1,246 +1,332 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Examine a binary - - - This is the place to start. Begin exploring a binary to learn what is consuming space, and where there is wasted space. - - - - - Start a diff - - - This is where to ask 'Did I make my product better?' by seeing how much you improved things between two versions of the - same binary. First select the 'before' then the 'after'. - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +