Switch to v5 UUIDs as profile GUIDs for the default profiles (#913)

This commit switches the GUIDs for default profiles from being randomly generated to being version 5 UUIDs. More info in #870.

## PR Checklist
* [x] Closes #870
* [x] CLA signed
* [x] Tests added/passed
* [x] Requires documentation to be updated (#883)
* [x] I've discussed this with core contributors already.

## Detailed Description of the Pull Request / Additional comments
This commit has a number of changes that seem ancillary, but they're general goodness. Let me explain:

* I've added a whole new Types test library with only two tests in
* Since UUIDv5 generation requires SHA1, we needed to take a dependency on bcrypt
* I honestly don't think we should have to link bcrypt in conhost, but LTO should take care of that
  * I considered adding a new Terminal-specific Utils/Types library, but that seemed like a waste
* The best way to link bcrypt turned out to be in line with a discussion @miniksa and I had, where we decided we both love APISets and think that the console should link against them exclusively... so I've added `onecore_apiset.lib` to the front of the link line, where it will deflect the linker away from most of the other libs automagically.

```
StartGroup: UuidTests::TestV5UuidU8String
Verify: AreEqual(uuidExpected, uuidActual)
EndGroup: UuidTests::TestV5UuidU8String [Passed]

StartGroup: UuidTests::TestV5UuidU16String
Verify: AreEqual(uuidExpected, uuidActual)
EndGroup: UuidTests::TestV5UuidU16String [Passed]
```
This commit is contained in:
Dustin L. Howett (MSFT) 2019-05-21 13:29:16 -07:00 коммит произвёл GitHub
Родитель fd2fb07bcf
Коммит 8da6737d64
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 285 добавлений и 13 удалений

Просмотреть файл

@ -228,6 +228,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Buffer", "Buffer", "{1E4A06
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{89CDCC5C-9F53-4054-97A4-639D99F169CD}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Types.Unit.Tests", "src\types\ut_types\Types.Unit.Tests.vcxproj", "{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
AuditMode|ARM64 = AuditMode|ARM64
@ -1099,6 +1101,24 @@ Global
{EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x64.Build.0 = Release|x64
{EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x86.ActiveCfg = Release|Win32
{EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x86.Build.0 = Release|Win32
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|ARM64.Build.0 = AuditMode|ARM64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|x64.ActiveCfg = AuditMode|x64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|x64.Build.0 = AuditMode|x64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|x86.ActiveCfg = AuditMode|Win32
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.AuditMode|x86.Build.0 = AuditMode|Win32
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|ARM64.ActiveCfg = Debug|ARM64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|ARM64.Build.0 = Debug|ARM64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|x64.ActiveCfg = Debug|x64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|x64.Build.0 = Debug|x64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|x86.ActiveCfg = Debug|Win32
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Debug|x86.Build.0 = Debug|Win32
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|ARM64.ActiveCfg = Release|ARM64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|ARM64.Build.0 = Release|ARM64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x64.ActiveCfg = Release|x64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x64.Build.0 = Release|x64
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x86.ActiveCfg = Release|Win32
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1157,6 +1177,7 @@ Global
{F1995847-4AE5-479A-BBAF-382E51A63532} = {89CDCC5C-9F53-4054-97A4-639D99F169CD}
{05500DEF-2294-41E3-AF9A-24E580B82836} = {89CDCC5C-9F53-4054-97A4-639D99F169CD}
{1E4A062E-293B-4817-B20D-BF16B979E350} = {89CDCC5C-9F53-4054-97A4-639D99F169CD}
{34DE34D3-1CD6-4EE3-8BD9-A26B5B27EC73} = {89CDCC5C-9F53-4054-97A4-639D99F169CD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271}

Просмотреть файл

@ -13,6 +13,11 @@ using namespace ::TerminalApp;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace winrt::TerminalApp;
// {2bde4a90-d05f-401c-9492-e40884ead1d8}
// uuidv5 properties: name format is UTF-16LE bytes
static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID =
{ 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } };
CascadiaSettings::CascadiaSettings() :
_globals{},
_profiles{}
@ -182,16 +187,15 @@ void CascadiaSettings::_CreateDefaultSchemes()
// - <none>
void CascadiaSettings::_CreateDefaultProfiles()
{
Profile cmdProfile{};
auto cmdProfile{ _CreateDefaultProfile(L"cmd") };
cmdProfile.SetFontFace(L"Consolas");
cmdProfile.SetCommandline(L"cmd.exe");
cmdProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
cmdProfile.SetColorScheme({ L"Campbell" });
cmdProfile.SetAcrylicOpacity(0.75);
cmdProfile.SetUseAcrylic(true);
cmdProfile.SetName(L"cmd");
Profile powershellProfile{};
auto powershellProfile{ _CreateDefaultProfile(L"PowerShell") };
// If the user has installed PowerShell Core, we add PowerShell Core as a default.
// PowerShell Core default folder is "%PROGRAMFILES%\PowerShell\[Version]\".
std::wstring psCmdline = L"powershell.exe";
@ -210,7 +214,6 @@ void CascadiaSettings::_CreateDefaultProfiles()
powershellProfile.SetColorScheme({ L"Campbell" });
powershellProfile.SetDefaultBackground(RGB(1, 36, 86));
powershellProfile.SetUseAcrylic(false);
powershellProfile.SetName(L"PowerShell");
_profiles.emplace_back(powershellProfile);
_profiles.emplace_back(cmdProfile);
@ -462,3 +465,24 @@ std::wstring CascadiaSettings::ExpandEnvironmentVariableString(std::wstring_view
result.resize(requiredSize-1);
return result;
}
// Method Description:
// - Helper function for creating a skeleton default profile with a pre-populated
// guid and name.
// Arguments:
// - name: the name of the new profile.
// Return Value:
// - A Profile, ready to be filled in
Profile CascadiaSettings::_CreateDefaultProfile(const std::wstring_view& name)
{
Profile newProfile{
Microsoft::Console::Utils::CreateV5Uuid(
TERMINAL_PROFILE_NAMESPACE_GUID,
gsl::as_bytes(gsl::make_span(name))
)
};
newProfile.SetName(static_cast<std::wstring>(name));
return newProfile;
}

Просмотреть файл

@ -69,4 +69,5 @@ private:
static std::optional<winrt::hstring> _LoadAsUnpackagedApp();
static bool _IsPowerShellCoreInstalled(std::wstring_view programFileEnv, std::filesystem::path& cmdline);
static std::wstring ExpandEnvironmentVariableString(std::wstring_view source);
static Profile _CreateDefaultProfile(const std::wstring_view& name);
};

Просмотреть файл

@ -49,7 +49,12 @@ static const std::wstring CURSORSHAPE_FILLEDBOX{ L"filledBox" };
static const std::wstring CURSORSHAPE_EMPTYBOX{ L"emptyBox" };
Profile::Profile() :
_guid{},
Profile(Utils::CreateGuid())
{
}
Profile::Profile(const winrt::guid& guid):
_guid(guid),
_name{ L"Default" },
_schemeName{},
@ -73,7 +78,6 @@ Profile::Profile() :
_padding{ DEFAULT_PADDING },
_icon{ }
{
UuidCreate(&_guid);
}
Profile::~Profile()

Просмотреть файл

@ -25,7 +25,9 @@ class TerminalApp::Profile final
{
public:
Profile(const winrt::guid& guid);
Profile();
~Profile();
winrt::Microsoft::Terminal::Settings::TerminalSettings CreateTerminalSettings(const std::vector<::TerminalApp::ColorScheme>& schemes) const;

Просмотреть файл

@ -6,7 +6,7 @@
</ClCompile>
<Link>
<ProgramDatabaseFile>$(OutDir)$(TargetName)FullPDB.pdb</ProgramDatabaseFile>
<AdditionalDependencies>dwrite.lib;dxgi.lib;d2d1.lib;d3d11.lib;shcore.lib;uxtheme.lib;dwmapi.lib;winmm.lib;pathcch.lib;propsys.lib;uiautomationcore.lib;Shlwapi.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>onecore_apiset.lib;dwrite.lib;dxgi.lib;d2d1.lib;d3d11.lib;shcore.lib;uxtheme.lib;dwmapi.lib;winmm.lib;pathcch.lib;propsys.lib;uiautomationcore.lib;Shlwapi.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
<!--
There's a property that dictates which libraries are linked by default: MinimalCoreWin.
When it's enabled, only a sparing few libraries are injected into Link.AdditionalDependencies.

Просмотреть файл

@ -7,5 +7,6 @@
{ "FilePath": "Microsoft.Console.VirtualTerminal.Adapter.UnitTests.testmd" },
{ "FilePath": "Microsoft.Console.VirtualTerminal.Parser.UnitTests.testmd" },
{ "FilePath": "Microsoft.Console.Conpty.UnitTests.testmd" },
{ "FilePath": "Microsoft.Console.Types.UnitTests.testmd" },
]
}

Просмотреть файл

@ -1,3 +1,3 @@
DIRS=lib \
ut_types \

Просмотреть файл

@ -17,6 +17,7 @@ namespace Microsoft::Console::Utils
std::wstring GuidToString(const GUID guid);
GUID GuidFromString(const std::wstring wstr);
GUID CreateGuid();
std::wstring ColorToHexString(const COLORREF color);
COLORREF ColorFromHexString(const std::wstring wstr);
@ -24,4 +25,33 @@ namespace Microsoft::Console::Utils
void InitializeCampbellColorTable(gsl::span<COLORREF>& table);
void Initialize256ColorTable(gsl::span<COLORREF>& table);
void SetColorTableAlpha(gsl::span<COLORREF>& table, const BYTE newAlpha);
constexpr uint16_t EndianSwap(uint16_t value)
{
return (value & 0xFF00) >> 8 |
(value & 0x00FF) << 8;
}
constexpr uint32_t EndianSwap(uint32_t value)
{
return (value & 0xFF000000) >> 24 |
(value & 0x00FF0000) >> 8 |
(value & 0x0000FF00) << 8 |
(value & 0x000000FF) << 24;
}
constexpr unsigned long EndianSwap(unsigned long value)
{
return static_cast<unsigned long>(EndianSwap(static_cast<uint32_t>(value)));
}
constexpr GUID EndianSwap(GUID value)
{
value.Data1 = EndianSwap(value.Data1);
value.Data2 = EndianSwap(value.Data2);
value.Data3 = EndianSwap(value.Data3);
return value;
}
GUID CreateV5Uuid(const GUID& namespaceGuid, const gsl::span<const gsl::byte>& name);
}

Просмотреть файл

@ -17,15 +17,21 @@ Abstract:
#pragma once
// This includes support libraries from the CRT, STL, WIL, and GSL
#include "LibraryIncludes.h"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
// Windows Header Files:
#include <windows.h>
#include <objbase.h>
#include <bcrypt.h>
// This includes support libraries from the CRT, STL, WIL, and GSL
#include "LibraryIncludes.h"
typedef long NTSTATUS;
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

Просмотреть файл

@ -0,0 +1,12 @@
//Autogenerated file name + version resource file for Device Guard whitelisting effort
#include <windows.h>
#include <ntverp.h>
#define VER_FILETYPE VFT_UNKNOWN
#define VER_FILESUBTYPE VFT2_UNKNOWN
#define VER_FILEDESCRIPTION_STR ___TARGETNAME
#define VER_INTERNALNAME_STR ___TARGETNAME
#define VER_ORIGINALFILENAME_STR ___TARGETNAME
#include "common.ver"

Просмотреть файл

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<ItemGroup>
<ClCompile Include="UuidTests.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\lib\types.vcxproj">
<Project>{18d09a24-8240-42d6-8cb6-236eee820263}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h" />
</ItemGroup>
<PropertyGroup>
<ProjectGuid>{34de34d3-1cd6-4ee3-8bd9-a26b5b27ec73}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>TypesUnitTests</RootNamespace>
<ProjectName>Types.Unit.Tests</ProjectName>
<TargetName>Types.Unit.Tests</TargetName>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
<Import Project="$(SolutionDir)src\common.build.dll.props" />
<Import Project="$(SolutionDir)src\common.build.post.props" />
<Import Project="$(SolutionDir)src\common.build.tests.props" />
</Project>

Просмотреть файл

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "WexTestClass.h"
#include "..\..\inc\consoletaeftemplates.hpp"
#include "..\inc\utils.hpp"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace Microsoft::Console::Utils;
class UuidTests
{
// {AD56DE9E-5167-41B6-80EB-FB19F7927D1A}
static constexpr GUID TEST_NAMESPACE_GUID{ 0xad56de9e, 0x5167, 0x41b6,
{ 0x80, 0xeb, 0xfb, 0x19, 0xf7, 0x92, 0x7d, 0x1a } };
TEST_CLASS(UuidTests);
TEST_METHOD(TestV5UuidU8String)
{
const GUID uuidExpected{ 0x8b9d4336, 0x0c82, 0x54c4,
{ 0xb3, 0x15, 0xf1, 0xd2, 0xd2, 0x7e, 0xc6, 0xda} };
std::string name{ "testing" };
auto uuidActual = CreateV5Uuid(TEST_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name)));
VERIFY_ARE_EQUAL(uuidExpected, uuidActual);
}
TEST_METHOD(TestV5UuidU16String)
{
const GUID uuidExpected{ 0xe04fb1f7, 0x739d, 0x5d63,
{ 0xbb, 0x18, 0xe0, 0xea, 0x00, 0xb1, 0x9e, 0xe8 } };
// This'll come out in little endian; the reference GUID was generated as such.
std::wstring name{ L"testing" };
auto uuidActual = CreateV5Uuid(TEST_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name)));
VERIFY_ARE_EQUAL(uuidExpected, uuidActual);
}
};

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="ProductBuild" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(NTMAKEENV)\UniversalTest\Microsoft.TestInfrastructure.UniversalTest.props" />
</Project>

