Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ryfu-msft committed Dec 5, 2024
1 parent 42e53bf commit 4efa09f
Show file tree
Hide file tree
Showing 27 changed files with 335 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Commands/FontCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace AppInstaller::CLI
{
return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({
std::make_unique<FontListCommand>(FullName()),
std::make_unique<FontInstallCommand>(FullName()),
});
}

Expand Down Expand Up @@ -49,7 +50,6 @@ namespace AppInstaller::CLI
return {
Argument::ForType(Args::Type::Manifest),
Argument{ Args::Type::InstallScope, Resource::String::InstallScopeDescription, ArgumentType::Standard, Argument::Visibility::Help },
Argument::ForType(Args::Type::Force),
};
}

Expand Down
55 changes: 42 additions & 13 deletions src/AppInstallerCLICore/FontInstaller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "pch.h"
#include "ExecutionContext.h"
#include "FontInstaller.h"
#include <winget/Fonts.h>
#include <winget/Manifest.h>
#include <winget/ManifestCommon.h>
#include <winget/Filesystem.h>
Expand All @@ -14,37 +15,65 @@ namespace AppInstaller::CLI::Font
namespace
{
constexpr std::wstring_view s_FontsPathSubkey = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts";
}
constexpr std::wstring_view s_TrueType = L" (TrueType)";

bool IsTrueTypeFont(DWRITE_FONT_FILE_TYPE fileType)
{
return (
fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE ||
fileType == DWRITE_FONT_FILE_TYPE_TRUETYPE_COLLECTION
);
}
}

FontInstaller::FontInstaller(Manifest::ScopeEnum scope) : m_scope(scope)
{
if (scope == Manifest::ScopeEnum::Machine)
{
m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsMachineInstallLocation);
m_key = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, std::wstring{ s_FontsPathSubkey });
m_key = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, std::wstring{ s_FontsPathSubkey }, 0, KEY_WRITE );
}
else
{
m_installLocation = Runtime::GetPathTo(Runtime::PathName::FontsUserInstallLocation);
m_key = Registry::Key::OpenIfExists(HKEY_CURRENT_USER, std::wstring{ s_FontsPathSubkey });
m_key = Registry::Key::OpenIfExists(HKEY_CURRENT_USER, std::wstring{ s_FontsPathSubkey }, 0, KEY_WRITE);
}
}

void FontInstaller::Install(const std::map<std::wstring, std::filesystem::path> fontFiles)
void FontInstaller::Install(const std::vector<FontFile>& fontFiles)
{
// TODO: Get all font files and check if font file already exists.
for (const auto& [fontName, fontFilePath] : fontFiles)
for (const auto& fontFile : fontFiles)
{
std::filesystem::path fileName = fontFilePath.filename();
std::filesystem::path fontFileInstallationPath = m_installLocation / fileName;
const auto& filePath = fontFile.FilePath;
const auto& fileName = filePath.filename();
const auto& destinationPath = m_installLocation / fileName;

AICLI_LOG(CLI, Info, << "Getting Font title");

AICLI_LOG(CLI, Info, << "Moving font file to: " << fontFileInstallationPath.u8string());
AppInstaller::Filesystem::RenameFile(fontFilePath, fontFileInstallationPath);
std::wstring title = AppInstaller::Fonts::GetFontFileTitle(filePath);

// TODO: Need to fix access permission for writing to the registry.
AICLI_LOG(CLI, Info, << "Creating font subkey for: " << AppInstaller::Utility::ConvertToUTF8(fontName));
m_key.SetValue(fontName, fontFileInstallationPath, REG_SZ);
if (IsTrueTypeFont(fontFile.FileType))
{
title += s_TrueType;
}

AICLI_LOG(CLI, Info, << "Moving font file to: " << destinationPath);
AppInstaller::Filesystem::RenameFile(filePath, destinationPath);

AICLI_LOG(CLI, Info, << "Creating font subkey with name: " << AppInstaller::Utility::ConvertToUTF8(title));
if (m_scope == Manifest::ScopeEnum::Machine)
{
m_key.SetValue(title, fileName, REG_SZ);
}
else
{
m_key.SetValue(title, destinationPath, REG_SZ);
}
}
}

