terminal/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp
Leonard Hecker fe26a6efc0
Fix loading of fragments that update multiple profiles (#11598)
The "updates" key is an alternative "guid" key for fragment profiles.
But SettingsLoader::_appendProfile stores and deduplicates profiles according
to their "guid" only. We need to modify the function to optionally store
profiles by their "updates" key as well, otherwise multiple fragment
profiles without "guid" might collide as they produce the same default GUID.

## PR Checklist
* [x] Closes #11597
* [x] I work here
* [ ] Tests added/passed
* [ ] Schema updated.
* [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx

## Validation Steps Performed
* Unit tests pass ✔️
* Issue #11597 doesn't reproduce anymore ✔️
2021-10-27 01:43:10 +00:00

2016 lines
85 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "../TerminalSettingsModel/ColorScheme.h"
#include "../TerminalSettingsModel/CascadiaSettings.h"
#include "JsonTestClass.h"
#include "TestUtils.h"
#include <defaults.h>
#include <userDefaults.h>
using namespace Microsoft::Console;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Control;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
namespace SettingsModelLocalTests
{
// TODO:microsoft/terminal#3838:
// Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for
// an updated TAEF that will let us install framework packages when the test
// package is deployed. Until then, these tests won't deploy in CI.
class DeserializationTests : public JsonTestClass
{
// Use a custom AppxManifest to ensure that we can activate winrt types
// from our test. This property will tell taef to manually use this as
// the AppxManifest for this test class.
// This does not yet work for anything XAML-y. See TabTests.cpp for more
// details on that.
BEGIN_TEST_CLASS(DeserializationTests)
TEST_CLASS_PROPERTY(L"RunAs", L"UAP")
TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml")
END_TEST_CLASS()
TEST_METHOD(ValidateProfilesExist);
TEST_METHOD(ValidateDefaultProfileExists);
TEST_METHOD(ValidateDuplicateProfiles);
TEST_METHOD(ValidateManyWarnings);
TEST_METHOD(LayerGlobalProperties);
TEST_METHOD(ValidateProfileOrdering);
TEST_METHOD(ValidateHideProfiles);
TEST_METHOD(TestReorderWithNullGuids);
TEST_METHOD(TestReorderingWithoutGuid);
TEST_METHOD(TestLayeringNameOnlyProfiles);
TEST_METHOD(TestHideAllProfiles);
TEST_METHOD(TestInvalidColorSchemeName);
TEST_METHOD(TestHelperFunctions);
TEST_METHOD(TestProfileBackgroundImageWithEnvVar);
TEST_METHOD(TestProfileBackgroundImageWithDesktopWallpaper);
TEST_METHOD(TestCloseOnExitParsing);
TEST_METHOD(TestCloseOnExitCompatibilityShim);
TEST_METHOD(TestLayerUserDefaultsBeforeProfiles);
TEST_METHOD(TestDontLayerGuidFromUserDefaults);
TEST_METHOD(TestLayerUserDefaultsOnDynamics);
TEST_METHOD(FindMissingProfile);
TEST_METHOD(ValidateKeybindingsWarnings);
TEST_METHOD(ValidateColorSchemeInCommands);
TEST_METHOD(ValidateExecuteCommandlineWarning);
TEST_METHOD(TestTrailingCommas);
TEST_METHOD(TestCommandsAndKeybindings);
TEST_METHOD(TestNestedCommandWithoutName);
TEST_METHOD(TestNestedCommandWithBadSubCommands);
TEST_METHOD(TestUnbindNestedCommand);
TEST_METHOD(TestRebindNestedCommand);
TEST_METHOD(TestCopy);
TEST_METHOD(TestCloneInheritanceTree);
TEST_METHOD(TestValidDefaults);
TEST_METHOD(TestInheritedCommand);
TEST_METHOD(LoadFragmentsWithMultipleUpdates);
private:
static winrt::com_ptr<implementation::CascadiaSettings> createSettings(const std::string_view& userJSON)
{
static constexpr std::string_view inboxJSON{ R"({
"schemes": [
{
"name": "Campbell",
"foreground": "#CCCCCC",
"background": "#0C0C0C",
"cursorColor": "#FFFFFF",
"black": "#0C0C0C",
"red": "#C50F1F",
"green": "#13A10E",
"yellow": "#C19C00",
"blue": "#0037DA",
"purple": "#881798",
"cyan": "#3A96DD",
"white": "#CCCCCC",
"brightBlack": "#767676",
"brightRed": "#E74856",
"brightGreen": "#16C60C",
"brightYellow": "#F9F1A5",
"brightBlue": "#3B78FF",
"brightPurple": "#B4009E",
"brightCyan": "#61D6D6",
"brightWhite": "#F2F2F2"
}
]
})" };
return winrt::make_self<implementation::CascadiaSettings>(userJSON, inboxJSON);
}
static void _logCommandNames(winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, Command> commands, const int indentation = 1)
{
if (indentation == 1)
{
Log::Comment((commands.Size() == 0) ? L"Commands:\n <none>" : L"Commands:");
}
for (const auto& nameAndCommand : commands)
{
Log::Comment(fmt::format(L"{0:>{1}}* {2}->{3}",
L"",
indentation,
nameAndCommand.Key(),
nameAndCommand.Value().Name())
.c_str());
winrt::com_ptr<implementation::Command> cmdImpl;
cmdImpl.copy_from(winrt::get_self<implementation::Command>(nameAndCommand.Value()));
if (cmdImpl->HasNestedCommands())
{
_logCommandNames(cmdImpl->_subcommands.GetView(), indentation + 2);
}
}
}
};
void DeserializationTests::ValidateProfilesExist()
{
static constexpr std::string_view settingsWithProfiles{ R"(
{
"profiles": [
{
"name" : "profile0"
}
]
})" };
static constexpr std::string_view settingsWithoutProfiles{ R"(
{
"defaultProfile": "{6239a42c-1de4-49a3-80bd-e8fdd045185c}"
})" };
static constexpr std::string_view settingsWithEmptyProfiles{ R"(
{
"profiles": []
})" };
{
// Case 1: Good settings
auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsWithProfiles);
}
{
// Case 2: Bad settings
bool caughtExpectedException = false;
try
{
auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsWithoutProfiles);
}
catch (const implementation::SettingsException& ex)
{
VERIFY_IS_TRUE(ex.Error() == SettingsLoadErrors::NoProfiles);
caughtExpectedException = true;
}
VERIFY_IS_TRUE(caughtExpectedException);
}
{
// Case 3: Bad settings
bool caughtExpectedException = false;
try
{
auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsWithEmptyProfiles);
}
catch (const implementation::SettingsException& ex)
{
VERIFY_IS_TRUE(ex.Error() == SettingsLoadErrors::NoProfiles);
caughtExpectedException = true;
}
VERIFY_IS_TRUE(caughtExpectedException);
}
}
void DeserializationTests::ValidateDefaultProfileExists()
{
static constexpr std::string_view goodProfiles{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile0",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
}
]
})" };
static constexpr std::string_view badProfiles{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
}
]
})" };
static constexpr std::string_view goodProfilesSpecifiedByName{ R"(
{
"defaultProfile": "profile1",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
}
]
})" };
{
// Case 1: Good settings
Log::Comment(NoThrowString().Format(
L"Testing a pair of profiles with unique guids, and the defaultProfile is one of those guids"));
const auto settings = createSettings(goodProfiles);
VERIFY_ARE_EQUAL(static_cast<size_t>(0), settings->Warnings().Size());
VERIFY_ARE_EQUAL(static_cast<size_t>(2), settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(0).Guid());
}
{
// Case 2: Bad settings
Log::Comment(NoThrowString().Format(
L"Testing a pair of profiles with unique guids, but the defaultProfile is NOT one of those guids"));
const auto settings = createSettings(badProfiles);
VERIFY_ARE_EQUAL(static_cast<size_t>(1), settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(static_cast<size_t>(2), settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(0).Guid());
}
{
// Case 2: Bad settings
Log::Comment(NoThrowString().Format(
L"Testing a pair of profiles with unique guids, and no defaultProfile at all"));
const auto settings = createSettings(badProfiles);
VERIFY_ARE_EQUAL(static_cast<size_t>(1), settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(static_cast<size_t>(2), settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(0).Guid());
}
{
// Case 4: Good settings, default profile is a string
Log::Comment(NoThrowString().Format(
L"Testing a pair of profiles with unique guids, and the defaultProfile is one of the profile names"));
const auto settings = createSettings(goodProfilesSpecifiedByName);
VERIFY_ARE_EQUAL(static_cast<size_t>(0), settings->Warnings().Size());
VERIFY_ARE_EQUAL(static_cast<size_t>(2), settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(1).Guid());
}
}
void DeserializationTests::ValidateDuplicateProfiles()
{
static constexpr std::string_view veryBadProfiles{ R"(
{
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile2",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile3",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile4",
"guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile5",
"guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile6",
"guid": "{6239a42c-7777-49a3-80bd-e8fdd045185c}"
}
]
})" };
const auto settings = createSettings(veryBadProfiles);
VERIFY_ARE_EQUAL(static_cast<size_t>(1), settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::DuplicateProfile, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(static_cast<size_t>(4), settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(L"profile0", settings->AllProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(L"profile1", settings->AllProfiles().GetAt(1).Name());
VERIFY_ARE_EQUAL(L"profile4", settings->AllProfiles().GetAt(2).Name());
VERIFY_ARE_EQUAL(L"profile6", settings->AllProfiles().GetAt(3).Name());
}
void DeserializationTests::ValidateManyWarnings()
{
static constexpr std::string_view badProfiles{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile2",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile3",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile4",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
}
]
})" };
const auto settings = createSettings(badProfiles);
VERIFY_ARE_EQUAL(2u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::DuplicateProfile, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->Warnings().GetAt(1));
VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Guid(), settings->GlobalSettings().DefaultProfile());
}
void DeserializationTests::LayerGlobalProperties()
{
static constexpr std::string_view inboxSettings{ R"({
"alwaysShowTabs": true,
"initialCols" : 120,
"initialRows" : 30
})" };
static constexpr std::string_view userSettings{ R"({
"showTabsInTitlebar": false,
"initialCols" : 240,
"initialRows" : 60,
"profiles": [
{
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
}
]
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userSettings, inboxSettings);
VERIFY_ARE_EQUAL(true, settings->GlobalSettings().AlwaysShowTabs());
VERIFY_ARE_EQUAL(240, settings->GlobalSettings().InitialCols());
VERIFY_ARE_EQUAL(60, settings->GlobalSettings().InitialRows());
VERIFY_ARE_EQUAL(false, settings->GlobalSettings().ShowTabsInTitlebar());
}
void DeserializationTests::ValidateProfileOrdering()
{
static constexpr std::string_view userProfiles0String{ R"(
{
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
}
]
})" };
static constexpr std::string_view defaultProfilesString{ R"(
{
"profiles": [
{
"name" : "profile2",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile3",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
}
]
})" };
static constexpr std::string_view userProfiles1String{ R"(
{
"profiles": [
{
"name" : "profile4",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile5",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
}
]
})" };
{
Log::Comment(NoThrowString().Format(
L"Case 1: Simple swapping of the ordering. The user has the "
L"default profiles in the opposite order of the default ordering."));
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userProfiles0String, defaultProfilesString);
VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(L"profile0", settings->AllProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(L"profile1", settings->AllProfiles().GetAt(1).Name());
}
{
Log::Comment(NoThrowString().Format(
L"Case 2: Make sure all the user's profiles appear before the defaults."));
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userProfiles1String, defaultProfilesString);
VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(L"profile4", settings->AllProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(L"profile5", settings->AllProfiles().GetAt(1).Name());
VERIFY_ARE_EQUAL(L"profile2", settings->AllProfiles().GetAt(2).Name());
}
}
void DeserializationTests::ValidateHideProfiles()
{
static constexpr std::string_view defaultProfilesString{ R"(
{
"profiles": [
{
"name" : "profile2",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile3",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
}
]
})" };
static constexpr std::string_view userProfiles0String{ R"(
{
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"hidden": true
},
{
"name" : "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
}
]
})" };
static constexpr std::string_view userProfiles1String{ R"(
{
"profiles": [
{
"name" : "profile4",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"hidden": true
},
{
"name" : "profile5",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile6",
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}",
"hidden": true
}
]
})" };
{
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userProfiles0String, defaultProfilesString);
VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(1u, settings->ActiveProfiles().Size());
VERIFY_ARE_EQUAL(L"profile1", settings->ActiveProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(false, settings->ActiveProfiles().GetAt(0).Hidden());
}
{
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userProfiles1String, defaultProfilesString);
VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(2u, settings->ActiveProfiles().Size());
VERIFY_ARE_EQUAL(L"profile5", settings->ActiveProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(L"profile2", settings->ActiveProfiles().GetAt(1).Name());
VERIFY_ARE_EQUAL(false, settings->ActiveProfiles().GetAt(0).Hidden());
VERIFY_ARE_EQUAL(false, settings->ActiveProfiles().GetAt(1).Hidden());
}
}
void DeserializationTests::TestReorderWithNullGuids()
{
static constexpr std::string_view settings0String{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1"
},
{
"name" : "cmdFromUserSettings",
"guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}" // from defaults.json
}
]
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settings0String, DefaultJson);
VERIFY_ARE_EQUAL(0u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(0).HasGuid());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(1).HasGuid());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(2).HasGuid());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(3).HasGuid());
VERIFY_ARE_EQUAL(L"profile0", settings->AllProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(L"profile1", settings->AllProfiles().GetAt(1).Name());
VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings->AllProfiles().GetAt(2).Name());
VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->AllProfiles().GetAt(3).Name());
}
void DeserializationTests::TestReorderingWithoutGuid()
{
Log::Comment(NoThrowString().Format(
L"During the GH#2515 PR, this set of settings was found to cause an"
L" exception, crashing the terminal. This test ensures that it doesn't."));
Log::Comment(NoThrowString().Format(
L"While similar to TestReorderWithNullGuids, there's something else"
L" about this scenario specifically that causes a crash, when "
L" TestReorderWithNullGuids did _not_."));
static constexpr std::string_view settings0String{ R"(
{
"defaultProfile" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"profiles": [
{
"guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"acrylicOpacity" : 0.5,
"closeOnExit" : true,
"background" : "#8A00FF",
"foreground" : "#F2F2F2",
"commandline" : "cmd.exe",
"cursorColor" : "#FFFFFF",
"fontFace" : "Cascadia Code",
"fontSize" : 10,
"historySize" : 9001,
"padding" : "20",
"snapOnInput" : true,
"startingDirectory" : "%USERPROFILE%",
"useAcrylic" : true
},
{
"name" : "ThisProfileShouldNotCrash",
"tabTitle" : "Ubuntu",
"acrylicOpacity" : 0.5,
"background" : "#2C001E",
"closeOnExit" : true,
"colorScheme" : "Campbell",
"commandline" : "wsl.exe",
"cursorColor" : "#FFFFFF",
"cursorShape" : "bar",
"fontSize" : 10,
"historySize" : 9001,
"padding" : "0, 0, 0, 0",
"snapOnInput" : true,
"useAcrylic" : true
},
{
// This is the same profile that would be generated by the WSL profile generator.
"name" : "Ubuntu",
"guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}",
"acrylicOpacity" : 0.5,
"background" : "#2C001E",
"closeOnExit" : false,
"cursorColor" : "#FFFFFF",
"cursorShape" : "bar",
"fontSize" : 10,
"historySize" : 9001,
"snapOnInput" : true,
"useAcrylic" : true
}
]
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settings0String, DefaultJson);
VERIFY_ARE_EQUAL(0u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(0).HasGuid());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(1).HasGuid());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(2).HasGuid());
VERIFY_IS_TRUE(settings->AllProfiles().GetAt(3).HasGuid());
VERIFY_ARE_EQUAL(L"Command Prompt", settings->AllProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings->AllProfiles().GetAt(1).Name());
VERIFY_ARE_EQUAL(L"Ubuntu", settings->AllProfiles().GetAt(2).Name());
VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->AllProfiles().GetAt(3).Name());
}
void DeserializationTests::TestLayeringNameOnlyProfiles()
{
// This is a test discovered during GH#2782. When we add a name-only
// profile, it should only layer with other name-only profiles with the
// _same name_
static constexpr std::string_view settings0String{ R"(
{
"defaultProfile" : "{00000000-0000-5f56-a8ff-afceeeaa6101}",
"profiles": [
{
"guid" : "{00000000-0000-5f56-a8ff-afceeeaa6101}",
"name" : "ThisProfileIsGood"
},
{
"name" : "ThisProfileShouldNotLayer"
},
{
"name" : "NeitherShouldThisOne"
}
]
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settings0String, DefaultJson);
const auto profiles = settings->AllProfiles();
VERIFY_ARE_EQUAL(5u, profiles.Size());
VERIFY_ARE_EQUAL(L"ThisProfileIsGood", profiles.GetAt(0).Name());
VERIFY_ARE_EQUAL(L"ThisProfileShouldNotLayer", profiles.GetAt(1).Name());
VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", profiles.GetAt(2).Name());
VERIFY_ARE_EQUAL(L"Windows PowerShell", profiles.GetAt(3).Name());
VERIFY_ARE_EQUAL(L"Command Prompt", profiles.GetAt(4).Name());
}
void DeserializationTests::TestHideAllProfiles()
{
static constexpr std::string_view settingsWithProfiles{ R"(
{
"profiles": [
{
"name" : "profile0",
"hidden": false
},
{
"name" : "profile1",
"hidden": true
}
]
})" };
static constexpr std::string_view settingsWithoutProfiles{ R"(
{
"profiles": [
{
"name" : "profile0",
"hidden": true
},
{
"name" : "profile1",
"hidden": true
}
]
})" };
{
// Case 1: Good settings
const auto settings = createSettings(settingsWithProfiles);
VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(1u, settings->ActiveProfiles().Size());
}
{
// Case 2: Bad settings
VERIFY_THROWS_SPECIFIC(winrt::make_self<implementation::CascadiaSettings>(settingsWithoutProfiles), const implementation::SettingsException, [](const auto& ex) {
return ex.Error() == SettingsLoadErrors::AllProfilesHidden;
});
}
}
void DeserializationTests::TestInvalidColorSchemeName()
{
Log::Comment(NoThrowString().Format(
L"Ensure that setting a profile's scheme to a non-existent scheme causes a warning."));
static constexpr std::string_view settings0String{ R"({
"profiles": [
{
"name" : "profile0",
"colorScheme": "Campbell"
},
{
"name" : "profile1",
"colorScheme": "InvalidSchemeName"
},
{
"name" : "profile2"
// Will use the Profile default value, "Campbell"
}
]
})" };
const auto settings = createSettings(settings0String);
VERIFY_ARE_EQUAL(1u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::UnknownColorScheme, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size());
for (const auto& profile : settings->AllProfiles())
{
VERIFY_ARE_EQUAL(L"Campbell", profile.DefaultAppearance().ColorSchemeName());
}
}
void DeserializationTests::ValidateColorSchemeInCommands()
{
Log::Comment(NoThrowString().Format(
L"Ensure that setting a command's color scheme to a non-existent scheme causes a warning."));
static constexpr std::string_view settings0String{ R"(
{
"profiles": [
{
"name" : "profile0",
"colorScheme": "Campbell"
}
],
"actions": [
{
"command": { "action": "setColorScheme", "colorScheme": "Campbell" }
},
{
"command": { "action": "setColorScheme", "colorScheme": "invalidScheme" }
}
]
})" };
static constexpr std::string_view settings1String{ R"(
{
"profiles": [
{
"name" : "profile0",
"colorScheme": "Campbell"
}
],
"actions": [
{
"command": { "action": "setColorScheme", "colorScheme": "Campbell" }
},
{
"name": "parent",
"commands": [
{ "command": { "action": "setColorScheme", "colorScheme": "invalidScheme" } }
]
}
]
})" };
static constexpr std::string_view settings2String{ R"(
{
"profiles": [
{
"name" : "profile0",
"colorScheme": "Campbell"
}
],
"actions": [
{
"command": { "action": "setColorScheme", "colorScheme": "Campbell" }
},
{
"name": "grandparent",
"commands": [
{
"name": "parent",
"commands": [
{
"command": { "action": "setColorScheme", "colorScheme": "invalidScheme" }
}
]
}
]
}
]
})" };
{
// Case 1: setColorScheme command with invalid scheme
Log::Comment(NoThrowString().Format(
L"Testing a simple command with invalid scheme"));
const auto settings = createSettings(settings0String);
VERIFY_ARE_EQUAL(1u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::InvalidColorSchemeInCmd, settings->Warnings().GetAt(0));
}
{
// Case 2: nested setColorScheme command with invalid scheme
Log::Comment(NoThrowString().Format(
L"Testing a nested command with invalid scheme"));
const auto settings = createSettings(settings1String);
VERIFY_ARE_EQUAL(1u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::InvalidColorSchemeInCmd, settings->Warnings().GetAt(0));
}
{
// Case 3: nested-in-nested setColorScheme command with invalid scheme
Log::Comment(NoThrowString().Format(
L"Testing a nested-in-nested command with invalid scheme"));
const auto settings = createSettings(settings2String);
VERIFY_ARE_EQUAL(1u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::InvalidColorSchemeInCmd, settings->Warnings().GetAt(0));
}
}
void DeserializationTests::TestHelperFunctions()
{
static constexpr std::string_view settings0String{ R"(
{
"defaultProfile" : "{2C4DE342-38B7-51CF-B940-2309A097F518}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}"
},
{
"name" : "ThisProfileShouldNotThrow"
},
{
"name" : "Ubuntu",
"guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}"
}
]
})" };
const auto name0{ L"profile0" };
const auto name1{ L"profile1" };
const auto name2{ L"Ubuntu" };
const auto name3{ L"ThisProfileShouldNotThrow" };
const auto badName{ L"DoesNotExist" };
const winrt::guid guid0{ Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") };
const winrt::guid guid1{ Utils::GuidFromString(L"{6239a42c-6666-49a3-80bd-e8fdd045185c}") };
const winrt::guid guid2{ Utils::GuidFromString(L"{2C4DE342-38B7-51CF-B940-2309A097F518}") };
const winrt::guid fakeGuid{ Utils::GuidFromString(L"{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}") };
const winrt::guid autogeneratedGuid{ implementation::Profile::_GenerateGuidForProfile(name3, L"") };
const auto settings = createSettings(settings0String);
VERIFY_ARE_EQUAL(guid0, settings->GetProfileByName(name0).Guid());
VERIFY_ARE_EQUAL(guid1, settings->GetProfileByName(name1).Guid());
VERIFY_ARE_EQUAL(guid2, settings->GetProfileByName(name2).Guid());
VERIFY_ARE_EQUAL(autogeneratedGuid, settings->GetProfileByName(name3).Guid());
VERIFY_IS_NULL(settings->GetProfileByName(badName));
VERIFY_ARE_EQUAL(name0, settings->FindProfile(guid0).Name());
VERIFY_ARE_EQUAL(name1, settings->FindProfile(guid1).Name());
VERIFY_ARE_EQUAL(name2, settings->FindProfile(guid2).Name());
VERIFY_IS_NULL(settings->FindProfile(fakeGuid));
}
void DeserializationTests::TestProfileBackgroundImageWithEnvVar()
{
const auto expectedPath = wil::ExpandEnvironmentStringsW<std::wstring>(L"%WINDIR%\\System32\\x_80.png");
static constexpr std::string_view settingsJson{ R"(
{
"profiles": [
{
"name": "profile0",
"backgroundImage": "%WINDIR%\\System32\\x_80.png"
}
]
})" };
const auto settings = createSettings(settingsJson);
VERIFY_ARE_NOT_EQUAL(0u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(expectedPath, settings->AllProfiles().GetAt(0).DefaultAppearance().ExpandedBackgroundImagePath());
}
void DeserializationTests::TestProfileBackgroundImageWithDesktopWallpaper()
{
const winrt::hstring expectedBackgroundImagePath{ L"desktopWallpaper" };
static constexpr std::string_view settingsJson{ R"(
{
"profiles": [
{
"name": "profile0",
"backgroundImage": "desktopWallpaper"
}
]
})" };
const auto settings = createSettings(settingsJson);
VERIFY_ARE_EQUAL(expectedBackgroundImagePath, settings->AllProfiles().GetAt(0).DefaultAppearance().BackgroundImagePath());
VERIFY_ARE_NOT_EQUAL(expectedBackgroundImagePath, settings->AllProfiles().GetAt(0).DefaultAppearance().ExpandedBackgroundImagePath());
}
void DeserializationTests::TestCloseOnExitParsing()
{
static constexpr std::string_view settingsJson{ R"(
{
"profiles": [
{
"name": "profile0",
"closeOnExit": "graceful"
},
{
"name": "profile1",
"closeOnExit": "always"
},
{
"name": "profile2",
"closeOnExit": "never"
},
{
"name": "profile3",
"closeOnExit": null
}
]
})" };
const auto settings = createSettings(settingsJson);
VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(0).CloseOnExit());
VERIFY_ARE_EQUAL(CloseOnExitMode::Always, settings->AllProfiles().GetAt(1).CloseOnExit());
VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings->AllProfiles().GetAt(2).CloseOnExit());
// Unknown modes parse as "Graceful"
VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(3).CloseOnExit());
}
void DeserializationTests::TestCloseOnExitCompatibilityShim()
{
static constexpr std::string_view settingsJson{ R"(
{
"profiles": [
{
"name": "profile0",
"closeOnExit": true
},
{
"name": "profile1",
"closeOnExit": false
}
]
})" };
const auto settings = createSettings(settingsJson);
VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(0).CloseOnExit());
VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings->AllProfiles().GetAt(1).CloseOnExit());
}
void DeserializationTests::TestLayerUserDefaultsBeforeProfiles()
{
// Test for microsoft/terminal#2325. For this test, we'll be setting the
// "historySize" in the "defaultSettings", so it should apply to all
// profiles, unless they override it. In one of the user's profiles,
// we'll override that value, and in the other, we'll leave it
// untouched.
static constexpr std::string_view settings0String{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": {
"defaults": {
"historySize": 1234
},
"list": [
{
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"name": "profile0",
"historySize": 2345
},
{
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"name": "profile1"
}
]
}
})" };
const auto settings = createSettings(settings0String);
VERIFY_IS_NOT_NULL(settings->ProfileDefaults());
VERIFY_ARE_EQUAL(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}", settings->GlobalSettings().UnparsedDefaultProfile());
VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(2345, settings->AllProfiles().GetAt(0).HistorySize());
VERIFY_ARE_EQUAL(1234, settings->AllProfiles().GetAt(1).HistorySize());
}
void DeserializationTests::TestDontLayerGuidFromUserDefaults()
{
// Test for microsoft/terminal#2325. We don't want the user to put a
// "guid" in the "defaultSettings", and have that apply to all the other
// profiles
static constexpr std::string_view settings0String{ R"({
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": {
"defaults": {
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
},
"list": [
{
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"name": "profile0",
"historySize": 2345
},
{
// Doesn't have a GUID, we'll auto-generate one
"name": "profile1"
}
]
}
})" };
const auto guid1String = L"{6239a42c-1111-49a3-80bd-e8fdd045185c}";
const winrt::guid guid1{ Utils::GuidFromString(guid1String) };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settings0String, DefaultJson);
VERIFY_ARE_EQUAL(guid1String, settings->GlobalSettings().UnparsedDefaultProfile());
VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(guid1, settings->AllProfiles().GetAt(0).Guid());
VERIFY_ARE_NOT_EQUAL(guid1, settings->AllProfiles().GetAt(1).Guid());
}
void DeserializationTests::TestLayerUserDefaultsOnDynamics()
{
// Test for microsoft/terminal#2325. For this test, we'll be setting the
// "historySize" in the "defaultSettings", so it should apply to all
// profiles, unless they override it. The dynamic profiles will _also_
// set this value, but from discussion in GH#2325, we decided that
// settings in defaultSettings should apply _on top_ of settings from
// dynamic profiles.
const winrt::guid guid1{ Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}") };
const winrt::guid guid2{ Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}") };
const winrt::guid guid3{ Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}") };
const winrt::guid guid4{ Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") };
static constexpr std::string_view dynamicProfiles{ R"({
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.0",
"historySize": 1111
},
{
"name": "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.1",
"historySize": 2222
},
{
"name": "profile2",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.1",
"historySize": 4444
}
]
})" };
static constexpr std::string_view userProfiles{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": {
"defaults": {
"historySize": 1234
},
"list": [
{
"name" : "profile0FromUserSettings",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.0"
},
{
"name" : "profile1FromUserSettings",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.1",
"historySize": 4444
},
{
"name" : "profile2FromUserSettings",
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}",
"historySize": 5555
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userProfiles, dynamicProfiles);
const auto allProfiles = settings->AllProfiles();
Log::Comment(NoThrowString().Format(
L"All profiles with the same name have the same GUID. However, they"
L" will not be layered, because they have different source's"));
VERIFY_ARE_EQUAL(4u, allProfiles.Size());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", allProfiles.GetAt(0).Source());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", allProfiles.GetAt(1).Source());
VERIFY_ARE_EQUAL(L"", allProfiles.GetAt(2).Source());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", allProfiles.GetAt(3).Source());
VERIFY_ARE_EQUAL(guid1, allProfiles.GetAt(0).Guid());
VERIFY_ARE_EQUAL(guid2, allProfiles.GetAt(1).Guid());
VERIFY_ARE_EQUAL(guid3, allProfiles.GetAt(2).Guid());
VERIFY_ARE_EQUAL(guid4, allProfiles.GetAt(3).Guid());
VERIFY_ARE_EQUAL(L"profile0FromUserSettings", allProfiles.GetAt(0).Name());
VERIFY_ARE_EQUAL(L"profile1FromUserSettings", allProfiles.GetAt(1).Name());
VERIFY_ARE_EQUAL(L"profile2FromUserSettings", allProfiles.GetAt(2).Name());
VERIFY_ARE_EQUAL(L"profile2", allProfiles.GetAt(3).Name());
Log::Comment(NoThrowString().Format(
L"This is the real meat of the test: The two dynamic profiles that "
L"_didn't_ have historySize set in the userSettings should have "
L"1234 as their historySize(from the defaultSettings).The other two"
L" profiles should have their custom historySize value."));
VERIFY_ARE_EQUAL(1234, allProfiles.GetAt(0).HistorySize());
VERIFY_ARE_EQUAL(4444, allProfiles.GetAt(1).HistorySize());
VERIFY_ARE_EQUAL(5555, allProfiles.GetAt(2).HistorySize());
VERIFY_ARE_EQUAL(1234, allProfiles.GetAt(3).HistorySize());
}
void DeserializationTests::FindMissingProfile()
{
// Test that CascadiaSettings::FindProfile returns null for a GUID that
// doesn't exist
static constexpr std::string_view settingsString{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
}
]
})" };
const auto settings = createSettings(settingsString);
const auto guid1 = Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
const auto guid2 = Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const auto guid3 = Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}");
const auto profile1 = settings->FindProfile(guid1);
const auto profile2 = settings->FindProfile(guid2);
const auto profile3 = settings->FindProfile(guid3);
VERIFY_IS_NOT_NULL(profile1);
VERIFY_IS_NOT_NULL(profile2);
VERIFY_IS_NULL(profile3);
VERIFY_ARE_EQUAL(L"profile0", profile1.Name());
VERIFY_ARE_EQUAL(L"profile1", profile2.Name());
}
void DeserializationTests::ValidateKeybindingsWarnings()
{
static constexpr std::string_view badSettings{ R"(
{
"defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}"
}
],
"keybindings": [
{ "command": { "action": "splitPane", "split":"auto" }, "keys": [ "ctrl+alt+t", "ctrl+a" ] },
{ "command": { "action": "moveFocus" }, "keys": [ "ctrl+a" ] },
{ "command": { "action": "resizePane" }, "keys": [ "ctrl+b" ] },
{ "name": "invalid nested", "commands":[ { "name" : "hello" }, { "name" : "world" } ] }
]
})" };
const auto settings = createSettings(badSettings);
// KeyMap: ctrl+a/b are mapped to "invalid"
// ActionMap: "splitPane" and "invalid" are the only deserialized actions
// NameMap: "splitPane" has no key binding, but it is still added to the name map
const auto actionMap = winrt::get_self<implementation::ActionMap>(settings->GlobalSettings().ActionMap());
VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size());
VERIFY_ARE_EQUAL(2u, actionMap->_ActionMap.size());
VERIFY_ARE_EQUAL(1u, actionMap->NameMap().Size());
VERIFY_ARE_EQUAL(5u, settings->Warnings().Size());
const auto globalAppSettings = winrt::get_self<implementation::GlobalAppSettings>(settings->GlobalSettings());
const auto& keybindingsWarnings = globalAppSettings->KeybindingsWarnings();
VERIFY_ARE_EQUAL(4u, keybindingsWarnings.size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::TooManyKeysForChord, keybindingsWarnings.at(0));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(1));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(2));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::FailedToParseSubCommands, keybindingsWarnings.at(3));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::TooManyKeysForChord, settings->Warnings().GetAt(1));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(2));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(3));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::FailedToParseSubCommands, settings->Warnings().GetAt(4));
}
void DeserializationTests::ValidateExecuteCommandlineWarning()
{
static constexpr std::string_view badSettings{ R"(
{
"defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}"
}
],
"keybindings": [
{ "name":null, "command": { "action": "wt" }, "keys": [ "ctrl+a" ] },
{ "name":null, "command": { "action": "wt", "commandline":"" }, "keys": [ "ctrl+b" ] },
{ "name":null, "command": { "action": "wt", "commandline":null }, "keys": [ "ctrl+c" ] }
]
})" };
const auto settings = createSettings(badSettings);
const auto actionMap = winrt::get_self<implementation::ActionMap>(settings->GlobalSettings().ActionMap());
VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size());
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('A'), 0 }));
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('B'), 0 }));
VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast<int32_t>('C'), 0 }));
const auto globalAppSettings = winrt::get_self<implementation::GlobalAppSettings>(settings->GlobalSettings());
const auto& keybindingsWarnings = globalAppSettings->KeybindingsWarnings();
VERIFY_ARE_EQUAL(3u, keybindingsWarnings.size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(0));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(1));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(2));
VERIFY_ARE_EQUAL(4u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(1));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(2));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(3));
}
void DeserializationTests::TestTrailingCommas()
{
static constexpr std::string_view badSettings{ R"({
"profiles": [{ "name": "profile0" }],
})" };
try
{
const auto settings = createSettings(badSettings);
}
catch (...)
{
VERIFY_IS_TRUE(false, L"This call to LayerJson should succeed, even with the trailing comma");
}
}
void DeserializationTests::TestCommandsAndKeybindings()
{
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"commandline": "pwsh.exe"
},
{
"name": "profile2",
"historySize": 3,
"commandline": "wsl.exe"
}
],
"actions": [
{ "keys": "ctrl+a", "command": { "action": "splitPane", "split": "vertical" } },
{ "name": "ctrl+b", "command": { "action": "splitPane", "split": "vertical" } },
{ "keys": "ctrl+c", "name": "ctrl+c", "command": { "action": "splitPane", "split": "vertical" } },
{ "keys": "ctrl+d", "command": { "action": "splitPane", "split": "vertical" } },
{ "keys": "ctrl+e", "command": { "action": "splitPane", "split": "horizontal" } },
{ "keys": "ctrl+f", "name":null, "command": { "action": "splitPane", "split": "horizontal" } }
]
})" };
const auto settings = createSettings(settingsJson);
VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size());
const auto profile2Guid = settings->AllProfiles().GetAt(2).Guid();
VERIFY_ARE_NOT_EQUAL(winrt::guid{}, profile2Guid);
auto actionMap = winrt::get_self<implementation::ActionMap>(settings->GlobalSettings().ActionMap());
VERIFY_ARE_EQUAL(5u, actionMap->KeyBindings().Size());
// A/D, B, C, E will be in the list of commands, for 4 total.
// * A and D share the same name, so they'll only generate a single action.
// * F's name is set manually to `null`
const auto& nameMap{ actionMap->NameMap() };
VERIFY_ARE_EQUAL(1u, nameMap.Size());
{
const KeyChord kc{ true, false, false, false, static_cast<int32_t>('A'), 0 };
const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set.");
{
const KeyChord kc{ true, false, false, false, static_cast<int32_t>('C'), 0 };
const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
{
const KeyChord kc{ true, false, false, false, static_cast<int32_t>('D'), 0 };
const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
{
const KeyChord kc{ true, false, false, false, static_cast<int32_t>('E'), 0 };
const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitDirection::Down, realArgs.SplitDirection());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
{
const KeyChord kc{ true, false, false, false, static_cast<int32_t>('F'), 0 };
const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitDirection::Down, realArgs.SplitDirection());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
Log::Comment(L"Now verify the commands");
_logCommandNames(nameMap);
{
// This was renamed to "ctrl+c" in C. So this does not exist.
const auto command = nameMap.TryLookup(L"Split pane, split: vertical");
VERIFY_IS_NULL(command);
}
{
// This was renamed to "ctrl+c" in C. So this does not exist.
const auto command = nameMap.TryLookup(L"ctrl+b");
VERIFY_IS_NULL(command);
}
{
const auto command = nameMap.TryLookup(L"ctrl+c");
VERIFY_IS_NOT_NULL(command);
const auto actionAndArgs = command.ActionAndArgs();
VERIFY_IS_NOT_NULL(actionAndArgs);
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<SplitPaneArgs>();
VERIFY_IS_NOT_NULL(realArgs);
// Verify the args have the expected value
VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection());
VERIFY_IS_NOT_NULL(realArgs.TerminalArgs());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty());
VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty());
}
{
// This was renamed to null (aka removed from the name map) in F. So this does not exist.
const auto command = nameMap.TryLookup(L"Split pane, split: horizontal");
VERIFY_IS_NULL(command);
}
}
void DeserializationTests::TestNestedCommandWithoutName()
{
// This test tests a nested command without a name specified. This type
// of command should just be ignored, since we can't auto-generate names
// for nested commands, they _must_ have names specified.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"commandline": "pwsh.exe"
},
{
"name": "profile2",
"historySize": 3,
"commandline": "wsl.exe"
}
],
"actions": [
{
"commands": [
{
"name": "child1",
"command": { "action": "newTab", "commandline": "ssh me@first.com" }
},
{
"name": "child2",
"command": { "action": "newTab", "commandline": "ssh me@second.com" }
}
]
},
]
})" };
const auto settings = createSettings(settingsJson);
VERIFY_ARE_EQUAL(0u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size());
// Because the "parent" command didn't have a name, it couldn't be
// placed into the list of commands. It and it's children are just
// ignored.
VERIFY_ARE_EQUAL(0u, settings->ActionMap().NameMap().Size());
}
void DeserializationTests::TestNestedCommandWithBadSubCommands()
{
// This test tests a nested command without a name specified. This type
// of command should just be ignored, since we can't auto-generate names
// for nested commands, they _must_ have names specified.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
}
],
"actions": [
{
"name": "nested command",
"commands": [
{
"name": "child1"
},
{
"name": "child2"
}
]
},
]
})" };
const auto settings = createSettings(settingsJson);
VERIFY_ARE_EQUAL(2u, settings->Warnings().Size());
VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->Warnings().GetAt(0));
VERIFY_ARE_EQUAL(SettingsLoadWarnings::FailedToParseSubCommands, settings->Warnings().GetAt(1));
const auto& nameMap{ settings->ActionMap().NameMap() };
VERIFY_ARE_EQUAL(0u, nameMap.Size());
}
void DeserializationTests::TestUnbindNestedCommand()
{
// Test that layering a command with `"commands": null` set will unbind a command that already exists.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"commandline": "pwsh.exe"
},
{
"name": "profile2",
"historySize": 3,
"commandline": "wsl.exe"
}
],
"actions": [
{
"name": "parent",
"commands": [
{
"name": "child1",
"command": { "action": "newTab", "commandline": "ssh me@first.com" }
},
{
"name": "child2",
"command": { "action": "newTab", "commandline": "ssh me@second.com" }
}
]
},
]
})" };
static constexpr std::string_view settings1Json{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"actions": [
{
"name": "parent",
"commands": null
},
],
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settings1Json, settingsJson);
VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size());
VERIFY_ARE_EQUAL(0u, settings->ActionMap().NameMap().Size());
}
void DeserializationTests::TestRebindNestedCommand()
{
// Test that layering a command with an action set on top of a command
// with nested commands replaces the nested commands with an action.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"commandline": "pwsh.exe"
},
{
"name": "profile2",
"historySize": 3,
"commandline": "wsl.exe"
}
],
"actions": [
{
"name": "parent",
"commands": [
{
"name": "child1",
"command": { "action": "newTab", "commandline": "ssh me@first.com" }
},
{
"name": "child2",
"command": { "action": "newTab", "commandline": "ssh me@second.com" }
}
]
},
]
})" };
static constexpr std::string_view settings1Json{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"actions": [
{
"name": "parent",
"command": "newTab"
},
],
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settings1Json, settingsJson);
const auto nameMap = settings->ActionMap().NameMap();
VERIFY_ARE_EQUAL(1u, nameMap.Size());
{
const winrt::hstring commandName{ L"parent" };
const auto commandProj = nameMap.TryLookup(commandName);
VERIFY_IS_NOT_NULL(commandProj);
const auto actionAndArgs = commandProj.ActionAndArgs();
VERIFY_IS_NOT_NULL(actionAndArgs);
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action());
const auto& realArgs = actionAndArgs.Args().try_as<NewTabArgs>();
VERIFY_IS_NOT_NULL(realArgs);
winrt::com_ptr<implementation::Command> commandImpl;
commandImpl.copy_from(winrt::get_self<implementation::Command>(commandProj));
VERIFY_IS_FALSE(commandImpl->HasNestedCommands());
}
}
void DeserializationTests::TestCopy()
{
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"initialCols": 50,
"profiles":
[
{
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"name": "Custom Profile",
"fontFace": "Cascadia Code"
}
],
"schemes":
[
{
"name": "Campbell, but for a test",
"foreground": "#CCCCCC",
"background": "#0C0C0C",
"cursorColor": "#FFFFFF",
"black": "#0C0C0C",
"red": "#C50F1F",
"green": "#13A10E",
"yellow": "#C19C00",
"blue": "#0037DA",
"purple": "#881798",
"cyan": "#3A96DD",
"white": "#CCCCCC",
"brightBlack": "#767676",
"brightRed": "#E74856",
"brightGreen": "#16C60C",
"brightYellow": "#F9F1A5",
"brightBlue": "#3B78FF",
"brightPurple": "#B4009E",
"brightCyan": "#61D6D6",
"brightWhite": "#F2F2F2"
}
],
"actions":
[
{ "command": "openSettings", "keys": "ctrl+," },
{ "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," },
{
"name": { "key": "SetColorSchemeParentCommandName" },
"commands": [
{
"iterateOn": "schemes",
"name": "${scheme.name}",
"command": { "action": "setColorScheme", "colorScheme": "${scheme.name}" }
}
]
}
]
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
const auto copy{ settings->Copy() };
const auto copyImpl{ winrt::get_self<implementation::CascadiaSettings>(copy) };
// test globals
VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), copyImpl->GlobalSettings().DefaultProfile());
// test profiles
VERIFY_ARE_EQUAL(settings->AllProfiles().Size(), copyImpl->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Name(), copyImpl->AllProfiles().GetAt(0).Name());
// test schemes
const auto schemeName{ L"Campbell, but for a test" };
VERIFY_ARE_EQUAL(settings->GlobalSettings().ColorSchemes().Size(), copyImpl->GlobalSettings().ColorSchemes().Size());
VERIFY_ARE_EQUAL(settings->GlobalSettings().ColorSchemes().HasKey(schemeName), copyImpl->GlobalSettings().ColorSchemes().HasKey(schemeName));
// test actions
VERIFY_ARE_EQUAL(settings->GlobalSettings().ActionMap().KeyBindings().Size(), copyImpl->GlobalSettings().ActionMap().KeyBindings().Size());
const auto& nameMapOriginal{ settings->GlobalSettings().ActionMap().NameMap() };
const auto& nameMapCopy{ copyImpl->GlobalSettings().ActionMap().NameMap() };
VERIFY_ARE_EQUAL(nameMapOriginal.Size(), nameMapCopy.Size());
// Test that changing the copy should not change the original
VERIFY_ARE_EQUAL(settings->GlobalSettings().WordDelimiters(), copyImpl->GlobalSettings().WordDelimiters());
copyImpl->GlobalSettings().WordDelimiters(L"changed value");
VERIFY_ARE_NOT_EQUAL(settings->GlobalSettings().WordDelimiters(), copyImpl->GlobalSettings().WordDelimiters());
}
void DeserializationTests::TestCloneInheritanceTree()
{
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}",
"profiles":
{
"defaults": {
"name": "PROFILE DEFAULTS"
},
"list": [
{
"guid": "{61c54bbd-1111-5271-96e7-009a87ff44bf}",
"name": "CMD"
},
{
"guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}",
"name": "PowerShell"
},
{
"guid": "{61c54bbd-3333-5271-96e7-009a87ff44bf}"
}
]
}
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
const auto copy{ settings->Copy() };
const auto copyImpl{ winrt::get_self<implementation::CascadiaSettings>(copy) };
// test globals
VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), copyImpl->GlobalSettings().DefaultProfile());
// test profiles
VERIFY_ARE_EQUAL(settings->AllProfiles().Size(), copyImpl->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Name(), copyImpl->AllProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(1).Name(), copyImpl->AllProfiles().GetAt(1).Name());
VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(2).Name(), copyImpl->AllProfiles().GetAt(2).Name());
VERIFY_ARE_EQUAL(settings->ProfileDefaults().Name(), copyImpl->ProfileDefaults().Name());
// Modifying profile.defaults should...
VERIFY_ARE_EQUAL(settings->ProfileDefaults().HasName(), copyImpl->ProfileDefaults().HasName());
copyImpl->ProfileDefaults().Name(L"changed value");
// ...keep the same name for the first two profiles
VERIFY_ARE_EQUAL(settings->AllProfiles().Size(), copyImpl->AllProfiles().Size());
VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Name(), copyImpl->AllProfiles().GetAt(0).Name());
VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(1).Name(), copyImpl->AllProfiles().GetAt(1).Name());
// ...but change the name for the one that inherited it from profile.defaults
VERIFY_ARE_NOT_EQUAL(settings->AllProfiles().GetAt(2).Name(), copyImpl->AllProfiles().GetAt(2).Name());
// profile.defaults should be different between the two graphs
VERIFY_ARE_EQUAL(settings->ProfileDefaults().HasName(), copyImpl->ProfileDefaults().HasName());
VERIFY_ARE_NOT_EQUAL(settings->ProfileDefaults().Name(), copyImpl->ProfileDefaults().Name());
Log::Comment(L"Test empty profiles.defaults");
static constexpr std::string_view emptyPDJson{ R"(
{
"defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}",
"profiles":
{
"defaults": {
},
"list": [
{
"guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}",
"name": "PowerShell"
}
]
}
})" };
static constexpr std::string_view missingPDJson{ R"(
{
"defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}",
"profiles":
[
{
"guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}",
"name": "PowerShell"
}
]
})" };
auto verifyEmptyPD = [this](const auto json) {
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(json) };
const auto copy{ settings->Copy() };
const auto copyImpl{ winrt::get_self<implementation::CascadiaSettings>(copy) };
// if we don't have profiles.defaults, it should still be in the tree
VERIFY_IS_NOT_NULL(settings->ProfileDefaults());
VERIFY_IS_NOT_NULL(copyImpl->ProfileDefaults());
VERIFY_ARE_EQUAL(settings->ActiveProfiles().Size(), 1u);
VERIFY_ARE_EQUAL(settings->ActiveProfiles().Size(), copyImpl->ActiveProfiles().Size());
// so we should only have one parent, instead of two
const auto srcProfile{ winrt::get_self<implementation::Profile>(settings->ActiveProfiles().GetAt(0)) };
const auto copyProfile{ winrt::get_self<implementation::Profile>(copyImpl->ActiveProfiles().GetAt(0)) };
VERIFY_ARE_EQUAL(srcProfile->Parents().size(), 1u);
VERIFY_ARE_EQUAL(srcProfile->Parents().size(), copyProfile->Parents().size());
};
verifyEmptyPD(emptyPDJson);
verifyEmptyPD(missingPDJson);
}
void DeserializationTests::TestValidDefaults()
{
// GH#8146: A LoadDefaults call should populate the list of active profiles
const auto settings{ CascadiaSettings::LoadDefaults() };
VERIFY_ARE_EQUAL(settings.ActiveProfiles().Size(), settings.AllProfiles().Size());
VERIFY_ARE_EQUAL(settings.AllProfiles().Size(), 2u);
}
void DeserializationTests::TestInheritedCommand()
{
// Test unbinding a command's key chord or name that originated in another layer.
static constexpr std::string_view settings1Json{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"commandline": "cmd.exe"
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"commandline": "pwsh.exe"
},
{
"name": "profile2",
"historySize": 3,
"commandline": "wsl.exe"
}
],
"actions": [
{
"name": "foo",
"command": "closePane",
"keys": "ctrl+shift+w"
}
]
})" };
static constexpr std::string_view settings2Json{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"actions": [
{
"command": null,
"keys": "ctrl+shift+w"
},
{
"name": "bar",
"command": "closePane"
},
],
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settings2Json, settings1Json);
const KeyChord expectedKeyChord{ true, false, true, false, static_cast<int>('W'), 0 };
const auto nameMap = settings->ActionMap().NameMap();
VERIFY_ARE_EQUAL(1u, nameMap.Size());
{
// Verify NameMap returns correct value
const auto& cmd{ nameMap.TryLookup(L"bar") };
VERIFY_IS_NOT_NULL(cmd);
VERIFY_IS_NULL(cmd.Keys());
VERIFY_ARE_EQUAL(L"bar", cmd.Name());
}
{
// Verify ActionMap::GetActionByKeyChord API
const auto& cmd{ settings->ActionMap().GetActionByKeyChord(expectedKeyChord) };
VERIFY_IS_NULL(cmd);
}
{
// Verify ActionMap::GetKeyBindingForAction API
const auto& actualKeyChord{ settings->ActionMap().GetKeyBindingForAction(ShortcutAction::ClosePane) };
VERIFY_IS_NULL(actualKeyChord);
}
}
// This test ensures GH#11597 doesn't regress.
void DeserializationTests::LoadFragmentsWithMultipleUpdates()
{
static constexpr std::wstring_view fragmentSource{ L"fragment" };
static constexpr std::string_view fragmentJson{ R"({
"profiles": [
{
"updates": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"cursorShape": "filledBox"
},
{
"updates": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"cursorShape": "filledBox"
},
{
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"commandline": "cmd.exe"
}
]
})" };
implementation::SettingsLoader loader{ std::string_view{}, DefaultJson };
loader.MergeInboxIntoUserSettings();
loader.MergeFragmentIntoUserSettings(winrt::hstring{ fragmentSource }, fragmentJson);
loader.FinalizeLayering();
VERIFY_IS_FALSE(loader.duplicateProfile);
VERIFY_ARE_EQUAL(3u, loader.userSettings.profiles.size());
}
}