Просмотреть файл

@ -0,0 +1,34 @@
!include ..\..\project.unittest.inc
# -------------------------------------
# Program Information
# -------------------------------------
TARGETNAME = Microsoft.Console.Types.UnitTests
TARGETTYPE = DYNLINK
DLLDEF =
# -------------------------------------
# Sources, Headers, and Libraries
# -------------------------------------
SOURCES = \
$(SOURCES) \
UuidTests.cpp \
DefaultResource.rc \
INCLUDES = \
.. \
$(INCLUDES) \
TARGETLIBS = \
$(WINCORE_OBJ_PATH)\console\open\src\types\lib\$(O)\ConTypes.lib \
$(TARGETLIBS) \
# -------------------------------------
# Localization
# -------------------------------------
# Autogenerated. Sets file name for Device Guard whitelisting effort, used in RC.exe.
C_DEFINES = $(C_DEFINES) -D___TARGETNAME="""$(TARGETNAME).$(TARGETTYPE)"""
MUI_VERIFY_NO_LOC_RESOURCE = 1

Просмотреть файл

@ -3,7 +3,7 @@
#include "precomp.h"
#include "inc/utils.hpp"
#include <Objbase.h>
using namespace Microsoft::Console;
// Function Description:
@ -43,6 +43,17 @@ GUID Utils::GuidFromString(const std::wstring wstr)
return result;
}
// Method Description:
// - Creates a GUID, but not via an out parameter.
// Return Value:
// - A GUID if there's enough randomness; otherwise, an exception.
GUID Utils::CreateGuid()
{
GUID result{};
THROW_IF_FAILED(::CoCreateGuid(&result));
return result;
}
// Function Description:
// - Creates a String representation of a color, in the format "#RRGGBB"
// Arguments:
@ -406,3 +417,43 @@ void Utils::SetColorTableAlpha(gsl::span<COLORREF>& table, const BYTE newAlpha)
WI_UpdateFlagsInMask(color, 0xff000000, shiftedAlpha);
}
}
// Function Description:
// - Generate a Version 5 UUID (specified in RFC4122 4.3)
// v5 UUIDs are stable given the same namespace and "name".
// Arguments:
// - namespaceGuid: The GUID of the v5 UUID namespace, which provides both
// a seed and a tacit agreement that all UUIDs generated
// with it will follow the same data format.
// - name: Bytes comprising the name (in a namespace-specific format)
// Return Value:
// - a new stable v5 UUID
GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const gsl::span<const gsl::byte>& name)
{
// v5 uuid generation happens over values in network byte order, so let's enforce that
auto correctEndianNamespaceGuid{ EndianSwap(namespaceGuid) };
wil::unique_bcrypt_hash hash;
THROW_IF_NTSTATUS_FAILED(BCryptCreateHash(BCRYPT_SHA1_ALG_HANDLE, &hash, nullptr, 0, nullptr, 0, 0));
// According to N4713 8.2.1.11 [basic.lval], accessing the bytes underlying an object
// through unsigned char or char pointer *is defined*.
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast<PUCHAR>(&correctEndianNamespaceGuid), sizeof(GUID), 0));
// BCryptHashData is ill-specified in that it leaves off "const" qualification for pbInput
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast<PUCHAR>(const_cast<gsl::byte*>(name.data())), gsl::narrow<ULONG>(name.size()), 0));
std::array<uint8_t, 20> buffer;
THROW_IF_NTSTATUS_FAILED(BCryptFinishHash(hash.get(), buffer.data(), gsl::narrow<ULONG>(buffer.size()), 0));
buffer[6] = (buffer[6] & 0x0F) | 0x50; // set the uuid version to 5
buffer[8] = (buffer[8] & 0x3F) | 0x80; // set the variant to 2 (RFC4122)
// We're using memcpy here pursuant to N4713 6.7.2/3 [basic.types],
// "...the underlying bytes making up the object can be copied into an array
// of char or unsigned char...array is copied back into the object..."
// std::copy may compile down to ::memcpy for these types, but using it might
// contravene the standard and nobody's got time for that.
GUID newGuid{ 0 };
::memcpy_s(&newGuid, sizeof(GUID), buffer.data(), sizeof(GUID));
return EndianSwap(newGuid);
}