void FontInstaller::Uninstall(const std::wstring& familyName)
{
UNREFERENCED_PARAMETER(familyName);
}
}
14 changes: 13 additions & 1 deletion src/AppInstallerCLICore/FontInstaller.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@
// Licensed under the MIT License.
#pragma once
#include <filesystem>
#include <winget/Fonts.h>

namespace AppInstaller::CLI::Font
{
struct FontFile
{
FontFile(std::filesystem::path filePath, DWRITE_FONT_FILE_TYPE fileType)
: FilePath(filePath), FileType(fileType) {}

std::filesystem::path FilePath;
DWRITE_FONT_FILE_TYPE FileType;
};

struct FontInstaller
{
FontInstaller(Manifest::ScopeEnum scope);

std::filesystem::path FontFileLocation;

void Install(const std::map<std::wstring, std::filesystem::path> fontFiles);
void Install(const std::vector<FontFile>& fontFiles);

void Uninstall(const std::wstring& familyName);

private:
Manifest::ScopeEnum m_scope;
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(FontFaces);
WINGET_DEFINE_RESOURCE_STRINGID(FontFamily);
WINGET_DEFINE_RESOURCE_STRINGID(FontFamilyNameArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontFileNotSupported);
WINGET_DEFINE_RESOURCE_STRINGID(FontFilePaths);
WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(FontInstallCommandShortDescription);
Expand Down
46 changes: 31 additions & 15 deletions src/AppInstallerCLICore/Workflows/FontFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,34 +126,50 @@ namespace AppInstaller::CLI::Workflow

void FontInstallImpl(Execution::Context& context)
{
Manifest::ScopeEnum scope = AppInstaller::Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
FontInstaller fontInstaller = FontInstaller(scope);
std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>();
std::map<std::wstring, std::filesystem::path> fontFileMap{};

try
{
context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl;
const auto& installerPath = context.Get<Execution::Data::InstallerPath>();
std::vector<std::filesystem::path> filePaths;

// InstallerPath will point to a directory if it is extracted from an archive.
// InstallerPath will point to a directory if extracted from an archive.
if (std::filesystem::is_directory(installerPath))
{
const std::vector<Manifest::NestedInstallerFile>& nestedInstallerFiles = context.Get<Execution::Data::Installer>()->NestedInstallerFiles;

for (const auto& nestedInstallerFile : nestedInstallerFiles)
{
const std::filesystem::path& fontFilePath = installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath);
auto fontFileEntryName = AppInstaller::Fonts::GetFontFileTitle(fontFilePath);
fontFileMap.emplace(fontFileEntryName, fontFilePath);
{
filePaths.emplace_back(installerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath));
}
}
else
{
auto fontName = AppInstaller::Fonts::GetFontFileTitle(installerPath);
fontFileMap.emplace(fontName, installerPath);
filePaths.emplace_back(installerPath);
}

fontInstaller.Install(fontFileMap);
std::vector<FontFile> fontFiles;
Fonts::FontCatalog fontCatalog;

for (const auto& file : filePaths)
{
DWRITE_FONT_FILE_TYPE fileType;
if (!fontCatalog.IsFontFileSupported(file, fileType))
{
AICLI_LOG(CLI, Warning, << "Font file is not supported: " << file);
context.Reporter.Error() << Resource::String::FontFileNotSupported << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_FONT_FILE_NOT_SUPPORTED);
}
else
{
AICLI_LOG(CLI, Warning, << "Font file is supported: " << file);
fontFiles.emplace_back(FontFile(file, fileType));
}
}

context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl;

Manifest::ScopeEnum scope = AppInstaller::Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope));
FontInstaller fontInstaller = FontInstaller(scope);

