mirror of
https://github.com/microsoft/terminal.git
synced 2026-07-08 17:56:28 +00:00
This upgrades to [check-spelling v0.0.24]. A number of GitHub APIs are being turned off shortly, so we need to upgrade or various uncertain outcomes will occur. There are some minor bugs that I'm aware of and which I've fixed since this release (including a couple I discovered while preparing this PR). There's a new accessibility forbidden pattern: #### Should be `cannot` (or `can't`) See https://www.grammarly.com/blog/cannot-or-can-not/ > Don't use `can not` when you mean `cannot`. The only time you're likely to see `can not` written as separate words is when the word `can` happens to precede some other phrase that happens to start with `not`. > `Can't` is a contraction of `cannot`, and it's best suited for informal writing. > In formal writing and where contractions are frowned upon, use `cannot`. > It is possible to write `can not`, but you generally find it only as part of some other construction, such as `not only . . . but also.` - if you encounter such a case, add a pattern for that case to patterns.txt. ``` \b[Cc]an not\b ``` [check-spelling v0.0.24]: https://github.com/check-spelling/check-spelling/releases/tag/v0.0.24 Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
1791 lines
75 KiB
C++
1791 lines
75 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include <io.h>
|
|
#include <fcntl.h>
|
|
|
|
#include "../../types/inc/IInputEvent.hpp"
|
|
|
|
#define JAPANESE_CP 932u
|
|
|
|
// CHAR_INFO's .Char member is a union of a wchar_t UnicodeChar and char AsciiChar.
|
|
// If they share the same offsetof we can write the lower byte of the former to
|
|
// overwrite the latter, while ensuring that the high byte is properly cleared to 0.
|
|
static_assert(offsetof(CHAR_INFO, Char.UnicodeChar) == offsetof(CHAR_INFO, Char.AsciiChar));
|
|
|
|
template<typename T>
|
|
constexpr CHAR_INFO makeCharInfo(T ch, WORD attr)
|
|
{
|
|
CHAR_INFO info{};
|
|
// If T is a char, it'll be a signed integer, whereas UnicodeChar is an unsigned one.
|
|
// A negative char like -1 would then result in a wchar_t of 0xffff instead of the expected 0xff.
|
|
// Casting ch to a unsigned integer first prevents such "sign extension".
|
|
info.Char.UnicodeChar = static_cast<WCHAR>(til::as_unsigned(ch));
|
|
info.Attributes = attr;
|
|
return info;
|
|
}
|
|
|
|
using CharInfoPattern = std::array<CHAR_INFO, 16>;
|
|
|
|
// These two are the same strings but in different encodings.
|
|
// Both strings are exactly 16 "cells" wide which matches the size of CharInfoPattern.
|
|
static constexpr std::string_view dbcsInput{ "Q\x82\xA2\x82\xa9\x82\xc8ZYXWVUT\x82\xc9" }; // Shift-JIS (Codepage 932)
|
|
static constexpr std::wstring_view unicodeInput{ L"QいかなZYXWVUTに" }; // Regular UTF-16
|
|
|
|
using namespace WEX::Logging;
|
|
using WEX::TestExecution::TestData;
|
|
using namespace WEX::Common;
|
|
|
|
namespace DbcsWriteRead
|
|
{
|
|
enum WriteMode
|
|
{
|
|
CrtWrite = 0,
|
|
WriteConsoleOutputFunc = 1,
|
|
WriteConsoleOutputCharacterFunc = 2,
|
|
WriteConsoleFunc = 3
|
|
};
|
|
|
|
enum ReadMode
|
|
{
|
|
ReadConsoleOutputFunc = 0,
|
|
ReadConsoleOutputCharacterFunc = 1
|
|
};
|
|
|
|
enum UnicodeMode
|
|
{
|
|
Ascii = 0,
|
|
UnicodeSingle,
|
|
UnicodeDoubled,
|
|
};
|
|
|
|
void TestRunner(_In_opt_ WORD* const pwAttrOverride,
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const UnicodeMode fWriteInUnicode,
|
|
const DbcsWriteRead::ReadMode ReadMode,
|
|
const bool fReadWithUnicode);
|
|
|
|
bool Setup(_Out_ HANDLE* const phOut,
|
|
_Out_ WORD* const pwAttributes);
|
|
|
|
void SendOutput(const HANDLE hOut,
|
|
const WriteMode WriteMode,
|
|
const UnicodeMode fIsUnicode,
|
|
const WORD wAttr);
|
|
|
|
void RetrieveOutput(const HANDLE hOut,
|
|
const DbcsWriteRead::ReadMode ReadMode,
|
|
const bool fReadUnicode,
|
|
CharInfoPattern& rgChars);
|
|
|
|
void Verify(std::span<CHAR_INFO> rgExpected, std::span<CHAR_INFO> rgActual);
|
|
|
|
void PrepExpected(
|
|
const WORD wAttrWritten,
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const DbcsWriteRead::UnicodeMode fWriteWithUnicode,
|
|
const DbcsWriteRead::ReadMode ReadMode,
|
|
const bool fReadWithUnicode,
|
|
CharInfoPattern& expected);
|
|
|
|
const CharInfoPattern& PrepReadConsoleOutput(
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const UnicodeMode fWriteWithUnicode,
|
|
const bool fReadWithUnicode);
|
|
|
|
const CharInfoPattern& PrepReadConsoleOutputCharacter(
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const UnicodeMode fWriteWithUnicode,
|
|
const bool fReadWithUnicode);
|
|
};
|
|
|
|
class DbcsTests
|
|
{
|
|
BEGIN_TEST_CLASS(DbcsTests)
|
|
TEST_CLASS_PROPERTY(L"IsolationLevel", L"Class")
|
|
END_TEST_CLASS();
|
|
|
|
TEST_METHOD_SETUP(DbcsTestSetup);
|
|
|
|
// This test must come before ones that launch another process as launching another process can tamper with the codepage
|
|
// in ways that this test is not expecting.
|
|
TEST_METHOD(TestMultibyteInputRetrieval);
|
|
TEST_METHOD(TestMultibyteInputCoalescing);
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsWriteRead)
|
|
TEST_METHOD_PROPERTY(L"Data:WriteMode", L"{0, 1, 2, 3}")
|
|
TEST_METHOD_PROPERTY(L"Data:fWriteInUnicode", L"{0, 1, 2}")
|
|
TEST_METHOD_PROPERTY(L"Data:ReadMode", L"{0, 1}")
|
|
TEST_METHOD_PROPERTY(L"Data:fReadInUnicode", L"{true, false}")
|
|
END_TEST_METHOD()
|
|
|
|
TEST_METHOD(TestDbcsBisect);
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsBeginW)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsEndW)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsBeginA)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsEndA)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsOneByOne)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsTrailLead)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsStdCoutScenario)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestDbcsBackupRestore)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
BEGIN_TEST_METHOD(TestInvalidTrailer)
|
|
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
|
|
END_TEST_METHOD()
|
|
|
|
TEST_METHOD(TestNarrowSurrogate);
|
|
};
|
|
|
|
bool DbcsTests::DbcsTestSetup()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool DbcsWriteRead::Setup(_Out_ HANDLE* const phOut,
|
|
_Out_ WORD* const pwAttributes)
|
|
{
|
|
const auto hOut = GetStdOutputHandle();
|
|
|
|
// Ensure that the console is set into the appropriate codepage for the test
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(SetConsoleCP(JAPANESE_CP));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(SetConsoleOutputCP(JAPANESE_CP));
|
|
|
|
// Now set up the font. Many of these APIs are oddly dependent on font, so set as appropriate.
|
|
CONSOLE_FONT_INFOEX cfiex = { 0 };
|
|
cfiex.cbSize = sizeof(cfiex);
|
|
wcscpy_s(cfiex.FaceName, L"MS Gothic");
|
|
cfiex.dwFontSize.Y = 16;
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(OneCoreDelay::SetCurrentConsoleFontEx(hOut, FALSE, &cfiex));
|
|
|
|
// Ensure that we set the font we expected to set
|
|
CONSOLE_FONT_INFOEX cfiexGet = { 0 };
|
|
cfiexGet.cbSize = sizeof(cfiexGet);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(OneCoreDelay::GetCurrentConsoleFontEx(hOut, FALSE, &cfiexGet));
|
|
|
|
if (0 != NoThrowString(cfiex.FaceName).CompareNoCase(cfiexGet.FaceName))
|
|
{
|
|
Log::Comment(L"Could not change font. This system doesn't have the fonts we need to perform this test. Skipping.");
|
|
Log::Result(WEX::Logging::TestResults::Result::Skipped);
|
|
return false;
|
|
}
|
|
|
|
// Retrieve some of the information about the preferences/settings for the console buffer including
|
|
// the size of the buffer and the default colors (attributes) to use.
|
|
CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 };
|
|
sbiex.cbSize = sizeof(sbiex);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(GetConsoleScreenBufferInfoEx(hOut, &sbiex));
|
|
|
|
// ensure first line of console is cleared out with spaces so nothing interferes with the text these tests will be writing.
|
|
COORD coordZero = { 0 };
|
|
DWORD dwWritten;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FillConsoleOutputCharacterW(hOut, L'\x20', sbiex.dwSize.X, coordZero, &dwWritten));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FillConsoleOutputAttribute(hOut, sbiex.wAttributes, sbiex.dwSize.X, coordZero, &dwWritten));
|
|
|
|
// Move the cursor to the 0,0 position into our empty line so the tests can write (important for the CRT tests that specify no location)
|
|
if (!SetConsoleCursorPosition(GetStdOutputHandle(), coordZero))
|
|
{
|
|
VERIFY_FAIL(L"Failed to set cursor position");
|
|
}
|
|
|
|
// Give back the output handle and the default attributes so tests can verify attributes didn't change on roundtrip
|
|
*phOut = hOut;
|
|
*pwAttributes = sbiex.wAttributes;
|
|
|
|
return true;
|
|
}
|
|
|
|
void DbcsWriteRead::SendOutput(const HANDLE hOut,
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const UnicodeMode fIsUnicode,
|
|
const WORD wAttr)
|
|
{
|
|
// These parameters will be used to print out the written rectangle if we used the console APIs (not the CRT APIs)
|
|
// This information will be stored and printed out at the very end after we move the cursor off of the text we just printed.
|
|
// The cursor auto-moves for CRT, but we have to manually move it for some of the Console APIs.
|
|
auto fUseRectWritten = false;
|
|
SMALL_RECT srWrittenExpected = { 0 };
|
|
SMALL_RECT srWritten = { 0 };
|
|
|
|
auto fUseDwordWritten = false;
|
|
DWORD dwWrittenExpected = 0;
|
|
DWORD dwWritten = 0;
|
|
|
|
switch (WriteMode)
|
|
{
|
|
case DbcsWriteRead::WriteMode::CrtWrite:
|
|
{
|
|
// Align the CRT's mode with the text we're about to write.
|
|
// If you call a W function on the CRT while the mode is still set to A,
|
|
// the CRT will helpfully back-convert your text from W to A before sending it to the driver.
|
|
if (fIsUnicode)
|
|
{
|
|
_setmode(_fileno(stdout), _O_WTEXT);
|
|
}
|
|
else
|
|
{
|
|
_setmode(_fileno(stdout), _O_TEXT);
|
|
}
|
|
|
|
// Write each character in the string individually out through the CRT
|
|
if (fIsUnicode)
|
|
{
|
|
for (const auto& ch : unicodeInput)
|
|
{
|
|
putwchar(ch);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (const auto& ch : dbcsInput)
|
|
{
|
|
putchar(ch);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case DbcsWriteRead::WriteMode::WriteConsoleOutputFunc:
|
|
{
|
|
// If we're going to be using WriteConsoleOutput, we need to create up a nice
|
|
// CHAR_INFO buffer to pass into the method containing the string and possibly attributes
|
|
std::vector<CHAR_INFO> rgChars;
|
|
rgChars.reserve(dbcsInput.size());
|
|
|
|
switch (fIsUnicode)
|
|
{
|
|
case UnicodeMode::UnicodeSingle:
|
|
for (const auto& ch : unicodeInput)
|
|
{
|
|
rgChars.push_back(makeCharInfo(ch, wAttr));
|
|
}
|
|
break;
|
|
case UnicodeMode::UnicodeDoubled:
|
|
for (const auto& ch : unicodeInput)
|
|
{
|
|
// For the sake of this test we're going to simply assume that any non-ASCII character is wide.
|
|
if (ch < 0x80)
|
|
{
|
|
rgChars.push_back(makeCharInfo(ch, wAttr));
|
|
}
|
|
else
|
|
{
|
|
rgChars.push_back(makeCharInfo(ch, wAttr | COMMON_LVB_LEADING_BYTE));
|
|
rgChars.push_back(makeCharInfo(ch, wAttr | COMMON_LVB_TRAILING_BYTE));
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
for (const auto& ch : dbcsInput)
|
|
{
|
|
rgChars.push_back(makeCharInfo(ch, wAttr));
|
|
}
|
|
break;
|
|
}
|
|
|
|
// This is the stated size of the buffer we're passing.
|
|
// This console API can treat the buffer as a 2D array. We're only doing 1 dimension so the Y is 1 and the X is the number of CHAR_INFO characters.
|
|
COORD coordBufferSize = { 0 };
|
|
coordBufferSize.Y = 1;
|
|
coordBufferSize.X = gsl::narrow<SHORT>(rgChars.size());
|
|
|
|
// We want to write to the coordinate 0,0 of the buffer. The test setup function has blanked out that line.
|
|
COORD coordBufferTarget = { 0 };
|
|
|
|
// inclusive rectangle (bottom and right are INSIDE the read area. usually are exclusive.)
|
|
SMALL_RECT srWriteRegion = { 0 };
|
|
|
|
// Since we could have full-width characters, we have to "allow" the console to write up to the entire A string length (up to double the W length)
|
|
srWriteRegion.Right = gsl::narrow<SHORT>(dbcsInput.size()) - 1;
|
|
|
|
// Save the expected written rectangle for comparison after the call
|
|
srWrittenExpected = { 0 };
|
|
srWrittenExpected.Right = coordBufferSize.X - 1; // we expect that the written report will be the number of characters inserted, not the size of buffer consumed
|
|
|
|
// NOTE: Don't VERIFY these calls or we will overwrite the text in the buffer with the log message.
|
|
if (fIsUnicode)
|
|
{
|
|
WriteConsoleOutputW(hOut, rgChars.data(), coordBufferSize, coordBufferTarget, &srWriteRegion);
|
|
}
|
|
else
|
|
{
|
|
WriteConsoleOutputA(hOut, rgChars.data(), coordBufferSize, coordBufferTarget, &srWriteRegion);
|
|
}
|
|
|
|
// Save write region so we can print it out after we move the cursor out of the way
|
|
srWritten = srWriteRegion;
|
|
fUseRectWritten = true;
|
|
break;
|
|
}
|
|
case DbcsWriteRead::WriteMode::WriteConsoleOutputCharacterFunc:
|
|
{
|
|
COORD coordBufferTarget = { 0 };
|
|
|
|
if (fIsUnicode)
|
|
{
|
|
dwWrittenExpected = gsl::narrow<DWORD>(unicodeInput.size());
|
|
WriteConsoleOutputCharacterW(hOut, unicodeInput.data(), dwWrittenExpected, coordBufferTarget, &dwWritten);
|
|
}
|
|
else
|
|
{
|
|
dwWrittenExpected = gsl::narrow<DWORD>(dbcsInput.size());
|
|
WriteConsoleOutputCharacterA(hOut, dbcsInput.data(), dwWrittenExpected, coordBufferTarget, &dwWritten);
|
|
}
|
|
|
|
fUseDwordWritten = true;
|
|
break;
|
|
}
|
|
case DbcsWriteRead::WriteMode::WriteConsoleFunc:
|
|
{
|
|
if (fIsUnicode)
|
|
{
|
|
dwWrittenExpected = gsl::narrow<DWORD>(unicodeInput.size());
|
|
WriteConsoleW(hOut, unicodeInput.data(), dwWrittenExpected, &dwWritten, nullptr);
|
|
}
|
|
else
|
|
{
|
|
dwWrittenExpected = gsl::narrow<DWORD>(dbcsInput.size());
|
|
WriteConsoleA(hOut, dbcsInput.data(), dwWrittenExpected, &dwWritten, nullptr);
|
|
}
|
|
|
|
fUseDwordWritten = true;
|
|
break;
|
|
}
|
|
default:
|
|
VERIFY_FAIL(L"Unsupported write mode.");
|
|
}
|
|
|
|
// Move the cursor down a line in case log info prints out.
|
|
COORD coordSetCursor = { 0 };
|
|
coordSetCursor.Y = 1;
|
|
SetConsoleCursorPosition(hOut, coordSetCursor);
|
|
|
|
// If we had log info to print, print it now that it's safe (cursor out of the test data we printed)
|
|
// This only matters for when the test is run in the same window as the runner and could print log information.
|
|
if (fUseRectWritten)
|
|
{
|
|
Log::Comment(NoThrowString().Format(L"WriteRegion T: %d L: %d B: %d R: %d", srWritten.Top, srWritten.Left, srWritten.Bottom, srWritten.Right));
|
|
VERIFY_ARE_EQUAL(srWrittenExpected, srWritten);
|
|
}
|
|
else if (fUseDwordWritten)
|
|
{
|
|
Log::Comment(NoThrowString().Format(L"Chars Written: %d", dwWritten));
|
|
VERIFY_ARE_EQUAL(dwWrittenExpected, dwWritten);
|
|
}
|
|
}
|
|
|
|
namespace PrepPattern
|
|
{
|
|
static constexpr WORD zeroed = 0x0000;
|
|
static constexpr WORD white = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
|
|
// If the lower byte in our test data is 0xff it indicates that it's "flexible"
|
|
// and supposed to be replaced with whatever color attributes were written.
|
|
// The upper byte contains leading/trailing flags we're testing for.
|
|
static constexpr WORD colored = 0x00ff;
|
|
|
|
static constexpr WORD leading = COMMON_LVB_LEADING_BYTE;
|
|
static constexpr WORD trailing = COMMON_LVB_TRAILING_BYTE;
|
|
|
|
constexpr void replaceColorPlaceholders(std::span<CHAR_INFO> pattern, WORD attr)
|
|
{
|
|
for (auto& info : pattern)
|
|
{
|
|
if ((info.Attributes & colored) == colored)
|
|
{
|
|
info.Attributes &= 0xff00 | attr;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Receive Output Table:
|
|
// attr | wchar (char) | symbol
|
|
// ------------------------------------
|
|
// 0x029 | 0x0051 (0x51) | Q
|
|
// 0x029 | 0x3044 (0x44) | Hiragana I
|
|
// 0x029 | 0x304B (0x4B) | Hiragana KA
|
|
// 0x029 | 0x306A (0x6A) | Hiragana NA
|
|
// 0x029 | 0x005A (0x5A) | Z
|
|
// 0x029 | 0x0059 (0x59) | Y
|
|
// 0x029 | 0x0058 (0x58) | X
|
|
// 0x029 | 0x0057 (0x57) | W
|
|
// 0x029 | 0x0056 (0x56) | V
|
|
// 0x029 | 0x0055 (0x55) | U
|
|
// 0x029 | 0x0054 (0x54) | T
|
|
// 0x029 | 0x306B (0x6B) | Hiragana NI
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// ...
|
|
// "Space Padded" means any unused data in the buffer will be filled with spaces and the default attribute.
|
|
// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer)
|
|
// will be returned as single copies.
|
|
// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.)
|
|
static constexpr CharInfoPattern SpacePaddedDedupeW{
|
|
makeCharInfo(0x0051, colored),
|
|
makeCharInfo(0x3044, colored),
|
|
makeCharInfo(0x304b, colored),
|
|
makeCharInfo(0x306a, colored),
|
|
makeCharInfo(0x005a, colored),
|
|
makeCharInfo(0x0059, colored),
|
|
makeCharInfo(0x0058, colored),
|
|
makeCharInfo(0x0057, colored),
|
|
makeCharInfo(0x0056, colored),
|
|
makeCharInfo(0x0055, colored),
|
|
makeCharInfo(0x0054, colored),
|
|
makeCharInfo(0x306b, colored),
|
|
makeCharInfo(0x0020, white),
|
|
makeCharInfo(0x0020, white),
|
|
makeCharInfo(0x0020, white),
|
|
makeCharInfo(0x0020, white),
|
|
};
|
|
|
|
// Receive Output Table:
|
|
// attr | wchar (char) | symbol
|
|
// ------------------------------------
|
|
// 0x029 | 0x0051 (0x51) | Q
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x005A (0x5A) | Z
|
|
// 0x029 | 0x0059 (0x59) | Y
|
|
// 0x029 | 0x0058 (0x58) | X
|
|
// 0x029 | 0x0057 (0x57) | W
|
|
// 0x029 | 0x0056 (0x56) | V
|
|
// 0x029 | 0x0055 (0x55) | U
|
|
// 0x029 | 0x0054 (0x54) | T
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// 0x007 | 0x0020 (0x20) | <space>
|
|
// ...
|
|
// "Space Padded" means any unused data in the buffer will be filled with spaces and the default attribute.
|
|
// "Dedupe" means that any full-width characters in the buffer will be returned as single copies.
|
|
// But due to the target being a DBCS character set that can't represent these in a single char, it's null.
|
|
// "A" means that we intend in-codepage (char) data to be browsed in the resulting struct
|
|
static constexpr CharInfoPattern SpacePaddedDedupeInvalidA{
|
|
makeCharInfo(0x0051, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x005a, colored),
|
|
makeCharInfo(0x0059, colored),
|
|
makeCharInfo(0x0058, colored),
|
|
makeCharInfo(0x0057, colored),
|
|
makeCharInfo(0x0056, colored),
|
|
makeCharInfo(0x0055, colored),
|
|
makeCharInfo(0x0054, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0020, white),
|
|
makeCharInfo(0x0020, white),
|
|
makeCharInfo(0x0020, white),
|
|
makeCharInfo(0x0020, white),
|
|
};
|
|
|
|
// Receive Output Table:
|
|
// attr | wchar (char) | symbol
|
|
// ------------------------------------
|
|
// 0x029 | 0x0051 (0x51) | Q
|
|
// 0x129 | 0x3044 (0x44) | Hiragana I
|
|
// 0x229 | 0x3044 (0x44) | Hiragana I
|
|
// 0x129 | 0x304B (0x4B) | Hiragana KA
|
|
// 0x229 | 0x304B (0x4B) | Hiragana KA
|
|
// 0x129 | 0x306A (0x6A) | Hiragana NA
|
|
// 0x229 | 0x306A (0x6A) | Hiragana NA
|
|
// 0x029 | 0x005A (0x5A) | Z
|
|
// 0x029 | 0x0059 (0x59) | Y
|
|
// 0x029 | 0x0058 (0x58) | X
|
|
// 0x029 | 0x0057 (0x57) | W
|
|
// 0x029 | 0x0056 (0x56) | V
|
|
// 0x029 | 0x0055 (0x55) | U
|
|
// 0x029 | 0x0054 (0x54) | T
|
|
// 0x129 | 0x306B (0x6B) | Hiragana NI
|
|
// 0x229 | 0x306B (0x6B) | Hiragana NI
|
|
// ...
|
|
// "Doubled" means that any full-width characters in the buffer are returned twice with a leading and trailing byte marker.
|
|
// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.)
|
|
static constexpr CharInfoPattern DoubledW{
|
|
makeCharInfo(0x0051, colored),
|
|
makeCharInfo(0x3044, colored | leading),
|
|
makeCharInfo(0x3044, colored | trailing),
|
|
makeCharInfo(0x304b, colored | leading),
|
|
makeCharInfo(0x304b, colored | trailing),
|
|
makeCharInfo(0x306a, colored | leading),
|
|
makeCharInfo(0x306a, colored | trailing),
|
|
makeCharInfo(0x005a, colored),
|
|
makeCharInfo(0x0059, colored),
|
|
makeCharInfo(0x0058, colored),
|
|
makeCharInfo(0x0057, colored),
|
|
makeCharInfo(0x0056, colored),
|
|
makeCharInfo(0x0055, colored),
|
|
makeCharInfo(0x0054, colored),
|
|
makeCharInfo(0x306b, colored | leading),
|
|
makeCharInfo(0x306b, colored | trailing),
|
|
};
|
|
|
|
// Receive Output Table:
|
|
// attr | wchar (char) | symbol
|
|
// ------------------------------------
|
|
// 0x029 | 0x0051 (0x51) | Q
|
|
// 0x129 | 0x0082 (0x82) | Hiragana I Shift-JIS Codepage 932 Lead Byte
|
|
// 0x229 | 0x00A2 (0xA2) | Hiragana I Shift-JIS Codepage 932 Trail Byte
|
|
// 0x129 | 0x0082 (0x82) | Hiragana KA Shift-JIS Codepage 932 Lead Byte
|
|
// 0x229 | 0x00A9 (0xA9) | Hiragana KA Shift-JIS Codepage 932 Trail Byte
|
|
// 0x129 | 0x0082 (0x82) | Hiragana NA Shift-JIS Codepage 932 Lead Byte
|
|
// 0x229 | 0x00C8 (0xC8) | Hiragana NA Shift-JIS Codepage 932 Trail Byte
|
|
// 0x029 | 0x005A (0x5A) | Z
|
|
// 0x029 | 0x0059 (0x59) | Y
|
|
// 0x029 | 0x0058 (0x58) | X
|
|
// 0x029 | 0x0057 (0x57) | W
|
|
// 0x029 | 0x0056 (0x56) | V
|
|
// 0x029 | 0x0055 (0x55) | U
|
|
// 0x029 | 0x0054 (0x54) | T
|
|
// 0x129 | 0x0082 (0x82) | Hiragana NI Shift-JIS Codepage 932 Lead Byte
|
|
// 0x229 | 0x00C9 (0xC9) | Hiragana NI Shift-JIS Codepage 932 Trail Byte
|
|
// ...
|
|
// "A" means that we intend in-codepage (char) data to be browsed in the resulting struct.
|
|
// This one returns pretty much exactly as expected.
|
|
static constexpr CharInfoPattern A{
|
|
makeCharInfo(0x0051, colored),
|
|
makeCharInfo(0x0082, colored | leading),
|
|
makeCharInfo(0x00a2, colored | trailing),
|
|
makeCharInfo(0x0082, colored | leading),
|
|
makeCharInfo(0x00a9, colored | trailing),
|
|
makeCharInfo(0x0082, colored | leading),
|
|
makeCharInfo(0x00c8, colored | trailing),
|
|
makeCharInfo(0x005a, colored),
|
|
makeCharInfo(0x0059, colored),
|
|
makeCharInfo(0x0058, colored),
|
|
makeCharInfo(0x0057, colored),
|
|
makeCharInfo(0x0056, colored),
|
|
makeCharInfo(0x0055, colored),
|
|
makeCharInfo(0x0054, colored),
|
|
makeCharInfo(0x0082, colored | leading),
|
|
makeCharInfo(0x00c9, colored | trailing),
|
|
};
|
|
|
|
// Receive Output Table:
|
|
// attr | wchar (char) | symbol
|
|
// ------------------------------------
|
|
// 0x029 | 0x0051 (0x51) | Q
|
|
// 0x129 | 0x3044 (0x44) | Hiragana I
|
|
// 0x229 | 0x304B (0x4B) | Hiragana KA
|
|
// 0x129 | 0x306A (0x6A) | Hiragana NA
|
|
// 0x229 | 0x005A (0x5A) | Z
|
|
// 0x129 | 0x0059 (0x59) | Y
|
|
// 0x229 | 0x0058 (0x58) | X
|
|
// 0x029 | 0x0057 (0x57) | W
|
|
// 0x029 | 0x0056 (0x56) | V
|
|
// 0x029 | 0x0055 (0x55) | U
|
|
// 0x029 | 0x0054 (0x54) | T
|
|
// 0x029 | 0x306B (0x6B) | Hiragana NI
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x129 | 0x0000 (0x00) | <null>
|
|
// 0x229 | 0x0000 (0x00) | <null>
|
|
// ...
|
|
// "Null" means any unused data in the buffer will be filled with null.
|
|
// "CoverAChar" means that the attributes belong to the A version of the call, but we've placed de-duped W characters over the top.
|
|
// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.)
|
|
static constexpr CharInfoPattern WNullCoverAChar{
|
|
makeCharInfo(0x0051, colored),
|
|
makeCharInfo(0x3044, colored | leading),
|
|
makeCharInfo(0x304b, colored | trailing),
|
|
makeCharInfo(0x306a, colored | leading),
|
|
makeCharInfo(0x005a, colored | trailing),
|
|
makeCharInfo(0x0059, colored | leading),
|
|
makeCharInfo(0x0058, colored | trailing),
|
|
makeCharInfo(0x0057, colored),
|
|
makeCharInfo(0x0056, colored),
|
|
makeCharInfo(0x0055, colored),
|
|
makeCharInfo(0x0054, colored),
|
|
makeCharInfo(0x306b, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored | leading),
|
|
makeCharInfo(0x0000, colored | trailing),
|
|
};
|
|
|
|
// Receive Output Table:
|
|
// attr | wchar (char) | symbol
|
|
// ------------------------------------
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x029 | 0x0000 (0x00) | <null>
|
|
// 0x007 | 0x0000 (0x00) | <null>
|
|
// 0x007 | 0x0000 (0x00) | <null>
|
|
// 0x007 | 0x0000 (0x00) | <null>
|
|
// 0x007 | 0x0000 (0x00) | <null>
|
|
// ...
|
|
// "Space Padded" means most of the unused data in the buffer will be filled with spaces and the default attribute.
|
|
// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer)
|
|
// will be returned as single copies.
|
|
// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.)
|
|
// "Truncated" means that this pattern trims off some of the end of the buffer with NULLs.
|
|
// "A Cover Attr" means that after all the other operations, we will finally run through and cover up the attributes
|
|
// again with what they would have been for multi-byte data (leading and trailing flags)
|
|
static constexpr CharInfoPattern TrueTypeCharANullWithAttrs{
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, colored),
|
|
makeCharInfo(0x0000, white),
|
|
makeCharInfo(0x0000, white),
|
|
makeCharInfo(0x0000, white),
|
|
makeCharInfo(0x0000, white),
|
|
};
|
|
}
|
|
|
|
const CharInfoPattern& DbcsWriteRead::PrepReadConsoleOutput(
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const UnicodeMode fWriteWithUnicode,
|
|
const bool fReadWithUnicode)
|
|
{
|
|
switch (WriteMode)
|
|
{
|
|
case DbcsWriteRead::WriteMode::WriteConsoleOutputFunc:
|
|
switch (fWriteWithUnicode)
|
|
{
|
|
case UnicodeMode::UnicodeSingle:
|
|
if (fReadWithUnicode)
|
|
{
|
|
// When written with WriteConsoleOutputW and read back with ReadConsoleOutputW when the font is TrueType,
|
|
// we will get a deduplicated set of Unicode characters with no lead/trailing markings and space padded at the end.
|
|
return PrepPattern::SpacePaddedDedupeW;
|
|
}
|
|
else
|
|
{
|
|
// Since the narrow unicode chars can't be represented as narrow DBCS (since those don't exist) we get SpacePaddedDedupeInvalidA.
|
|
return PrepPattern::SpacePaddedDedupeInvalidA;
|
|
}
|
|
break;
|
|
case UnicodeMode::UnicodeDoubled:
|
|
if (fReadWithUnicode)
|
|
{
|
|
// In a TrueType font, we will get back Unicode characters doubled up and marked with leading and trailing bytes.
|
|
return PrepPattern::DoubledW;
|
|
}
|
|
else
|
|
{
|
|
// In a TrueType font, we will get back Unicode characters doubled up and marked with leading and trailing bytes.
|
|
return PrepPattern::A;
|
|
}
|
|
break;
|
|
default:
|
|
if (fReadWithUnicode)
|
|
{
|
|
// When written with WriteConsoleOutputW and read back with ReadConsoleOutputA when the font is TrueType,
|
|
// we will get back Unicode characters doubled up and marked with leading and trailing bytes.
|
|
return PrepPattern::DoubledW;
|
|
}
|
|
else
|
|
{
|
|
// When written with WriteConsoleOutputA and read back with ReadConsoleOutputA,
|
|
// we will get back the double-byte sequences appropriately labeled with leading/trailing bytes.
|
|
return PrepPattern::A;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case DbcsWriteRead::WriteMode::CrtWrite:
|
|
case DbcsWriteRead::WriteMode::WriteConsoleOutputCharacterFunc:
|
|
case DbcsWriteRead::WriteMode::WriteConsoleFunc:
|
|
// Writing with the CRT down here.
|
|
if (fReadWithUnicode)
|
|
{
|
|
// In a TrueType font, we will get back Unicode characters doubled up and marked with leading and trailing bytes.
|
|
return PrepPattern::DoubledW;
|
|
}
|
|
else
|
|
{
|
|
// If we wrote with the CRT and are reading with A functions, the font doesn't matter.
|
|
// We will always get back the double-byte sequences appropriately labeled with leading/trailing bytes.
|
|
return PrepPattern::A;
|
|
}
|
|
break;
|
|
default:
|
|
VERIFY_FAIL(L"Unsupported write mode");
|
|
std::terminate();
|
|
}
|
|
}
|
|
|
|
const CharInfoPattern& DbcsWriteRead::PrepReadConsoleOutputCharacter(
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const UnicodeMode fWriteWithUnicode,
|
|
const bool fReadWithUnicode)
|
|
{
|
|
if (DbcsWriteRead::WriteMode::WriteConsoleOutputFunc == WriteMode)
|
|
{
|
|
switch (fWriteWithUnicode)
|
|
{
|
|
case UnicodeMode::UnicodeSingle:
|
|
if (fReadWithUnicode)
|
|
{
|
|
return PrepPattern::SpacePaddedDedupeW;
|
|
}
|
|
else
|
|
{
|
|
return PrepPattern::TrueTypeCharANullWithAttrs;
|
|
}
|
|
break;
|
|
case UnicodeMode::UnicodeDoubled:
|
|
if (fReadWithUnicode)
|
|
{
|
|
return PrepPattern::WNullCoverAChar;
|
|
}
|
|
else
|
|
{
|
|
return PrepPattern::A;
|
|
}
|
|
break;
|
|
default:
|
|
if (fReadWithUnicode)
|
|
{
|
|
return PrepPattern::WNullCoverAChar;
|
|
}
|
|
else
|
|
{
|
|
return PrepPattern::A;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (fReadWithUnicode)
|
|
{
|
|
return PrepPattern::WNullCoverAChar;
|
|
}
|
|
else
|
|
{
|
|
return PrepPattern::A;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DbcsWriteRead::PrepExpected(const WORD wAttrWritten,
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const DbcsWriteRead::UnicodeMode fWriteWithUnicode,
|
|
const DbcsWriteRead::ReadMode ReadMode,
|
|
const bool fReadWithUnicode,
|
|
CharInfoPattern& expected)
|
|
{
|
|
switch (ReadMode)
|
|
{
|
|
case DbcsWriteRead::ReadMode::ReadConsoleOutputFunc:
|
|
{
|
|
expected = DbcsWriteRead::PrepReadConsoleOutput(WriteMode, fWriteWithUnicode, fReadWithUnicode);
|
|
break;
|
|
}
|
|
case DbcsWriteRead::ReadMode::ReadConsoleOutputCharacterFunc:
|
|
{
|
|
expected = DbcsWriteRead::PrepReadConsoleOutputCharacter(WriteMode, fWriteWithUnicode, fReadWithUnicode);
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
VERIFY_FAIL(L"Unknown read mode.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
PrepPattern::replaceColorPlaceholders(expected, wAttrWritten);
|
|
}
|
|
|
|
void DbcsWriteRead::RetrieveOutput(const HANDLE hOut,
|
|
const DbcsWriteRead::ReadMode ReadMode,
|
|
const bool fReadUnicode,
|
|
CharInfoPattern& rgChars)
|
|
{
|
|
COORD coordBufferTarget = { 0 };
|
|
|
|
switch (ReadMode)
|
|
{
|
|
case DbcsWriteRead::ReadMode::ReadConsoleOutputFunc:
|
|
{
|
|
// Since we wrote (in SendOutput function) to the 0,0 line, we need to read back the same width from that line.
|
|
COORD coordBufferSize = { 0 };
|
|
coordBufferSize.Y = 1;
|
|
coordBufferSize.X = gsl::narrow<SHORT>(rgChars.size());
|
|
|
|
SMALL_RECT srReadRegion = { 0 }; // inclusive rectangle (bottom and right are INSIDE the read area. usually are exclusive.)
|
|
srReadRegion.Right = coordBufferSize.X - 1;
|
|
|
|
// return value for read region shouldn't change
|
|
const auto srReadRegionExpected = srReadRegion;
|
|
|
|
if (!fReadUnicode)
|
|
{
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputA(hOut, rgChars.data(), coordBufferSize, coordBufferTarget, &srReadRegion));
|
|
}
|
|
else
|
|
{
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputW(hOut, rgChars.data(), coordBufferSize, coordBufferTarget, &srReadRegion));
|
|
}
|
|
|
|
Log::Comment(NoThrowString().Format(L"ReadRegion T: %d L: %d B: %d R: %d", srReadRegion.Top, srReadRegion.Left, srReadRegion.Bottom, srReadRegion.Right));
|
|
VERIFY_ARE_EQUAL(srReadRegionExpected, srReadRegion);
|
|
break;
|
|
}
|
|
case DbcsWriteRead::ReadMode::ReadConsoleOutputCharacterFunc:
|
|
{
|
|
const auto cChars = gsl::narrow<DWORD>(rgChars.size());
|
|
DWORD dwRead = 0;
|
|
if (!fReadUnicode)
|
|
{
|
|
auto psRead = new char[cChars];
|
|
VERIFY_IS_NOT_NULL(psRead);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputCharacterA(hOut, psRead, cChars, coordBufferTarget, &dwRead));
|
|
|
|
for (size_t i = 0; i < dwRead; i++)
|
|
{
|
|
rgChars[i].Char.AsciiChar = psRead[i];
|
|
}
|
|
|
|
delete[] psRead;
|
|
}
|
|
else
|
|
{
|
|
auto pwsRead = new wchar_t[cChars];
|
|
VERIFY_IS_NOT_NULL(pwsRead);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputCharacterW(hOut, pwsRead, cChars, coordBufferTarget, &dwRead));
|
|
|
|
for (size_t i = 0; i < dwRead; i++)
|
|
{
|
|
rgChars[i].Char.UnicodeChar = pwsRead[i];
|
|
}
|
|
|
|
delete[] pwsRead;
|
|
}
|
|
|
|
auto pwAttrs = new WORD[cChars];
|
|
VERIFY_IS_NOT_NULL(pwAttrs);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputAttribute(hOut, pwAttrs, cChars, coordBufferTarget, &dwRead));
|
|
|
|
for (size_t i = 0; i < dwRead; i++)
|
|
{
|
|
rgChars[i].Attributes = pwAttrs[i];
|
|
}
|
|
|
|
delete[] pwAttrs;
|
|
break;
|
|
}
|
|
default:
|
|
VERIFY_FAIL(L"Unknown read mode");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void DbcsWriteRead::Verify(std::span<CHAR_INFO> rgExpected, std::span<CHAR_INFO> rgActual)
|
|
{
|
|
VERIFY_ARE_EQUAL(rgExpected.size(), rgActual.size());
|
|
// We will walk through for the number of CHAR_INFOs expected.
|
|
for (size_t i = 0; i < rgExpected.size(); i++)
|
|
{
|
|
// Uncomment these lines for help debugging the verification.
|
|
/*
|
|
Log::Comment(NoThrowString().Format(L"Index: %d:", i));
|
|
Log::Comment(VerifyOutputTraits<CHAR_INFO>::ToString(rgExpected[i]));
|
|
Log::Comment(VerifyOutputTraits<CHAR_INFO>::ToString(rgActual[i]));
|
|
*/
|
|
|
|
VERIFY_ARE_EQUAL(rgExpected[i], rgActual[i]);
|
|
}
|
|
}
|
|
|
|
void DbcsWriteRead::TestRunner(_In_opt_ WORD* const pwAttrOverride,
|
|
const DbcsWriteRead::WriteMode WriteMode,
|
|
const UnicodeMode fWriteInUnicode,
|
|
const DbcsWriteRead::ReadMode ReadMode,
|
|
const bool fReadWithUnicode)
|
|
{
|
|
// First we need to set up the tests by clearing out the first line of the buffer,
|
|
// retrieving the appropriate output handle, and getting the colors (attributes)
|
|
// used by default in the buffer (set during clearing as well).
|
|
HANDLE hOut;
|
|
WORD wAttributes;
|
|
if (!DbcsWriteRead::Setup(&hOut, &wAttributes))
|
|
{
|
|
// If we can't set up (setup will detect systems where this test cannot operate) then return early.
|
|
return;
|
|
}
|
|
|
|
// Some tests might want to override the colors applied to ensure both parts of the CHAR_INFO union
|
|
// work for methods that support sending that union. (i.e. not the CRT path)
|
|
if (nullptr != pwAttrOverride)
|
|
{
|
|
wAttributes = *pwAttrOverride;
|
|
}
|
|
|
|
// Write the string under test into the appropriate WRITE API for this test.
|
|
DbcsWriteRead::SendOutput(hOut, WriteMode, fWriteInUnicode, wAttributes);
|
|
|
|
// Prepare the array of CHAR_INFO structs that we expect to receive back when we will call read in a moment.
|
|
// This can vary based on font, unicode/non-unicode (when reading AND writing), and codepage.
|
|
CharInfoPattern pciExpected;
|
|
DbcsWriteRead::PrepExpected(wAttributes, WriteMode, fWriteInUnicode, ReadMode, fReadWithUnicode, pciExpected);
|
|
|
|
// Now call the appropriate READ API for this test.
|
|
CharInfoPattern pciActual{};
|
|
DbcsWriteRead::RetrieveOutput(hOut, ReadMode, fReadWithUnicode, pciActual);
|
|
|
|
// Loop through and verify that our expected array matches what was actually returned by the given API.
|
|
DbcsWriteRead::Verify(pciExpected, pciActual);
|
|
}
|
|
|
|
void DbcsTests::TestDbcsWriteRead()
|
|
{
|
|
int iWriteMode;
|
|
VERIFY_SUCCEEDED(TestData::TryGetValue(L"WriteMode", iWriteMode));
|
|
auto WriteMode = (DbcsWriteRead::WriteMode)iWriteMode;
|
|
|
|
int iWriteInUnicode;
|
|
VERIFY_SUCCEEDED(TestData::TryGetValue(L"fWriteInUnicode", iWriteInUnicode));
|
|
auto fWriteInUnicode = (DbcsWriteRead::UnicodeMode)iWriteInUnicode;
|
|
|
|
int iReadMode;
|
|
VERIFY_SUCCEEDED(TestData::TryGetValue(L"ReadMode", iReadMode));
|
|
auto ReadMode = (DbcsWriteRead::ReadMode)iReadMode;
|
|
|
|
bool fReadInUnicode;
|
|
VERIFY_SUCCEEDED(TestData::TryGetValue(L"fReadInUnicode", fReadInUnicode));
|
|
|
|
// UnicodeDoubled is only relevant for WriteConsoleOutputW
|
|
if (fWriteInUnicode == DbcsWriteRead::UnicodeMode::UnicodeDoubled && WriteMode != DbcsWriteRead::WriteMode::WriteConsoleOutputFunc)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto pwszWriteMode = L"";
|
|
switch (WriteMode)
|
|
{
|
|
case DbcsWriteRead::WriteMode::CrtWrite:
|
|
pwszWriteMode = L"CRT";
|
|
break;
|
|
case DbcsWriteRead::WriteMode::WriteConsoleOutputFunc:
|
|
pwszWriteMode = L"WriteConsoleOutput";
|
|
break;
|
|
case DbcsWriteRead::WriteMode::WriteConsoleOutputCharacterFunc:
|
|
pwszWriteMode = L"WriteConsoleOutputCharacter";
|
|
break;
|
|
case DbcsWriteRead::WriteMode::WriteConsoleFunc:
|
|
pwszWriteMode = L"WriteConsole";
|
|
break;
|
|
default:
|
|
VERIFY_FAIL(L"Write mode not supported");
|
|
}
|
|
|
|
auto pwszReadMode = L"";
|
|
switch (ReadMode)
|
|
{
|
|
case DbcsWriteRead::ReadMode::ReadConsoleOutputFunc:
|
|
pwszReadMode = L"ReadConsoleOutput";
|
|
break;
|
|
case DbcsWriteRead::ReadMode::ReadConsoleOutputCharacterFunc:
|
|
pwszReadMode = L"ReadConsoleOutputCharacter";
|
|
break;
|
|
default:
|
|
VERIFY_FAIL(L"Read mode not supported");
|
|
}
|
|
|
|
auto testInfo = NoThrowString().Format(L"\r\n\r\n\r\nWrite with %s '%s'%s. Check Read with %s '%s' API. Use %d codepage.\r\n",
|
|
pwszWriteMode,
|
|
fWriteInUnicode ? L"W" : L"A",
|
|
fWriteInUnicode == DbcsWriteRead::UnicodeMode::UnicodeDoubled ? L" (doubled)" : L"",
|
|
pwszReadMode,
|
|
fReadInUnicode ? L"W" : L"A",
|
|
JAPANESE_CP);
|
|
|
|
Log::Comment(testInfo);
|
|
|
|
WORD wAttributes = 0;
|
|
|
|
if (WriteMode == 1)
|
|
{
|
|
Log::Comment(L"We will also try to change the color since WriteConsoleOutput supports it.");
|
|
wAttributes = FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_GREEN;
|
|
}
|
|
|
|
DbcsWriteRead::TestRunner(wAttributes != 0 ? &wAttributes : nullptr,
|
|
WriteMode,
|
|
fWriteInUnicode,
|
|
ReadMode,
|
|
fReadInUnicode);
|
|
|
|
Log::Comment(testInfo);
|
|
}
|
|
|
|
// This test covers bisect-prevention handling. This is the behavior where a double-wide character will not be spliced
|
|
// across a line boundary and will instead be advanced onto the next line.
|
|
// It additionally exercises the word wrap functionality to ensure that the bisect calculations continue
|
|
// to apply properly when wrap occurs.
|
|
void DbcsTests::TestDbcsBisect()
|
|
{
|
|
const auto hOut = GetStdOutputHandle();
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleCP(JAPANESE_CP));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(JAPANESE_CP));
|
|
|
|
auto dwCP = GetConsoleCP();
|
|
VERIFY_ARE_EQUAL(dwCP, JAPANESE_CP);
|
|
|
|
auto dwOutputCP = GetConsoleOutputCP();
|
|
VERIFY_ARE_EQUAL(dwOutputCP, JAPANESE_CP);
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 };
|
|
sbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
|
|
auto fSuccess = GetConsoleScreenBufferInfoEx(hOut, &sbiex);
|
|
|
|
if (CheckLastError(fSuccess, L"GetConsoleScreenBufferInfoEx"))
|
|
{
|
|
Log::Comment(L"Set cursor position to the last column in the buffer width.");
|
|
sbiex.dwCursorPosition.X = sbiex.dwSize.X - 1;
|
|
|
|
const auto coordEndOfLine = sbiex.dwCursorPosition; // this is the end of line position we're going to write at
|
|
COORD coordStartOfNextLine;
|
|
coordStartOfNextLine.X = 0;
|
|
coordStartOfNextLine.Y = sbiex.dwCursorPosition.Y + 1;
|
|
|
|
fSuccess = SetConsoleCursorPosition(hOut, sbiex.dwCursorPosition);
|
|
if (CheckLastError(fSuccess, L"SetConsoleScreenBufferInfoEx"))
|
|
{
|
|
Log::Comment(L"Attempt to write (standard WriteConsole) a double-wide character and ensure that it is placed onto the following line, not bisected.");
|
|
DWORD dwWritten = 0;
|
|
const auto wchHiraganaU = L'\x3046';
|
|
const auto wchSpace = L' ';
|
|
fSuccess = WriteConsoleW(hOut, &wchHiraganaU, 1, &dwWritten, nullptr);
|
|
|
|
if (CheckLastError(fSuccess, L"WriteConsoleW"))
|
|
{
|
|
VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have only written the one character.");
|
|
|
|
// Read the end of line character and the start of the next line.
|
|
// A proper bisect should have left the end of line character empty (a space)
|
|
// and then put the character at the beginning of the next line.
|
|
|
|
Log::Comment(L"Confirm that the end of line was left empty to prevent bisect.");
|
|
WCHAR wchBuffer;
|
|
fSuccess = ReadConsoleOutputCharacterW(hOut, &wchBuffer, 1, coordEndOfLine, &dwWritten);
|
|
if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW"))
|
|
{
|
|
VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have only read one character back at the end of the line.");
|
|
|
|
VERIFY_ARE_EQUAL(wchSpace, wchBuffer, L"A space character should have been left at the end of the line.");
|
|
|
|
Log::Comment(L"Confirm that the wide character was written on the next line down instead.");
|
|
WCHAR wchBuffer2[2];
|
|
fSuccess = ReadConsoleOutputCharacterW(hOut, wchBuffer2, 2, coordStartOfNextLine, &dwWritten);
|
|
if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW"))
|
|
{
|
|
VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have only read one character back at the beginning of the next line.");
|
|
|
|
VERIFY_ARE_EQUAL(wchHiraganaU, wchBuffer2[0], L"The same character we passed in should have been read back.");
|
|
|
|
Log::Comment(L"Confirm that the cursor has advanced past the double wide character.");
|
|
fSuccess = GetConsoleScreenBufferInfoEx(hOut, &sbiex);
|
|
if (CheckLastError(fSuccess, L"GetConsoleScreenBufferInfoEx"))
|
|
{
|
|
VERIFY_ARE_EQUAL(coordStartOfNextLine.Y, sbiex.dwCursorPosition.Y, L"Cursor has moved down to next line.");
|
|
VERIFY_ARE_EQUAL(coordStartOfNextLine.X + 2, sbiex.dwCursorPosition.X, L"Cursor has advanced two spaces on next line for double wide character.");
|
|
|
|
// TODO: This bit needs to move into a UIA test
|
|
/*Log::Comment(L"We can only run the resize test in the v2 console. We'll skip it if it turns out v2 is off.");
|
|
if (IsV2Console())
|
|
{
|
|
Log::Comment(L"Test that the character moves back up when the window is unwrapped. Make the window one larger.");
|
|
sbiex.srWindow.Right++;
|
|
sbiex.dwSize.X++;
|
|
fSuccess = SetConsoleScreenBufferInfoEx(hOut, &sbiex);
|
|
if (CheckLastError(fSuccess, L"SetConsoleScreenBufferInfoEx"))
|
|
{
|
|
ZeroMemory(wchBuffer2, ARRAYSIZE(wchBuffer2) * sizeof(WCHAR));
|
|
Log::Comment(L"Check that the character rolled back up onto the previous line.");
|
|
fSuccess = ReadConsoleOutputCharacterW(hOut, wchBuffer2, 2, coordEndOfLine, &dwWritten);
|
|
if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW"))
|
|
{
|
|
VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have read 1 character up on the previous line.");
|
|
|
|
VERIFY_ARE_EQUAL(wchHiraganaU, wchBuffer2[0], L"The character should now be up one line.");
|
|
|
|
Log::Comment(L"Now shrink the window one more time and make sure the character rolls back down a line.");
|
|
sbiex.srWindow.Right--;
|
|
sbiex.dwSize.X--;
|
|
fSuccess = SetConsoleScreenBufferInfoEx(hOut, &sbiex);
|
|
if (CheckLastError(fSuccess, L"SetConsoleScreenBufferInfoEx"))
|
|
{
|
|
ZeroMemory(wchBuffer2, ARRAYSIZE(wchBuffer2) * sizeof(WCHAR));
|
|
Log::Comment(L"Check that the character rolled down onto the next line again.");
|
|
fSuccess = ReadConsoleOutputCharacterW(hOut, wchBuffer2, 2, coordStartOfNextLine, &dwWritten);
|
|
if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW"))
|
|
{
|
|
VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have read 1 character back down again on the next line.");
|
|
|
|
VERIFY_ARE_EQUAL(wchHiraganaU, wchBuffer2[0], L"The character should now be down on the 2nd line again.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}*/
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The following W versions of the tests check that we can't insert a bisecting cell even
|
|
// when we try to force one in by writing cell-by-cell.
|
|
// NOTE: This is a change in behavior from the legacy behavior.
|
|
// V1 console would allow a lead byte to be stored in the final cell and then display it improperly.
|
|
// It would also allow this data to be read back.
|
|
// I believe this was a long-standing bug because every other API entry fastidiously checked that it wasn't possible to
|
|
// "bisect" a cell and all sorts of portions of the rest of the console code try to enforce that bisects across lines can't happen.
|
|
// For the most recent revision of the V2 console (approx November 2018), we're trying to make sure that the TextBuffer's internal state
|
|
// is always correct at insert (instead of correcting it on every read).
|
|
// If it turns out that we are proven wrong in the future and this causes major problems,
|
|
// the legacy behavior is to just let it be stored and compensate for it later. (On read in every API but ReadConsoleOutput and in the selection).
|
|
void DbcsTests::TestDbcsBisectWriteCellsEndW()
|
|
{
|
|
const auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 };
|
|
info.cbSize = sizeof(info);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info));
|
|
|
|
CHAR_INFO originalCell;
|
|
originalCell.Char.UnicodeChar = L'\x30a2'; // Japanese full-width katakana A
|
|
originalCell.Attributes = COMMON_LVB_LEADING_BYTE | FOREGROUND_RED;
|
|
|
|
SMALL_RECT writeRegion;
|
|
writeRegion.Top = 0;
|
|
writeRegion.Bottom = 0;
|
|
writeRegion.Left = info.dwSize.X - 1;
|
|
writeRegion.Right = info.dwSize.X - 1;
|
|
|
|
const auto originalWriteRegion = writeRegion;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion));
|
|
VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion);
|
|
|
|
auto readRegion = originalWriteRegion;
|
|
const auto originalReadRegion = readRegion;
|
|
CHAR_INFO readCell;
|
|
|
|
CHAR_INFO expectedCell;
|
|
expectedCell.Char.UnicodeChar = L' ';
|
|
expectedCell.Attributes = originalCell.Attributes;
|
|
WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE);
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion));
|
|
VERIFY_ARE_EQUAL(originalReadRegion, readRegion);
|
|
|
|
VERIFY_ARE_NOT_EQUAL(originalCell, readCell);
|
|
VERIFY_ARE_EQUAL(expectedCell, readCell);
|
|
}
|
|
|
|
// This test also reflects a change in the legacy behavior (see above)
|
|
void DbcsTests::TestDbcsBisectWriteCellsBeginW()
|
|
{
|
|
const auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 };
|
|
info.cbSize = sizeof(info);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info));
|
|
|
|
CHAR_INFO originalCell;
|
|
originalCell.Char.UnicodeChar = L'\x30a2';
|
|
originalCell.Attributes = COMMON_LVB_TRAILING_BYTE | FOREGROUND_RED;
|
|
|
|
SMALL_RECT writeRegion;
|
|
writeRegion.Top = 0;
|
|
writeRegion.Bottom = 0;
|
|
writeRegion.Left = 0;
|
|
writeRegion.Right = 0;
|
|
const auto originalWriteRegion = writeRegion;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion));
|
|
VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion);
|
|
|
|
auto readRegion = originalWriteRegion;
|
|
const auto originalReadRegion = readRegion;
|
|
CHAR_INFO readCell;
|
|
|
|
CHAR_INFO expectedCell;
|
|
expectedCell.Char.UnicodeChar = L' ';
|
|
expectedCell.Attributes = originalCell.Attributes;
|
|
WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE);
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion));
|
|
VERIFY_ARE_EQUAL(originalReadRegion, readRegion);
|
|
|
|
VERIFY_ARE_NOT_EQUAL(originalCell, readCell);
|
|
VERIFY_ARE_EQUAL(expectedCell, readCell);
|
|
}
|
|
|
|
void DbcsTests::TestDbcsBisectWriteCellsEndA()
|
|
{
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleCP(JAPANESE_CP));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(JAPANESE_CP));
|
|
|
|
const auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 };
|
|
info.cbSize = sizeof(info);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info));
|
|
|
|
CHAR_INFO originalCell;
|
|
originalCell.Char.AsciiChar = '\x82';
|
|
originalCell.Attributes = COMMON_LVB_LEADING_BYTE | FOREGROUND_RED;
|
|
|
|
SMALL_RECT writeRegion;
|
|
writeRegion.Top = 0;
|
|
writeRegion.Bottom = 0;
|
|
writeRegion.Left = info.dwSize.X - 1;
|
|
writeRegion.Right = info.dwSize.X - 1;
|
|
const auto originalWriteRegion = writeRegion;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputA(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion));
|
|
VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion);
|
|
|
|
auto readRegion = originalWriteRegion;
|
|
const auto originalReadRegion = readRegion;
|
|
CHAR_INFO readCell;
|
|
|
|
CHAR_INFO expectedCell;
|
|
expectedCell.Char.UnicodeChar = L' ';
|
|
expectedCell.Attributes = originalCell.Attributes;
|
|
WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE);
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputA(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion));
|
|
VERIFY_ARE_EQUAL(originalReadRegion, readRegion);
|
|
|
|
VERIFY_ARE_NOT_EQUAL(originalCell, readCell);
|
|
VERIFY_ARE_EQUAL(expectedCell, readCell);
|
|
}
|
|
|
|
// This test maintains the legacy behavior for the 932 A codepage route.
|
|
void DbcsTests::TestDbcsBisectWriteCellsBeginA()
|
|
{
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleCP(JAPANESE_CP));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(JAPANESE_CP));
|
|
|
|
const auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 };
|
|
info.cbSize = sizeof(info);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info));
|
|
|
|
CHAR_INFO originalCell;
|
|
originalCell.Char.AsciiChar = '\xA9';
|
|
originalCell.Attributes = COMMON_LVB_TRAILING_BYTE | FOREGROUND_RED;
|
|
|
|
SMALL_RECT writeRegion;
|
|
writeRegion.Top = 0;
|
|
writeRegion.Bottom = 0;
|
|
writeRegion.Left = 0;
|
|
writeRegion.Right = 0;
|
|
const auto originalWriteRegion = writeRegion;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputA(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion));
|
|
VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion);
|
|
|
|
auto readRegion = originalWriteRegion;
|
|
const auto originalReadRegion = readRegion;
|
|
CHAR_INFO readCell;
|
|
|
|
CHAR_INFO expectedCell{};
|
|
expectedCell.Char.AsciiChar = originalCell.Char.AsciiChar;
|
|
expectedCell.Attributes = originalCell.Attributes;
|
|
WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE);
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputA(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion));
|
|
VERIFY_ARE_EQUAL(originalReadRegion, readRegion);
|
|
|
|
VERIFY_ARE_NOT_EQUAL(originalCell, readCell);
|
|
VERIFY_ARE_EQUAL(expectedCell, readCell);
|
|
}
|
|
|
|
struct MultibyteInputData
|
|
{
|
|
PCWSTR pwszInputText;
|
|
PCSTR pszExpectedText;
|
|
};
|
|
|
|
// clang-format off
|
|
const MultibyteInputData MultibyteTestDataSet[] = {
|
|
{ L"\x3042", "\x82\xa0" },
|
|
{ L"\x3042" L"3", "\x82\xa0\x33" },
|
|
{ L"3" L"\x3042", "\x33\x82\xa0" },
|
|
{ L"3" L"\x3042" L"\x3044", "\x33\x82\xa0\x82\xa2" },
|
|
{ L"3" L"\x3042" L"\x3044" L"\x3042", "\x33\x82\xa0\x82\xa2\x82\xa0" },
|
|
{ L"3" L"\x3042" L"\x3044" L"\x3042" L"\x3044", "\x33\x82\xa0\x82\xa2\x82\xa0\x82\xa2" },
|
|
};
|
|
// clang-format on
|
|
|
|
void WriteStringToInput(HANDLE hIn, PCWSTR pwszString)
|
|
{
|
|
const auto cchString = wcslen(pwszString);
|
|
const auto cRecords = cchString * 2; // We need double the input records for button down then button up.
|
|
|
|
const auto irString = new INPUT_RECORD[cRecords];
|
|
VERIFY_IS_NOT_NULL(irString);
|
|
|
|
for (size_t i = 0; i < cRecords; i++)
|
|
{
|
|
irString[i].EventType = KEY_EVENT;
|
|
irString[i].Event.KeyEvent.bKeyDown = (i % 2 == 0) ? TRUE : FALSE;
|
|
irString[i].Event.KeyEvent.dwControlKeyState = 0;
|
|
irString[i].Event.KeyEvent.uChar.UnicodeChar = pwszString[i / 2];
|
|
irString[i].Event.KeyEvent.wRepeatCount = 1;
|
|
irString[i].Event.KeyEvent.wVirtualKeyCode = 0;
|
|
irString[i].Event.KeyEvent.wVirtualScanCode = 0;
|
|
}
|
|
|
|
DWORD dwWritten;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputW(hIn, irString, (DWORD)cRecords, &dwWritten));
|
|
|
|
VERIFY_ARE_EQUAL(cRecords, dwWritten, L"We should have written the number of records that were sent in by our buffer.");
|
|
|
|
delete[] irString;
|
|
}
|
|
|
|
void ReadStringWithGetCh(PCSTR pszExpectedText)
|
|
{
|
|
const auto cchString = strlen(pszExpectedText);
|
|
|
|
for (size_t i = 0; i < cchString; i++)
|
|
{
|
|
if (!VERIFY_ARE_EQUAL((BYTE)pszExpectedText[i], _getch()))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ReadStringWithReadConsoleInputAHelper(HANDLE hIn, PCSTR pszExpectedText, size_t cbBuffer)
|
|
{
|
|
Log::Comment(String().Format(L" = Attempting to read back the text with a %d record length buffer. =", cbBuffer));
|
|
|
|
// Find out how many bytes we need to read.
|
|
const auto cchExpectedText = strlen(pszExpectedText);
|
|
|
|
// Increment read buffer of the size we were told.
|
|
const auto irRead = new INPUT_RECORD[cbBuffer];
|
|
VERIFY_IS_NOT_NULL(irRead);
|
|
|
|
// Loop reading and comparing until we've read enough times to get all the text we expect.
|
|
size_t cchRead = 0;
|
|
|
|
while (cchRead < cchExpectedText)
|
|
{
|
|
DWORD dwRead;
|
|
if (!VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputA(hIn, irRead, (DWORD)cbBuffer, &dwRead), L"Attempt to read input into buffer."))
|
|
{
|
|
break;
|
|
}
|
|
|
|
VERIFY_IS_GREATER_THAN_OR_EQUAL(dwRead, (DWORD)0, L"Verify we read non-negative bytes.");
|
|
|
|
for (size_t i = 0; i < dwRead; i++)
|
|
{
|
|
// We might read more events than the ones we're looking for because some other type of event was
|
|
// inserted into the queue by outside action. Only look at the key down events.
|
|
if (irRead[i].EventType == KEY_EVENT &&
|
|
irRead[i].Event.KeyEvent.bKeyDown == TRUE)
|
|
{
|
|
if (!VERIFY_ARE_EQUAL((BYTE)pszExpectedText[cchRead], (BYTE)irRead[i].Event.KeyEvent.uChar.AsciiChar))
|
|
{
|
|
break;
|
|
}
|
|
cchRead++;
|
|
}
|
|
}
|
|
}
|
|
|
|
delete[] irRead;
|
|
}
|
|
|
|
void ReadStringWithReadConsoleInputA(HANDLE hIn, PCWSTR pwszWriteText, PCSTR pszExpectedText)
|
|
{
|
|
// Figure out how long the expected length is.
|
|
const auto cchExpectedText = strlen(pszExpectedText);
|
|
|
|
// Test every buffer size variation from 1 to the size of the string.
|
|
for (size_t i = 1; i <= cchExpectedText; i++)
|
|
{
|
|
FlushConsoleInputBuffer(hIn);
|
|
WriteStringToInput(hIn, pwszWriteText);
|
|
ReadStringWithReadConsoleInputAHelper(hIn, pszExpectedText, i);
|
|
}
|
|
}
|
|
|
|
void DbcsTests::TestMultibyteInputRetrieval()
|
|
{
|
|
SetConsoleCP(932);
|
|
|
|
auto dwCP = GetConsoleCP();
|
|
if (!VERIFY_ARE_EQUAL(JAPANESE_CP, dwCP, L"Ensure input codepage is Japanese."))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto hIn = GetStdHandle(STD_INPUT_HANDLE);
|
|
if (!VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, hIn, L"Get input handle."))
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto cDataSet = ARRAYSIZE(MultibyteTestDataSet);
|
|
|
|
// for each item in our test data set...
|
|
for (size_t i = 0; i < cDataSet; i++)
|
|
{
|
|
auto data = MultibyteTestDataSet[i];
|
|
|
|
Log::Comment(String().Format(L"=== TEST #%d ===", i));
|
|
Log::Comment(String().Format(L"=== Input '%ws' ===", data.pwszInputText));
|
|
|
|
// test by writing the string and reading back the _getch way.
|
|
Log::Comment(L" == SUBTEST A: Use _getch to retrieve. == ");
|
|
FlushConsoleInputBuffer(hIn);
|
|
WriteStringToInput(hIn, data.pwszInputText);
|
|
ReadStringWithGetCh(data.pszExpectedText);
|
|
|
|
// test by writing the string and reading back with variable length buffers the ReadConsoleInputA way.
|
|
Log::Comment(L" == SUBTEST B: Use ReadConsoleInputA with variable length buffers to retrieve. == ");
|
|
ReadStringWithReadConsoleInputA(hIn, data.pwszInputText, data.pszExpectedText);
|
|
}
|
|
|
|
FlushConsoleInputBuffer(hIn);
|
|
}
|
|
|
|
// This test ensures that two separate WriteConsoleInputA with trailing/leading DBCS are joined (coalesced) into a single wide character.
|
|
void DbcsTests::TestMultibyteInputCoalescing()
|
|
{
|
|
SetConsoleCP(932);
|
|
|
|
const auto in = GetStdHandle(STD_INPUT_HANDLE);
|
|
FlushConsoleInputBuffer(in);
|
|
|
|
DWORD count;
|
|
{
|
|
const auto record = SynthesizeKeyEvent(true, 1, 123, 456, 0x82, 789);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count));
|
|
}
|
|
{
|
|
const auto record = SynthesizeKeyEvent(true, 1, 234, 567, 0xA2, 890);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(in, &record, 1, &count));
|
|
}
|
|
|
|
// Asking for 2 records and asserting we only got 1 ensures
|
|
// that we receive the exact number of expected records.
|
|
INPUT_RECORD actual[2];
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputW(in, &actual[0], 2, &count));
|
|
VERIFY_ARE_EQUAL(1u, count);
|
|
|
|
const auto expected = SynthesizeKeyEvent(true, 1, 123, 456, L'い', 789);
|
|
VERIFY_ARE_EQUAL(expected, actual[0]);
|
|
}
|
|
|
|
void DbcsTests::TestDbcsOneByOne()
|
|
{
|
|
const auto hOut = GetStdOutputHandle();
|
|
VERIFY_IS_NOT_NULL(hOut, L"Verify output handle is valid.");
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(936), L"Ensure output codepage is set to Simplified Chinese 936.");
|
|
|
|
// This is Unicode characters U+6D4B U+8BD5 U+4E2D U+6587 in Simplified Chinese Codepage 936.
|
|
// The English translation is "Test Chinese".
|
|
// We write the bytes in hex to prevent storage/interpretation issues by the source control and compiler.
|
|
char test[] = "\xb2\xe2\xca\xd4\xd6\xd0\xce\xc4";
|
|
|
|
// Prepare structures for readback.
|
|
COORD coordReadPos = { 0 };
|
|
const auto cchReadBack = 2u;
|
|
char chReadBack[2];
|
|
DWORD dwReadOrWritten = 0u;
|
|
|
|
for (size_t i = 0; i < strlen(test); i++)
|
|
{
|
|
const auto fIsLeadByte = (i % 2 == 0);
|
|
Log::Comment(fIsLeadByte ? L"Writing lead byte." : L"Writing trailing byte.");
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, &(test[i]), 1u, &dwReadOrWritten, nullptr));
|
|
VERIFY_ARE_EQUAL(1u, dwReadOrWritten, L"Verify the byte was reported written.");
|
|
|
|
dwReadOrWritten = 0;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, cchReadBack, coordReadPos, &dwReadOrWritten), L"Read back character.");
|
|
if (fIsLeadByte)
|
|
{
|
|
Log::Comment(L"Characters should be empty (space) because we only wrote a lead. It should be held for later.");
|
|
VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[0]);
|
|
VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[1]);
|
|
}
|
|
else
|
|
{
|
|
Log::Comment(L"After trailing is written, character should be valid from Chinese plane (not checking exactly, just that it was composed.");
|
|
VERIFY_IS_LESS_THAN((unsigned char)'\x80', (unsigned char)chReadBack[0]);
|
|
VERIFY_IS_LESS_THAN((unsigned char)'\x80', (unsigned char)chReadBack[1]);
|
|
coordReadPos.X += 2; // advance X for next read back. Move 2 positions because it's a wide char.
|
|
}
|
|
}
|
|
}
|
|
|
|
void DbcsTests::TestDbcsTrailLead()
|
|
{
|
|
const auto hOut = GetStdOutputHandle();
|
|
VERIFY_IS_NOT_NULL(hOut, L"Verify output handle is valid.");
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(936), L"Ensure output codepage is set to Simplified Chinese 936.");
|
|
|
|
// This is Unicode characters U+6D4B U+8BD5 U+4E2D U+6587 in Simplified Chinese Codepage 936.
|
|
// The English translation is "Test Chinese".
|
|
// We write the bytes in hex to prevent storage/interpretation issues by the source control and compiler.
|
|
char test[] = "\xb2";
|
|
char test2[] = "\xe2\xca";
|
|
char test3[] = "\xd4\xd6\xd0\xce\xc4";
|
|
|
|
// Prepare structures for readback.
|
|
const COORD coordReadPos = { 0 };
|
|
const auto cchReadBack = 8u;
|
|
char chReadBack[9];
|
|
DWORD dwReadOrWritten = 0u;
|
|
DWORD cchTestLength = 0;
|
|
|
|
Log::Comment(L"1. Write lead byte only.");
|
|
cchTestLength = (DWORD)strlen(test);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, test, cchTestLength, &dwReadOrWritten, nullptr), L"Write the string.");
|
|
VERIFY_ARE_EQUAL(cchTestLength, dwReadOrWritten, L"Verify all characters reported as written.");
|
|
dwReadOrWritten = 0;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, 2, coordReadPos, &dwReadOrWritten), L"Read back buffer.");
|
|
Log::Comment(L"Verify nothing is written/displayed yet. The read byte should have been consumed/stored but not yet displayed.");
|
|
VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[0]);
|
|
VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[1]);
|
|
|
|
Log::Comment(L"2. Write trailing and next lead.");
|
|
cchTestLength = (DWORD)strlen(test2);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, test2, cchTestLength, &dwReadOrWritten, nullptr), L"Write the string.");
|
|
VERIFY_ARE_EQUAL(cchTestLength, dwReadOrWritten, L"Verify all characters reported as written.");
|
|
dwReadOrWritten = 0;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, 4, coordReadPos, &dwReadOrWritten), L"Read back buffer.");
|
|
Log::Comment(L"Verify previous lead and the trailing we just wrote formed a character. The final lead should have been consumed/stored and not yet displayed.");
|
|
VERIFY_ARE_EQUAL((unsigned char)test[0], (unsigned char)chReadBack[0]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test2[0], (unsigned char)chReadBack[1]);
|
|
VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[2]);
|
|
VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[3]);
|
|
|
|
Log::Comment(L"3. Write trailing and finish string.");
|
|
cchTestLength = (DWORD)strlen(test3);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, test3, cchTestLength, &dwReadOrWritten, nullptr), L"Write the string.");
|
|
VERIFY_ARE_EQUAL(cchTestLength, dwReadOrWritten, L"Verify all characters reported as written.");
|
|
dwReadOrWritten = 0;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, cchReadBack, coordReadPos, &dwReadOrWritten), L"Read back buffer.");
|
|
Log::Comment(L"Verify everything is displayed now that we've finished it off with the final trailing and rest of the string.");
|
|
VERIFY_ARE_EQUAL((unsigned char)test[0], (unsigned char)chReadBack[0]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test2[0], (unsigned char)chReadBack[1]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test2[1], (unsigned char)chReadBack[2]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test3[0], (unsigned char)chReadBack[3]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test3[1], (unsigned char)chReadBack[4]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test3[2], (unsigned char)chReadBack[5]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test3[3], (unsigned char)chReadBack[6]);
|
|
VERIFY_ARE_EQUAL((unsigned char)test3[4], (unsigned char)chReadBack[7]);
|
|
}
|
|
|
|
void DbcsTests::TestDbcsStdCoutScenario()
|
|
{
|
|
const auto hOut = GetStdOutputHandle();
|
|
VERIFY_IS_NOT_NULL(hOut, L"Verify output handle is valid.");
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(936), L"Ensure output codepage is set to Simplified Chinese 936.");
|
|
|
|
// This is Unicode characters U+6D4B U+8BD5 U+4E2D U+6587 in Simplified Chinese Codepage 936.
|
|
// The English translation is "Test Chinese".
|
|
// We write the bytes in hex to prevent storage/interpretation issues by the source control and compiler.
|
|
char test[] = "\xb2\xe2\xca\xd4\xd6\xd0\xce\xc4";
|
|
Log::Comment(L"Write string using printf.");
|
|
printf("%s\n", test);
|
|
|
|
// Prepare structures for readback.
|
|
COORD coordReadPos = { 0 };
|
|
const auto cchReadBack = (DWORD)strlen(test);
|
|
const auto psReadBack = wil::make_unique_failfast<char[]>(cchReadBack + 1);
|
|
DWORD dwRead = 0;
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, psReadBack.get(), cchReadBack, coordReadPos, &dwRead), L"Read back printf line.");
|
|
VERIFY_ARE_EQUAL(cchReadBack, dwRead, L"We should have read as many characters as we expected (length of original printed line.)");
|
|
VERIFY_ARE_EQUAL(String(test), String(psReadBack.get()), L"String should match what we wrote.");
|
|
|
|
// Clean up and move down a line for next test.
|
|
ZeroMemory(psReadBack.get(), cchReadBack);
|
|
dwRead = 0;
|
|
coordReadPos.Y++;
|
|
|
|
Log::Comment(L"Write string using std::cout.");
|
|
std::cout << test << std::endl;
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, psReadBack.get(), cchReadBack, coordReadPos, &dwRead), L"Read back std::cout line.");
|
|
VERIFY_ARE_EQUAL(cchReadBack, dwRead, L"We should have read as many characters as we expected (length of original printed line.)");
|
|
VERIFY_ARE_EQUAL(String(test), String(psReadBack.get()), L"String should match what we wrote.");
|
|
}
|
|
|
|
// Read/WriteConsoleOutput allow a user to implement a restricted form of buffer "backup" and "restore".
|
|
// But what if the saved region clips ("bisects") a wide character? This test ensures that we restore proper
|
|
// wide characters when given an unpaired trailing/leading CHAR_INFO in the first/last column of the given region.
|
|
// In other words, writing a trailing CHAR_INFO will also automatically write a leading CHAR_INFO in the preceding cell.
|
|
void DbcsTests::TestDbcsBackupRestore()
|
|
{
|
|
static_assert(PrepPattern::DoubledW.size() == 16);
|
|
|
|
const auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
|
|
// We backup/restore 2 lines at once to ensure that it works even then. After all, an incorrect implementation
|
|
// might ignore all but the absolutely first CHAR_INFO instead of handling the first CHAR_INFO *on each row*.
|
|
std::array<CHAR_INFO, 32> expected;
|
|
std::ranges::copy(PrepPattern::DoubledW, expected.begin() + 0);
|
|
std::ranges::copy(PrepPattern::DoubledW, expected.begin() + 16);
|
|
|
|
PrepPattern::replaceColorPlaceholders(expected, FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_GREEN);
|
|
|
|
// DoubledW will show up like this in the top/left corner of the terminal:
|
|
// +----------------
|
|
// |QいかなZYXWVUTに
|
|
// |QいかなZYXWVUTに
|
|
//
|
|
// Since those 4 Japanese characters probably aren't going to be monospace for you in your editor
|
|
// (as they most likely aren't exactly 2 ASCII characters wide), I'll continue referring to them like this:
|
|
// +----------------
|
|
// |QaabbccZYXWVUTdd
|
|
// |QaabbccZYXWVUTdd
|
|
{
|
|
SMALL_RECT region{ 0, 0, 15, 1 };
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, expected.data(), { 16, 2 }, {}, ®ion));
|
|
}
|
|
|
|
// Make a "backup" of the viewport. The twist is that our backup region only
|
|
// copies the trailing/leading half of the first/last glyph respectively like so:
|
|
// +----------------
|
|
// | abbccZYXWVUTd
|
|
std::array<CHAR_INFO, 26> backup{};
|
|
constexpr COORD backupSize{ 13, 2 };
|
|
SMALL_RECT backupRegion{ 2, 0, 14, 1 };
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, backup.data(), backupSize, {}, &backupRegion));
|
|
|
|
// Destroy the text with some narrow ASCII characters, resulting in:
|
|
// +----------------
|
|
// |Qxxxxxxxxxxxxxxx
|
|
// |Qxxxxxxxxxxxxxxx
|
|
{
|
|
DWORD ignored;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(out, L'x', 15, { 1, 0 }, &ignored));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(out, L'x', 15, { 1, 1 }, &ignored));
|
|
}
|
|
|
|
// Restore our "backup". The trailing half of the first wide glyph (indicated as "a" above)
|
|
// as well as the leading half of the last wide glyph ("d"), will automatically get a
|
|
// matching leading/trailing half respectively. In other words, this:
|
|
// +----------------
|
|
// | abbccZYXWVUTd
|
|
// | abbccZYXWVUTd
|
|
//
|
|
// turns into this:
|
|
// +----------------
|
|
// | aabbccZYXWVUTdd
|
|
// | aabbccZYXWVUTdd
|
|
//
|
|
// and so we restore this, overwriting all the "x" characters in the process:
|
|
// +----------------
|
|
// |QいかなZYXWVUTに
|
|
// |QいかなZYXWVUTに
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, backup.data(), backupSize, {}, &backupRegion));
|
|
|
|
std::array<CHAR_INFO, 32> infos{};
|
|
{
|
|
SMALL_RECT region{ 0, 0, 15, 1 };
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, infos.data(), { 16, 2 }, {}, ®ion));
|
|
}
|
|
DbcsWriteRead::Verify(expected, infos);
|
|
}
|
|
|
|
// As tested by TestDbcsBackupRestore(), we do want to allow users to write trailers into the buffer, to allow
|
|
// for an area of the buffer to be backed up and restored via Read/WriteConsoleOutput. But apart from that use
|
|
// case, we'd generally do best to avoid trailers whenever possible, as conhost basically ignored them in the
|
|
// past and only rendered leaders. Applications might now be relying on us effectively ignoring trailers.
|
|
void DbcsTests::TestInvalidTrailer()
|
|
{
|
|
auto expected = PrepPattern::DoubledW;
|
|
auto input = expected;
|
|
decltype(input) output{};
|
|
|
|
for (auto& v : input)
|
|
{
|
|
if (WI_IsFlagSet(v.Attributes, COMMON_LVB_TRAILING_BYTE))
|
|
{
|
|
v.Char.UnicodeChar = 0xfffd;
|
|
}
|
|
}
|
|
|
|
{
|
|
static constexpr COORD bufferSize{ 16, 1 };
|
|
SMALL_RECT region{ 0, 0, 15, 0 };
|
|
const auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, input.data(), bufferSize, {}, ®ion));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, output.data(), bufferSize, {}, ®ion));
|
|
}
|
|
|
|
DbcsWriteRead::Verify(expected, output);
|
|
}
|
|
|
|
// The various console APIs that read back from the buffer are generally incompatible with UTF16 and surrogate pairs.
|
|
// ReadConsoleOutputCharacterW in particular has a nLength parameter which is a column count but also the buffer size.
|
|
// This makes it impossible to reliably return arbitrarily long graphemes per-cell in the output buffer.
|
|
// The test ensures that we replace them with U+FFFD which makes the behavior more consistent for the caller.
|
|
void DbcsTests::TestNarrowSurrogate()
|
|
{
|
|
const auto out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
wchar_t buf[3];
|
|
DWORD read;
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputCharacterW(out, L"a\U00010000b", 4, {}, &read));
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterW(out, &buf[0], ARRAYSIZE(buf), {}, &read));
|
|
VERIFY_ARE_EQUAL(std::wstring_view(L"a\U0000FFFDb"), std::wstring_view(&buf[0], read));
|
|
}
|