mirror of
https://github.com/microsoft/PowerToys.git
synced 2024-11-27 14:59:16 +08:00
Used System.IO.Abstractions and improved tests
This commit is contained in:
parent
437bda544f
commit
2b711397f7
@ -0,0 +1,172 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Models.KernelQueryCache;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Settings;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace AdvancedPaste.UnitTests.ServicesTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class CustomActionKernelQueryCacheServiceTests
|
||||
{
|
||||
private static readonly CacheKey CustomActionTestKey = new() { Prompt = "TestPrompt1", AvailableFormats = ClipboardFormat.Text };
|
||||
private static readonly CacheKey CustomActionTestKey2 = new() { Prompt = "TestPrompt2", AvailableFormats = ClipboardFormat.File | ClipboardFormat.Image };
|
||||
private static readonly CacheKey MarkdownTestKey = new() { Prompt = "Paste as Markdown", AvailableFormats = ClipboardFormat.Text };
|
||||
private static readonly CacheKey JSONTestKey = new() { Prompt = "Paste as JSON", AvailableFormats = ClipboardFormat.Text };
|
||||
private static readonly CacheKey PasteAsTxtFileKey = new() { Prompt = "Paste as .txt file", AvailableFormats = ClipboardFormat.File };
|
||||
private static readonly CacheKey PasteAsPngFileKey = new() { Prompt = "Paste as .png file", AvailableFormats = ClipboardFormat.Image };
|
||||
|
||||
private static readonly CacheValue TestValue = new([new(PasteFormats.PlainText, [])]);
|
||||
private static readonly CacheValue TestValue2 = new([new(PasteFormats.KernelQuery, new() { { "a", "b" }, { "c", "d" } })]);
|
||||
|
||||
private CustomActionKernelQueryCacheService _cacheService;
|
||||
private Mock<IUserSettings> _userSettings;
|
||||
private MockFileSystem _fileSystem;
|
||||
|
||||
[TestInitialize]
|
||||
public void TestInitialize()
|
||||
{
|
||||
_userSettings = new();
|
||||
UpdateUserActions([], []);
|
||||
|
||||
_fileSystem = new();
|
||||
_cacheService = new(_userSettings.Object, _fileSystem);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Cache_Always_Accepts_Core_Action_Prompt()
|
||||
{
|
||||
await AssertAcceptsAsync(MarkdownTestKey);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Cache_Accepts_Prompt_When_Custom_Action()
|
||||
{
|
||||
await AssertRejectsAsync(CustomActionTestKey);
|
||||
|
||||
UpdateUserActions([], [new() { Name = nameof(CustomActionTestKey), Prompt = CustomActionTestKey.Prompt, IsShown = true }]);
|
||||
|
||||
await AssertAcceptsAsync(CustomActionTestKey);
|
||||
await AssertRejectsAsync(CustomActionTestKey2, PasteAsTxtFileKey);
|
||||
|
||||
UpdateUserActions([], []);
|
||||
await AssertRejectsAsync(CustomActionTestKey);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Cache_Accepts_Prompt_When_User_Additional_Action()
|
||||
{
|
||||
await AssertRejectsAsync(PasteAsTxtFileKey, PasteAsPngFileKey);
|
||||
|
||||
UpdateUserActions([PasteFormats.PasteAsHtmlFile, PasteFormats.PasteAsTxtFile], []);
|
||||
|
||||
await AssertAcceptsAsync(PasteAsTxtFileKey);
|
||||
await AssertRejectsAsync(PasteAsPngFileKey, CustomActionTestKey);
|
||||
|
||||
UpdateUserActions([], []);
|
||||
await AssertRejectsAsync(PasteAsTxtFileKey);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Cache_Overwrites_Latest_Value()
|
||||
{
|
||||
await _cacheService.WriteAsync(JSONTestKey, TestValue);
|
||||
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
|
||||
|
||||
await _cacheService.WriteAsync(JSONTestKey, TestValue2);
|
||||
await _cacheService.WriteAsync(MarkdownTestKey, TestValue);
|
||||
|
||||
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(JSONTestKey));
|
||||
AssertAreEqual(TestValue, _cacheService.ReadOrNull(MarkdownTestKey));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Cache_Uses_Case_Insensitive_Prompt_Comparison()
|
||||
{
|
||||
static CacheKey CreateUpperCaseKey(CacheKey key) =>
|
||||
new() { Prompt = key.Prompt.ToUpperInvariant(), AvailableFormats = key.AvailableFormats };
|
||||
|
||||
await _cacheService.WriteAsync(CreateUpperCaseKey(JSONTestKey), TestValue);
|
||||
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
|
||||
|
||||
AssertAreEqual(TestValue, _cacheService.ReadOrNull(JSONTestKey));
|
||||
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(MarkdownTestKey));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Cache_Uses_Clipboard_Formats_In_Key()
|
||||
{
|
||||
CacheKey key1 = new() { Prompt = JSONTestKey.Prompt, AvailableFormats = ClipboardFormat.File };
|
||||
CacheKey key2 = new() { Prompt = JSONTestKey.Prompt, AvailableFormats = ClipboardFormat.Image };
|
||||
|
||||
await _cacheService.WriteAsync(key1, TestValue);
|
||||
|
||||
Assert.IsNotNull(_cacheService.ReadOrNull(key1));
|
||||
Assert.IsNull(_cacheService.ReadOrNull(key2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Cache_Is_Persistent()
|
||||
{
|
||||
await _cacheService.WriteAsync(JSONTestKey, TestValue);
|
||||
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
|
||||
|
||||
_cacheService = new(_userSettings.Object, _fileSystem); // recreate using same mock file-system to simulate app restart
|
||||
|
||||
AssertAreEqual(TestValue, _cacheService.ReadOrNull(JSONTestKey));
|
||||
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(MarkdownTestKey));
|
||||
}
|
||||
|
||||
private async Task AssertRejectsAsync(params CacheKey[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
Assert.IsNull(_cacheService.ReadOrNull(key));
|
||||
await _cacheService.WriteAsync(key, TestValue);
|
||||
Assert.IsNull(_cacheService.ReadOrNull(key));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AssertAcceptsAsync(params CacheKey[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
{
|
||||
Assert.IsNull(_cacheService.ReadOrNull(key));
|
||||
await _cacheService.WriteAsync(key, TestValue);
|
||||
AssertAreEqual(TestValue, _cacheService.ReadOrNull(key));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertAreEqual(CacheValue valueA, CacheValue valueB)
|
||||
{
|
||||
Assert.IsNotNull(valueA);
|
||||
Assert.IsNotNull(valueB);
|
||||
|
||||
Assert.AreEqual(valueA.ActionChain.Count, valueB.ActionChain.Count);
|
||||
|
||||
foreach (var (itemA, itemB) in valueA.ActionChain.Zip(valueB.ActionChain))
|
||||
{
|
||||
Assert.AreEqual(itemA.Format, itemB.Format);
|
||||
Assert.AreEqual(itemA.Arguments.Count, itemB.Arguments.Count);
|
||||
Assert.IsFalse(itemA.Arguments.Except(itemB.Arguments).Any());
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUserActions(PasteFormats[] additionalActions, AdvancedPasteCustomAction[] customActions)
|
||||
{
|
||||
_userSettings.Setup(settingsObj => settingsObj.AdditionalActions).Returns(additionalActions);
|
||||
_userSettings.Setup(settingsObj => settingsObj.CustomActions).Returns(customActions);
|
||||
_userSettings.Raise(settingsObj => settingsObj.Changed += null, EventArgs.Empty);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
@ -74,6 +75,7 @@ namespace AdvancedPaste
|
||||
|
||||
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
services.AddSingleton<IAICredentialsProvider, Services.OpenAI.VaultCredentialsProvider>();
|
||||
services.AddSingleton<ICustomTextTransformService, Services.OpenAI.CustomTextTransformService>();
|
||||
|
@ -43,9 +43,9 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public UserSettings()
|
||||
public UserSettings(IFileSystem fileSystem)
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_settingsUtils = new SettingsUtils(fileSystem);
|
||||
|
||||
IsAdvancedAIEnabled = false;
|
||||
ShowCustomPreview = true;
|
||||
@ -56,7 +56,7 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
LoadSettingsFromJson();
|
||||
|
||||
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", OnSettingsFileChanged);
|
||||
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", OnSettingsFileChanged, fileSystem);
|
||||
}
|
||||
|
||||
private void OnSettingsFileChanged()
|
||||
|
@ -6,4 +6,4 @@ using System.Collections.Generic;
|
||||
|
||||
namespace AdvancedPaste.Models;
|
||||
|
||||
public record class ActionChainItem(PasteFormats Format, Dictionary<string, object> Arguments);
|
||||
public record class ActionChainItem(PasteFormats Format, Dictionary<string, string> Arguments);
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
@ -22,17 +22,21 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
|
||||
{
|
||||
private const string PersistedCacheFileName = "kernelQueryCache.json";
|
||||
|
||||
private readonly SettingsUtils _settingsUtil = new();
|
||||
private readonly HashSet<string> _cacheablePrompts = new(CacheKey.PromptComparer);
|
||||
private readonly Dictionary<CacheKey, CacheValue> _memoryCache = [];
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
private HashSet<string> _cacheablePrompts = [];
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly SettingsUtils _settingsUtil;
|
||||
|
||||
private static string Version => Assembly.GetExecutingAssembly()?.GetName()?.Version?.ToString() ?? string.Empty;
|
||||
|
||||
public CustomActionKernelQueryCacheService(IUserSettings userSettings)
|
||||
public CustomActionKernelQueryCacheService(IUserSettings userSettings, IFileSystem fileSystem)
|
||||
{
|
||||
_userSettings = userSettings;
|
||||
_fileSystem = fileSystem;
|
||||
_settingsUtil = new SettingsUtils(fileSystem);
|
||||
|
||||
_userSettings.Changed += OnUserSettingsChanged;
|
||||
|
||||
RefreshCacheablePrompts();
|
||||
@ -66,7 +70,7 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
|
||||
return [];
|
||||
}
|
||||
|
||||
var jsonString = File.ReadAllText(_settingsUtil.GetSettingsFilePath(AdvancedPasteSettings.ModuleName, PersistedCacheFileName));
|
||||
var jsonString = _fileSystem.File.ReadAllText(_settingsUtil.GetSettingsFilePath(AdvancedPasteSettings.ModuleName, PersistedCacheFileName));
|
||||
var persistedCache = PersistedCache.FromJsonString(jsonString);
|
||||
|
||||
if (persistedCache.Version == Version)
|
||||
@ -98,16 +102,19 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
|
||||
|
||||
private void RefreshCacheablePrompts()
|
||||
{
|
||||
var localizedActionNames = from metadata in PasteFormat.MetadataDict.Values
|
||||
var localizedActionNames = from pair in PasteFormat.MetadataDict
|
||||
let format = pair.Key
|
||||
let metadata = pair.Value
|
||||
where !string.IsNullOrEmpty(metadata.ResourceId)
|
||||
where metadata.IsCoreAction || _userSettings.AdditionalActions.Contains(format)
|
||||
select ResourceLoaderInstance.ResourceLoader.GetString(metadata.ResourceId);
|
||||
|
||||
var customActionPrompts = from customAction in _userSettings.CustomActions
|
||||
select customAction.Prompt;
|
||||
|
||||
// Only cache queries with these prompts to prevent the cache from getting too large and to avoid potential privacy issues.
|
||||
_cacheablePrompts = localizedActionNames.Concat(customActionPrompts)
|
||||
.ToHashSet(CacheKey.PromptComparer);
|
||||
_cacheablePrompts.Clear();
|
||||
_cacheablePrompts.UnionWith(localizedActionNames.Concat(customActionPrompts));
|
||||
}
|
||||
|
||||
private bool RemoveInapplicableCacheKeys()
|
||||
@ -134,7 +141,7 @@ public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheServi
|
||||
|
||||
_settingsUtil.SaveSettings(cache.ToJsonString(), AdvancedPasteSettings.ModuleName, PersistedCacheFileName);
|
||||
|
||||
Logger.LogDebug($"Kernel query cache saved with {_memoryCache.Count} items");
|
||||
Logger.LogDebug($"Kernel query cache saved with {_memoryCache.Count} item(s)");
|
||||
|
||||
await Task.CompletedTask; // Async placeholder until _settingsUtil.SaveSettings has an async implementation
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
{
|
||||
if (item.Arguments.Count > 0)
|
||||
{
|
||||
await ExecutePromptTransformAsync(kernel, item.Format, (string)item.Arguments[PromptParameterName]);
|
||||
await ExecutePromptTransformAsync(kernel, item.Format, item.Arguments[PromptParameterName]);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -6,7 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -85,7 +85,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public event EventHandler PreviewRequested;
|
||||
|
||||
public OptionsViewModel(IAICredentialsProvider aiCredentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider aiCredentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
{
|
||||
_aiCredentialsProvider = aiCredentialsProvider;
|
||||
_userSettings = userSettings;
|
||||
@ -119,7 +119,7 @@ namespace AdvancedPaste.ViewModels
|
||||
try
|
||||
{
|
||||
// Delete file that is no longer needed but might have been written by previous version and contain sensitive information.
|
||||
File.Delete(new SettingsUtils().GetSettingsFilePath(Constants.AdvancedPasteModuleName, "lastQuery.json"));
|
||||
fileSystem.File.Delete(new SettingsUtils(fileSystem).GetSettingsFilePath(Constants.AdvancedPasteModuleName, "lastQuery.json"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -7,7 +7,6 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Security.Principal;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library.CustomAction;
|
||||
@ -54,16 +53,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Utilities
|
||||
return sendCustomAction.ToJsonString();
|
||||
}
|
||||
|
||||
public static IFileSystemWatcher GetFileWatcher(string moduleName, string fileName, Action onChangedCallback)
|
||||
public static IFileSystemWatcher GetFileWatcher(string moduleName, string fileName, Action onChangedCallback, IFileSystem fileSystem = null)
|
||||
{
|
||||
var path = FileSystem.Path.Combine(LocalApplicationDataFolder(), $"Microsoft\\PowerToys\\{moduleName}");
|
||||
fileSystem ??= FileSystem;
|
||||
|
||||
if (!FileSystem.Directory.Exists(path))
|
||||
var path = fileSystem.Path.Combine(LocalApplicationDataFolder(), $"Microsoft\\PowerToys\\{moduleName}");
|
||||
|
||||
if (!fileSystem.Directory.Exists(path))
|
||||
{
|
||||
FileSystem.Directory.CreateDirectory(path);
|
||||
fileSystem.Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
var watcher = FileSystem.FileSystemWatcher.CreateNew();
|
||||
var watcher = fileSystem.FileSystemWatcher.CreateNew();
|
||||
watcher.Path = path;
|
||||
watcher.Filter = fileName;
|
||||
watcher.NotifyFilter = NotifyFilters.LastWrite;
|
||||
|
Loading…
Reference in New Issue
Block a user