mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
CmdPal: Make Bookmarks Great and Fast Again (#41961)
## Summary of the Pull Request This PR improves recognition and classification of bookmarks, allowing CmdPal to recognize almost anything sensible a user can throw at it—while being forgiving of common input issues (such as unquoted spaces in paths, etc.). Extended classification and exploration of edge cases also revealed limitations in the current implementation, which reloaded all bookmarks on every change. This caused visible UI lag and could lead to issues like unintentionally adding the same bookmark multiple times. ### tl;dr More details below - Introduces `BookmarkManager` (async saves, thread-safe, immutable, unique IDs, separate persistence). - Adds `BookmarkResolver` (classification, Shell-like path/exe resolution, better icons). - `BookmarkListItem` now refreshes independently; Name is optional (Shell fallback). - Uses Shell API for user-friendly names and paths. - Adds `IIconLocator`, protocol icon support, Steam custom icon, fallback icons and improved `FaviconLoader` (handles redirects). Every bookmark should now have icon, so we have consistent UI without gaps. - Refactors placeholders (`IPlaceholderParser`), adds tests, restricts names to `[a-zA-Z0-9_-]`, excludes GUIDs. - Reorganizes structure, syncs icons/key chords with AllApps/Indexer. - For web and protocol bookmarks URL-encodes placeholder values - **Performance:** avoids full reloads, improves scalability, reduces UI lag. - **Breaking change:** stricter placeholder rules, bookmark command ids. <img width="786" height="1392" alt="image" src="https://github.com/user-attachments/assets/88d6617a-9f7c-47d1-bd60-80593fe414d3" /> <img width="786" height="1389" alt="image" src="https://github.com/user-attachments/assets/8cdd3a09-73ae-439a-94ef-4e14d14c1ef3" /> <img width="896" height="461" alt="image" src="https://github.com/user-attachments/assets/1f32e230-7d32-4710-b4c5-28e202c0e37b" /> <img width="862" height="391" alt="image" src="https://github.com/user-attachments/assets/7649ce6a-3471-46f2-adc4-fb21bd4ecfed" /> <img width="844" height="356" alt="image" src="https://github.com/user-attachments/assets/0c0b1941-fe5c-474e-94e9-de3817cb5470" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #41705 - [x] Closes: #41892 - [x] Closes: #41872 - [x] Closes: #41545 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ### Changes - **Bookmark Manager** - Introduces a `BookmarkManager` class that: - Holds bookmarks in memory and saves changes asynchronously. - Is safe to operate from multiple threads. - Uses immutable data for transport. - Separates the **persistence model** from in-memory data. - Assigns explicit unique IDs to bookmarks. - These IDs also serve as stable top-level command identifiers, enabling aliases and shortcuts to be bound reliably. - **Bookmark Resolver** - Determines the type of a bookmark (`CommandKind`: file, web link, command, etc.). - Detects its target and parameters. - Returns a `Classification` object containing all information needed to present the bookmark to the user (icon, primary command, context menu actions, etc.). - For unquoted local paths, attempts to find the *longest viable matching path* to a file or executable, automatically handling spaces in paths (e.g., `C:\Program Files`). - The resolution of executables from the command line now more closely matches **Windows Shell** behavior. - Users are more likely to get the correct result. - Icons can be determined more reliably. - **Bookmark List Items** - Each top-level bookmark item (`BookmarkListItem`) is now responsible for presenting itself. - Items refresh their state independently on load or after changes. - The **Name** field is now optional. - If no explicit name is provided, a user-friendly fallback name is computed automatically using the Shell API. - Context actions are now more in line with **All Apps** and **Indexer** built-in extensions, matching items, icons, and shortcuts (still a work in progress). - **Shell API Integration** - Uses the Shell API to provide friendly names and paths for shell or file system items, keeping the UI aligned with the OS. - **Protocol and Icon Support** - Adds `IIconLocator` and protocol icon support. - Provides a custom icon for **Steam**, since Steam registers its protocol to an executable not on the path (and the Steam protocol is expected to be a common case). - Adds `FaviconLoader` for web links. - Can now follow redirects and retrieve the favicon even if the server takes the request on a “sightseeing tour.” - Provides **Fluent Segoe fallback icons** that match the bookmark classification when no specific icon is available. - **Refactors and Reorganization** - Extracts `IPlaceholderParser` for testability and reusability. - Renames `Bookmarks` → `BookmarksData` to prevent naming collisions. - Reorganizes the structure (reducing root-level file clutter). - Synchronizes icons and key chords with AllApps/Indexer. - Refactors placeholder parsing logic and **adds tests** to improve reliability. - **Misc** - Correctly URL-encodes placeholder values in Web URL or protocol bookmarks. --- ### Performance Improvements - Eliminates full reloads of all bookmarks on every change. - Improves scalability when working with a large number of bookmarks. - Independent refresh of list items reduces UI lag and improves responsiveness. - Asynchronous persistence prevents blocking the UI thread on saves. --- ### Breaking Changes - **Placeholders** - Placeholder names are now restricted to letters (`a–z`, `A–Z`), digits (`0–9`), uderscore (`_`), hyphen (`-`). - GUIDs are explicitly excluded as valid placeholders to prevent collisions with shell IDs. - When presented to the user, placeholders are considered case-insensitive. - ** Bookmark Top-Level Command - **Bookmark Top-Level Command** - IDs for bookmark commands are now based on a unique identifier. - This breaks existing bindings to shortcuts and aliases. - Newly created bindings will be stable regardless of changes to the bookmark (name, address, or having placeholders). - <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com>
This commit is contained in:
@@ -1,42 +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.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class BookmarkDataTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void BookmarkDataWebUrlDetection()
|
||||
{
|
||||
// Act
|
||||
var webBookmark = new BookmarkData
|
||||
{
|
||||
Name = "Test Site",
|
||||
Bookmark = "https://test.com",
|
||||
};
|
||||
|
||||
var nonWebBookmark = new BookmarkData
|
||||
{
|
||||
Name = "Local File",
|
||||
Bookmark = "C:\\temp\\file.txt",
|
||||
};
|
||||
|
||||
var placeholderBookmark = new BookmarkData
|
||||
{
|
||||
Name = "Placeholder",
|
||||
Bookmark = "{Placeholder}",
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(webBookmark.IsWebUrl());
|
||||
Assert.IsFalse(webBookmark.IsPlaceholder);
|
||||
Assert.IsFalse(nonWebBookmark.IsWebUrl());
|
||||
Assert.IsFalse(nonWebBookmark.IsPlaceholder);
|
||||
|
||||
Assert.IsTrue(placeholderBookmark.IsPlaceholder);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
@@ -191,7 +193,7 @@ public class BookmarkJsonParserTests
|
||||
public void SerializeBookmarks_ValidBookmarks_ReturnsJsonString()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarks = new Bookmarks
|
||||
var bookmarks = new BookmarksData
|
||||
{
|
||||
Data = new List<BookmarkData>
|
||||
{
|
||||
@@ -216,7 +218,7 @@ public class BookmarkJsonParserTests
|
||||
public void SerializeBookmarks_EmptyBookmarks_ReturnsValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarks = new Bookmarks();
|
||||
var bookmarks = new BookmarksData();
|
||||
|
||||
// Act
|
||||
var result = _parser.SerializeBookmarks(bookmarks);
|
||||
@@ -241,7 +243,7 @@ public class BookmarkJsonParserTests
|
||||
public void ParseBookmarks_RoundTripSerialization_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
var originalBookmarks = new Bookmarks
|
||||
var originalBookmarks = new BookmarksData
|
||||
{
|
||||
Data = new List<BookmarkData>
|
||||
{
|
||||
@@ -263,7 +265,6 @@ public class BookmarkJsonParserTests
|
||||
{
|
||||
Assert.AreEqual(originalBookmarks.Data[i].Name, parsedBookmarks.Data[i].Name);
|
||||
Assert.AreEqual(originalBookmarks.Data[i].Bookmark, parsedBookmarks.Data[i].Bookmark);
|
||||
Assert.AreEqual(originalBookmarks.Data[i].IsPlaceholder, parsedBookmarks.Data[i].IsPlaceholder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,70 +297,6 @@ public class BookmarkJsonParserTests
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3, result.Data.Count);
|
||||
|
||||
Assert.IsFalse(result.Data[0].IsPlaceholder);
|
||||
Assert.IsTrue(result.Data[1].IsPlaceholder);
|
||||
Assert.IsTrue(result.Data[2].IsPlaceholder);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_IsWebUrl_CorrectlyIdentifiesWebUrls()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "HTTPS Website",
|
||||
"Bookmark": "https://www.google.com"
|
||||
},
|
||||
{
|
||||
"Name": "HTTP Website",
|
||||
"Bookmark": "http://example.com"
|
||||
},
|
||||
{
|
||||
"Name": "Website without protocol",
|
||||
"Bookmark": "www.github.com"
|
||||
},
|
||||
{
|
||||
"Name": "Local File Path",
|
||||
"Bookmark": "C:\\Users\\test\\Documents\\file.txt"
|
||||
},
|
||||
{
|
||||
"Name": "Network Path",
|
||||
"Bookmark": "\\\\server\\share\\file.txt"
|
||||
},
|
||||
{
|
||||
"Name": "Executable",
|
||||
"Bookmark": "notepad.exe"
|
||||
},
|
||||
{
|
||||
"Name": "File URI",
|
||||
"Bookmark": "file:///C:/temp/file.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(7, result.Data.Count);
|
||||
|
||||
// Web URLs should return true
|
||||
Assert.IsTrue(result.Data[0].IsWebUrl(), "HTTPS URL should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTP URL should be identified as web URL");
|
||||
|
||||
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
|
||||
// Assert.IsTrue(result.Data[2].IsWebUrl(), "Domain without protocol should be identified as web URL");
|
||||
|
||||
// Non-web URLs should return false
|
||||
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file path should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[4].IsWebUrl(), "Network path should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[5].IsWebUrl(), "Executable should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[6].IsWebUrl(), "File URI should not be identified as web URL");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -415,23 +352,10 @@ public class BookmarkJsonParserTests
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(9, result.Data.Count);
|
||||
|
||||
// Should be identified as placeholders
|
||||
Assert.IsTrue(result.Data[0].IsPlaceholder, "Simple placeholder should be identified");
|
||||
Assert.IsTrue(result.Data[1].IsPlaceholder, "Multiple placeholders should be identified");
|
||||
Assert.IsTrue(result.Data[2].IsPlaceholder, "Web URL with placeholder should be identified");
|
||||
Assert.IsTrue(result.Data[3].IsPlaceholder, "Complex placeholder should be identified");
|
||||
Assert.IsTrue(result.Data[8].IsPlaceholder, "Empty placeholder should be identified");
|
||||
|
||||
// Should NOT be identified as placeholders
|
||||
Assert.IsFalse(result.Data[4].IsPlaceholder, "Regular URL should not be placeholder");
|
||||
Assert.IsFalse(result.Data[5].IsPlaceholder, "Local file should not be placeholder");
|
||||
Assert.IsFalse(result.Data[6].IsPlaceholder, "Only opening brace should not be placeholder");
|
||||
Assert.IsFalse(result.Data[7].IsPlaceholder, "Only closing brace should not be placeholder");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesBothWebUrlAndPlaceholder()
|
||||
public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesPlaceholder()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
@@ -463,73 +387,5 @@ public class BookmarkJsonParserTests
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(4, result.Data.Count);
|
||||
|
||||
// Web URL with placeholder
|
||||
Assert.IsTrue(result.Data[0].IsWebUrl(), "Web URL with placeholder should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[0].IsPlaceholder, "Web URL with placeholder should be identified as placeholder");
|
||||
|
||||
// Web URL without placeholder
|
||||
Assert.IsTrue(result.Data[1].IsWebUrl(), "Web URL without placeholder should be identified as web URL");
|
||||
Assert.IsFalse(result.Data[1].IsPlaceholder, "Web URL without placeholder should not be identified as placeholder");
|
||||
|
||||
// Local file with placeholder
|
||||
Assert.IsFalse(result.Data[2].IsWebUrl(), "Local file with placeholder should not be identified as web URL");
|
||||
Assert.IsTrue(result.Data[2].IsPlaceholder, "Local file with placeholder should be identified as placeholder");
|
||||
|
||||
// Local file without placeholder
|
||||
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file without placeholder should not be identified as web URL");
|
||||
Assert.IsFalse(result.Data[3].IsPlaceholder, "Local file without placeholder should not be identified as placeholder");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParseBookmarks_EdgeCaseUrls_CorrectlyIdentifiesWebUrls()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"Data": [
|
||||
{
|
||||
"Name": "FTP URL",
|
||||
"Bookmark": "ftp://files.example.com"
|
||||
},
|
||||
{
|
||||
"Name": "HTTPS with port",
|
||||
"Bookmark": "https://localhost:8080"
|
||||
},
|
||||
{
|
||||
"Name": "IP Address",
|
||||
"Bookmark": "http://192.168.1.1"
|
||||
},
|
||||
{
|
||||
"Name": "Subdomain",
|
||||
"Bookmark": "https://api.github.com"
|
||||
},
|
||||
{
|
||||
"Name": "Domain only",
|
||||
"Bookmark": "example.com"
|
||||
},
|
||||
{
|
||||
"Name": "Not a URL - no dots",
|
||||
"Bookmark": "localhost"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = _parser.ParseBookmarks(json);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(6, result.Data.Count);
|
||||
|
||||
Assert.IsFalse(result.Data[0].IsWebUrl(), "FTP URL should not be identified as web URL");
|
||||
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTPS with port should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[2].IsWebUrl(), "IP Address with HTTP should be identified as web URL");
|
||||
Assert.IsTrue(result.Data[3].IsWebUrl(), "Subdomain should be identified as web URL");
|
||||
|
||||
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
|
||||
// Assert.IsTrue(result.Data[4].IsWebUrl(), "Domain only should be identified as web URL");
|
||||
Assert.IsFalse(result.Data[5].IsWebUrl(), "Single word without dots should not be identified as web URL");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class BookmarkManagerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void BookmarkManager_CanBeInstantiated()
|
||||
{
|
||||
// Arrange & Act
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(bookmarkManager);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BookmarkManager_InitialBookmarksEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
|
||||
|
||||
// Act
|
||||
var bookmarks = bookmarkManager.Bookmarks;
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(bookmarks);
|
||||
Assert.AreEqual(0, bookmarks.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BookmarkManager_InitialBookmarksCorruptedData()
|
||||
{
|
||||
// Arrange
|
||||
var json = "@*>$ß Corrupted data. Hey, this is not JSON!";
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource(json));
|
||||
|
||||
// Act
|
||||
var bookmarks = bookmarkManager.Bookmarks;
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(bookmarks);
|
||||
Assert.AreEqual(0, bookmarks.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BookmarkManager_InitializeWithExistingData()
|
||||
{
|
||||
// Arrange
|
||||
const string json = """
|
||||
{
|
||||
"Data":[
|
||||
{"Id":"d290f1ee-6c54-4b01-90e6-d701748f0851","Name":"Bookmark1","Bookmark":"C:\\Path1"},
|
||||
{"Id":"c4a760a4-5b63-4c9e-b8b3-2c3f5f3e6f7a","Name":"Bookmark2","Bookmark":"D:\\Path2"}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource(json));
|
||||
|
||||
// Act
|
||||
var bookmarks = bookmarkManager.Bookmarks?.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(bookmarks);
|
||||
Assert.AreEqual(2, bookmarks.Count);
|
||||
|
||||
Assert.AreEqual("Bookmark1", bookmarks[0].Name);
|
||||
Assert.AreEqual("C:\\Path1", bookmarks[0].Bookmark);
|
||||
Assert.AreEqual(Guid.Parse("d290f1ee-6c54-4b01-90e6-d701748f0851"), bookmarks[0].Id);
|
||||
|
||||
Assert.AreEqual("Bookmark2", bookmarks[1].Name);
|
||||
Assert.AreEqual("D:\\Path2", bookmarks[1].Bookmark);
|
||||
Assert.AreEqual(Guid.Parse("c4a760a4-5b63-4c9e-b8b3-2c3f5f3e6f7a"), bookmarks[1].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BookmarkManager_InitializeWithLegacyData_GeneratesIds()
|
||||
{
|
||||
// Arrange
|
||||
const string json = """
|
||||
{
|
||||
"Data":
|
||||
[
|
||||
{ "Name":"Bookmark1", "Bookmark":"C:\\Path1" },
|
||||
{ "Name":"Bookmark2", "Bookmark":"D:\\Path2" }
|
||||
]
|
||||
}
|
||||
""";
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource(json));
|
||||
|
||||
// Act
|
||||
var bookmarks = bookmarkManager.Bookmarks?.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(bookmarks);
|
||||
Assert.AreEqual(2, bookmarks.Count);
|
||||
|
||||
Assert.AreEqual("Bookmark1", bookmarks[0].Name);
|
||||
Assert.AreEqual("C:\\Path1", bookmarks[0].Bookmark);
|
||||
Assert.AreNotEqual(Guid.Empty, bookmarks[0].Id);
|
||||
|
||||
Assert.AreEqual("Bookmark2", bookmarks[1].Name);
|
||||
Assert.AreEqual("D:\\Path2", bookmarks[1].Bookmark);
|
||||
Assert.AreNotEqual(Guid.Empty, bookmarks[1].Id);
|
||||
|
||||
Assert.AreNotEqual(bookmarks[0].Id, bookmarks[1].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BookmarkManager_AddBookmark_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
|
||||
var bookmarkAddedEventFired = false;
|
||||
bookmarkManager.BookmarkAdded += (bookmark) =>
|
||||
{
|
||||
bookmarkAddedEventFired = true;
|
||||
Assert.AreEqual("TestBookmark", bookmark.Name);
|
||||
Assert.AreEqual("C:\\TestPath", bookmark.Bookmark);
|
||||
};
|
||||
|
||||
// Act
|
||||
var addedBookmark = bookmarkManager.Add("TestBookmark", "C:\\TestPath");
|
||||
|
||||
// Assert
|
||||
var bookmarks = bookmarkManager.Bookmarks;
|
||||
Assert.AreEqual(1, bookmarks.Count);
|
||||
Assert.AreEqual(addedBookmark, bookmarks.First());
|
||||
Assert.IsTrue(bookmarkAddedEventFired);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BookmarkManager_RemoveBookmark_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
|
||||
var addedBookmark = bookmarkManager.Add("TestBookmark", "C:\\TestPath");
|
||||
var bookmarkRemovedEventFired = false;
|
||||
bookmarkManager.BookmarkRemoved += (bookmark) =>
|
||||
{
|
||||
bookmarkRemovedEventFired = true;
|
||||
Assert.AreEqual(addedBookmark, bookmark);
|
||||
};
|
||||
|
||||
// Act
|
||||
var removeResult = bookmarkManager.Remove(addedBookmark.Id);
|
||||
|
||||
// Assert
|
||||
var bookmarks = bookmarkManager.Bookmarks;
|
||||
Assert.IsTrue(removeResult);
|
||||
Assert.AreEqual(0, bookmarks.Count);
|
||||
Assert.IsTrue(bookmarkRemovedEventFired);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BookmarkManager_UpdateBookmark_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var bookmarkManager = new BookmarksManager(new MockBookmarkDataSource());
|
||||
var addedBookmark = bookmarkManager.Add("TestBookmark", "C:\\TestPath");
|
||||
var bookmarkUpdatedEventFired = false;
|
||||
bookmarkManager.BookmarkUpdated += (data, bookmarkData) =>
|
||||
{
|
||||
bookmarkUpdatedEventFired = true;
|
||||
Assert.AreEqual(addedBookmark, data);
|
||||
Assert.AreEqual("UpdatedBookmark", bookmarkData.Name);
|
||||
Assert.AreEqual("D:\\UpdatedPath", bookmarkData.Bookmark);
|
||||
};
|
||||
|
||||
// Act
|
||||
var updatedBookmark = bookmarkManager.Update(addedBookmark.Id, "UpdatedBookmark", "D:\\UpdatedPath");
|
||||
|
||||
// Assert
|
||||
var bookmarks = bookmarkManager.Bookmarks;
|
||||
Assert.IsNotNull(updatedBookmark);
|
||||
Assert.AreEqual(1, bookmarks.Count);
|
||||
Assert.AreEqual(updatedBookmark, bookmarks.First());
|
||||
Assert.AreEqual("UpdatedBookmark", updatedBookmark.Name);
|
||||
Assert.AreEqual("D:\\UpdatedPath", updatedBookmark.Bookmark);
|
||||
Assert.IsTrue(bookmarkUpdatedEventFired);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public partial class BookmarkResolverTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DynamicData(nameof(CommonClassificationData.CommonCases), typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Common_ValidateCommonClassification(PlaceholderClassificationCase c) => await RunShared(c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(nameof(CommonClassificationData.UwpAumidCases), typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Common_ValidateUwpAumidClassification(PlaceholderClassificationCase c) => await RunShared(c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(CommonClassificationData.UnquotedRelativePaths), dynamicDataDeclaringType: typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Common_ValidateUnquotedRelativePathScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(CommonClassificationData.UnquotedShellProtocol), dynamicDataDeclaringType: typeof(CommonClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Common_ValidateUnquotedShellProtocolScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
private static class CommonClassificationData
|
||||
{
|
||||
public static IEnumerable<object[]> CommonCases()
|
||||
{
|
||||
return
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "HTTPS URL",
|
||||
Input: "https://microsoft.com",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.WebUrl,
|
||||
ExpectedTarget: "https://microsoft.com",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "WWW URL without scheme",
|
||||
Input: "www.example.com",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.WebUrl,
|
||||
ExpectedTarget: "https://www.example.com",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "HTTP URL with query",
|
||||
Input: "http://yahoo.com?p=search",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.WebUrl,
|
||||
ExpectedTarget: "http://yahoo.com?p=search",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Mailto protocol",
|
||||
Input: "mailto:user@example.com",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Protocol,
|
||||
ExpectedTarget: "mailto:user@example.com",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "MS-Settings protocol",
|
||||
Input: "ms-settings:display",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Protocol,
|
||||
ExpectedTarget: "ms-settings:display",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Custom protocol",
|
||||
Input: "myapp:doit",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Protocol,
|
||||
ExpectedTarget: "myapp:doit",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Not really a valid protocol",
|
||||
Input: "this is not really a protocol myapp: doit",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "this",
|
||||
ExpectedArguments: "is not really a protocol myapp: doit",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Drive",
|
||||
Input: "C:",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: "C:\\",
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Non-existing path with extension",
|
||||
Input: "C:\\this-folder-should-not-exist-12345\\file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "C:\\this-folder-should-not-exist-12345\\file.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unknown fallback",
|
||||
Input: "some_unlikely_command_name_12345",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "some_unlikely_command_name_12345",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
|
||||
[new PlaceholderClassificationCase(
|
||||
Name: "Simple unquoted executable path",
|
||||
Input: "C:\\Windows\\System32\\notepad.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Windows\\System32\\notepad.exe",
|
||||
ExpectedArguments: string.Empty,
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unquoted document path (non existed file)",
|
||||
Input: "C:\\Users\\John\\Documents\\file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "C:\\Users\\John\\Documents\\file.txt",
|
||||
ExpectedArguments: string.Empty,
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> UwpAumidCases() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "UWP AUMID with AppsFolder prefix",
|
||||
Input: "shell:AppsFolder\\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Aumid,
|
||||
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App",
|
||||
ExpectedLaunch: LaunchMethod.ActivateAppId,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "UWP AUMID with AppsFolder prefix and argument (Trap)",
|
||||
Input: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App --maximized",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Aumid,
|
||||
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App --maximized",
|
||||
ExpectedArguments: string.Empty,
|
||||
ExpectedLaunch: LaunchMethod.ActivateAppId,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "UWP AUMID via AppsFolder",
|
||||
Input: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Aumid,
|
||||
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
|
||||
ExpectedLaunch: LaunchMethod.ActivateAppId,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> UnquotedShellProtocol() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell for This PC (shell:::{20D04FE0-3AEA-1069-A2D8-08002B30309D})",
|
||||
Input: "shell:::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.VirtualShellItem,
|
||||
ExpectedTarget: "shell:::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell for This PC (::{20D04FE0-3AEA-1069-A2D8-08002B30309D})",
|
||||
Input: "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.VirtualShellItem,
|
||||
ExpectedTarget: "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell protocol for My Documents (shell:::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
|
||||
Input: "shell:::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell protocol for My Documents (::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
|
||||
Input: "::{450D8FBA-AD25-11D0-98A8-0800361B1103}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell protocol for AppData (shell:appdata)",
|
||||
Input: "shell:appdata",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
|
||||
// let's pray this works on all systems
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell protocol for AppData + subpath (shell:appdata\\microsoft)",
|
||||
Input: "shell:appdata\\microsoft",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft"),
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> UnquotedRelativePaths() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unquoted relative current path",
|
||||
Input: ".\\file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "file.txt")),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
#if CMDPAL_ENABLE_UNSAFE_TESTS
|
||||
It's not really a good idea blindly write to directory out of user profile
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unquoted relative parent path",
|
||||
Input: "..\\parent folder\\file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "..", "parent folder", "file.txt")),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
#endif // CMDPAL_ENABLE_UNSAFE_TESTS
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unquoted relative home folder",
|
||||
Input: $"~\\{_testDirName}\\app.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: Path.Combine(_testDirPath, "app.exe"),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Services;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
public partial class BookmarkResolverTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DynamicData(nameof(PlaceholderClassificationData.PlaceholderCases), typeof(PlaceholderClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Placeholders_ValidatePlaceholderClassification(PlaceholderClassificationCase c) => await RunShared(c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(nameof(PlaceholderClassificationData.EdgeCases), typeof(PlaceholderClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Placeholders_ValidatePlaceholderEdgeCases(PlaceholderClassificationCase c)
|
||||
{
|
||||
// Arrange
|
||||
IBookmarkResolver resolver = new BookmarkResolver(new PlaceholderParser());
|
||||
|
||||
// Act & Assert - Should not throw exceptions
|
||||
var classification = await resolver.TryClassifyAsync(c.Input, CancellationToken.None);
|
||||
|
||||
Assert.IsNotNull(classification);
|
||||
Assert.AreEqual(c.ExpectSuccess, classification.Success);
|
||||
|
||||
if (c.ExpectSuccess && classification.Result != null)
|
||||
{
|
||||
Assert.AreEqual(c.ExpectedIsPlaceholder, classification.Result.IsPlaceholder);
|
||||
Assert.AreEqual(c.Input, classification.Result.Input, "OriginalInput should be preserved");
|
||||
}
|
||||
}
|
||||
|
||||
private static class PlaceholderClassificationData
|
||||
{
|
||||
public static IEnumerable<object[]> PlaceholderCases()
|
||||
{
|
||||
// UWP/AUMID with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "UWP AUMID with package placeholder",
|
||||
Input: "shell:AppsFolder\\{packageFamily}!{appId}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Aumid,
|
||||
ExpectedTarget: "shell:AppsFolder\\{packageFamily}!{appId}",
|
||||
ExpectedLaunch: LaunchMethod.ActivateAppId,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
|
||||
// Expects no special handling
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Bare UWP AUMID with placeholders",
|
||||
Input: "{packageFamily}!{appId}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{packageFamily}!{appId}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Web URLs with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "HTTPS URL with domain placeholder",
|
||||
Input: "https://{domain}/path",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.WebUrl,
|
||||
ExpectedTarget: "https://{domain}/path",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "WWW URL with site placeholder",
|
||||
Input: "www.{site}.com",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.WebUrl,
|
||||
ExpectedTarget: "https://www.{site}.com",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "WWW URL - Yahoo with Search",
|
||||
Input: "http://yahoo.com?p={search}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.WebUrl,
|
||||
ExpectedTarget: "http://yahoo.com?p={search}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Protocol URLs with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Mailto protocol with email placeholder",
|
||||
Input: "mailto:{email}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Protocol,
|
||||
ExpectedTarget: "mailto:{email}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "MS-Settings protocol with category placeholder",
|
||||
Input: "ms-settings:{category}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Protocol,
|
||||
ExpectedTarget: "ms-settings:{category}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// File executables with placeholders - These might classify as Unknown currently
|
||||
// due to nonexistent paths, but should preserve placeholder flag
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Executable with profile path placeholder",
|
||||
Input: "{userProfile}\\Documents\\app.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable, // May be Unknown if path doesn't exist
|
||||
ExpectedTarget: "{userProfile}\\Documents\\app.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Executable with program files placeholder",
|
||||
Input: "{programFiles}\\MyApp\\tool.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable, // May be Unknown if path doesn't exist
|
||||
ExpectedTarget: "{programFiles}\\MyApp\\tool.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Commands with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Command with placeholder and arguments",
|
||||
Input: "{editor} {filename}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown, // Likely Unknown since command won't be found in PATH
|
||||
ExpectedTarget: "{editor}",
|
||||
ExpectedArguments: "{filename}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Directory paths with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Directory with user profile placeholder",
|
||||
Input: "{userProfile}\\Documents",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown, // May be Unknown if path doesn't exist during classification
|
||||
ExpectedTarget: "{userProfile}\\Documents",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Complex quoted paths with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted executable path with placeholders and args",
|
||||
Input: "\"{programFiles}\\{appName}\\{executable}.exe\" --verbose",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable, // Likely Unknown due to nonexistent path
|
||||
ExpectedTarget: "{programFiles}\\{appName}\\{executable}.exe",
|
||||
ExpectedArguments: "--verbose",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Shell paths with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell folder with placeholder",
|
||||
Input: "shell:{folder}\\{filename}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.VirtualShellItem,
|
||||
ExpectedTarget: "shell:{folder}\\{filename}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Shell paths with placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell folder with placeholder",
|
||||
Input: "shell:knownFolder\\{filename}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.VirtualShellItem,
|
||||
ExpectedTarget: "shell:knownFolder\\{filename}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
yield return
|
||||
[
|
||||
|
||||
// cmd /K {param1}
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Command with braces in arguments",
|
||||
Input: "cmd /K {param1}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: "C:\\Windows\\system32\\cmd.EXE",
|
||||
ExpectedArguments: "/K {param1}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Mixed literal and placeholder paths
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Mixed literal and placeholder path",
|
||||
Input: "C:\\{folder}\\app.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable, // Behavior depends on partial path resolution
|
||||
ExpectedTarget: "C:\\{folder}\\app.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Multiple placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Multiple placeholders in path",
|
||||
Input: "{drive}\\{folder}\\{subfolder}\\{file}.{ext}",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{drive}\\{folder}\\{subfolder}\\{file}.{ext}",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> EdgeCases()
|
||||
{
|
||||
// Empty and malformed placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Empty placeholder",
|
||||
Input: "{} file.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{} file.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unclosed placeholder",
|
||||
Input: "{unclosed file.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{unclosed file.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Placeholder with spaces",
|
||||
Input: "{with spaces}\\file.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{with spaces}\\file.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Nested placeholders",
|
||||
Input: "{outer{inner}}\\file.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{outer{inner}}\\file.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Only closing brace",
|
||||
Input: "file} something",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "file} something",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
];
|
||||
|
||||
// Very long placeholder names
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Very long placeholder name",
|
||||
Input: "{thisIsVeryLongPlaceholderNameThatShouldStillWorkProperly}\\file.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{thisIsVeryLongPlaceholderNameThatShouldStillWorkProperly}\\file.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
// Special characters in placeholders
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Placeholder with underscores",
|
||||
Input: "{user_profile}\\file.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{user_profile}\\file.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
|
||||
yield return
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Placeholder with numbers",
|
||||
Input: "{path123}\\file.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "{path123}\\file.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: true)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
public partial class BookmarkResolverTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.MixedQuotesScenarios), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateMixedQuotesScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.EscapedQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateEscapedQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.PartialMalformedQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidatePartialMalformedQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.EnvironmentVariablesWithQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateEnvironmentVariablesWithQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.ShellProtocolPathsWithQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateShellProtocolPathsWithQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.CommandFlagsAndOptions), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateCommandFlagsAndOptions(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.NetworkPathsUnc), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateNetworkPathsUnc(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.RelativePathsWithQuotes), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateRelativePathsWithQuotes(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.EmptyAndWhitespaceCases), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateEmptyAndWhitespaceCases(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.RealWorldCommandScenarios), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateRealWorldCommandScenarios(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.SpecialCharactersInPaths), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateSpecialCharactersInPaths(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.QuotedPathsCurrentlyBroken), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateQuotedPathsCurrentlyBroken(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.QuotedPathsInCommands), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateQuotedPathsInCommands(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
[DataTestMethod]
|
||||
[DynamicData(dynamicDataSourceName: nameof(QuotedClassificationData.QuotedAumid), dynamicDataDeclaringType: typeof(QuotedClassificationData), DynamicDataDisplayName = nameof(FromCase))]
|
||||
public async Task Quoted_ValidateQuotedUwpAppAumidCommands(PlaceholderClassificationCase c) => await RunShared(c: c);
|
||||
|
||||
public static class QuotedClassificationData
|
||||
{
|
||||
public static IEnumerable<object[]> MixedQuotesScenarios() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Executable with quoted argument",
|
||||
Input: "C:\\Windows\\notepad.exe \"C:\\my file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Windows\\notepad.exe",
|
||||
ExpectedArguments: "\"C:\\my file.txt\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "App with quoted argument containing spaces",
|
||||
Input: "app.exe \"argument with spaces\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "app.exe",
|
||||
ExpectedArguments: "\"argument with spaces\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Tool with input flag and quoted file",
|
||||
Input: "C:\\tool.exe -input \"data file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\tool.exe",
|
||||
ExpectedArguments: "-input \"data file.txt\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Multiple quoted arguments after path",
|
||||
Input: "\"C:\\Program Files\\app.exe\" -file \"C:\\data\\input.txt\" -output \"C:\\results\\output.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Program Files\\app.exe",
|
||||
ExpectedArguments: "-file \"C:\\data\\input.txt\" -output \"C:\\results\\output.txt\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Command with two quoted paths",
|
||||
Input: "cmd /c \"C:\\First Path\\tool.exe\" \"C:\\Second Path\\file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: "C:\\Windows\\system32\\cmd.EXE",
|
||||
ExpectedArguments: "/c \"C:\\First Path\\tool.exe\" \"C:\\Second Path\\file.txt\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> EscapedQuotes() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Path with escaped quotes in folder name",
|
||||
Input: "C:\\Windows\\\\\\\"System32\\\\\\\"CatRoot\\\\",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "C:\\Windows\\\\\\\"System32\\\\\\\"CatRoot\\\\",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with trailing escaped quote",
|
||||
Input: "\"C:\\Windows\\\\\\\"\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: "C:\\Windows\\",
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> PartialMalformedQuotes() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unclosed quote at start",
|
||||
Input: "\"C:\\Program Files\\app.exe",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Program Files\\app.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quote in middle of unquoted path",
|
||||
Input: "C:\\Some\\\"Path\\file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "C:\\Some\\\"Path\\file.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Unclosed quote - never ends",
|
||||
Input: "\"Starts quoted but never ends",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "Starts quoted but never ends",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> EnvironmentVariablesWithQuotes() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted environment variable path with spaces",
|
||||
Input: "\"%ProgramFiles%\\MyApp\\app.exe\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "MyApp", "app.exe"),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted USERPROFILE with document path",
|
||||
Input: "\"%USERPROFILE%\\Documents\\file with spaces.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "file with spaces.txt"),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Environment variable with trailing args",
|
||||
Input: "\"%ProgramFiles%\\App\" with args",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "App"),
|
||||
ExpectedArguments: "with args",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Environment variable with trailing args",
|
||||
Input: "%ProgramFiles%\\App with args",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "App"),
|
||||
ExpectedArguments: "with args",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> ShellProtocolPathsWithQuotes() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted shell:Downloads",
|
||||
Input: "\"shell:Downloads\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"),
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted shell:Downloads with subpath",
|
||||
Input: "\"shell:Downloads\\file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads", "file.txt"),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Shell Desktop with subpath",
|
||||
Input: "shell:Desktop\\file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "file.txt"),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted shell path with trailing text",
|
||||
Input: "\"shell:Programs\" extra",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Directory,
|
||||
ExpectedTarget: Path.Combine(paths: Environment.GetFolderPath(Environment.SpecialFolder.Programs)),
|
||||
ExpectedLaunch: LaunchMethod.ExplorerOpen,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> CommandFlagsAndOptions() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Path followed by flag with quoted value",
|
||||
Input: "C:\\app.exe -flag \"value\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\app.exe",
|
||||
ExpectedArguments: "-flag \"value\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted tool with equals-style flag",
|
||||
Input: "\"C:\\Program Files\\tool.exe\" --input=file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Program Files\\tool.exe",
|
||||
ExpectedArguments: "--input=file.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Path with slash option and quoted value",
|
||||
Input: "C:\\tool.exe /option \"quoted value\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\tool.exe",
|
||||
ExpectedArguments: "/option \"quoted value\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Flag before quoted path",
|
||||
Input: "--path \"C:\\Program Files\\app.exe\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "--path",
|
||||
ExpectedArguments: "\"C:\\Program Files\\app.exe\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> NetworkPathsUnc() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "UNC path unquoted",
|
||||
Input: "\\\\server\\share\\folder\\file.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "\\\\server\\share\\folder\\file.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted UNC path with spaces",
|
||||
Input: "\"\\\\server\\share with spaces\\file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "\\\\server\\share with spaces\\file.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "UNC path with trailing args",
|
||||
Input: "\"\\\\server\\share\\\" with args",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: "\\\\server\\share\\",
|
||||
ExpectedArguments: "with args",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted UNC app with flag",
|
||||
Input: "\"\\\\server\\My Share\\app.exe\" --flag",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "\\\\server\\My Share\\app.exe",
|
||||
ExpectedArguments: "--flag",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> RelativePathsWithQuotes() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted relative current path",
|
||||
Input: "\".\\file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "file.txt")),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted relative parent path",
|
||||
Input: "\"..\\parent folder\\file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "..", "parent folder", "file.txt")),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted relative home folder",
|
||||
Input: "\"~\\current folder\\app.exe\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "current folder\\app.exe"),
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> EmptyAndWhitespaceCases() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Empty string",
|
||||
Input: string.Empty,
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: string.Empty,
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Only whitespace",
|
||||
Input: " ",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: " ",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Just empty quotes",
|
||||
Input: "\"\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: string.Empty,
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted single space",
|
||||
Input: "\" \"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Unknown,
|
||||
ExpectedTarget: " ",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> RealWorldCommandScenarios() =>
|
||||
[
|
||||
#if CMDPAL_ENABLE_UNSAFE_TESTS
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Git clone command with full exe path with quoted path",
|
||||
Input: "\"C:\\Program Files\\Git\\bin\\git.exe\" clone repo",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Program Files\\Git\\bin\\git.exe",
|
||||
ExpectedArguments: "clone repo",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Git clone command with quoted path",
|
||||
Input: "git clone repo",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: "C:\\Program Files\\Git\\cmd\\git.EXE",
|
||||
ExpectedArguments: "clone repo",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Visual Studio devenv with solution",
|
||||
Input: "\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Preview\\Common7\\IDE\\devenv.exe\" solution.sln",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Program Files\\Microsoft Visual Studio\\2022\\Preview\\Common7\\IDE\\devenv.exe",
|
||||
ExpectedArguments: "solution.sln",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Double-quoted Windows cmd pattern",
|
||||
Input: "cmd /c \"\"C:\\Program Files\\app.exe\" arg1 arg2\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: "C:\\Windows\\system32\\cmd.EXE",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedArguments: "/c \"\"C:\\Program Files\\app.exe\" arg1 arg2\"",
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
#endif
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "PowerShell script with execution policy",
|
||||
Input: "powershell -ExecutionPolicy Bypass -File \"C:\\Scripts\\My Script.ps1\" -param \"value\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\PowerShell.exe",
|
||||
ExpectedArguments: "-ExecutionPolicy Bypass -File \"C:\\Scripts\\My Script.ps1\" -param \"value\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> SpecialCharactersInPaths() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with square brackets",
|
||||
Input: "\"C:\\Path\\file[1].txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "C:\\Path\\file[1].txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with parentheses",
|
||||
Input: "\"C:\\Folder (2)\\app.exe\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Folder (2)\\app.exe",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with hyphens and underscores",
|
||||
Input: "\"C:\\Path\\file_name-123.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "C:\\Path\\file_name-123.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> QuotedPathsCurrentlyBroken() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with spaces - complete path",
|
||||
Input: "\"C:\\Program Files\\MyApp\\app.exe\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Program Files\\MyApp\\app.exe",
|
||||
ExpectedArguments: string.Empty,
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with spaces in user folder",
|
||||
Input: "\"C:\\Users\\John Doe\\Documents\\file.txt\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "C:\\Users\\John Doe\\Documents\\file.txt",
|
||||
ExpectedArguments: string.Empty,
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with trailing arguments",
|
||||
Input: "\"C:\\Program Files\\app.exe\" --flag",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Program Files\\app.exe",
|
||||
ExpectedArguments: "--flag",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with multiple arguments",
|
||||
Input: "\"C:\\My Documents\\file.txt\" -output result.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileDocument,
|
||||
ExpectedTarget: "C:\\My Documents\\file.txt",
|
||||
ExpectedArguments: "-output result.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted path with trailing flag and value",
|
||||
Input: "\"C:\\Tools\\converter.exe\" input.txt output.txt",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.FileExecutable,
|
||||
ExpectedTarget: "C:\\Tools\\converter.exe",
|
||||
ExpectedArguments: "input.txt output.txt",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> QuotedPathsInCommands() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "cmd /c with quoted path",
|
||||
Input: "cmd /c \"C:\\Program Files\\tool.exe\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: "C:\\Windows\\system32\\cmd.exe",
|
||||
ExpectedArguments: "/c \"C:\\Program Files\\tool.exe\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "PowerShell with quoted script path",
|
||||
Input: "powershell -File \"C:\\Scripts\\my script.ps1\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: Path.Combine(path1: Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell", "v1.0", path4: "powershell.exe"),
|
||||
ExpectedArguments: "-File \"C:\\Scripts\\my script.ps1\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "runas with quoted executable",
|
||||
Input: "runas /user:admin \"C:\\Windows\\System32\\cmd.exe\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.PathCommand,
|
||||
ExpectedTarget: "C:\\Windows\\system32\\runas.exe",
|
||||
ExpectedArguments: "/user:admin \"C:\\Windows\\System32\\cmd.exe\"",
|
||||
ExpectedLaunch: LaunchMethod.ShellExecute,
|
||||
ExpectedIsPlaceholder: false)
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> QuotedAumid() =>
|
||||
[
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted UWP AUMID via AppsFolder",
|
||||
Input: "\"shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App\"",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Aumid,
|
||||
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
|
||||
ExpectedLaunch: LaunchMethod.ActivateAppId,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
[
|
||||
new PlaceholderClassificationCase(
|
||||
Name: "Quoted UWP AUMID with AppsFolder prefix and argument",
|
||||
Input: "\"shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App\" --maximized",
|
||||
ExpectSuccess: true,
|
||||
ExpectedKind: CommandKind.Aumid,
|
||||
ExpectedTarget: "shell:AppsFolder\\Microsoft.WindowsTerminal_8wekyb3d8bbwe!App",
|
||||
ExpectedArguments: "--maximized",
|
||||
ExpectedLaunch: LaunchMethod.ActivateAppId,
|
||||
ExpectedIsPlaceholder: false),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Services;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
public partial class BookmarkResolverTests
|
||||
{
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private static string _testDirPath;
|
||||
private static string _userHomeDirPath;
|
||||
private static string _testDirName;
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
|
||||
[ClassInitialize]
|
||||
public static void ClassSetup(TestContext context)
|
||||
{
|
||||
_userHomeDirPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
_testDirName = "CmdPalBookmarkTests" + Guid.NewGuid().ToString("N");
|
||||
_testDirPath = Path.Combine(_userHomeDirPath, _testDirName);
|
||||
Directory.CreateDirectory(_testDirPath);
|
||||
|
||||
// test files in user home
|
||||
File.WriteAllText(Path.Combine(_userHomeDirPath, "file.txt"), "This is a test text file.");
|
||||
|
||||
// test files in test dir
|
||||
File.WriteAllText(Path.Combine(_testDirPath, "file.txt"), "This is a test text file.");
|
||||
File.WriteAllText(Path.Combine(_testDirPath, "app.exe"), "This is a test text file.");
|
||||
}
|
||||
|
||||
[ClassCleanup]
|
||||
public static void ClassCleanup()
|
||||
{
|
||||
if (Directory.Exists(_testDirPath))
|
||||
{
|
||||
Directory.Delete(_testDirPath, true);
|
||||
}
|
||||
|
||||
if (File.Exists(Path.Combine(_userHomeDirPath, "file.txt")))
|
||||
{
|
||||
File.Delete(Path.Combine(_userHomeDirPath, "file.txt"));
|
||||
}
|
||||
}
|
||||
|
||||
// must be public static to be used as DataTestMethod data source
|
||||
public static string FromCase(MethodInfo method, object[] data)
|
||||
=> data is [PlaceholderClassificationCase c]
|
||||
? c.Name
|
||||
: $"{method.Name}({string.Join(", ", data.Select(row => row.ToString()))})";
|
||||
|
||||
private static async Task RunShared(PlaceholderClassificationCase c)
|
||||
{
|
||||
// Arrange
|
||||
IBookmarkResolver resolver = new BookmarkResolver(new PlaceholderParser());
|
||||
|
||||
// Act
|
||||
var classification = await resolver.TryClassifyAsync(c.Input, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(classification);
|
||||
Assert.AreEqual(c.ExpectSuccess, classification.Success, "Success flag mismatch.");
|
||||
|
||||
if (c.ExpectSuccess)
|
||||
{
|
||||
Assert.IsNotNull(classification.Result, "Result should not be null for successful classification.");
|
||||
Assert.AreEqual(c.ExpectedKind, classification.Result.Kind, $"CommandKind mismatch for input: {c.Input}");
|
||||
Assert.AreEqual(c.ExpectedTarget, classification.Result.Target, StringComparer.OrdinalIgnoreCase, $"Target mismatch for input: {c.Input}");
|
||||
Assert.AreEqual(c.ExpectedLaunch, classification.Result.Launch, $"LaunchMethod mismatch for input: {c.Input}");
|
||||
Assert.AreEqual(c.ExpectedArguments, classification.Result.Arguments, $"Arguments mismatch for input: {c.Input}");
|
||||
Assert.AreEqual(c.ExpectedIsPlaceholder, classification.Result.IsPlaceholder, $"IsPlaceholder mismatch for input: {c.Input}");
|
||||
|
||||
if (c.ExpectedDisplayName != null)
|
||||
{
|
||||
Assert.AreEqual(c.ExpectedDisplayName, classification.Result.DisplayName, $"DisplayName mismatch for input: {c.Input}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record PlaceholderClassificationCase(
|
||||
string Name, // Friendly name for Test Explorer
|
||||
string Input, // Input string passed to classifier
|
||||
bool ExpectSuccess, // Expected Success flag
|
||||
CommandKind ExpectedKind, // Expected Result.Kind
|
||||
string ExpectedTarget, // Expected Result.Target (normalized)
|
||||
LaunchMethod ExpectedLaunch, // Expected Result.Launch
|
||||
bool ExpectedIsPlaceholder, // Expected Result.IsPlaceholder
|
||||
string ExpectedArguments = "", // Expected Result.Arguments
|
||||
string? ExpectedDisplayName = null // Expected Result.DisplayName
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
@@ -16,8 +16,8 @@ public class BookmarksCommandProviderTests
|
||||
public void ProviderHasCorrectId()
|
||||
{
|
||||
// Setup
|
||||
var mockDataSource = new MockBookmarkDataSource();
|
||||
var provider = new BookmarksCommandProvider(mockDataSource);
|
||||
var mockBookmarkManager = new MockBookmarkManager();
|
||||
var provider = new BookmarksCommandProvider(mockBookmarkManager);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Bookmarks", provider.Id);
|
||||
@@ -27,8 +27,8 @@ public class BookmarksCommandProviderTests
|
||||
public void ProviderHasDisplayName()
|
||||
{
|
||||
// Setup
|
||||
var mockDataSource = new MockBookmarkDataSource();
|
||||
var provider = new BookmarksCommandProvider(mockDataSource);
|
||||
var mockBookmarkManager = new MockBookmarkManager();
|
||||
var provider = new BookmarksCommandProvider(mockBookmarkManager);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.DisplayName);
|
||||
@@ -39,7 +39,8 @@ public class BookmarksCommandProviderTests
|
||||
public void ProviderHasIcon()
|
||||
{
|
||||
// Setup
|
||||
var provider = new BookmarksCommandProvider();
|
||||
var mockBookmarkManager = new MockBookmarkManager();
|
||||
var provider = new BookmarksCommandProvider(mockBookmarkManager);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(provider.Icon);
|
||||
@@ -49,7 +50,8 @@ public class BookmarksCommandProviderTests
|
||||
public void TopLevelCommandsNotEmpty()
|
||||
{
|
||||
// Setup
|
||||
var provider = new BookmarksCommandProvider();
|
||||
var mockBookmarkManager = new MockBookmarkManager();
|
||||
var provider = new BookmarksCommandProvider(mockBookmarkManager);
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
@@ -60,47 +62,40 @@ public class BookmarksCommandProviderTests
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderWithMockData_LoadsBookmarksCorrectly()
|
||||
[Timeout(5000)]
|
||||
public async Task ProviderWithMockData_LoadsBookmarksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var jsonData = @"{
|
||||
""Data"": [
|
||||
{
|
||||
""Name"": ""Test Bookmark"",
|
||||
""Bookmark"": ""https://test.com""
|
||||
},
|
||||
{
|
||||
""Name"": ""Another Bookmark"",
|
||||
""Bookmark"": ""https://another.com""
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
var dataSource = new MockBookmarkDataSource(jsonData);
|
||||
var provider = new BookmarksCommandProvider(dataSource);
|
||||
var mockBookmarkManager = new MockBookmarkManager(
|
||||
new BookmarkData("Test Bookmark", "http://test.com"),
|
||||
new BookmarkData("Another Bookmark", "http://another.com"));
|
||||
var provider = new BookmarksCommandProvider(mockBookmarkManager);
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(commands);
|
||||
|
||||
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
|
||||
var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault();
|
||||
Assert.IsNotNull(commands, "commands != null");
|
||||
|
||||
// Should have three commands:Add + two custom bookmarks
|
||||
Assert.AreEqual(3, commands.Length);
|
||||
|
||||
Assert.IsNotNull(addCommand);
|
||||
Assert.IsNotNull(testBookmark);
|
||||
// Wait until all BookmarkListItem commands are initialized
|
||||
await Task.WhenAll(commands.OfType<Pages.BookmarkListItem>().Select(t => t.IsInitialized));
|
||||
|
||||
var addCommand = commands.FirstOrDefault(c => c.Title.Contains("Add bookmark"));
|
||||
var testBookmark = commands.FirstOrDefault(c => c.Title.Contains("Test Bookmark"));
|
||||
|
||||
Assert.IsNotNull(addCommand, "addCommand != null");
|
||||
Assert.IsNotNull(testBookmark, "testBookmark != null");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProviderWithEmptyData_HasOnlyAddCommand()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }");
|
||||
var provider = new BookmarksCommandProvider(dataSource);
|
||||
var mockBookmarkManager = new MockBookmarkManager();
|
||||
var provider = new BookmarksCommandProvider(mockBookmarkManager);
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
@@ -111,7 +106,7 @@ public class BookmarksCommandProviderTests
|
||||
// Only have Add command
|
||||
Assert.AreEqual(1, commands.Length);
|
||||
|
||||
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
|
||||
var addCommand = commands.FirstOrDefault(c => c.Title.Contains("Add bookmark"));
|
||||
Assert.IsNotNull(addCommand);
|
||||
}
|
||||
|
||||
@@ -120,7 +115,7 @@ public class BookmarksCommandProviderTests
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new MockBookmarkDataSource("invalid json");
|
||||
var provider = new BookmarksCommandProvider(dataSource);
|
||||
var provider = new BookmarksCommandProvider(new MockBookmarkManager());
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
@@ -131,7 +126,7 @@ public class BookmarksCommandProviderTests
|
||||
// Only have one command. Will ignore json parse error.
|
||||
Assert.AreEqual(1, commands.Length);
|
||||
|
||||
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
|
||||
var addCommand = commands.FirstOrDefault(c => c.Title.Contains("Add bookmark"));
|
||||
Assert.IsNotNull(addCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class CommandLineHelperTests
|
||||
{
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private static string _tempTestDir;
|
||||
|
||||
private static string _tempTestFile;
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
|
||||
[ClassInitialize]
|
||||
public static void ClassSetup(TestContext context)
|
||||
{
|
||||
// Create temporary test directory and file
|
||||
_tempTestDir = Path.Combine(Path.GetTempPath(), "CommandLineHelperTests_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_tempTestDir);
|
||||
|
||||
_tempTestFile = Path.Combine(_tempTestDir, "testfile.txt");
|
||||
File.WriteAllText(_tempTestFile, "test");
|
||||
}
|
||||
|
||||
[ClassCleanup]
|
||||
public static void ClassCleanup()
|
||||
{
|
||||
// Clean up test directory
|
||||
if (Directory.Exists(_tempTestDir))
|
||||
{
|
||||
Directory.Delete(_tempTestDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("%TEMP%", false, true, DisplayName = "Expands TEMP environment variable")]
|
||||
[DataRow("%USERPROFILE%", false, true, DisplayName = "Expands USERPROFILE environment variable")]
|
||||
[DataRow("%SystemRoot%", false, true, DisplayName = "Expands SystemRoot environment variable")]
|
||||
public void Expand_WithEnvironmentVariables_ExpandsCorrectly(string input, bool expandShell, bool shouldExist)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(shouldExist, result, $"Expected result {shouldExist} for input '{input}'");
|
||||
if (shouldExist)
|
||||
{
|
||||
Assert.IsFalse(full.Contains('%'), "Output should not contain % symbols after expansion");
|
||||
Assert.IsTrue(Path.Exists(full), $"Expanded path '{full}' should exist");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("shell:Downloads", true, DisplayName = "Expands shell:Downloads when expandShell is true")]
|
||||
[DataRow("shell:Desktop", true, DisplayName = "Expands shell:Desktop when expandShell is true")]
|
||||
[DataRow("shell:Documents", true, DisplayName = "Expands shell:Documents when expandShell is true")]
|
||||
public void Expand_WithShellPaths_ExpandsWhenFlagIsTrue(string input, bool expandShell)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Assert
|
||||
if (result)
|
||||
{
|
||||
Assert.IsFalse(full.StartsWith("shell:", StringComparison.OrdinalIgnoreCase), "Shell prefix should be resolved");
|
||||
Assert.IsTrue(Path.Exists(full), $"Expanded shell path '{full}' should exist");
|
||||
}
|
||||
|
||||
// Note: Result may be false if ShellNames.TryGetFileSystemPath fails
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("shell:Personal", false, DisplayName = "Does not expand shell: when expandShell is false")]
|
||||
public void Expand_WithShellPaths_DoesNotExpandWhenFlagIsFalse(string input, bool expandShell)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Assert - shell: paths won't exist as literal paths
|
||||
Assert.IsFalse(result, "Should return false for unexpanded shell path");
|
||||
Assert.AreEqual(input, full, "Output should match input when not expanding shell paths");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("shell:Personal\\subfolder", true, "\\subfolder", DisplayName = "Expands shell path with subfolder")]
|
||||
[DataRow("shell:Desktop\\test.txt", true, "\\test.txt", DisplayName = "Expands shell path with file")]
|
||||
public void Expand_WithShellPathsAndSubpaths_CombinesCorrectly(string input, bool expandShell, string expectedEnding)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Note: Result depends on whether the combined path exists
|
||||
if (result)
|
||||
{
|
||||
Assert.IsFalse(full.StartsWith("shell:", StringComparison.OrdinalIgnoreCase), "Shell prefix should be resolved");
|
||||
Assert.IsTrue(full.EndsWith(expectedEnding, StringComparison.OrdinalIgnoreCase), "Output should end with the subpath");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Expand_WithExistingDirectory_ReturnsFullPath()
|
||||
{
|
||||
// Arrange
|
||||
var input = _tempTestDir;
|
||||
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, false, out var full);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result, "Should return true for existing directory");
|
||||
Assert.AreEqual(Path.GetFullPath(_tempTestDir), full, "Should return full path");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Expand_WithExistingFile_ReturnsFullPath()
|
||||
{
|
||||
// Arrange
|
||||
var input = _tempTestFile;
|
||||
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, false, out var full);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result, "Should return true for existing file");
|
||||
Assert.AreEqual(Path.GetFullPath(_tempTestFile), full, "Should return full path");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("C:\\NonExistent\\Path\\That\\Does\\Not\\Exist", false, "C:\\NonExistent\\Path\\That\\Does\\Not\\Exist", DisplayName = "Nonexistent absolute path")]
|
||||
[DataRow("NonExistentFile.txt", false, "NonExistentFile.txt", DisplayName = "Nonexistent relative path")]
|
||||
public void Expand_WithNonExistentPath_ReturnsFalse(string input, bool expandShell, string expectedFull)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Should return false for nonexistent path");
|
||||
Assert.AreEqual(expectedFull, full, "Output should be empty string");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("", false, DisplayName = "Empty string")]
|
||||
[DataRow(" ", false, DisplayName = "Whitespace only")]
|
||||
public void Expand_WithEmptyOrWhitespace_ReturnsFalse(string input, bool expandShell)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result, "Should return false for empty/whitespace input");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("%TEMP%\\testsubdir", false, DisplayName = "Env var with subdirectory")]
|
||||
[DataRow("%USERPROFILE%\\Desktop", false, DisplayName = "USERPROFILE with Desktop")]
|
||||
public void Expand_WithEnvironmentVariableAndSubpath_ExpandsCorrectly(string input, bool expandShell)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Result depends on whether the path exists
|
||||
if (result)
|
||||
{
|
||||
Assert.IsFalse(full.Contains('%'), "Should expand environment variables");
|
||||
Assert.IsTrue(Path.Exists(full), "Expanded path should exist");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Expand_WithRelativePath_ConvertsToAbsoluteWhenExists()
|
||||
{
|
||||
// Arrange
|
||||
var relativePath = Path.GetRelativePath(Environment.CurrentDirectory, _tempTestDir);
|
||||
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(relativePath, false, out var full);
|
||||
|
||||
// Assert
|
||||
if (result)
|
||||
{
|
||||
Assert.IsTrue(Path.IsPathRooted(full), "Output should be absolute path");
|
||||
Assert.IsTrue(Path.Exists(full), "Expanded path should exist");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("InvalidShell:Path", true, DisplayName = "Invalid shell path format")]
|
||||
public void Expand_WithInvalidShellPath_ReturnsFalse(string input, bool expandShell)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.ExpandPathToPhysicalFile(input, expandShell, out var full);
|
||||
|
||||
// Assert
|
||||
// If ShellNames.TryGetFileSystemPath returns false, method returns false
|
||||
Assert.IsFalse(result || Path.Exists(full), "Should return false or path should not exist");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
|
||||
// basic
|
||||
[DataRow("cmd ping", "cmd", "ping")]
|
||||
[DataRow("cmd ping pong", "cmd", "ping pong")]
|
||||
[DataRow("cmd \"ping pong\"", "cmd", "\"ping pong\"")]
|
||||
|
||||
// no tail / trailing whitespace after head
|
||||
[DataRow("cmd", "cmd", "")]
|
||||
[DataRow("cmd ", "cmd", "")]
|
||||
|
||||
// spacing & tabs between args should be preserved in tail
|
||||
[DataRow("cmd ping pong", "cmd", "ping pong")]
|
||||
[DataRow("cmd\tping\tpong", "cmd", "ping\tpong")]
|
||||
|
||||
// leading whitespace before head
|
||||
[DataRow(" cmd ping", "", "cmd ping")]
|
||||
[DataRow("\t cmd ping", "", "cmd ping")]
|
||||
|
||||
// quoted tail variants
|
||||
[DataRow("cmd \"\"", "cmd", "\"\"")]
|
||||
[DataRow("cmd \"a \\\"quoted\\\" arg\" b", "cmd", "\"a \\\"quoted\\\" arg\" b")]
|
||||
|
||||
// quoted head (spaces in path)
|
||||
[DataRow(@"""C:\Program Files\nodejs\node.exe"" -v", @"C:\Program Files\nodejs\node.exe", "-v")]
|
||||
[DataRow(@"""C:\Program Files\Git\bin\bash.exe""", @"C:\Program Files\Git\bin\bash.exe", "")]
|
||||
[DataRow(@" ""C:\Program Files\Git\bin\bash.exe"" -lc ""hi""", @"", @"""C:\Program Files\Git\bin\bash.exe"" -lc ""hi""")]
|
||||
[DataRow(@"""C:\Program Files (x86)\MSBuild\Current\Bin\MSBuild.exe"" Test.sln", @"C:\Program Files (x86)\MSBuild\Current\Bin\MSBuild.exe", "Test.sln")]
|
||||
|
||||
// quoted simple head (still should strip quotes for head)
|
||||
[DataRow(@"""cmd"" ping", "cmd", "ping")]
|
||||
|
||||
// common CLI shapes
|
||||
[DataRow("git --version", "git", "--version")]
|
||||
[DataRow("dotnet build -c Release", "dotnet", "build -c Release")]
|
||||
|
||||
// UNC paths
|
||||
[DataRow("\"\\\\server\\share\\\" with args", "\\\\server\\share\\", "with args")]
|
||||
public void SplitHeadAndArgs(string input, string expectedHead, string expectedTail)
|
||||
{
|
||||
// Act
|
||||
var result = CommandLineHelper.SplitHeadAndArgs(input);
|
||||
|
||||
// Assert
|
||||
// If ShellNames.TryGetFileSystemPath returns false, method returns false
|
||||
Assert.AreEqual(expectedHead, result.Head);
|
||||
Assert.AreEqual(expectedTail, result.Tail);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(@"C:\program files\myapp\app.exe -param ""1"" -param 2", @"C:\program files\myapp\app.exe -param", @"""1"" -param 2")]
|
||||
[DataRow(@"git commit -m test", "git commit -m test", "")]
|
||||
[DataRow(@"""C:\Program Files\App\app.exe"" -v", "", @"""C:\Program Files\App\app.exe"" -v")]
|
||||
[DataRow(@"tool a\\\""b c ""d e"" f", @"tool a\\\""b c", @"""d e"" f")] // escaped quote before first real one
|
||||
[DataRow("C:\\Some\\\"Path\\file.txt", "C:\\Some\\\"Path\\file.txt", "")]
|
||||
[DataRow(@" ""C:\p\app.exe"" -v", "", @"""C:\p\app.exe"" -v")] // first token is quoted
|
||||
public void SplitLongestHeadBeforeQuotedArg_Tests(string input, string expectedHead, string expectedTail)
|
||||
{
|
||||
var (head, tail) = CommandLineHelper.SplitLongestHeadBeforeQuotedArg(input);
|
||||
Assert.AreEqual(expectedHead, head);
|
||||
Assert.AreEqual(expectedTail, tail);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
// 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.Ext.Bookmarks.Persistence;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
internal sealed class MockBookmarkDataSource : IBookmarkDataSource
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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.Collections.Generic;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Persistence;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
#pragma warning disable CS0067
|
||||
|
||||
internal sealed class MockBookmarkManager : IBookmarksManager
|
||||
{
|
||||
private readonly List<BookmarkData> _bookmarks;
|
||||
|
||||
public event Action<BookmarkData> BookmarkAdded;
|
||||
|
||||
public event Action<BookmarkData, BookmarkData> BookmarkUpdated;
|
||||
|
||||
public event Action<BookmarkData> BookmarkRemoved;
|
||||
|
||||
public IReadOnlyCollection<BookmarkData> Bookmarks => _bookmarks;
|
||||
|
||||
public BookmarkData Add(string name, string bookmark) => throw new NotImplementedException();
|
||||
|
||||
public bool Remove(Guid id) => throw new NotImplementedException();
|
||||
|
||||
public BookmarkData Update(Guid id, string name, string bookmark) => throw new NotImplementedException();
|
||||
|
||||
public MockBookmarkManager(params IEnumerable<BookmarkData> bookmarks)
|
||||
{
|
||||
_bookmarks = [.. bookmarks];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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.Collections.Generic;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Services;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class PlaceholderInfoNameEqualityComparerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Equals_BothNull_ReturnsTrue()
|
||||
{
|
||||
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
|
||||
var result = comparer.Equals(null, null);
|
||||
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Equals_OneNull_ReturnsFalse()
|
||||
{
|
||||
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
var p = new PlaceholderInfo("name", 0);
|
||||
|
||||
Assert.IsFalse(comparer.Equals(p, null));
|
||||
Assert.IsFalse(comparer.Equals(null, p));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Equals_SameNameDifferentIndex_ReturnsTrue()
|
||||
{
|
||||
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
var p1 = new PlaceholderInfo("name", 0);
|
||||
var p2 = new PlaceholderInfo("name", 10);
|
||||
|
||||
Assert.IsTrue(comparer.Equals(p1, p2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Equals_DifferentNameSameIndex_ReturnsFalse()
|
||||
{
|
||||
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
var p1 = new PlaceholderInfo("first", 3);
|
||||
var p2 = new PlaceholderInfo("second", 3);
|
||||
|
||||
Assert.IsFalse(comparer.Equals(p1, p2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Equals_CaseInsensitive_ReturnsTrue()
|
||||
{
|
||||
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
var p1 = new PlaceholderInfo("Name", 0);
|
||||
var p2 = new PlaceholderInfo("name", 5);
|
||||
|
||||
Assert.IsTrue(comparer.Equals(p1, p2));
|
||||
Assert.AreEqual(comparer.GetHashCode(p1), comparer.GetHashCode(p2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetHashCode_SameNameDifferentIndex_SameHash()
|
||||
{
|
||||
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
var p1 = new PlaceholderInfo("same", 1);
|
||||
var p2 = new PlaceholderInfo("same", 99);
|
||||
|
||||
Assert.AreEqual(comparer.GetHashCode(p1), comparer.GetHashCode(p2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetHashCode_Null_ThrowsArgumentNullException()
|
||||
{
|
||||
var comparer = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
Assert.ThrowsException<ArgumentNullException>(() => comparer.GetHashCode(null!));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Instance_ReturnsSingleton()
|
||||
{
|
||||
var a = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
var b = PlaceholderInfoNameEqualityComparer.Instance;
|
||||
|
||||
Assert.IsNotNull(a);
|
||||
Assert.AreSame(a, b);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void HashSet_UsesNameEquality_IgnoresIndex()
|
||||
{
|
||||
var set = new HashSet<PlaceholderInfo>(PlaceholderInfoNameEqualityComparer.Instance)
|
||||
{
|
||||
new("dup", 0),
|
||||
new("DUP", 10),
|
||||
new("unique", 0),
|
||||
};
|
||||
|
||||
Assert.AreEqual(2, set.Count);
|
||||
Assert.IsTrue(set.Contains(new PlaceholderInfo("dup", 123)));
|
||||
Assert.IsTrue(set.Contains(new PlaceholderInfo("UNIQUE", 999)));
|
||||
Assert.IsFalse(set.Contains(new PlaceholderInfo("missing", 0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Services;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class PlaceholderParserTests
|
||||
{
|
||||
private IPlaceholderParser _parser;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_parser = new PlaceholderParser();
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ValidPlaceholderTestData =>
|
||||
[
|
||||
[
|
||||
"Hello {name}!",
|
||||
true,
|
||||
"Hello ",
|
||||
new[] { "name" },
|
||||
new[] { 6 }
|
||||
],
|
||||
[
|
||||
"User {user_name} has {count} items",
|
||||
true,
|
||||
"User ",
|
||||
new[] { "user_name", "count" },
|
||||
new[] { 5, 21 }
|
||||
],
|
||||
[
|
||||
"Order {order-id} for {name} by {name}",
|
||||
true,
|
||||
"Order ",
|
||||
new[] { "order-id", "name", "name" },
|
||||
new[] { 6, 21, 31 }
|
||||
],
|
||||
[
|
||||
"{start} and {end}",
|
||||
true,
|
||||
string.Empty,
|
||||
new[] { "start", "end" },
|
||||
new[] { 0, 12 }
|
||||
],
|
||||
[
|
||||
"Number {123} and text {abc}",
|
||||
true,
|
||||
"Number ",
|
||||
new[] { "123", "abc" },
|
||||
new[] { 7, 22 }
|
||||
]
|
||||
];
|
||||
|
||||
public static IEnumerable<object[]> InvalidPlaceholderTestData =>
|
||||
[
|
||||
[string.Empty, false, string.Empty, Array.Empty<string>()],
|
||||
["No placeholders here", false, "No placeholders here", Array.Empty<string>()],
|
||||
["GUID: {550e8400-e29b-41d4-a716-446655440000}", false, "GUID: {550e8400-e29b-41d4-a716-446655440000}", Array.Empty<string>()],
|
||||
["Invalid {user.name} placeholder", false, "Invalid {user.name} placeholder", Array.Empty<string>()],
|
||||
["Empty {} placeholder", false, "Empty {} placeholder", Array.Empty<string>()],
|
||||
["Unclosed {placeholder", false, "Unclosed {placeholder", Array.Empty<string>()],
|
||||
["No opening brace placeholder}", false, "No opening brace placeholder}", Array.Empty<string>()],
|
||||
["Invalid chars {user@domain}", false, "Invalid chars {user@domain}", Array.Empty<string>()],
|
||||
["Spaces { name }", false, "Spaces { name }", Array.Empty<string>()]
|
||||
];
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(ValidPlaceholderTestData))]
|
||||
public void ParsePlaceholders_ValidInput_ReturnsExpectedResults(
|
||||
string input,
|
||||
bool expectedResult,
|
||||
string expectedHead,
|
||||
string[] expectedPlaceholderNames,
|
||||
int[] expectedIndexes)
|
||||
{
|
||||
// Act
|
||||
var result = _parser.ParsePlaceholders(input, out var head, out var placeholders);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expectedResult, result);
|
||||
Assert.AreEqual(expectedHead, head);
|
||||
Assert.AreEqual(expectedPlaceholderNames.Length, placeholders.Count);
|
||||
|
||||
var actualNames = placeholders.Select(p => p.Name).ToArray();
|
||||
var actualIndexes = placeholders.Select(p => p.Index).ToArray();
|
||||
|
||||
// Validate names and indexes (allow duplicates, ignore order)
|
||||
CollectionAssert.AreEquivalent(expectedPlaceholderNames, actualNames);
|
||||
CollectionAssert.AreEquivalent(expectedIndexes, actualIndexes);
|
||||
|
||||
// Validate name-index pairing exists for each expected placeholder occurrence
|
||||
for (var i = 0; i < expectedPlaceholderNames.Length; i++)
|
||||
{
|
||||
var expectedName = expectedPlaceholderNames[i];
|
||||
var expectedIndex = expectedIndexes[i];
|
||||
Assert.IsTrue(
|
||||
placeholders.Any(p => p.Name == expectedName && p.Index == expectedIndex),
|
||||
$"Expected placeholder '{{{expectedName}}}' at index {expectedIndex} was not found.");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(InvalidPlaceholderTestData))]
|
||||
public void ParsePlaceholders_InvalidInput_ReturnsExpectedResults(
|
||||
string input,
|
||||
bool expectedResult,
|
||||
string expectedHead,
|
||||
string[] expectedPlaceholderNames)
|
||||
{
|
||||
// Act
|
||||
var result = _parser.ParsePlaceholders(input, out var head, out var placeholders);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(expectedResult, result);
|
||||
Assert.AreEqual(expectedHead, head);
|
||||
Assert.AreEqual(expectedPlaceholderNames.Length, placeholders.Count);
|
||||
|
||||
var actualNames = placeholders.Select(p => p.Name).ToArray();
|
||||
CollectionAssert.AreEquivalent(expectedPlaceholderNames, actualNames);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePlaceholders_NullInput_ThrowsArgumentNullException()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => _parser.ParsePlaceholders(null!, out _, out _));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Placeholder_Equality_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var placeholder1 = new PlaceholderInfo("name", 0);
|
||||
var placeholder2 = new PlaceholderInfo("name", 0);
|
||||
var placeholder3 = new PlaceholderInfo("other", 0);
|
||||
var placeholder4 = new PlaceholderInfo("name", 1);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(placeholder1, placeholder2);
|
||||
Assert.AreNotEqual(placeholder1, placeholder3);
|
||||
Assert.AreEqual(placeholder1.GetHashCode(), placeholder2.GetHashCode());
|
||||
Assert.AreNotEqual(placeholder1, placeholder4);
|
||||
Assert.AreNotEqual(placeholder1.GetHashCode(), placeholder4.GetHashCode());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Placeholder_ToString_ReturnsName()
|
||||
{
|
||||
// Arrange
|
||||
var placeholder = new PlaceholderInfo("userName", 0);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("userName", placeholder.ToString());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Placeholder_Constructor_ThrowsOnNull()
|
||||
{
|
||||
// Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PlaceholderInfo(null!, 0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Placeholder_Constructor_ThrowsArgumentOutOfRange()
|
||||
{
|
||||
// Assert
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => new PlaceholderInfo("Name", -1));
|
||||
}
|
||||
}
|
||||
@@ -40,16 +40,4 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
Assert.IsNotNull(githubBookmark);
|
||||
Assert.AreEqual("https://github.com", githubBookmark.Bookmark);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidateWebUrlDetection()
|
||||
{
|
||||
// Setup
|
||||
var bookmarks = Settings.CreateDefaultBookmarks();
|
||||
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(microsoftBookmark);
|
||||
Assert.IsTrue(microsoftBookmark.IsWebUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
// 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.Ext.Bookmarks.Persistence;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
public static class Settings
|
||||
{
|
||||
public static Bookmarks CreateDefaultBookmarks()
|
||||
public static BookmarksData CreateDefaultBookmarks()
|
||||
{
|
||||
var bookmarks = new Bookmarks();
|
||||
var bookmarks = new BookmarksData();
|
||||
|
||||
// Add some test bookmarks
|
||||
bookmarks.Data.Add(new BookmarkData
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Bookmarks.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class UriHelperTests
|
||||
{
|
||||
private static bool TryGetScheme(ReadOnlySpan<char> input, out string scheme, out string remainder)
|
||||
{
|
||||
return UriHelper.TryGetScheme(input, out scheme, out remainder);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("http://example.com", "http", "//example.com")]
|
||||
[DataRow("ftp:", "ftp", "")]
|
||||
[DataRow("my-app:payload", "my-app", "payload")]
|
||||
[DataRow("x-cmdpal://settings/", "x-cmdpal", "//settings/")]
|
||||
[DataRow("custom+ext.-scheme:xyz", "custom+ext.-scheme", "xyz")]
|
||||
[DataRow("MAILTO:foo@bar", "MAILTO", "foo@bar")]
|
||||
[DataRow("a:b", "a", "b")]
|
||||
public void TryGetScheme_ValidSchemes_ReturnsTrueAndSplits(string input, string expectedScheme, string expectedRemainder)
|
||||
{
|
||||
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
|
||||
|
||||
Assert.IsTrue(ok, "Expected valid scheme.");
|
||||
Assert.AreEqual(expectedScheme, scheme);
|
||||
Assert.AreEqual(expectedRemainder, remainder);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetScheme_OnlySchemeAndColon_ReturnsEmptyRemainder()
|
||||
{
|
||||
var ok = TryGetScheme("http:".AsSpan(), out var scheme, out var remainder);
|
||||
|
||||
Assert.IsTrue(ok);
|
||||
Assert.AreEqual("http", scheme);
|
||||
Assert.AreEqual(string.Empty, remainder);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("123:http")] // starts with digit
|
||||
[DataRow(":nope")] // colon at start
|
||||
[DataRow("noColon")] // no colon at all
|
||||
[DataRow("bad_scheme:")] // underscore not allowed
|
||||
[DataRow("bad*scheme:")] // asterisk not allowed
|
||||
[DataRow(":")] // syntactically invalid literal just for completeness; won't compile, example only
|
||||
public void TryGetScheme_InvalidInputs_ReturnsFalse(string input)
|
||||
{
|
||||
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
|
||||
|
||||
Assert.IsFalse(ok);
|
||||
Assert.AreEqual(string.Empty, scheme);
|
||||
Assert.AreEqual(string.Empty, remainder);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetScheme_MultipleColons_SplitsOnFirst()
|
||||
{
|
||||
const string input = "shell:::{645FF040-5081-101B-9F08-00AA002F954E}";
|
||||
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
|
||||
|
||||
Assert.IsTrue(ok);
|
||||
Assert.AreEqual("shell", scheme);
|
||||
Assert.AreEqual("::{645FF040-5081-101B-9F08-00AA002F954E}", remainder);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetScheme_MinimumLength_OneLetterAndColon()
|
||||
{
|
||||
var ok = TryGetScheme("a:".AsSpan(), out var scheme, out var remainder);
|
||||
|
||||
Assert.IsTrue(ok);
|
||||
Assert.AreEqual("a", scheme);
|
||||
Assert.AreEqual(string.Empty, remainder);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetScheme_TooShort_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(TryGetScheme("a".AsSpan(), out _, out _), "No colon.");
|
||||
Assert.IsFalse(TryGetScheme(":".AsSpan(), out _, out _), "Colon at start; no scheme.");
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("HTTP://x", "HTTP", "//x")]
|
||||
[DataRow("hTtP:rest", "hTtP", "rest")]
|
||||
public void TryGetScheme_CaseIsPreserved(string input, string expectedScheme, string expectedRemainder)
|
||||
{
|
||||
var ok = TryGetScheme(input.AsSpan(), out var scheme, out var remainder);
|
||||
|
||||
Assert.IsTrue(ok);
|
||||
Assert.AreEqual(expectedScheme, scheme);
|
||||
Assert.AreEqual(expectedRemainder, remainder);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetScheme_WhitespaceInsideScheme_Fails()
|
||||
{
|
||||
Assert.IsFalse(TryGetScheme("ht tp:rest".AsSpan(), out _, out _));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryGetScheme_PlusMinusDot_AllowedInMiddleOnly()
|
||||
{
|
||||
Assert.IsTrue(TryGetScheme("a+b.c-d:rest".AsSpan(), out var s1, out var r1));
|
||||
Assert.AreEqual("a+b.c-d", s1);
|
||||
Assert.AreEqual("rest", r1);
|
||||
|
||||
// The first character must be a letter; plus is not allowed as first char
|
||||
Assert.IsFalse(TryGetScheme("+abc:rest".AsSpan(), out _, out _));
|
||||
Assert.IsFalse(TryGetScheme(".abc:rest".AsSpan(), out _, out _));
|
||||
Assert.IsFalse(TryGetScheme("-abc:rest".AsSpan(), out _, out _));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user