mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This PR fixes a problem with invisible icons in newly create Command Palette extensions, when created through built-in command: - Avoids modifying binary files during extension creation from the template to prevent corruption. - Refactors template expansion and physical extension creation into a separate ExtensionTemplateService. - Adds unit tests. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #46448 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **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 <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
189 lines
8.3 KiB
C#
189 lines
8.3 KiB
C#
// 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.IO.Compression;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
|
|
|
[TestClass]
|
|
public class ExtensionTemplateServiceTests
|
|
{
|
|
private string _templateRoot = null!;
|
|
private string _outputRoot = null!;
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), $"{nameof(ExtensionTemplateServiceTests)}_{Guid.NewGuid():N}");
|
|
_templateRoot = Path.Combine(tempRoot, "template");
|
|
_outputRoot = Path.Combine(tempRoot, "output");
|
|
|
|
Directory.CreateDirectory(_templateRoot);
|
|
Directory.CreateDirectory(_outputRoot);
|
|
}
|
|
|
|
[TestCleanup]
|
|
public void Cleanup()
|
|
{
|
|
var tempRoot = Directory.GetParent(_templateRoot)?.FullName;
|
|
if (!string.IsNullOrEmpty(tempRoot) && Directory.Exists(tempRoot))
|
|
{
|
|
Directory.Delete(tempRoot, recursive: true);
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void CreateExtension_BuildsExtensionFromTemplateArchive()
|
|
{
|
|
// Arrange
|
|
var archiveRoot = Path.Combine(_templateRoot, "archive");
|
|
var templateProjectRoot = Path.Combine(archiveRoot, "TemplateCmdPalExtension", "TemplateCmdPalExtension");
|
|
Directory.CreateDirectory(Path.Combine(templateProjectRoot, "Assets"));
|
|
|
|
File.WriteAllText(
|
|
Path.Combine(templateProjectRoot, "Program.cs"),
|
|
"TemplateCmdPalExtension TemplateDisplayName FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
|
|
File.WriteAllBytes(Path.Combine(templateProjectRoot, "Assets", "Logo.png"), [0x89, 0x50, 0x4E, 0x47]);
|
|
|
|
var templateZipPath = Path.Combine(_templateRoot, "template.zip");
|
|
ZipFile.CreateFromDirectory(archiveRoot, templateZipPath);
|
|
|
|
var service = new ExtensionTemplateService(templateZipPath);
|
|
|
|
// Act
|
|
service.CreateExtension("MyExtension", "My Display Name", _outputRoot);
|
|
|
|
// Assert
|
|
var programFile = Path.Combine(_outputRoot, "MyExtension", "MyExtension", "Program.cs");
|
|
var imageFile = Path.Combine(_outputRoot, "MyExtension", "MyExtension", "Assets", "Logo.png");
|
|
|
|
Assert.IsTrue(File.Exists(programFile));
|
|
Assert.IsTrue(File.Exists(imageFile));
|
|
StringAssert.Contains(File.ReadAllText(programFile), "MyExtension");
|
|
StringAssert.Contains(File.ReadAllText(programFile), "My Display Name");
|
|
Assert.IsFalse(File.ReadAllText(programFile).Contains("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", StringComparison.Ordinal));
|
|
CollectionAssert.AreEqual(new byte[] { 0x89, 0x50, 0x4E, 0x47 }, File.ReadAllBytes(imageFile));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void CopyTemplateFile_RewritesTextFiles()
|
|
{
|
|
// Arrange
|
|
var sourceFile = Path.Combine(_templateRoot, "TemplateCmdPalExtension", "Program.cs");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(sourceFile)!);
|
|
File.WriteAllText(sourceFile, "TemplateCmdPalExtension TemplateDisplayName FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
|
|
|
|
// Act
|
|
ExtensionTemplateService.CopyTemplateFile(_templateRoot, sourceFile, _outputRoot, "MyExtension", "My Display Name", "11111111-1111-1111-1111-111111111111");
|
|
|
|
// Assert
|
|
var outputFile = Path.Combine(_outputRoot, "MyExtension", "Program.cs");
|
|
Assert.IsTrue(File.Exists(outputFile));
|
|
Assert.AreEqual(
|
|
"MyExtension My Display Name 11111111-1111-1111-1111-111111111111",
|
|
File.ReadAllText(outputFile));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void CopyTemplateFile_CopiesUnchangedTextFilesVerbatim()
|
|
{
|
|
// Arrange
|
|
var sourceFile = Path.Combine(_templateRoot, "TemplateCmdPalExtension", "Properties", "launchSettings.json");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(sourceFile)!);
|
|
|
|
var sourceBytes = Encoding.UTF8.GetPreamble()
|
|
.Concat(Encoding.UTF8.GetBytes("{\"profiles\":{\"CmdPal\":{}}}"))
|
|
.ToArray();
|
|
File.WriteAllBytes(sourceFile, sourceBytes);
|
|
|
|
// Act
|
|
ExtensionTemplateService.CopyTemplateFile(_templateRoot, sourceFile, _outputRoot, "MyExtension", "My Display Name", "11111111-1111-1111-1111-111111111111");
|
|
|
|
// Assert
|
|
var outputFile = Path.Combine(_outputRoot, "MyExtension", "Properties", "launchSettings.json");
|
|
Assert.IsTrue(File.Exists(outputFile));
|
|
CollectionAssert.AreEqual(sourceBytes, File.ReadAllBytes(outputFile));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void CopyTemplateFile_CopiesBinaryFilesWithoutRewritingContents()
|
|
{
|
|
// Arrange
|
|
var sourceFile = Path.Combine(_templateRoot, "TemplateCmdPalExtension", "Assets", "Logo.png");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(sourceFile)!);
|
|
|
|
var binaryContent = new byte[]
|
|
{
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00,
|
|
};
|
|
var embeddedText = Encoding.UTF8.GetBytes("TemplateCmdPalExtension TemplateDisplayName");
|
|
File.WriteAllBytes(sourceFile, [.. binaryContent, .. embeddedText]);
|
|
|
|
// Act
|
|
ExtensionTemplateService.CopyTemplateFile(_templateRoot, sourceFile, _outputRoot, "MyExtension", "My Display Name", "11111111-1111-1111-1111-111111111111");
|
|
|
|
// Assert
|
|
var outputFile = Path.Combine(_outputRoot, "MyExtension", "Assets", "Logo.png");
|
|
Assert.IsTrue(File.Exists(outputFile));
|
|
CollectionAssert.AreEqual(File.ReadAllBytes(sourceFile), File.ReadAllBytes(outputFile));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void TemplateFileHandling_ThrowsForUnknownExtension()
|
|
{
|
|
var ex = Assert.ThrowsException<InvalidOperationException>(() => ExtensionTemplateService.GetTemplateFileHandling("template.svg"));
|
|
|
|
StringAssert.Contains(ex.Message, ".svg");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void TemplateExtensionCategories_AreDisjointAndCoverTemplateZip()
|
|
{
|
|
var replaceTokens = ExtensionTemplateService.ReplaceTokensTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
var copyAsIs = ExtensionTemplateService.CopyAsIsTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
var allAccountedFor = replaceTokens.Concat(copyAsIs).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
var templateZipExtensions = GetTemplateZipExtensions();
|
|
|
|
CollectionAssert.AreEqual(Array.Empty<string>(), replaceTokens.Intersect(copyAsIs, StringComparer.OrdinalIgnoreCase).ToArray());
|
|
CollectionAssert.AreEquivalent(templateZipExtensions.OrderBy(x => x).ToArray(), allAccountedFor.OrderBy(x => x).ToArray());
|
|
}
|
|
|
|
[TestMethod]
|
|
public void TemplateZipFiles_AllUseKnownHandling()
|
|
{
|
|
using var archive = ZipFile.OpenRead(TemplateZipPath);
|
|
var replaceTokens = ExtensionTemplateService.ReplaceTokensTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
var copyAsIs = ExtensionTemplateService.CopyAsIsTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var entry in archive.Entries.Where(entry => !string.IsNullOrEmpty(Path.GetExtension(entry.FullName))))
|
|
{
|
|
var extension = Path.GetExtension(entry.FullName);
|
|
var expectedHandling = replaceTokens.Contains(extension)
|
|
? ExtensionTemplateService.TemplateFileHandling.ReplaceTokens
|
|
: ExtensionTemplateService.TemplateFileHandling.CopyAsIs;
|
|
|
|
Assert.AreEqual(expectedHandling, ExtensionTemplateService.GetTemplateFileHandling(entry.FullName), entry.FullName);
|
|
Assert.IsTrue(replaceTokens.Contains(extension) || copyAsIs.Contains(extension), entry.FullName);
|
|
}
|
|
}
|
|
|
|
private static string TemplateZipPath => Path.Combine(AppContext.BaseDirectory, "Assets", "template.zip");
|
|
|
|
private static HashSet<string> GetTemplateZipExtensions()
|
|
{
|
|
using var archive = ZipFile.OpenRead(TemplateZipPath);
|
|
return archive.Entries
|
|
.Where(entry => !string.IsNullOrEmpty(Path.GetExtension(entry.FullName)))
|
|
.Select(entry => Path.GetExtension(entry.FullName))
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
}
|