fontInstaller.Install(fontFiles);
context.Add<Execution::Data::OperationReturnCode>(ERROR_SUCCESS);
}
catch (...)
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
<None Remove="TestData\Configuration\ShowDetails_TestRepo_0_3.yml" />
<None Remove="TestData\Configuration\WithParameters_0_3.yml" />
<None Remove="TestData\empty" />
<None Remove="TestData\Manifests\TestFont.yaml" />
<None Remove="TestData\Manifests\TestInvalidFont.yaml" />
<None Remove="TestData\Manifests\TestUpgradeAddsDependency.1.0.yaml" />
<None Remove="TestData\Manifests\TestUpgradeAddsDependency.2.0.yaml" />
<None Remove="TestData\Manifests\TestUpgradeAddsDependencyDependent.1.0.yaml" />
Expand Down
7 changes: 7 additions & 0 deletions src/AppInstallerCLIE2ETests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class Constants
public const string MsiInstallerPathParameter = "MsiTestInstallerPath";
public const string MsiInstallerV2PathParameter = "MsiTestInstallerV2Path";
public const string MsixInstallerPathParameter = "MsixTestInstallerPath";
public const string FontPathParameter = "FontTestPath";
public const string PackageCertificatePathParameter = "PackageCertificatePath";
public const string PowerShellModulePathParameter = "PowerShellModulePath";
public const string SkipTestSourceParameter = "SkipTestSource";
Expand Down Expand Up @@ -55,11 +56,13 @@ public class Constants
public const string MsiInstaller = "AppInstallerTestMsiInstaller";
public const string MsixInstaller = "AppInstallerTestMsixInstaller";
public const string ZipInstaller = "AppInstallerTestZipInstaller";
public const string Font = "AppInstallerTestFont";
public const string ExeInstallerFileName = "AppInstallerTestExeInstaller.exe";
public const string MsiInstallerFileName = "AppInstallerTestMsiInstaller.msi";
public const string MsiInstallerV2FileName = "AppInstallerTestMsiInstallerV2.msi";
public const string MsixInstallerFileName = "AppInstallerTestMsixInstaller.msix";
public const string ZipInstallerFileName = "AppInstallerTestZipInstaller.zip";
public const string FontFileName = "AppInstallerTestFont.ttf";
public const string ModifyRepairInstaller = "AppInstallerTest.TestModifyRepair";
public const string IndexPackage = "source.msix";
public const string MakeAppx = "makeappx.exe";
Expand Down Expand Up @@ -118,6 +121,8 @@ public class Constants
public const string UninstallSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall";
public const string PathSubKey_User = @"Environment";
public const string PathSubKey_Machine = @"SYSTEM\CurrentControlSet\Control\Session Manager\Environment";
public const string FontsSubKey = @"Software\Microsoft\Windows NT\CurrentVersion\Fonts";
public const string TestFontSubKeyName = "Cascadia Code PL (True Type)";

// User settings
public const string ArchiveExtractionMethod = "archiveExtractionMethod";
Expand Down Expand Up @@ -271,6 +276,8 @@ public class ErrorCode
public const int ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED = unchecked((int)0x8A15007D);

public const int ERROR_INSTALLER_ZERO_BYTE_FILE = unchecked((int)0x8A150086);
public const int ERROR_FONT_INSTALL_FAILED = unchecked((int)0x8A150087);
public const int ERROR_FONT_FILE_NOT_SUPPORTED = unchecked((int)0x8A150088);

public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101);
public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102);
Expand Down
61 changes: 61 additions & 0 deletions src/AppInstallerCLIE2ETests/FontCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// -----------------------------------------------------------------------------
// <copyright file="FontCommand.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
// </copyright>
// -----------------------------------------------------------------------------

