Files
eepp/src/tests/unit_tests/stringsoperations_tests.cpp
Martín Lucas Golini 95d52cdbb3 eepp:
- add reusable-buffer String::toUtf8() conversion
  - extract shared find-bar CSS into UIFindBarStyle
  - reuse the shared style from UIDocFindReplace

  ecode:
  - enable terminal find, next, and previous shortcuts
  - install terminal bindings after conflicting editor bindings are removed

  eterm:
  - add searchable terminal history with plain text, RegEx, and LuaPattern modes
  - support case-sensitive and whole-word matching across wrapped rows
  - debounce searches and avoid unnecessary string allocations
  - add an animated find bar with status and wrapped result navigation
  - expose find commands through configurable keybindings and the context menu
  - render and navigate search matches in terminal history
  - forward right-button releases when applications capture the mouse
  - fix UITabWidgetSplitter ownership during shutdown

  tests:
  - cover UTF-8 buffer reuse and terminal search behavior
  - verify wrapped lines, Unicode, pattern modes, invalid expressions, and navigation
  - verify SGR right-button release encoding

  tooling:
  - clarify the mandatory optimized build workflow for performance work
  - require direct release test execution before falling back to Xvfb
2026-09-10 00:57:56 -03:00

399 lines
12 KiB
C++

