mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Fixing slnx and slnf
This commit is contained in:
@@ -196,6 +196,10 @@
|
||||
<Folder Name="/modules/CommandPalette/">
|
||||
<Project Path="src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj" Id="5f63c743-f6ce-4dba-a200-2b3f8a14e8c2" />
|
||||
<Project Path="src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj" Id="0adeb797-c8c7-4ffa-acd5-2af6cad7ecd8" />
|
||||
<Project Path="src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Built-in Extensions/">
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj">
|
||||
@@ -295,6 +299,10 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Tests/">
|
||||
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Common.UnitTests/Microsoft.CmdPal.Common.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Microsoft.CmdPal.Core.Common.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI.ViewModels\\Microsoft.CmdPal.UI.ViewModels.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI\\Microsoft.CmdPal.UI.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.Terminal.UI\\Microsoft.Terminal.UI.vcxproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Common.UnitTests\\Microsoft.CmdPal.Common.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Apps.UnitTests\\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Calc.UnitTests\\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// 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.
|
||||
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Diagnostics.CodeAnalysis;
|
||||
global using System.Linq;
|
||||
global using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>Microsoft.CmdPal.Common.UnitTests</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,107 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.UnitTests.TestUtils;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
|
||||
|
||||
[TestClass]
|
||||
public class ConnectionStringRuleProviderTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void GetRules_ShouldReturnExpectedRules()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new ConnectionStringRuleProvider();
|
||||
|
||||
// Act
|
||||
var rules = provider.GetRules();
|
||||
|
||||
// Assert
|
||||
var ruleList = new List<SanitizationRule>(rules);
|
||||
Assert.AreEqual(1, ruleList.Count);
|
||||
Assert.AreEqual("Connection string parameters", ruleList[0].Description);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("Server=localhost;Database=mydb;User ID=admin;Password=secret123", "Server=[REDACTED];Database=[REDACTED];User ID=[REDACTED];Password=[REDACTED]")]
|
||||
[DataRow("Data Source=server.example.com;Initial Catalog=testdb;Uid=user;Pwd=pass", "Data Source=[REDACTED];Initial Catalog=[REDACTED];Uid=[REDACTED];Pwd=[REDACTED]")]
|
||||
[DataRow("Server=localhost;Password=my_secret", "Server=[REDACTED];Password=[REDACTED]")]
|
||||
[DataRow("No connection string here", "No connection string here")]
|
||||
public void ConnectionStringRules_ShouldMaskConnectionStringParameters(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new ConnectionStringRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("Password=\"complexPassword123!\"", "Password=[REDACTED]")]
|
||||
[DataRow("Password='myPassword'", "Password=[REDACTED]")]
|
||||
[DataRow("Password=unquotedSecret", "Password=[REDACTED]")]
|
||||
public void ConnectionStringRules_ShouldHandleQuotedAndUnquotedValues(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new ConnectionStringRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("SERVER=server1;PASSWORD=pass1", "SERVER=[REDACTED];PASSWORD=[REDACTED]")]
|
||||
[DataRow("server=server1;password=pass1", "server=[REDACTED];password=[REDACTED]")]
|
||||
[DataRow("Server=server1;Password=pass1", "Server=[REDACTED];Password=[REDACTED]")]
|
||||
public void ConnectionStringRules_ShouldBeCaseInsensitive(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new ConnectionStringRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("User ID=admin;Username=john;Password=secret", "User ID=[REDACTED];Username=[REDACTED];Password=[REDACTED]")]
|
||||
[DataRow("Database=mydb;Uid=user1;Pwd=pass1;Server=localhost", "Database=[REDACTED];Uid=[REDACTED];Pwd=[REDACTED];Server=[REDACTED]")]
|
||||
public void ConnectionStringRules_ShouldHandleMultipleParameters(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new ConnectionStringRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("Server = localhost ; Password = secret123", "Server=[REDACTED] ; Password=[REDACTED]")]
|
||||
[DataRow("Initial Catalog=db; User ID=admin; Password=pass", "Initial Catalog=[REDACTED]; User ID=[REDACTED]; Password=[REDACTED]")]
|
||||
public void ConnectionStringRules_ShouldHandleWhitespace(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new ConnectionStringRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
|
||||
|
||||
public partial class ErrorReportSanitizerTests
|
||||
{
|
||||
internal static class TestData
|
||||
{
|
||||
internal static string Input =>
|
||||
$"""
|
||||
HRESULT: 0x80004005
|
||||
HRESULT: -2147467259
|
||||
|
||||
Here is e-mail address <jane.doe@contoso.com>
|
||||
IPv4 address: 192.168.100.1
|
||||
IPv4 loopback address: 127.0.0.1
|
||||
MAC address: 00-14-22-01-23-45
|
||||
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
IPv6 loopback address: ::1
|
||||
Password: P@ssw0rd123!
|
||||
Password=secret
|
||||
Api key: 1234567890abcdef
|
||||
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
|
||||
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
|
||||
X-API-key: 1234567890abcdef
|
||||
Pet-Shop-Subscription-Key: 1234567890abcdef
|
||||
Here is a user name {Environment.UserName}
|
||||
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\RandomFolder
|
||||
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
|
||||
Here is machine name {Environment.MachineName}
|
||||
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
|
||||
User email john.doe@company.com failed validation
|
||||
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
|
||||
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
|
||||
Phone number 555-123-4567 is invalid
|
||||
API key abc123def456ghi789jkl012mno345pqr678 expired
|
||||
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
|
||||
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
|
||||
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
|
||||
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
|
||||
Email service error: mailto:admin@internal-company.com?subject=Alert
|
||||
""";
|
||||
|
||||
public const string Expected =
|
||||
$"""
|
||||
HRESULT: 0x80004005
|
||||
HRESULT: -2147467259
|
||||
|
||||
Here is e-mail address <[EMAIL_REDACTED]>
|
||||
IPv4 address: 192.168.100.1
|
||||
IPv4 loopback address: 127.0.0.1
|
||||
MAC address: [MAC_ADDRESS_REDACTED]
|
||||
IPv6 address: [IP6_REDACTED]
|
||||
IPv6 loopback address: [IP6_REDACTED]
|
||||
Password: [REDACTED]
|
||||
Password= [REDACTED]
|
||||
Api key: [REDACTED]
|
||||
PostgreSQL connection string: [REDACTED]
|
||||
InstrumentationKey= [REDACTED]
|
||||
X-API-key: [REDACTED]
|
||||
Pet-Shop-Subscription-Key: [REDACTED]
|
||||
Here is a user name [USERNAME_REDACTED]
|
||||
And here is a profile path [USER_PROFILE_DIR]RandomFolder
|
||||
Here is a local app data path [LOCALAPPLICATIONDATA_DIR]Microsoft\PowerToys\CmdPal
|
||||
Here is machine name [MACHINE_NAME_REDACTED]
|
||||
JWT token: [REDACTED]
|
||||
User email [EMAIL_REDACTED] failed validation
|
||||
File not found: [MYDOCUMENTS_DIR]se****.txt
|
||||
Connection string: [REDACTED] ID=[REDACTED];Password= [REDACTED]
|
||||
Phone number [PHONE_REDACTED] is invalid
|
||||
API key [TOKEN_REDACTED] expired
|
||||
Failed to connect to [URL_REDACTED]
|
||||
Error accessing [URL_REDACTED]
|
||||
JDBC connection failed: [URL_REDACTED]
|
||||
FTP upload error: [URL_REDACTED]
|
||||
Email service error: mailto:[EMAIL_REDACTED]?subject=Alert
|
||||
""";
|
||||
|
||||
internal static string Input2 =>
|
||||
$"""
|
||||
============================================================
|
||||
Hello World! Command Palette is starting.
|
||||
|
||||
Application:
|
||||
App version: 0.0.1.0
|
||||
Packaging flavor: Packaged
|
||||
Is elevated: no
|
||||
|
||||
Environment:
|
||||
OS version: Microsoft Windows 10.0.26220
|
||||
OS architecture: X64
|
||||
Runtime identifier: win-x64
|
||||
Framework: .NET 9.0.13
|
||||
Process architecture: X64
|
||||
Culture: cs-CZ
|
||||
UI culture: en-US
|
||||
|
||||
Paths:
|
||||
Log directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal\Logs\0.0.1.0
|
||||
Config directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState
|
||||
============================================================
|
||||
""";
|
||||
|
||||
public const string Expected2 =
|
||||
"""
|
||||
============================================================
|
||||
Hello World! Command Palette is starting.
|
||||
|
||||
Application:
|
||||
App version: 0.0.1.0
|
||||
Packaging flavor: Packaged
|
||||
Is elevated: no
|
||||
|
||||
Environment:
|
||||
OS version: Microsoft Windows 10.0.26220
|
||||
OS architecture: X64
|
||||
Runtime identifier: win-x64
|
||||
Framework: .NET 9.0.13
|
||||
Process architecture: X64
|
||||
Culture: cs-CZ
|
||||
UI culture: en-US
|
||||
|
||||
Paths:
|
||||
Log directory: [LOCALAPPLICATIONDATA_DIR]Microsoft\PowerToys\CmdPal\Logs\0.0.1.0
|
||||
Config directory: [LOCALAPPLICATIONDATA_DIR]Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState
|
||||
============================================================
|
||||
""";
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
|
||||
|
||||
[TestClass]
|
||||
public partial class ErrorReportSanitizerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Sanitize_ShouldMaskPiiInErrorReport()
|
||||
{
|
||||
// Arrange
|
||||
var reportSanitizer = new ErrorReportSanitizer();
|
||||
var input = TestData.Input;
|
||||
|
||||
// Act
|
||||
var result = reportSanitizer.Sanitize(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(TestData.Expected, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Sanitize_ShouldNotMaskTooMuchPiiInErrorReport()
|
||||
{
|
||||
// Arrange
|
||||
var reportSanitizer = new ErrorReportSanitizer();
|
||||
var input = TestData.Input2;
|
||||
|
||||
// Act
|
||||
var result = reportSanitizer.Sanitize(input);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(TestData.Expected2, result);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.UnitTests.TestUtils;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
|
||||
|
||||
[TestClass]
|
||||
public class FilenameMaskRuleProviderTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void GetRules_ShouldReturnExpectedRules()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new FilenameMaskRuleProvider();
|
||||
|
||||
// Act
|
||||
var rules = provider.GetRules();
|
||||
|
||||
// Assert
|
||||
var ruleList = new List<SanitizationRule>(rules);
|
||||
Assert.AreEqual(1, ruleList.Count);
|
||||
Assert.AreEqual("Mask filename in any path", ruleList[0].Description);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(@"C:\Users\Alice\Documents\secret.txt", @"C:\Users\Alice\Documents\se****.txt")]
|
||||
[DataRow(@"logs\error-report.log", @"logs\er**********.log")]
|
||||
[DataRow(@"/var/logs/trace.json", @"/var/logs/tr***.json")]
|
||||
public void FilenameRules_ShouldMaskFileNamesInPaths(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new FilenameMaskRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("C:\\Users\\Alice\\Documents\\", "C:\\Users\\Alice\\Documents\\")]
|
||||
[DataRow(@"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4", @"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4")]
|
||||
[DataRow(@"C:\Users\Alice\appsettings.json", @"C:\Users\Alice\appsettings.json")]
|
||||
[DataRow(@"C:\Users\Alice\.env", @"C:\Users\Alice\.env")]
|
||||
[DataRow(@"logs\readme", @"logs\readme")]
|
||||
public void FilenameRules_ShouldNotMaskNonSensitivePatterns(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new FilenameMaskRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.UnitTests.TestUtils;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
|
||||
|
||||
[TestClass]
|
||||
public class PiiRuleProviderTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void GetRules_ShouldReturnExpectedRules()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new PiiRuleProvider();
|
||||
|
||||
// Act
|
||||
var rules = provider.GetRules();
|
||||
|
||||
// Assert
|
||||
var ruleList = new List<SanitizationRule>(rules);
|
||||
Assert.AreEqual(4, ruleList.Count);
|
||||
Assert.AreEqual("Email addresses", ruleList[0].Description);
|
||||
Assert.AreEqual("Social Security Numbers", ruleList[1].Description);
|
||||
Assert.AreEqual("Credit card numbers", ruleList[2].Description);
|
||||
Assert.AreEqual("Phone numbers", ruleList[3].Description);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("Contact me at john.doe@contoso.com", "Contact me at [EMAIL_REDACTED]")]
|
||||
[DataRow("Contact me at a_b-c%2@foo-bar.example.co.uk", "Contact me at [EMAIL_REDACTED]")]
|
||||
[DataRow("My email is john@sub-domain.contoso.com.", "My email is [EMAIL_REDACTED].")]
|
||||
[DataRow("Two: a@b.com and c@d.org", "Two: [EMAIL_REDACTED] and [EMAIL_REDACTED]")]
|
||||
[DataRow("No email here", "No email here")]
|
||||
public void EmailRules_ShouldMaskEmailAddresses(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new PiiRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("Call me at 123-456-7890", "Call me at [PHONE_REDACTED]")]
|
||||
[DataRow("My number is (123) 456-7890.", "My number is [PHONE_REDACTED].")]
|
||||
[DataRow("Office: +1 123 456 7890", "Office: [PHONE_REDACTED]")]
|
||||
[DataRow("Two numbers: 123-456-7890 and +420 777123456", "Two numbers: [PHONE_REDACTED] and [PHONE_REDACTED]")]
|
||||
[DataRow("Czech phone +420 777 123 456", "Czech phone [PHONE_REDACTED]")]
|
||||
[DataRow("Slovak phone +421 777 12 34 56", "Slovak phone [PHONE_REDACTED]")]
|
||||
[DataRow("Version 1.2.3.4", "Version 1.2.3.4")]
|
||||
[DataRow("OS version: Microsoft Windows 10.0.26220", "OS version: Microsoft Windows 10.0.26220")]
|
||||
[DataRow("No phone number here", "No phone number here")]
|
||||
public void PhoneRules_ShouldMaskPhoneNumbers(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new PiiRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("My SSN is 123-45-6789", "My SSN is [SSN_REDACTED]")]
|
||||
[DataRow("No SSN here", "No SSN here")]
|
||||
public void SsnRules_ShouldMaskSsn(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new PiiRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("My credit card number is 1234-5678-9012-3456", "My credit card number is [CARD_REDACTED]")]
|
||||
[DataRow("My credit card number is 1234567890123456", "My credit card number is [CARD_REDACTED]")]
|
||||
[DataRow("No credit card here", "No credit card here")]
|
||||
public void CreditCardRules_ShouldMaskCreditCardNumbers(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new PiiRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("Error code: 0x80070005", "Error code: 0x80070005")]
|
||||
[DataRow("Error code: -2147467262", "Error code: -2147467262")]
|
||||
[DataRow("GUID: 123e4567-e89b-12d3-a456-426614174000", "GUID: 123e4567-e89b-12d3-a456-426614174000")]
|
||||
[DataRow("Timestamp: 2023-10-05T14:32:10Z", "Timestamp: 2023-10-05T14:32:10Z")]
|
||||
[DataRow("Version: 1.2.3", "Version: 1.2.3")]
|
||||
[DataRow("Version: 1.2.3.4", "Version: 1.2.3.4")]
|
||||
[DataRow("Version: 0.2.3.4", "Version: 0.2.3.4")]
|
||||
[DataRow("Version: 10.0.22631.3448", "Version: 10.0.22631.3448")]
|
||||
[DataRow("MAC: 00:1A:2B:3C:4D:5E", "MAC: 00:1A:2B:3C:4D:5E")]
|
||||
[DataRow("Date: 2023-10-05", "Date: 2023-10-05")]
|
||||
[DataRow("Date: 05/10/2023", "Date: 05/10/2023")]
|
||||
public void PiiRuleProvider_ShouldNotOverRedact(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new PiiRuleProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.UnitTests.TestUtils;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer;
|
||||
|
||||
[TestClass]
|
||||
public class SecretKeyValueRulesProviderTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void GetRules_ShouldReturnExpectedRules()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var rules = provider.GetRules();
|
||||
|
||||
// Assert
|
||||
var ruleList = new List<SanitizationRule>(rules);
|
||||
Assert.AreEqual(1, ruleList.Count);
|
||||
Assert.AreEqual("Sensitive key/value pairs", ruleList[0].Description);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("password=secret123", "password= [REDACTED]")]
|
||||
[DataRow("passphrase=myPassphrase", "passphrase= [REDACTED]")]
|
||||
[DataRow("pwd=test", "pwd= [REDACTED]")]
|
||||
[DataRow("passwd=pass1234", "passwd= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskPasswordSecrets(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("token=abc123def456", "token= [REDACTED]")]
|
||||
[DataRow("access_token=token_value", "access_token= [REDACTED]")]
|
||||
[DataRow("refresh-token=refresh_value", "refresh-token= [REDACTED]")]
|
||||
[DataRow("id token=id_token_value", "id token= [REDACTED]")]
|
||||
[DataRow("bearer token=bearer_value", "bearer token= [REDACTED]")]
|
||||
[DataRow("session token=session_value", "session token= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskTokens(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("api key=my_api_key", "api key= [REDACTED]")]
|
||||
[DataRow("api-key=key123", "api-key= [REDACTED]")]
|
||||
[DataRow("api_key=secret_key", "api_key= [REDACTED]")]
|
||||
[DataRow("x-api-key=api123", "x-api-key= [REDACTED]")]
|
||||
[DataRow("x api key=key456", "x api key= [REDACTED]")]
|
||||
[DataRow("client id=client123", "client id= [REDACTED]")]
|
||||
[DataRow("client-secret=secret123", "client-secret= [REDACTED]")]
|
||||
[DataRow("consumer secret=secret456", "consumer secret= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskApiCredentials(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("subscription key=sub_key_123", "subscription key= [REDACTED]")]
|
||||
[DataRow("instrumentation key=instr_key", "instrumentation key= [REDACTED]")]
|
||||
[DataRow("account key=account123", "account key= [REDACTED]")]
|
||||
[DataRow("storage account key=storage_key", "storage account key= [REDACTED]")]
|
||||
[DataRow("shared access key=sak123", "shared access key= [REDACTED]")]
|
||||
[DataRow("SAS token=sas123", "SAS token= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskCloudPlatformKeys(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("connection string=Server=localhost;Pwd=pass", "connection string= [REDACTED]")]
|
||||
[DataRow("conn string=conn_value", "conn string= [REDACTED]")]
|
||||
[DataRow("storage connection string=connection_value", "storage connection string= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskConnectionStrings(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("private key=pk123", "private key= [REDACTED]")]
|
||||
[DataRow("certificate password=cert_pass", "certificate password= [REDACTED]")]
|
||||
[DataRow("client certificate password=cert123", "client certificate password= [REDACTED]")]
|
||||
[DataRow("pfx password=pfx_pass", "pfx password= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskCertificateSecrets(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("aws access key id=AKIAIOSFODNN7EXAMPLE", "aws access key id= [REDACTED]")]
|
||||
[DataRow("aws secret access key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "aws secret access key= [REDACTED]")]
|
||||
[DataRow("aws session token=session_token_value", "aws session token= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskAwsKeys(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("password=\"complexPassword123!\"", "password= \"[REDACTED]\"")]
|
||||
[DataRow("api-key='secret-key'", "api-key= '[REDACTED]'")]
|
||||
[DataRow("token=\"bearer_token_value\"", "token= \"[REDACTED]\"")]
|
||||
public void SecretKeyValueRules_ShouldPreserveQuotesAroundRedactedValue(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("PASSWORD=secret", "PASSWORD= [REDACTED]")]
|
||||
[DataRow("Api-Key=key123", "Api-Key= [REDACTED]")]
|
||||
[DataRow("CLIENT_ID=client123", "CLIENT_ID= [REDACTED]")]
|
||||
[DataRow("Pwd=pass123", "Pwd= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldBeCaseInsensitive(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("regularKey=regularValue", "regularKey=regularValue")]
|
||||
[DataRow("config=myConfig", "config=myConfig")]
|
||||
[DataRow("hostname=server.example.com", "hostname=server.example.com")]
|
||||
[DataRow("port=8080", "port=8080")]
|
||||
public void SecretKeyValueRules_ShouldNotRedactNonSecretKeyValuePairs(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("password:secret123", "password: [REDACTED]")]
|
||||
[DataRow("api key:api_key_value", "api key: [REDACTED]")]
|
||||
[DataRow("client_secret:secret_value", "client_secret: [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldSupportColonSeparator(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("password = secret123", "password= [REDACTED]")]
|
||||
[DataRow("api key = api_key_value", "api key= [REDACTED]")]
|
||||
[DataRow("token : token_value", "token: [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldHandleWhitespace(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("password=secret API_KEY=key config=myConfig", "password= [REDACTED] API_KEY= [REDACTED] config=myConfig")]
|
||||
[DataRow("client_id=id123 name=admin pwd=pass123", "client_id= [REDACTED] name=admin pwd= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldHandleMultipleKeyValuePairsInSingleString(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("cosmos db key=cosmos_key", "cosmos db key= [REDACTED]")]
|
||||
[DataRow("service principal secret=sp_secret", "service principal secret= [REDACTED]")]
|
||||
[DataRow("shared access signature=sas_signature", "shared access signature= [REDACTED]")]
|
||||
public void SecretKeyValueRules_ShouldMaskServiceSpecificSecrets(string input, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new SecretKeyValueRulesProvider();
|
||||
|
||||
// Act
|
||||
var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules());
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// 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.Text.RegularExpressions;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.TestUtils;
|
||||
|
||||
/// <summary>
|
||||
/// Test-only helpers for applying SanitizationRule sets without relying on production ITextSanitizer implementation.
|
||||
/// </summary>
|
||||
public static class SanitizerTestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the provided rules to the input, in order, mimicking the production sanitizer behavior closely
|
||||
/// but without any external dependencies.
|
||||
/// </summary>
|
||||
public static string ApplyRules(string? input, IEnumerable<SanitizationRule> rules)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input ?? string.Empty;
|
||||
}
|
||||
|
||||
var result = input;
|
||||
foreach (var rule in rules ?? [])
|
||||
{
|
||||
try
|
||||
{
|
||||
var previous = result;
|
||||
result = rule.Evaluator is null
|
||||
? rule.Regex.Replace(previous, rule.Replacement ?? string.Empty)
|
||||
: rule.Regex.Replace(previous, rule.Evaluator);
|
||||
|
||||
// Guardrail to avoid accidental mass-redaction from a faulty rule
|
||||
if (result.Length < previous.Length * 0.3)
|
||||
{
|
||||
result = previous;
|
||||
}
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
// Ignore timeouts in tests
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a lightweight sanitizer instance backed by the given rules.
|
||||
/// Useful when a component expects an ITextSanitizer, but you want deterministic behavior in tests.
|
||||
/// </summary>
|
||||
public static ITextSanitizer CreateSanitizer(IEnumerable<SanitizationRule> rules)
|
||||
=> new InlineSanitizer(rules);
|
||||
|
||||
private sealed class InlineSanitizer : ITextSanitizer
|
||||
{
|
||||
private readonly List<SanitizationRule> _rules;
|
||||
|
||||
public InlineSanitizer(IEnumerable<SanitizationRule> rules)
|
||||
{
|
||||
_rules = rules?.ToList() ?? [];
|
||||
}
|
||||
|
||||
public string Sanitize(string? input) => ApplyRules(input, _rules);
|
||||
|
||||
public void AddRule(string pattern, string replacement, string description = "")
|
||||
{
|
||||
var rx = new Regex(pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
_rules.Add(new SanitizationRule(rx, replacement, description));
|
||||
}
|
||||
|
||||
public void RemoveRule(string description)
|
||||
{
|
||||
_rules.RemoveAll(r => r.Description.Equals(description, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public IReadOnlyList<SanitizationRule> GetRules() => _rules.AsReadOnly();
|
||||
|
||||
public string TestRule(string input, string ruleDescription)
|
||||
{
|
||||
var rule = _rules.FirstOrDefault(r => r.Description.Contains(ruleDescription, StringComparison.OrdinalIgnoreCase));
|
||||
if (rule.Regex is null)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (rule.Evaluator is not null)
|
||||
{
|
||||
return rule.Regex.Replace(input, rule.Evaluator);
|
||||
}
|
||||
|
||||
if (rule.Replacement is not null)
|
||||
{
|
||||
return rule.Regex.Replace(input, rule.Replacement);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore exceptions for test determinism
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Text;
|
||||
|
||||
[TestClass]
|
||||
public sealed class PrecomputedFuzzyMatcherEmojiTests
|
||||
{
|
||||
private readonly PrecomputedFuzzyMatcher _matcher = new();
|
||||
|
||||
[TestMethod]
|
||||
public void ExactMatch_SimpleEmoji_ReturnsScore()
|
||||
{
|
||||
const string needle = "🚀";
|
||||
const string haystack = "Launch 🚀 sequence";
|
||||
|
||||
var query = _matcher.PrecomputeQuery(needle);
|
||||
var target = _matcher.PrecomputeTarget(haystack);
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match for simple emoji");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExactMatch_SkinTone_ReturnsScore()
|
||||
{
|
||||
const string needle = "👍🏽"; // Medium skin tone
|
||||
const string haystack = "Thumbs up 👍🏽 here";
|
||||
|
||||
var query = _matcher.PrecomputeQuery(needle);
|
||||
var target = _matcher.PrecomputeTarget(haystack);
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match for emoji with skin tone");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ZWJSequence_Family_Match()
|
||||
{
|
||||
const string needle = "👨👩👧👦"; // Family: Man, Woman, Girl, Boy
|
||||
const string haystack = "Emoji 👨👩👧👦 Test";
|
||||
|
||||
var query = _matcher.PrecomputeQuery(needle);
|
||||
var target = _matcher.PrecomputeTarget(haystack);
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match for ZWJ sequence");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Flags_Match()
|
||||
{
|
||||
const string needle = "🇺🇸"; // US Flag (Regional Indicator U + Regional Indicator S)
|
||||
const string haystack = "USA 🇺🇸";
|
||||
|
||||
var query = _matcher.PrecomputeQuery(needle);
|
||||
var target = _matcher.PrecomputeTarget(haystack);
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match for flag emoji");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Emoji_MixedWithText_Search()
|
||||
{
|
||||
const string needle = "t🌮o"; // "t" + taco + "o"
|
||||
const string haystack = "taco 🌮 on tuesday";
|
||||
|
||||
var query = _matcher.PrecomputeQuery(needle);
|
||||
var target = _matcher.PrecomputeTarget(haystack);
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match for emoji mixed with text");
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Text;
|
||||
|
||||
[TestClass]
|
||||
public sealed class PrecomputedFuzzyMatcherOptionsTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Score_RemoveDiacriticsOption_AffectsMatching()
|
||||
{
|
||||
var withDiacriticsRemoved = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = true });
|
||||
var withoutDiacriticsRemoved = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = false });
|
||||
|
||||
const string needle = "cafe";
|
||||
const string haystack = "CAFÉ";
|
||||
|
||||
var scoreWithRemoval = withDiacriticsRemoved.Score(
|
||||
withDiacriticsRemoved.PrecomputeQuery(needle),
|
||||
withDiacriticsRemoved.PrecomputeTarget(haystack));
|
||||
var scoreWithoutRemoval = withoutDiacriticsRemoved.Score(
|
||||
withoutDiacriticsRemoved.PrecomputeQuery(needle),
|
||||
withoutDiacriticsRemoved.PrecomputeTarget(haystack));
|
||||
|
||||
Assert.IsTrue(scoreWithRemoval > 0, "Expected match when diacritics are removed.");
|
||||
Assert.AreEqual(0, scoreWithoutRemoval, "Expected no match when diacritics are preserved.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_SkipWordSeparatorsOption_AffectsMatching()
|
||||
{
|
||||
var skipSeparators = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { SkipWordSeparators = true });
|
||||
var keepSeparators = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { SkipWordSeparators = false });
|
||||
|
||||
const string needle = "a b";
|
||||
const string haystack = "ab";
|
||||
|
||||
var scoreSkip = skipSeparators.Score(
|
||||
skipSeparators.PrecomputeQuery(needle),
|
||||
skipSeparators.PrecomputeTarget(haystack));
|
||||
var scoreKeep = keepSeparators.Score(
|
||||
keepSeparators.PrecomputeQuery(needle),
|
||||
keepSeparators.PrecomputeTarget(haystack));
|
||||
|
||||
Assert.IsTrue(scoreSkip > 0, "Expected match when word separators are skipped.");
|
||||
Assert.AreEqual(0, scoreKeep, "Expected no match when word separators are preserved.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_IgnoreSameCaseBonusOption_AffectsLowercaseQuery()
|
||||
{
|
||||
var ignoreSameCase = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions
|
||||
{
|
||||
IgnoreSameCaseBonusIfQueryIsAllLowercase = true,
|
||||
SameCaseBonus = 10,
|
||||
});
|
||||
var applySameCase = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions
|
||||
{
|
||||
IgnoreSameCaseBonusIfQueryIsAllLowercase = false,
|
||||
SameCaseBonus = 10,
|
||||
});
|
||||
|
||||
const string needle = "test";
|
||||
const string haystack = "test";
|
||||
|
||||
var scoreIgnore = ignoreSameCase.Score(
|
||||
ignoreSameCase.PrecomputeQuery(needle),
|
||||
ignoreSameCase.PrecomputeTarget(haystack));
|
||||
var scoreApply = applySameCase.Score(
|
||||
applySameCase.PrecomputeQuery(needle),
|
||||
applySameCase.PrecomputeTarget(haystack));
|
||||
|
||||
Assert.IsTrue(scoreApply > scoreIgnore, "Expected same-case bonus to apply when not ignored.");
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Text;
|
||||
|
||||
[TestClass]
|
||||
public sealed class PrecomputedFuzzyMatcherSecondaryInputTests
|
||||
{
|
||||
private readonly PrecomputedFuzzyMatcher _matcher = new();
|
||||
private readonly StringFolder _folder = new();
|
||||
private readonly BloomFilter _bloom = new();
|
||||
|
||||
[TestMethod]
|
||||
public void Score_PrimaryQueryMatchesSecondaryTarget_ShouldMatch()
|
||||
{
|
||||
// Scenario: Searching for "calc" should match a file "calculator.exe" where primary is filename, secondary is path
|
||||
var query = CreateQuery("calc");
|
||||
var target = CreateTarget(primary: "important.txt", secondary: "C:\\Programs\\Calculator\\");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected primary query to match secondary target");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_SecondaryQueryMatchesPrimaryTarget_ShouldMatch()
|
||||
{
|
||||
// Scenario: User types "documents\\report" and we want to match against filename
|
||||
var query = CreateQuery(primary: "documents", secondary: "report");
|
||||
var target = CreateTarget(primary: "report.docx");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected secondary query to match primary target");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_SecondaryQueryMatchesSecondaryTarget_ShouldMatch()
|
||||
{
|
||||
// Scenario: Both query and target have secondary info that matches
|
||||
var query = CreateQuery(primary: "test", secondary: "documents");
|
||||
var target = CreateTarget(primary: "something.txt", secondary: "C:\\Users\\Documents\\");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected secondary query to match secondary target");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_PrimaryQueryMatchesBothTargets_ShouldReturnBestScore()
|
||||
{
|
||||
// The same query matches both primary and secondary of target
|
||||
var query = CreateQuery("test");
|
||||
var target = CreateTarget(primary: "test.txt", secondary: "test_folder\\");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected query to match when it appears in both primary and secondary");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_NoSecondaryInQuery_MatchesSecondaryTarget()
|
||||
{
|
||||
// Query without secondary can still match target's secondary
|
||||
var query = CreateQuery("downloads");
|
||||
var target = CreateTarget(primary: "file.txt", secondary: "C:\\Downloads\\");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected primary query to match secondary target");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_NoSecondaryInTarget_SecondaryQueryShouldNotMatch()
|
||||
{
|
||||
// Query with secondary but target without secondary - secondary query shouldn't interfere
|
||||
var query = CreateQuery(primary: "test", secondary: "extra");
|
||||
var target = CreateTarget(primary: "test.txt");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
// Primary should still match, secondary query just doesn't contribute
|
||||
Assert.IsTrue(score > 0, "Expected primary query to match primary target");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_SecondaryQueryNoMatch_PrimaryCanStillMatch()
|
||||
{
|
||||
// Secondary doesn't match anything, but primary does
|
||||
var query = CreateQuery(primary: "file", secondary: "nomatch");
|
||||
var target = CreateTarget(primary: "myfile.txt", secondary: "C:\\Documents\\");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected primary query to match even when secondary doesn't");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_OnlySecondaryMatches_ShouldReturnScore()
|
||||
{
|
||||
// Only the secondary parts match, primary doesn't
|
||||
var query = CreateQuery(primary: "xyz", secondary: "documents");
|
||||
var target = CreateTarget(primary: "abc.txt", secondary: "C:\\Users\\Documents\\");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match when only secondary parts match");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_BothQueriesMatchDifferentTargets_ShouldReturnBestScore()
|
||||
{
|
||||
// Primary query matches secondary target, secondary query matches primary target
|
||||
var query = CreateQuery(primary: "docs", secondary: "report");
|
||||
var target = CreateTarget(primary: "report.pdf", secondary: "C:\\Documents\\");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match when queries cross-match with targets");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_CompletelyDifferent_ShouldNotMatch()
|
||||
{
|
||||
var query = CreateQuery(primary: "xyz", secondary: "abc");
|
||||
var target = CreateTarget(primary: "hello", secondary: "world");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.AreEqual(0, score, "Expected no match when nothing matches");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_EmptySecondaryInputs_ShouldMatchOnPrimary()
|
||||
{
|
||||
var query = CreateQuery(primary: "test", secondary: string.Empty);
|
||||
var target = CreateTarget(primary: "test.txt", secondary: string.Empty);
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match on primary when secondaries are empty");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_WordSeparatorMatching_AcrossSecondary()
|
||||
{
|
||||
// Test that "Power Point" matches "PowerPoint" using secondary
|
||||
var query = CreateQuery(primary: "power", secondary: "point");
|
||||
var target = CreateTarget(primary: "PowerPoint.exe");
|
||||
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected 'power' + 'point' to match 'PowerPoint'");
|
||||
}
|
||||
|
||||
private FuzzyQuery CreateQuery(string primary, string? secondary = null)
|
||||
{
|
||||
var primaryFolded = _folder.Fold(primary, removeDiacritics: true);
|
||||
var primaryBloom = _bloom.Compute(primaryFolded);
|
||||
var primaryEffectiveLength = primaryFolded.Length;
|
||||
var primaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(primary);
|
||||
|
||||
string? secondaryFolded = null;
|
||||
ulong secondaryBloom = 0;
|
||||
var secondaryEffectiveLength = 0;
|
||||
var secondaryIsAllLowercase = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(secondary))
|
||||
{
|
||||
secondaryFolded = _folder.Fold(secondary, removeDiacritics: true);
|
||||
secondaryBloom = _bloom.Compute(secondaryFolded);
|
||||
secondaryEffectiveLength = secondaryFolded.Length;
|
||||
secondaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(secondary);
|
||||
}
|
||||
|
||||
return new FuzzyQuery(
|
||||
original: primary,
|
||||
folded: primaryFolded,
|
||||
bloom: primaryBloom,
|
||||
effectiveLength: primaryEffectiveLength,
|
||||
isAllLowercaseAsciiOrNonLetter: primaryIsAllLowercase,
|
||||
secondaryOriginal: secondary,
|
||||
secondaryFolded: secondaryFolded,
|
||||
secondaryBloom: secondaryBloom,
|
||||
secondaryEffectiveLength: secondaryEffectiveLength,
|
||||
secondaryIsAllLowercaseAsciiOrNonLetter: secondaryIsAllLowercase);
|
||||
}
|
||||
|
||||
private FuzzyTarget CreateTarget(string primary, string? secondary = null)
|
||||
{
|
||||
var primaryFolded = _folder.Fold(primary, removeDiacritics: true);
|
||||
var primaryBloom = _bloom.Compute(primaryFolded);
|
||||
|
||||
string? secondaryFolded = null;
|
||||
ulong secondaryBloom = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(secondary))
|
||||
{
|
||||
secondaryFolded = _folder.Fold(secondary, removeDiacritics: true);
|
||||
secondaryBloom = _bloom.Compute(secondaryFolded);
|
||||
}
|
||||
|
||||
return new FuzzyTarget(
|
||||
original: primary,
|
||||
folded: primaryFolded,
|
||||
bloom: primaryBloom,
|
||||
secondaryOriginal: secondary,
|
||||
secondaryFolded: secondaryFolded,
|
||||
secondaryBloom: secondaryBloom);
|
||||
}
|
||||
|
||||
private static bool IsAllLowercaseAsciiOrNonLetter(string s)
|
||||
{
|
||||
foreach (var c in s)
|
||||
{
|
||||
if ((uint)(c - 'A') <= ('Z' - 'A'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Text;
|
||||
|
||||
[TestClass]
|
||||
public class PrecomputedFuzzyMatcherTests
|
||||
{
|
||||
private readonly PrecomputedFuzzyMatcher _matcher = new();
|
||||
|
||||
public static IEnumerable<object[]> MatchData =>
|
||||
[
|
||||
["a", "a"],
|
||||
["abc", "abc"],
|
||||
["a", "ab"],
|
||||
["b", "ab"],
|
||||
["abc", "axbycz"],
|
||||
["pt", "PowerToys"],
|
||||
["calc", "Calculator"],
|
||||
["vs", "Visual Studio"],
|
||||
["code", "Visual Studio Code"],
|
||||
|
||||
// Diacritics
|
||||
["abc", "ÁBC"],
|
||||
|
||||
// Separators
|
||||
["p/t", "power\\toys"],
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> NonMatchData =>
|
||||
[
|
||||
["z", "abc"],
|
||||
["verylongstring", "short"],
|
||||
];
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(MatchData))]
|
||||
public void Score_Matches_ShouldHavePositiveScore(string needle, string haystack)
|
||||
{
|
||||
var query = _matcher.PrecomputeQuery(needle);
|
||||
var target = _matcher.PrecomputeTarget(haystack);
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, $"Expected positive score for needle='{needle}', haystack='{haystack}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(NonMatchData))]
|
||||
public void Score_NonMatches_ShouldHaveZeroScore(string needle, string haystack)
|
||||
{
|
||||
var query = _matcher.PrecomputeQuery(needle);
|
||||
var target = _matcher.PrecomputeTarget(haystack);
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.AreEqual(0, score, $"Expected 0 score for needle='{needle}', haystack='{haystack}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_EmptyQuery_ReturnsZero()
|
||||
{
|
||||
var query = _matcher.PrecomputeQuery(string.Empty);
|
||||
var target = _matcher.PrecomputeTarget("something");
|
||||
Assert.AreEqual(0, _matcher.Score(query, target));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_EmptyTarget_ReturnsZero()
|
||||
{
|
||||
var query = _matcher.PrecomputeQuery("something");
|
||||
var target = _matcher.PrecomputeTarget(string.Empty);
|
||||
Assert.AreEqual(0, _matcher.Score(query, target));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SchemaId_DefaultMatcher_IsConsistent()
|
||||
{
|
||||
var matcher1 = new PrecomputedFuzzyMatcher();
|
||||
var matcher2 = new PrecomputedFuzzyMatcher();
|
||||
|
||||
Assert.AreEqual(matcher1.SchemaId, matcher2.SchemaId, "Default matchers should have the same SchemaId");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SchemaId_SameOptions_ProducesSameId()
|
||||
{
|
||||
var options = new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = true };
|
||||
var matcher1 = new PrecomputedFuzzyMatcher(options);
|
||||
var matcher2 = new PrecomputedFuzzyMatcher(options);
|
||||
|
||||
Assert.AreEqual(matcher1.SchemaId, matcher2.SchemaId, "Matchers with same options should have the same SchemaId");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SchemaId_DifferentRemoveDiacriticsOption_ProducesDifferentId()
|
||||
{
|
||||
var matcherWithDiacriticsRemoval = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = true });
|
||||
var matcherWithoutDiacriticsRemoval = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = false });
|
||||
|
||||
Assert.AreNotEqual(
|
||||
matcherWithDiacriticsRemoval.SchemaId,
|
||||
matcherWithoutDiacriticsRemoval.SchemaId,
|
||||
"Different RemoveDiacritics option should produce different SchemaId");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SchemaId_ScoringOptionsDoNotAffectId()
|
||||
{
|
||||
// SchemaId should only be affected by options that affect folding/bloom, not scoring
|
||||
var matcher1 = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { CharMatchBonus = 1, CamelCaseBonus = 2 });
|
||||
var matcher2 = new PrecomputedFuzzyMatcher(
|
||||
new PrecomputedFuzzyMatcherOptions { CharMatchBonus = 100, CamelCaseBonus = 200 });
|
||||
|
||||
Assert.AreEqual(matcher1.SchemaId, matcher2.SchemaId, "Scoring options should not affect SchemaId");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_WordSeparatorMatching_PowerPoint()
|
||||
{
|
||||
// Test that "Power Point" can match "PowerPoint" when word separators are skipped
|
||||
var query = _matcher.PrecomputeQuery("Power Point");
|
||||
var target = _matcher.PrecomputeTarget("PowerPoint");
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected 'Power Point' to match 'PowerPoint'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_WordSeparatorMatching_UnderscoreDash()
|
||||
{
|
||||
// Test that different word separators match each other
|
||||
var query = _matcher.PrecomputeQuery("hello_world");
|
||||
var target = _matcher.PrecomputeTarget("hello-world");
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected 'hello_world' to match 'hello-world'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_WordSeparatorMatching_MixedSeparators()
|
||||
{
|
||||
// Test multiple different separators
|
||||
var query = _matcher.PrecomputeQuery("my.file_name");
|
||||
var target = _matcher.PrecomputeTarget("my-file.name");
|
||||
var score = _matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected mixed separators to match");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_PrecomputedQueryReuse_ShouldWorkConsistently()
|
||||
{
|
||||
// Test that precomputed query can be reused across multiple targets
|
||||
var query = _matcher.PrecomputeQuery("test");
|
||||
var target1 = _matcher.PrecomputeTarget("test123");
|
||||
var target2 = _matcher.PrecomputeTarget("mytest");
|
||||
var target3 = _matcher.PrecomputeTarget("unrelated");
|
||||
|
||||
var score1 = _matcher.Score(query, target1);
|
||||
var score2 = _matcher.Score(query, target2);
|
||||
var score3 = _matcher.Score(query, target3);
|
||||
|
||||
Assert.IsTrue(score1 > 0, "Expected query to match first target");
|
||||
Assert.IsTrue(score2 > 0, "Expected query to match second target");
|
||||
Assert.AreEqual(0, score3, "Expected query not to match third target");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_PrecomputedTargetReuse_ShouldWorkConsistently()
|
||||
{
|
||||
// Test that precomputed target can be reused across multiple queries
|
||||
var target = _matcher.PrecomputeTarget("calculator");
|
||||
var query1 = _matcher.PrecomputeQuery("calc");
|
||||
var query2 = _matcher.PrecomputeQuery("lator");
|
||||
var query3 = _matcher.PrecomputeQuery("xyz");
|
||||
|
||||
var score1 = _matcher.Score(query1, target);
|
||||
var score2 = _matcher.Score(query2, target);
|
||||
var score3 = _matcher.Score(query3, target);
|
||||
|
||||
Assert.IsTrue(score1 > 0, "Expected first query to match target");
|
||||
Assert.IsTrue(score2 > 0, "Expected second query to match target");
|
||||
Assert.AreEqual(0, score3, "Expected third query not to match target");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_CaseInsensitiveMatching_Works()
|
||||
{
|
||||
// Test various case combinations
|
||||
var query1 = _matcher.PrecomputeQuery("test");
|
||||
var query2 = _matcher.PrecomputeQuery("TEST");
|
||||
var query3 = _matcher.PrecomputeQuery("TeSt");
|
||||
|
||||
var target = _matcher.PrecomputeTarget("TestFile");
|
||||
|
||||
var score1 = _matcher.Score(query1, target);
|
||||
var score2 = _matcher.Score(query2, target);
|
||||
var score3 = _matcher.Score(query3, target);
|
||||
|
||||
Assert.IsTrue(score1 > 0, "Expected lowercase query to match");
|
||||
Assert.IsTrue(score2 > 0, "Expected uppercase query to match");
|
||||
Assert.IsTrue(score3 > 0, "Expected mixed case query to match");
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Text;
|
||||
|
||||
[TestClass]
|
||||
public sealed class PrecomputedFuzzyMatcherUnicodeTests
|
||||
{
|
||||
private readonly PrecomputedFuzzyMatcher _defaultMatcher = new();
|
||||
|
||||
[TestMethod]
|
||||
public void UnpairedHighSurrogateInNeedle_ShouldNotThrow()
|
||||
{
|
||||
const string needle = "\uD83D"; // high surrogate (unpaired)
|
||||
const string haystack = "abc";
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(needle);
|
||||
var t = _defaultMatcher.PrecomputeTarget(haystack);
|
||||
_ = _defaultMatcher.Score(q, t);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UnpairedLowSurrogateInNeedle_ShouldNotThrow()
|
||||
{
|
||||
const string needle = "\uDC00"; // low surrogate (unpaired)
|
||||
const string haystack = "abc";
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(needle);
|
||||
var t = _defaultMatcher.PrecomputeTarget(haystack);
|
||||
_ = _defaultMatcher.Score(q, t);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UnpairedHighSurrogateInHaystack_ShouldNotThrow()
|
||||
{
|
||||
const string needle = "a";
|
||||
const string haystack = "a\uD83D" + "bc"; // inject unpaired high surrogate
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(needle);
|
||||
var t = _defaultMatcher.PrecomputeTarget(haystack);
|
||||
_ = _defaultMatcher.Score(q, t);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MixedSurrogatesAndMarks_ShouldNotThrow()
|
||||
{
|
||||
// "Garbage smoothie": unpaired surrogate + combining mark + emoji surrogate pair
|
||||
const string needle = "a\uD83D\u0301"; // 'a' + unpaired high surrogate + combining acute
|
||||
const string haystack = "a\u0301 \U0001F600"; // 'a' + combining acute + space + 😀 (valid pair)
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(needle);
|
||||
var t = _defaultMatcher.PrecomputeTarget(haystack);
|
||||
_ = _defaultMatcher.Score(q, t);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidEmojiSurrogatePair_ShouldNotThrow_AndCanMatch()
|
||||
{
|
||||
// 😀 U+1F600 encoded as surrogate pair in UTF-16
|
||||
const string needle = "\U0001F600";
|
||||
const string haystack = "x \U0001F600 y";
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(needle);
|
||||
var t = _defaultMatcher.PrecomputeTarget(haystack);
|
||||
var score = _defaultMatcher.Score(q, t);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected emoji to produce a match score > 0.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RandomUtf16Garbage_ShouldNotThrow()
|
||||
{
|
||||
// Deterministic pseudo-random "UTF-16 garbage", including surrogates.
|
||||
var s1 = MakeDeterministicGarbage(seed: 1234, length: 512);
|
||||
var s2 = MakeDeterministicGarbage(seed: 5678, length: 1024);
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(s1);
|
||||
var t = _defaultMatcher.PrecomputeTarget(s2);
|
||||
_ = _defaultMatcher.Score(q, t);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void HighSurrogateAtEndOfHaystack_ShouldNotThrow()
|
||||
{
|
||||
const string needle = "a";
|
||||
const string haystack = "abc\uD83D"; // Ends with high surrogate
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(needle);
|
||||
var t = _defaultMatcher.PrecomputeTarget(haystack);
|
||||
_ = _defaultMatcher.Score(q, t);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VeryLongStrings_ShouldNotThrow()
|
||||
{
|
||||
var needle = new string('a', 100);
|
||||
var haystack = new string('b', 10000) + needle + new string('c', 10000);
|
||||
|
||||
var q = _defaultMatcher.PrecomputeQuery(needle);
|
||||
var t = _defaultMatcher.PrecomputeTarget(haystack);
|
||||
_ = _defaultMatcher.Score(q, t);
|
||||
}
|
||||
|
||||
private static string MakeDeterministicGarbage(int seed, int length)
|
||||
{
|
||||
// LCG for deterministic generation without Random’s platform/version surprises.
|
||||
var x = (uint)seed;
|
||||
var chars = length <= 2048 ? stackalloc char[length] : new char[length];
|
||||
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
{
|
||||
// LCG: x = (a*x + c) mod 2^32
|
||||
x = unchecked((1664525u * x) + 1013904223u);
|
||||
|
||||
// Take top 16 bits as UTF-16 code unit (includes surrogates).
|
||||
chars[i] = (char)(x >> 16);
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// 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.Globalization;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Text;
|
||||
|
||||
[TestClass]
|
||||
public class PrecomputedFuzzyMatcherWithPinyinTests
|
||||
{
|
||||
private PrecomputedFuzzyMatcherWithPinyin CreateMatcher(PinyinMode mode = PinyinMode.On, bool removeApostrophes = true)
|
||||
{
|
||||
return new PrecomputedFuzzyMatcherWithPinyin(
|
||||
new PrecomputedFuzzyMatcherOptions(),
|
||||
new PinyinFuzzyMatcherOptions { Mode = mode, RemoveApostrophesForQuery = removeApostrophes },
|
||||
new StringFolder(),
|
||||
new BloomFilter());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("bj", "北京")]
|
||||
[DataRow("sh", "上海")]
|
||||
[DataRow("nihao", "你好")]
|
||||
[DataRow("beijing", "北京")]
|
||||
[DataRow("ce", "测试")]
|
||||
public void Score_PinyinMatches_ShouldHavePositiveScore(string needle, string haystack)
|
||||
{
|
||||
var matcher = CreateMatcher(PinyinMode.On);
|
||||
var query = matcher.PrecomputeQuery(needle);
|
||||
var target = matcher.PrecomputeTarget(haystack);
|
||||
var score = matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, $"Expected positive score for needle='{needle}', haystack='{haystack}'");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_PinyinOff_ShouldNotMatchPinyin()
|
||||
{
|
||||
var matcher = CreateMatcher(PinyinMode.Off);
|
||||
var needle = "bj";
|
||||
var haystack = "北京";
|
||||
|
||||
var query = matcher.PrecomputeQuery(needle);
|
||||
var target = matcher.PrecomputeTarget(haystack);
|
||||
var score = matcher.Score(query, target);
|
||||
|
||||
Assert.AreEqual(0, score, "Pinyin match should be disabled.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_StandardMatch_WorksWithPinyinMatcher()
|
||||
{
|
||||
var matcher = CreateMatcher(PinyinMode.On);
|
||||
var needle = "abc";
|
||||
var haystack = "abc";
|
||||
|
||||
var query = matcher.PrecomputeQuery(needle);
|
||||
var target = matcher.PrecomputeTarget(haystack);
|
||||
var score = matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Standard match should still work.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Score_ApostropheRemoval_Works()
|
||||
{
|
||||
var matcher = CreateMatcher(PinyinMode.On, removeApostrophes: true);
|
||||
var needle = "xi'an";
|
||||
|
||||
// "xi'an" -> "xian" -> matches "西安" (Xi An)
|
||||
var haystack = "西安";
|
||||
|
||||
var query = matcher.PrecomputeQuery(needle);
|
||||
var target = matcher.PrecomputeTarget(haystack);
|
||||
var score = matcher.Score(query, target);
|
||||
|
||||
Assert.IsTrue(score > 0, "Expected match for 'xi'an' -> '西安' with apostrophe removal.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AutoMode_EnablesForChineseCulture()
|
||||
{
|
||||
var originalCulture = CultureInfo.CurrentUICulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("zh-CN");
|
||||
var matcher = CreateMatcher(PinyinMode.AutoSimplifiedChineseUi);
|
||||
|
||||
var score = matcher.Score(matcher.PrecomputeQuery("bj"), matcher.PrecomputeTarget("北京"));
|
||||
Assert.IsTrue(score > 0, "Should match when UI culture is zh-CN");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentUICulture = originalCulture;
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AutoMode_DisablesForNonChineseCulture()
|
||||
{
|
||||
var originalCulture = CultureInfo.CurrentUICulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentUICulture = new CultureInfo("en-US");
|
||||
var matcher = CreateMatcher(PinyinMode.AutoSimplifiedChineseUi);
|
||||
|
||||
var score = matcher.Score(matcher.PrecomputeQuery("bj"), matcher.PrecomputeTarget("北京"));
|
||||
Assert.AreEqual(0, score, "Should NOT match when UI culture is en-US");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentUICulture = originalCulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// 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 Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.UnitTests.Text;
|
||||
|
||||
[TestClass]
|
||||
public class StringFolderTests
|
||||
{
|
||||
private readonly StringFolder _folder = new();
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(null, "")]
|
||||
[DataRow("", "")]
|
||||
[DataRow("abc", "ABC")]
|
||||
[DataRow("ABC", "ABC")]
|
||||
[DataRow("a\\b", "A/B")]
|
||||
[DataRow("a/b", "A/B")]
|
||||
[DataRow("ÁBC", "ABC")] // Diacritic removal
|
||||
[DataRow("ñ", "N")]
|
||||
[DataRow("hello world", "HELLO WORLD")]
|
||||
public void Fold_RemoveDiacritics_Works(string input, string expected)
|
||||
{
|
||||
Assert.AreEqual(expected, _folder.Fold(input, removeDiacritics: true));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("abc", "ABC")]
|
||||
[DataRow("ÁBC", "ÁBC")] // No diacritic removal
|
||||
[DataRow("a\\b", "A/B")]
|
||||
public void Fold_KeepDiacritics_Works(string input, string expected)
|
||||
{
|
||||
Assert.AreEqual(expected, _folder.Fold(input, removeDiacritics: false));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Fold_IsAlreadyFolded_ReturnsSameInstance()
|
||||
{
|
||||
var input = "ALREADY/FOLDED";
|
||||
var result = _folder.Fold(input, removeDiacritics: true);
|
||||
Assert.AreSame(input, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Fold_WithNonAsciiButNoDiacritics_ReturnsFolded()
|
||||
{
|
||||
// E.g. Cyrillic or other scripts that might not decompose in a simple way or just upper case
|
||||
// "привет" -> "ПРИВЕТ"
|
||||
var input = "привет";
|
||||
var expected = "ПРИВЕТ";
|
||||
Assert.AreEqual(expected, _folder.Fold(input, removeDiacritics: true));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user