namespace AppInstallerCLIE2ETests
{
using AppInstallerCLIE2ETests.Helpers;
using NUnit.Framework;

/// <summary>
/// Test font command.
/// </summary>
public class FontCommand : BaseCommand
{
/// <summary>
/// One time set up.
/// </summary>
[SetUp]
public void OneTimeSetup()
{
WinGetSettingsHelper.ConfigureFeature("fonts", true);
}

/// <summary>
/// Test install a font with user scope.
/// </summary>
[Test]
public void InstallFont()
{
var result = TestCommon.RunAICLICommand("font install", $"-m {TestCommon.GetTestDataFile(@"Manifests\TestFont.yaml")}");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully installed"));
TestCommon.VerifyFontPackage(Constants.TestFontSubKeyName, Constants.FontFileName);
}

/// <summary>
/// Test install a font with machine scope.
/// </summary>
[Test]
public void InstallFont_MachineScope()
{
var result = TestCommon.RunAICLICommand("font install", $"-m {TestCommon.GetTestDataFile(@"Manifests\TestFont.yaml")} --scope Machine");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully installed"));
TestCommon.VerifyFontPackage(Constants.TestFontSubKeyName, Constants.FontFileName, TestCommon.Scope.Machine);
}

/// <summary>
/// Test install an invalid font file.
/// </summary>
[Test]
public void InstallInvalidFont()
{
var result = TestCommon.RunAICLICommand("font install", $"-m {TestCommon.GetTestDataFile(@"Manifests\TestInvalidFont.yaml")} --scope Machine");
Assert.AreEqual(Constants.ErrorCode.ERROR_FONT_FILE_NOT_SUPPORTED, result.ExitCode);
Assert.True(result.StdOut.Contains("The font file is not supported and cannot be installed."));
}
}
}
60 changes: 59 additions & 1 deletion src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,72 @@ public static string GetCheckpointsDirectory()
}
}

/// <summary>
/// Gets the fonts directory based on scope.
/// </summary>
/// <param name="scope">Scope.</param>
/// <returns>The path of the fonts directory.</returns>
public static string GetFontsDirectory(Scope scope)
{
if (scope == Scope.Machine)
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts");
}
else
{
return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "Windows", "Fonts");
}
}

/// <summary>
/// Verify font package.
/// </summary>
/// <param name="fontSubKeyName">Name of the font registry subkey entry.</param>
/// <param name="fontFileName">Filename of the installed font file.</param>
/// <param name="scope">Scope.</param>
/// <param name="shouldExist">Should exist.</param>
public static void VerifyFontPackage(
string fontSubKeyName,
string fontFileName,
Scope scope = Scope.User,
bool shouldExist = true)
{
// TODO: Update this function to be able to handle the font uninstall scenario.
string expectedFontInstallPath = Path.Combine(GetFontsDirectory(scope), fontFileName);
bool fontFileExists = File.Exists(expectedFontInstallPath);

bool fontEntryExists;
RegistryKey baseKey = scope == Scope.Machine ? Registry.LocalMachine : Registry.CurrentUser;
using (RegistryKey fontsRegistryKey = baseKey.OpenSubKey(Constants.FontsSubKey, true))
{
RegistryKey fontEntry = fontsRegistryKey.OpenSubKey(fontSubKeyName, true);
fontEntryExists = fontEntry != null;
}

if (shouldExist)
{
// TODO: Replace with font uninstall when implemented.
File.Delete(expectedFontInstallPath);

using (RegistryKey fontsRegistryKey = baseKey.OpenSubKey(Constants.FontsSubKey, true))
{
fontsRegistryKey.DeleteSubKey(fontSubKeyName);
RegistryKey fontEntry = fontsRegistryKey.OpenSubKey(fontSubKeyName, true);
}
}

Assert.AreEqual(shouldExist, fontFileExists, $"Expected font path: {expectedFontInstallPath}");
Assert.AreEqual(shouldExist, fontEntryExists, $"Expected {fontSubKeyName} subkey in registry path: {Constants.FontsSubKey}");
}

/// <summary>
/// Verify portable package.
/// </summary>
/// <param name="installDir">Install dir.</param>
/// <param name="commandAlias">Command alias.</param>
/// <param name="filename">File name.</param>
/// <param name="productCode">Product code.</param>
/// <param name="shouldExist">Should exists.</param>
/// <param name="shouldExist">Should exist.</param>
/// <param name="scope">Scope.</param>
/// <param name="installDirectoryAddedToPath">Install directory added to path instead of the symlink directory.</param>
public static void VerifyPortablePackage(
Expand Down
Loading

0 comments on commit 4efa09f

Please sign in to comment.