Просмотреть файл

@ -91,7 +91,7 @@ function Invoke-OpenConsoleTests()
[switch]$FTOnly,
[parameter(Mandatory=$false)]
[ValidateSet('host', 'interactivityWin32', 'terminal', 'adapter', 'feature', 'uia', 'textbuffer')]
[ValidateSet('host', 'interactivityWin32', 'terminal', 'adapter', 'feature', 'uia', 'textbuffer', 'types')]
[string]$Test,
[parameter(Mandatory=$false)]

Просмотреть файл

@ -9,4 +9,5 @@ call %TAEF% ^
%OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\Conhost.Interactivity.Win32.Unit.Tests.dll ^
%OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\ConParser.Unit.Tests.dll ^
%OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\ConAdapter.Unit.Tests.dll ^
%OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\Types.Unit.Tests.dll ^
%*

Просмотреть файл

@ -5,6 +5,7 @@
<test name="interactivityWin32" type="unit" binary="Conhost.Interactivity.Win32.Unit.Tests.dll" />
<test name="terminal" type="unit" binary="ConParser.Unit.Tests.dll" />
<test name="adapter" type="unit" binary="ConAdapter.Unit.Tests.dll" />
<test name="types" type="unit" binary="Types.Unit.Tests.dll" />
<test name="feature" type="ft" binary="Conhost.Feature.Tests.dll" />
<test name="uia" type="ft" binary="Conhost.UIA.Tests.dll" />
</tests>