Files
kte/tests/test_migration_coverage.cc
T
Claude 29d0ee8244 Remove Buffer::Rows() and the Line wrapper
The row cache rebuilt a string for every line of the file after each edit
and had one production caller left (filetype detection in the ImGui
frontend, which needs only the first line). Removed with it: rows_,
nrows_, the dirty flag set on every edit, ensure_rows_cache and a mutex
guarding only that cache. Tests read lines with GetLineString, Nrows or,
where they compare every line, a test-only LinesForTests().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
2026-09-24 12:19:59 -07:00

452 lines
12 KiB
C++

/*
* test_migration_coverage.cc - Edge case tests for Buffer::Line migration
*
* This file provides comprehensive test coverage for the migration from
* the removed Buffer::Rows() to direct PieceTable operations using Nrows(), GetLineString(),
* and GetLineView().
*
* Tests cover:
* - Edge cases: empty buffers, single lines, very long lines
* - Boundary conditions: first line, last line, out-of-bounds
* - Consistency: GetLineString vs GetLineView vs LinesForTests()
* - Performance: large files, many small operations
* - Correctness: special characters, newlines, unicode
*/
#include "Test.h"
#include "Buffer.h"
#include <string>
#include <vector>
// ============================================================================
// Edge Case Tests
// ============================================================================
TEST (Migration_EmptyBuffer_Nrows)
{
Buffer buf;
ASSERT_EQ(buf.Nrows(), (std::size_t) 1); // Empty buffer has 1 logical line
}
TEST (Migration_EmptyBuffer_GetLineString)
{
Buffer buf;
ASSERT_EQ(buf.GetLineString(0), std::string(""));
}
TEST (Migration_EmptyBuffer_GetLineView)
{
Buffer buf;
auto view = buf.GetLineView(0);
ASSERT_EQ(view.size(), (std::size_t) 0);
ASSERT_EQ(std::string(view), std::string(""));
}
TEST (Migration_SingleLine_NoNewline)
{
Buffer buf;
buf.insert_text(0, 0, std::string("hello"));
ASSERT_EQ(buf.Nrows(), (std::size_t) 1);
ASSERT_EQ(buf.GetLineString(0), std::string("hello"));
ASSERT_EQ(std::string(buf.GetLineView(0)), std::string("hello"));
}
TEST (Migration_SingleLine_WithNewline)
{
Buffer buf;
buf.insert_text(0, 0, std::string("hello\n"));
ASSERT_EQ(buf.Nrows(), (std::size_t) 2); // Line + empty line after newline
ASSERT_EQ(buf.GetLineString(0), std::string("hello"));
ASSERT_EQ(buf.GetLineString(1), std::string(""));
}
TEST (Migration_MultipleLines_TrailingNewline)
{
Buffer buf;
buf.insert_text(0, 0, std::string("line1\nline2\nline3\n"));
ASSERT_EQ(buf.Nrows(), (std::size_t) 4); // 3 lines + empty line
ASSERT_EQ(buf.GetLineString(0), std::string("line1"));
ASSERT_EQ(buf.GetLineString(1), std::string("line2"));
ASSERT_EQ(buf.GetLineString(2), std::string("line3"));
ASSERT_EQ(buf.GetLineString(3), std::string(""));
}
TEST (Migration_MultipleLines_NoTrailingNewline)
{
Buffer buf;
buf.insert_text(0, 0, std::string("line1\nline2\nline3"));
ASSERT_EQ(buf.Nrows(), (std::size_t) 3);
ASSERT_EQ(buf.GetLineString(0), std::string("line1"));
ASSERT_EQ(buf.GetLineString(1), std::string("line2"));
ASSERT_EQ(buf.GetLineString(2), std::string("line3"));
}
TEST (Migration_VeryLongLine)
{
Buffer buf;
std::string long_line(10000, 'x');
buf.insert_text(0, 0, long_line);
ASSERT_EQ(buf.Nrows(), (std::size_t) 1);
ASSERT_EQ(buf.GetLineString(0), long_line);
ASSERT_EQ(buf.GetLineString(0).size(), (std::size_t) 10000);
}
TEST (Migration_ManyEmptyLines)
{
Buffer buf;
std::string many_newlines(1000, '\n');
buf.insert_text(0, 0, many_newlines);
ASSERT_EQ(buf.Nrows(), (std::size_t) 1001); // 1000 newlines = 1001 lines
for (std::size_t i = 0; i < buf.Nrows(); ++i) {
ASSERT_EQ(buf.GetLineString(i), std::string(""));
}
}
// ============================================================================
// Consistency Tests: GetLineString vs GetLineView vs LinesForTests()
// ============================================================================
TEST (Migration_Consistency_AllMethods)
{
Buffer buf;
buf.insert_text(0, 0, std::string("abc\n123\nxyz"));
const auto &rows = buf.LinesForTests();
ASSERT_EQ(buf.Nrows(), rows.size());
for (std::size_t i = 0; i < buf.Nrows(); ++i) {
std::string via_string = buf.GetLineString(i);
std::string via_rows = std::string(rows[i]);
// GetLineString and LinesForTests() both strip newlines
ASSERT_EQ(via_string, via_rows);
// GetLineView includes the raw range (with newlines if present)
// Just verify it's accessible
(void) buf.GetLineView(i);
}
}
TEST (Migration_Consistency_AfterEdits)
{
Buffer buf;
buf.insert_text(0, 0, std::string("line1\nline2\nline3\n"));
// Edit: insert in middle
buf.insert_text(1, 2, std::string("XX"));
const auto &rows = buf.LinesForTests();
ASSERT_EQ(buf.Nrows(), rows.size());
for (std::size_t i = 0; i < buf.Nrows(); ++i) {
// GetLineString and LinesForTests() both strip newlines
ASSERT_EQ(buf.GetLineString(i), std::string(rows[i]));
}
// Edit: delete line
buf.delete_row(1);
const auto &rows2 = buf.LinesForTests();
ASSERT_EQ(buf.Nrows(), rows2.size());
for (std::size_t i = 0; i < buf.Nrows(); ++i) {
ASSERT_EQ(buf.GetLineString(i), std::string(rows2[i]));
}
}
// ============================================================================
// Boundary Tests
// ============================================================================
TEST (Migration_FirstLine_Access)
{
Buffer buf;
buf.insert_text(0, 0, std::string("first\nsecond\nthird"));
ASSERT_EQ(buf.GetLineString(0), std::string("first"));
// GetLineView includes newline: "first\n"
auto view0 = buf.GetLineView(0);
EXPECT_TRUE(view0.size() >= 5); // at least "first"
}
TEST (Migration_LastLine_Access)
{
Buffer buf;
buf.insert_text(0, 0, std::string("first\nsecond\nthird"));
std::size_t last = buf.Nrows() - 1;
ASSERT_EQ(buf.GetLineString(last), std::string("third"));
ASSERT_EQ(std::string(buf.GetLineView(last)), std::string("third"));
}
TEST (Migration_GetLineRange_Boundaries)
{
Buffer buf;
buf.insert_text(0, 0, std::string("abc\n123\nxyz"));
// First line
auto r0 = buf.GetLineRange(0);
ASSERT_EQ(r0.first, (std::size_t) 0);
ASSERT_EQ(r0.second, (std::size_t) 4); // "abc\n"
// Last line
std::size_t last = buf.Nrows() - 1;
(void) buf.GetLineRange(last); // Verify it doesn't crash
ASSERT_EQ(buf.GetLineString(last), std::string("xyz"));
}
// ============================================================================
// Special Characters and Unicode
// ============================================================================
TEST (Migration_SpecialChars_Tabs)
{
Buffer buf;
buf.insert_text(0, 0, std::string("line\twith\ttabs"));
ASSERT_EQ(buf.GetLineString(0), std::string("line\twith\ttabs"));
ASSERT_EQ(std::string(buf.GetLineView(0)), std::string("line\twith\ttabs"));
}
TEST (Migration_SpecialChars_CarriageReturn)
{
Buffer buf;
buf.insert_text(0, 0, std::string("line\rwith\rcr"));
ASSERT_EQ(buf.GetLineString(0), std::string("line\rwith\rcr"));
}
TEST (Migration_SpecialChars_NullBytes)
{
Buffer buf;
std::string with_null = "abc";
with_null.push_back('\0');
with_null += "def";
buf.insert_text(0, 0, with_null);
ASSERT_EQ(buf.GetLineString(0).size(), (std::size_t) 7);
ASSERT_EQ(buf.GetLineView(0).size(), (std::size_t) 7);
}
TEST (Migration_Unicode_BasicMultibyte)
{
Buffer buf;
std::string utf8 = "Hello 世界 🌍";
buf.insert_text(0, 0, utf8);
ASSERT_EQ(buf.GetLineString(0), utf8);
ASSERT_EQ(std::string(buf.GetLineView(0)), utf8);
}
// ============================================================================
// Large File Tests
// ============================================================================
TEST (Migration_LargeFile_10K_Lines)
{
Buffer buf;
std::string data;
for (int i = 0; i < 10000; ++i) {
data += "Line " + std::to_string(i) + "\n";
}
buf.insert_text(0, 0, data);
ASSERT_EQ(buf.Nrows(), (std::size_t) 10001); // +1 for final empty line
// Spot check some lines
ASSERT_EQ(buf.GetLineString(0), std::string("Line 0"));
ASSERT_EQ(buf.GetLineString(5000), std::string("Line 5000"));
ASSERT_EQ(buf.GetLineString(9999), std::string("Line 9999"));
ASSERT_EQ(buf.GetLineString(10000), std::string(""));
}
TEST (Migration_LargeFile_Iteration_Consistency)
{
Buffer buf;
std::string data;
for (int i = 0; i < 1000; ++i) {
data += "Line " + std::to_string(i) + "\n";
}
buf.insert_text(0, 0, data);
// Iterate with GetLineString (strips newlines, must add back)
std::string reconstructed1;
for (std::size_t i = 0; i < buf.Nrows(); ++i) {
if (i > 0) {
reconstructed1 += '\n';
}
reconstructed1 += buf.GetLineString(i);
}
// Iterate with GetLineView (includes newlines)
std::string reconstructed2;
for (std::size_t i = 0; i < buf.Nrows(); ++i) {
auto view = buf.GetLineView(i);
reconstructed2.append(view.data(), view.size());
}
// GetLineView should match original exactly
ASSERT_EQ(reconstructed2, data);
// GetLineString reconstruction should match (without final empty line)
EXPECT_TRUE(reconstructed1.size() > 0);
}
// ============================================================================
// Stress Tests: Many Small Operations
// ============================================================================
TEST (Migration_Stress_ManySmallInserts)
{
Buffer buf;
buf.insert_text(0, 0, std::string("start\n"));
for (int i = 0; i < 100; ++i) {
buf.insert_text(1, 0, std::string("x"));
}
ASSERT_EQ(buf.Nrows(), (std::size_t) 2);
ASSERT_EQ(buf.GetLineString(0), std::string("start"));
ASSERT_EQ(buf.GetLineString(1).size(), (std::size_t) 100);
// Verify consistency
const auto &rows = buf.LinesForTests();
ASSERT_EQ(buf.GetLineString(1), std::string(rows[1]));
}
TEST (Migration_Stress_ManyLineInserts)
{
Buffer buf;
for (int i = 0; i < 500; ++i) {
buf.insert_row(buf.Nrows() - 1, std::string_view("line"));
}
ASSERT_EQ(buf.Nrows(), (std::size_t) 501); // 500 + initial empty line
for (std::size_t i = 0; i < 500; ++i) {
ASSERT_EQ(buf.GetLineString(i), std::string("line"));
}
}
TEST (Migration_Stress_AlternatingInsertDelete)
{
Buffer buf;
buf.insert_text(0, 0, std::string("a\nb\nc\nd\ne\n"));
for (int i = 0; i < 50; ++i) {
std::size_t nrows = buf.Nrows();
if (nrows > 2) {
buf.delete_row(1);
}
buf.insert_row(1, std::string_view("new"));
}
// Verify consistency after many operations
const auto &rows = buf.LinesForTests();
ASSERT_EQ(buf.Nrows(), rows.size());
for (std::size_t i = 0; i < buf.Nrows(); ++i) {
// GetLineString and LinesForTests() both strip newlines
ASSERT_EQ(buf.GetLineString(i), std::string(rows[i]));
}
}
// ============================================================================
// Regression Tests: Specific Migration Scenarios
// ============================================================================
TEST (Migration_Shebang_Detection)
{
// Test the pattern used in Editor.cc for shebang detection
Buffer buf;
buf.insert_text(0, 0, std::string("#!/usr/bin/env python3\nprint('hello')"));
ASSERT_EQ(buf.Nrows(), (std::size_t) 2);
std::string first_line = "";
if (buf.Nrows() > 0) {
first_line = buf.GetLineString(0);
}
ASSERT_EQ(first_line, std::string("#!/usr/bin/env python3"));
}
TEST (Migration_EmptyBufferCheck_Pattern)
{
// Test the pattern used in Editor.cc for empty buffer detection
Buffer buf;
const std::size_t nrows = buf.Nrows();
const bool rows_empty = (nrows == 0);
const bool single_empty_line = (nrows == 1 && buf.GetLineView(0).size() == 0);
ASSERT_EQ(rows_empty, false);
ASSERT_EQ(single_empty_line, true);
}
TEST (Migration_SyntaxHighlighter_Pattern)
{
// Test the pattern used in syntax highlighters: loop Nrows(), fetch each
// line via GetLineString(row). Assert the pattern actually returns the
// right content, not just that it doesn't crash - see syntax highlighter
// correctness tests (test_syntax_highlighting.cc) for tokenization coverage.
Buffer buf;
buf.insert_text(0, 0, std::string("int main() {\n return 0;\n}"));
ASSERT_EQ(buf.Nrows(), static_cast<std::size_t>(3));
const std::vector<std::string> expected = {"int main() {", " return 0;", "}"};
for (std::size_t row = 0; row < buf.Nrows(); ++row) {
if (row >= buf.Nrows()) {
break; // Should never happen
}
std::string line = buf.GetLineString(row);
ASSERT_EQ(line, expected[row]);
}
}
TEST (Migration_SwapSnapshot_Pattern)
{
// Test the pattern used in Swap.cc for buffer snapshots
Buffer buf;
buf.insert_text(0, 0, std::string("line1\nline2\nline3\n"));
const std::size_t nrows = buf.Nrows();
std::string snapshot;
for (std::size_t i = 0; i < nrows; ++i) {
auto view = buf.GetLineView(i);
snapshot.append(view.data(), view.size());
}
EXPECT_TRUE(snapshot.size() > 0);
ASSERT_EQ(snapshot, std::string("line1\nline2\nline3\n"));
}