#include "utest.h"
#include <cstdlib>
#include <eepp/core/string.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <filesystem>
using namespace std::literals;
using namespace EE;
using namespace EE::System;
namespace {
class ScopedEnvironmentVariable {
public:
ScopedEnvironmentVariable( const char* name, const std::string& value ) : mName( name ) {
if ( const char* currentValue = std::getenv( name ) ) {
mHadValue = true;
mValue = currentValue;
}
set( value.c_str() );
}
~ScopedEnvironmentVariable() {
if ( mHadValue )
set( mValue.c_str() );
else
unset();
}
private:
void set( const char* value ) {
#if EE_PLATFORM == EE_PLATFORM_WIN
_putenv_s( mName, value );
#else
setenv( mName, value, 1 );
#endif
}
void unset() {
#if EE_PLATFORM == EE_PLATFORM_WIN
_putenv_s( mName, "" );
#else
unsetenv( mName );
#endif
}
const char* mName;
std::string mValue;
bool mHadValue{ false };
};
class ScopedTestDirectory {
public:
explicit ScopedTestDirectory( std::filesystem::path path ) : mPath( std::move( path ) ) {}
~ScopedTestDirectory() { FileSystem::dirRemoveAll( mPath.string() ); }
private:
std::filesystem::path mPath;
};
} // namespace
UTEST( String, countLines ) {
EXPECT_EQ( static_cast<size_t>( 0 ), String::countLines( "" ) );
EXPECT_EQ( static_cast<size_t>( 1 ), String::countLines( "A" ) );
EXPECT_EQ( static_cast<size_t>( 2 ), String::countLines( "A\n" ) );
EXPECT_EQ( static_cast<size_t>( 2 ), String::countLines( "A\nB" ) );
EXPECT_EQ( static_cast<size_t>( 3 ), String::countLines( "A\nB\n" ) );
EXPECT_EQ( static_cast<size_t>( 2 ), String::countLines( "\n" ) );
EXPECT_EQ( static_cast<size_t>( 3 ), String::countLines( "\n\n" ) );
}
UTEST( String, fromStringView ) {
const std::string values = "1234.5px";
Float floatValue = 0;
EXPECT_TRUE( String::fromString( floatValue, std::string_view( values ).substr( 0, 6 ) ) );
EXPECT_NEAR( 1234.5f, floatValue, 0.0001f );
EXPECT_FALSE( String::fromString( floatValue, std::string_view( values ) ) );
Int32 intValue = 0;
EXPECT_TRUE( String::fromString( intValue, std::string_view( values ).substr( 0, 4 ) ) );
EXPECT_EQ( 1234, intValue );
}
UTEST( String, reusableFormattingAndUtf8Assignment ) {
std::string formatted;
formatted.reserve( 128 );
const char* formattedStorage = formatted.data();
String::formatTo( formatted, "%s: %d", std::string_view{ "line" }, 42 );
EXPECT_STREQ( "line: 42", formatted.c_str() );
EXPECT_EQ( formattedStorage, formatted.data() );
String text;
text.reserve( 128 );
const auto* textStorage = text.getString().data();
text.assignUtf8( "áβ中" );
const std::string utf8Text = text.toUtf8();
EXPECT_STREQ( "áβ中", utf8Text.c_str() );
EXPECT_EQ( textStorage, text.getString().data() );
std::string reusableUtf8;
reusableUtf8.reserve( 128 );
const char* utf8Storage = reusableUtf8.data();
text.toUtf8( reusableUtf8 );
EXPECT_STREQ( "áβ中", reusableUtf8.c_str() );
EXPECT_EQ( utf8Storage, reusableUtf8.data() );
}
UTEST( FileSystem, fileCountLines ) {
std::string path = Sys::getTempPath() + "eepp_test_count_lines.txt";
FileSystem::fileWrite( path, "A\nB\nC" );
bool isBinary = false;
EXPECT_EQ( static_cast<size_t>( 3 ), FileSystem::fileCountLines( path, &isBinary ) );
EXPECT_FALSE( isBinary );
FileSystem::fileWrite( path, "A\nB\nC\n" );
EXPECT_EQ( static_cast<size_t>( 4 ), FileSystem::fileCountLines( path, &isBinary ) );
EXPECT_FALSE( isBinary );
// Empty file
FileSystem::fileWrite( path, "" );
EXPECT_EQ( static_cast<size_t>( 0 ), FileSystem::fileCountLines( path, &isBinary ) );
EXPECT_FALSE( isBinary );
// Binary test
std::string binaryData = "A\n";
binaryData += '\0';
binaryData += "B\n";
FileSystem::fileWrite( path, (const Uint8*)binaryData.data(), (Uint32)binaryData.size() );
EXPECT_EQ( static_cast<size_t>( 0 ), FileSystem::fileCountLines( path, &isBinary ) );
EXPECT_TRUE( isBinary );
FileSystem::fileRemove( path );
}
UTEST( Sys, whichUsesPathAndCustomSearchPaths ) {
const std::filesystem::path root = std::filesystem::path( Sys::getTempPath() ) /
( "eepp-sys-which-" + std::to_string( Sys::getProcessID() ) +
"-" + std::to_string( Sys::getTicks() ) );
ScopedTestDirectory cleanup( root );
const std::filesystem::path firstDir = root / "first";
const std::filesystem::path secondDir = root / "second";
ASSERT_TRUE( std::filesystem::create_directories( firstDir ) );
ASSERT_TRUE( std::filesystem::create_directories( secondDir ) );
#if EE_PLATFORM == EE_PLATFORM_WIN
static constexpr auto EXECUTABLE_NAME = "eepp-which-probe.EXE";
static constexpr auto LOOKUP_NAME = "eepp-which-probe";
static constexpr auto PATH_SEPARATOR = ';';
ScopedEnvironmentVariable pathExt( "PATHEXT", ".COM;.EXE;.BAT;.CMD" );
#else
static constexpr auto EXECUTABLE_NAME = "eepp-which-probe";
static constexpr auto LOOKUP_NAME = EXECUTABLE_NAME;
static constexpr auto PATH_SEPARATOR = ':';
#endif
const std::filesystem::path firstExecutable = firstDir / EXECUTABLE_NAME;
const std::filesystem::path secondExecutable = secondDir / EXECUTABLE_NAME;
ASSERT_TRUE( FileSystem::fileWrite( firstExecutable.string(), "first" ) );
ASSERT_TRUE( FileSystem::fileWrite( secondExecutable.string(), "second" ) );
#if EE_PLATFORM != EE_PLATFORM_WIN
std::error_code permissionError;
const auto executablePermissions = std::filesystem::perms::owner_exec |
std::filesystem::perms::group_exec |
std::filesystem::perms::others_exec;
std::filesystem::permissions( firstExecutable, executablePermissions,
std::filesystem::perm_options::add, permissionError );
ASSERT_FALSE( permissionError );
std::filesystem::permissions( secondExecutable, executablePermissions,
std::filesystem::perm_options::add, permissionError );
ASSERT_FALSE( permissionError );
#endif
const std::string path = firstDir.string() + PATH_SEPARATOR + secondDir.string();
ScopedEnvironmentVariable scopedPath( "PATH", path );
EXPECT_TRUE( Sys::which( LOOKUP_NAME ) == firstExecutable.string() );
EXPECT_TRUE( Sys::which( firstExecutable.string() ) == firstExecutable.string() );
EXPECT_TRUE( Sys::which( "eepp-which-missing" ).empty() );
FileSystem::fileRemove( firstExecutable.string() );
EXPECT_TRUE( Sys::which( LOOKUP_NAME ) == secondExecutable.string() );
ScopedEnvironmentVariable emptyPath( "PATH", "" );
EXPECT_TRUE( Sys::which( LOOKUP_NAME ).empty() );
EXPECT_TRUE( Sys::which( LOOKUP_NAME, { firstDir.string(), secondDir.string() } ) ==
secondExecutable.string() );
}
UTEST( String, isAscii ) {
// Empty string
EXPECT_TRUE( String::isAscii( String::View( U"" ) ) );
// Simple short ASCII string
String strAscii( "Hello World" );
EXPECT_TRUE( strAscii.isAscii() );
// String with non-ASCII at the end
String strNonAsciiEnd( "Hello world\u0080" );
EXPECT_FALSE( strNonAsciiEnd.isAscii() );
// String with non-ASCII at the beginning
String strNonAsciiBegin( "\u0080Hello world" );
EXPECT_FALSE( strNonAsciiBegin.isAscii() );
// String with non-ASCII in the middle
String strNonAsciiMid( "Hello \u0080 world" );
EXPECT_FALSE( strNonAsciiMid.isAscii() );
// Test boundary around 127
String str127;
str127 += (String::StringBaseType)127;
EXPECT_TRUE( str127.isAscii() );
String str128;
str128 += (String::StringBaseType)128;
EXPECT_FALSE( str128.isAscii() );
// Test SIMD chunk boundaries (assumed 8 elements for AVX2, 4 for NEON)
// We'll test lengths around 4, 8, 16, 32 to cover various chunk alignments
// 1. Exact chunks + 0 remainder
{
// 32 chars (4x8 AVX2, 8x4 NEON)
String longAscii( "01234567890123456789012345678901" );
EXPECT_TRUE( longAscii.isAscii() );
// 32 chars with invalid at last position 31
String longNonAscii = longAscii;
longNonAscii[31] = 129;
EXPECT_FALSE( longNonAscii.isAscii() );
// 32 chars with invalid at first position 0
longNonAscii = longAscii;
longNonAscii[0] = 129;
EXPECT_FALSE( longNonAscii.isAscii() );
}
// 2. Exact chunks + remainder
{
// 33 chars (one element remainder)
String longAscii( "01234567890123456789012345678901A" );
EXPECT_TRUE( longAscii.isAscii() );
// invalid at remainder
String longNonAscii = longAscii;
longNonAscii[32] = 130;
EXPECT_FALSE( longNonAscii.isAscii() );
}
// 3. Just below chunk size (7 chars)
{
String shortAscii( "0123456" );
EXPECT_TRUE( shortAscii.isAscii() );
String shortNonAscii = shortAscii;
shortNonAscii[6] = 131;
EXPECT_FALSE( shortNonAscii.isAscii() );
}
// Large string verification
{
String largeAscii;
for ( int i = 0; i < 1024; ++i )
largeAscii += "A";
EXPECT_TRUE( largeAscii.isAscii() );
String largeNonAscii = largeAscii;
largeNonAscii[512] = 200; // fail in the middle
EXPECT_FALSE( largeNonAscii.isAscii() );
}
}
UTEST( String, isLatin1 ) {
// Empty string
EXPECT_TRUE( String::isLatin1( String::View( U"" ) ) );
// ASCII is also Latin1
String strAscii( "Hello World" );
EXPECT_TRUE( strAscii.isLatin1() );
// Latin1 characters (128-255)
String strLatin1;
strLatin1 += (String::StringBaseType)0xFF; // 255
EXPECT_TRUE( strLatin1.isLatin1() );
// Non-Latin1 (>255)
String strNonLatin1;
strNonLatin1 += (String::StringBaseType)0x100; // 256
EXPECT_FALSE( strNonLatin1.isLatin1() );
// Boundary Check
String str255;
str255 += (String::StringBaseType)255;
EXPECT_TRUE( str255.isLatin1() );
// Complex string with Latin1 chars
String complexLatin1 =
String::fromUtf8( "Héllø Wørld"sv ); // Assuming these are in Latin1 range
// Note: 'ø' is 0xF8 (248), 'é' is 0xE9 (233). Both in Latin1.
EXPECT_TRUE( complexLatin1.isLatin1() );
// Verify SIMD paths for isLatin1 (uses same template logic but limit=255)
{
// 32 chars of 255
String longLatin1( 32, (String::StringBaseType)255 );
EXPECT_TRUE( longLatin1.isLatin1() );
}
}
UTEST( String, isAsciiHighBit ) {
// Test comparison safety (unsigned vs signed issue)
// 0x80000000 is a very large number, definitely not ASCII.
// If signed comparison was used, it might be interpreted as negative and thus < 127.
String strHigh;
strHigh += (String::StringBaseType)0x80000000;
EXPECT_FALSE( strHigh.isAscii() );
String strHigh2;
strHigh2 += (String::StringBaseType)0xFFFFFFFF;
EXPECT_FALSE( strHigh2.isAscii() );
// Mixed with ASCII
String strMixed = "Hello";
strMixed += (String::StringBaseType)0x80000000;
EXPECT_FALSE( strMixed.isAscii() );
}
UTEST( String, isAsciiPatterns ) {
// Alternating
String alt;
for ( int i = 0; i < 100; i++ ) {
alt += ( i % 2 == 0 ) ? 'a' : (char)128;
}
EXPECT_FALSE( alt.isAscii() );
// Block of invalid in middle of valid
String block( 100, 'a' );
for ( int i = 40; i < 60; i++ )
block[i] = 200;
EXPECT_FALSE( block.isAscii() );
}
UTEST( String, isLatin1HighBit ) {
String strHigh;
strHigh += (String::StringBaseType)0x80000000;
EXPECT_FALSE( strHigh.isLatin1() );
}
UTEST( String, stripAnsiCodes ) {
// 1. Basic color codes
std::string redBold = "\x1B[1;31mHello\x1B[0m";
String::stripAnsiCodes( redBold );
EXPECT_STREQ( "Hello", redBold.c_str() );
// 2. Cursor movement (CSI)
std::string clearScreen = "\x1B[2JMove";
String::stripAnsiCodes( clearScreen );
EXPECT_STREQ( "Move", clearScreen.c_str() );
// 3. No codes
std::string plain = "Just text";
String::stripAnsiCodes( plain );
EXPECT_STREQ( "Just text", plain.c_str() );
// 4. Multiple mixed codes
std::string complex = "A\x1B[32mB\x1B[33mC\x1B[0m";
String::stripAnsiCodes( complex );
EXPECT_STREQ( "ABC", complex.c_str() );
// 5. Code at end
std::string endCode = "End\x1B[K";
String::stripAnsiCodes( endCode );
EXPECT_STREQ( "End", endCode.c_str() );
// 6. Code at start
std::string startCode = "\x1B[HStart";
String::stripAnsiCodes( startCode );
EXPECT_STREQ( "Start", startCode.c_str() );
// 7. Long string (trigger SIMD paths)
std::string longStr;
std::string expected;
for ( int i = 0; i < 1000; i++ ) {
longStr += "a\x1B[31mb";
expected += "ab";
}
String::stripAnsiCodes( longStr );
EXPECT_STREQ( expected.c_str(), longStr.c_str() );
// 8. Adjacent codes
std::string adjacent = "Double\x1B[1m\x1B[31mColor";
String::stripAnsiCodes( adjacent );
EXPECT_STREQ( "DoubleColor", adjacent.c_str() );
}