Add grid tests for multicell integrity after insertions/deletions

Multicells currently don't get any special treatment when inserting
or deleting rows or columns so neither a multicell's main size nor
inside cells' sizes (which are offsets to the main cell) are updated.
Most tests fail and will be fixed by the next commit.

See #4238.
This commit is contained in:
Dimitri Schoolwerth
2021-01-23 23:23:23 +01:00
parent 04a3dda5c5
commit d282ccf696

View File

@@ -31,6 +31,60 @@
namespace
{
wxString CellCoordsToString(int row, int col)
{
return wxString::Format("R%dC%d", col + 1, row + 1);
}
wxString CellSizeToString(int rows, int cols)
{
return wxString::Format("%dx%d", rows, cols);
}
struct Multicell
{
int row, col, rows, cols;
Multicell(int row, int col, int rows, int cols)
: row(row), col(col), rows(rows), cols(cols) { }
wxString Coords() const { return CellCoordsToString(row, col); }
wxString Size() const { return CellSizeToString(rows, cols); }
wxString ToString() const
{
wxString s;
if ( rows == 1 && cols == 1 )
{
s = "cell";
}
else
{
s = Size() + " ";
if ( rows > 1 || cols > 1 )
s += "multicell";
else
s += "inside cell";
}
return wxString::Format("%s at %s", s, Coords());
}
};
// Stores insertion/deletion info of rows or columns.
struct EditInfo
{
int pos, count;
wxGridDirection direction;
EditInfo(int pos = 0,
int count = 0,
wxGridDirection direction = wxGRID_COLUMN)
: pos(pos), count(count), direction(direction) { }
};
// Derive a new class inheriting from wxGrid, also to get access to its
// protected GetCellAttr(). This is not pretty, but we don't have any other way
// of testing this function.
@@ -47,7 +101,8 @@ public:
return GetCellAttr(row, col);
}
bool HasAttr(int row, int col, wxGridCellAttr::wxAttrKind kind) const
bool HasAttr(int row, int col,
wxGridCellAttr::wxAttrKind kind = wxGridCellAttr::Cell) const
{
// Can't use GetCellAttr() here as it always returns an attr.
wxGridCellAttr* attr = GetTable()->GetAttr(row, col, kind);
@@ -66,16 +121,95 @@ public:
for ( int row = 0; row < GetNumberRows(); ++row )
{
for ( int col = 0; col < GetNumberCols(); ++col )
count += HasAttr(row, col, wxGridCellAttr::Cell);
count += HasAttr(row, col);
}
return count;
}
void SetMulticell(const Multicell& multi)
{
SetCellSize(multi.row, multi.col, multi.rows, multi.cols);
}
// Performs given insertions/deletions on either rows or columns.
void DoEdit(const EditInfo& edit);
// Returns annotated grid represented as a string.
wxString ToString() const;
// Used when drawing annotated grid to know what happens to it.
EditInfo m_edit;
// Grid as string before editing, with edit info annotated.
wxString m_beforeGridAnnotated;
};
// Compares two grids, checking for differences with attribute presence and
// cell sizes.
class GridAttrMatcher : public Catch::MatcherBase<TestableGrid>
{
public:
GridAttrMatcher(const TestableGrid& grid);
bool match(const TestableGrid& other) const wxOVERRIDE;
std::string describe() const wxOVERRIDE;
private:
const TestableGrid* m_grid;
mutable wxString m_diffDesc;
mutable wxString m_expectedGridDesc;
};
// Helper function for recreating a grid to fit (only) a multicell.
void FitGridToMulticell(TestableGrid* grid, const Multicell& multi)
{
const int oldRowCount = grid->GetNumberRows();
const int oldColCount = grid->GetNumberCols();
const int margin = 1;
const int newRowCount = multi.row + multi.rows + margin;
const int newColCount = multi.col + multi.cols + margin;
if ( !oldRowCount && !oldColCount )
{
grid->CreateGrid(newRowCount, newColCount);
}
else
{
grid->DeleteRows(0, oldRowCount);
grid->DeleteCols(0, oldColCount);
grid->AppendRows(newRowCount);
grid->AppendCols(newColCount);
}
}
} // anonymous namespace
namespace Catch
{
template <> struct StringMaker<TestableGrid>
{
static std::string convert(const TestableGrid& grid)
{
return ("Content before edit:\n" + grid.m_beforeGridAnnotated
+ "\nContent after edit:\n" + grid.ToString()).ToStdString();
}
};
template <> struct StringMaker<Multicell>
{
static std::string convert(const Multicell& multi)
{
return multi.ToString().ToStdString();
}
};
} // namespace Catch
class GridTestCase
{
public:
@@ -83,6 +217,26 @@ public:
~GridTestCase();
protected:
void InsertRows(int pos = 0, int count = 1)
{
m_grid->DoEdit(EditInfo(pos, count, wxGRID_ROW));
}
void InsertCols(int pos = 0, int count = 1)
{
m_grid->DoEdit(EditInfo(pos, count, wxGRID_COLUMN));
}
void DeleteRows(int pos = 0, int count = 1)
{
m_grid->DoEdit(EditInfo(pos, -count, wxGRID_ROW));
}
void DeleteCols(int pos = 0, int count = 1)
{
m_grid->DoEdit(EditInfo(pos, -count, wxGRID_COLUMN));
}
// The helper function to determine the width of the column label depending
// on whether the native column header is used.
int GetColumnLabelWidth(wxClientDC& dc, int col, int margin) const
@@ -142,12 +296,52 @@ protected:
m_grid->SetAttr(row, col, new wxGridCellAttr);
}
// Fills temp. grid with a multicell and returns a matcher with it.
GridAttrMatcher HasMulticellOnly(const Multicell& multi)
{
return CheckMulticell(multi);
}
// Returns a matcher with empty (temp.) grid.
GridAttrMatcher HasEmptyGrid()
{
return CheckMulticell(Multicell(0, 0, 1, 1));
}
// Helper function used by the previous two functions.
GridAttrMatcher CheckMulticell(const Multicell& multi)
{
TestableGrid* grid = GetTempGrid();
FitGridToMulticell(grid, multi);
if ( multi.rows > 1 || multi.cols > 1 )
grid->SetMulticell(multi);
return GridAttrMatcher(*grid);
}
TestableGrid* GetTempGrid()
{
if ( !m_tempGrid )
{
m_tempGrid = new TestableGrid(wxTheApp->GetTopWindow());
m_tempGrid->Hide();
}
return m_tempGrid;
}
TestableGrid *m_grid;
// Temporary/scratch grid filled with expected content, used when
// comparing against m_grid.
TestableGrid *m_tempGrid;
wxDECLARE_NO_COPY_CLASS(GridTestCase);
};
GridTestCase::GridTestCase()
GridTestCase::GridTestCase() : m_tempGrid(NULL)
{
m_grid = new TestableGrid(wxTheApp->GetTopWindow());
m_grid->CreateGrid(10, 2);
@@ -176,6 +370,7 @@ GridTestCase::~GridTestCase()
win->ReleaseMouse();
wxDELETE(m_grid);
delete m_tempGrid;
}
TEST_CASE_METHOD(GridTestCase, "Grid::CellEdit", "[grid]")
@@ -1556,6 +1751,313 @@ TEST_CASE_METHOD(GridTestCase, "Grid::CellAttribute", "[attr][cell][grid]")
}
}
#define CHECK_MULTICELL() CHECK_THAT( *m_grid, HasMulticellOnly(multi) )
#define CHECK_NO_MULTICELL() CHECK_THAT( *m_grid, HasEmptyGrid() )
#define WHEN_N(s, n) WHEN(wxString::Format(s, n).ToStdString())
TEST_CASE_METHOD(GridTestCase,
"Grid::InsertionsWithMulticell",
"[attr][cell][grid][insert][multicell]")
{
int insertions = 0, offset = 0;
Multicell multi(1, 1, 3, 5);
SECTION("Sanity checks")
{
FitGridToMulticell(m_grid, multi);
m_grid->SetMulticell(multi);
REQUIRE( static_cast<int>(m_grid->GetCellAttrCount())
== multi.rows * multi.cols );
int row, col, rows, cols;
// Check main cell.
row = multi.row,
col = multi.col;
wxGrid::CellSpan span = m_grid->GetCellSize(row, col, &rows, &cols);
REQUIRE( span == wxGrid::CellSpan_Main );
REQUIRE( rows == multi.rows );
REQUIRE( cols == multi.cols );
// Check inside cell at opposite of main.
row = multi.row + multi.rows - 1;
col = multi.col + multi.cols - 1;
span = m_grid->GetCellSize(row, col, &rows, &cols);
REQUIRE( span == wxGrid::CellSpan_Inside );
REQUIRE( rows == multi.row - row );
REQUIRE( cols == multi.col - col );
}
// Do some basic testing with column insertions first and do more tests
// with edge cases later on just with rows. It's not really needed to
// repeat the same tests for both rows and columns as the code for
// updating them works symmetrically.
GIVEN(Catch::toString(multi))
{
FitGridToMulticell(m_grid, multi);
m_grid->SetMulticell(multi);
insertions = 2;
WHEN("inserting any columns in multicell, at main")
{
InsertCols(multi.col + 0, insertions);
THEN("the position changes but not the size")
{
multi.col += insertions;
CHECK_MULTICELL();
}
}
WHEN("inserting any columns in multicell, just after main")
{
InsertCols(multi.col + 1, insertions);
THEN("the size changes but not the position")
{
multi.cols += insertions;
CHECK_MULTICELL();
}
}
}
// Do more extensive testing with rows.
wxSwap(multi.rows, multi.cols);
GIVEN(Catch::toString(multi))
{
FitGridToMulticell(m_grid, multi);
m_grid->SetMulticell(multi);
const int insertionCounts[] = {1, 2, multi.rows};
for ( size_t i = 0; i < WXSIZEOF(insertionCounts); ++i )
{
insertions = insertionCounts[i];
WHEN_N("inserting %d row(s), just before main", insertions)
{
InsertRows(multi.row - 1, insertions);
THEN("the position changes but not the size")
{
multi.row += insertions;
CHECK_MULTICELL();
}
}
WHEN_N("inserting %d row(s) in multicell, at main", insertions)
{
InsertRows(multi.row + 0, insertions);
THEN("the position changes but not the size")
{
multi.row += insertions;
CHECK_MULTICELL();
}
}
}
insertions = multi.rows / 2;
// Check insertions within multicell, at and near edges.
const int insertionOffsets[] = {1, 2, multi.rows - 2, multi.rows - 1};
for ( size_t i = 0; i < WXSIZEOF(insertionOffsets); ++i )
{
offset = insertionOffsets[i];
WHEN_N("inserting rows in multicell, %d row(s) after main", offset)
{
InsertRows(multi.row + offset, insertions);
THEN("the size changes but not the position")
{
multi.rows += insertions;
CHECK_MULTICELL();
}
}
}
// Check at least one case of inserting after multicell.
WHEN("inserting rows, just after multicell")
{
insertions = 2;
InsertRows(multi.row + multi.rows, insertions);
THEN("neither size nor position change")
{
CHECK_MULTICELL();
}
}
}
}
TEST_CASE_METHOD(GridTestCase,
"GridMulticell::DeletionsWithMulticell",
"[cellattr][delete][grid][multicell]")
{
int deletions = 0, offset = 0;
// Same as with the previous (insertions) test case but instead of some
// basic testing with columns first, this time use rows for that and do more
// extensive testing with columns.
Multicell multi(1, 1, 5, 3);
GIVEN(Catch::toString(multi))
{
FitGridToMulticell(m_grid, multi);
m_grid->SetMulticell(multi);
WHEN("deleting any rows, at main")
{
deletions = 1;
DeleteRows(multi.row + 0, deletions);
THEN("the multicell is deleted")
{
CHECK_NO_MULTICELL();
}
}
WHEN("deleting more rows than length of multicell,"
" at end of multicell")
{
deletions = multi.rows + 2;
offset = multi.rows - 1;
DeleteRows(multi.row + offset, deletions);
THEN("the size changes but not the position")
{
multi.rows = offset;
CHECK_MULTICELL();
}
}
}
// Do more extensive testing with columns.
wxSwap(multi.rows, multi.cols);
GIVEN(Catch::toString(multi))
{
FitGridToMulticell(m_grid, multi);
m_grid->SetMulticell(multi);
WHEN("deleting one column, just before main")
{
DeleteCols(multi.col - 1);
THEN("the position changes but not the size")
{
multi.col--;
CHECK_MULTICELL();
}
}
WHEN("deleting multiple columns, just before main")
{
deletions = 2; // Must be at least 2 to affect main.
DeleteCols(multi.col - 1, wxMax(2, deletions));
THEN("the multicell is deleted")
{
CHECK_NO_MULTICELL();
}
}
WHEN("deleting any columns, at main")
{
deletions = 1;
DeleteCols(multi.col + 0, deletions);
THEN("the multicell is deleted")
{
CHECK_NO_MULTICELL();
}
}
WHEN("deleting one column within multicell, after main")
{
offset = 1;
offset = wxClip(offset, 1, multi.cols - 1);
DeleteCols(multi.col + offset, 1);
THEN("the size changes but not the position")
{
multi.cols--;
CHECK_MULTICELL();
}
}
deletions = 2;
// Check deletions within multicell, at and near edges.
const int offsets[] = {1, 2, multi.cols - deletions};
for ( size_t i = 0; i < WXSIZEOF(offsets); ++i )
{
offset = offsets[i];
WHEN_N("deleting columns only within multicell,"
" %d column(s) after main", offset)
{
DeleteCols(multi.col + offset, deletions);
THEN("the size changes but not the position")
{
multi.cols -= deletions;
CHECK_MULTICELL();
}
}
}
// Instead of stuffing the multicell's length logic in the above test,
// separately check at least two cases of starting many deletions
// within multicell.
WHEN("deleting more columns than length of multicell, just after main")
{
deletions = multi.cols + 2;
offset = 1;
DeleteCols(multi.col + offset, deletions);
THEN("the size changes but not the position")
{
multi.cols = offset;
CHECK_MULTICELL();
}
}
WHEN("deleting more columns than length of multicell,"
" at end of multicell")
{
deletions = multi.cols + 2;
offset = multi.cols - 1;
DeleteCols(multi.col + offset, deletions);
THEN("the size changes but not the position")
{
multi.cols = offset;
CHECK_MULTICELL();
}
}
}
}
// Test wxGridBlockCoords here because it'a a part of grid sources.
std::ostream& operator<<(std::ostream& os, const wxGridBlockCoords& block) {
@@ -1742,4 +2244,219 @@ TEST_CASE("GridBlockCoords::SymDifference", "[grid]")
}
}
//
// TestableGrid
//
void TestableGrid::DoEdit(const EditInfo& edit)
{
m_edit = edit;
m_beforeGridAnnotated = ToString();
switch ( edit.direction )
{
case wxGRID_COLUMN:
if ( edit.count < 0 )
DeleteCols(edit.pos, -edit.count);
else
InsertCols(edit.pos, edit.count);
break;
case wxGRID_ROW:
if ( edit.count < 0 )
DeleteRows(edit.pos, -edit.count);
else
InsertRows(edit.pos, edit.count);
break;
}
}
wxString TestableGrid::ToString() const
{
const int numRows = GetNumberRows();
const int numCols = GetNumberCols();
const int colMargin = GetRowLabelValue(numRows - 1).length();
const wxString leftIndent = wxString(' ', colMargin + 1);
// String s contains the rendering of the grid, start with drawing
// the header columns.
wxString s = leftIndent;
const int base = 10;
// Draw the multiples of 10.
for ( int col = base; col <= numCols; col += base)
{
s += wxString(' ', base - 1);
s += ('0' + (col / base));
}
if ( numCols >= base )
s += "\n" + leftIndent;
// Draw the single digits.
for ( int col = 1; col <= numCols; ++col )
s += '0' + (col % base);
s += "\n";
// Draw horizontal divider.
s += wxString(' ', colMargin) + '+' + wxString('-', numCols) + "\n";
const int absEditCount = abs(m_edit.count);
wxString action;
action.Printf(" %s: %d",
m_edit.count < 0 ? "deletions" : "insertions",
absEditCount);
// Will contain summary of grid (only multicells mentioned).
wxString content;
// Draw grid content.
for ( int row = 0; row < numRows; ++row )
{
const wxString label = GetRowLabelValue(row);
s += wxString(' ', colMargin - label.length());
s += label + '|';
for ( int col = 0; col < numCols; ++col )
{
char c = 'x';
int rows, cols;
switch ( GetCellSize(row, col, &rows, &cols) )
{
case wxGrid::CellSpan_None:
c = HasAttr(row, col) ? '*' : '.';
break;
case wxGrid::CellSpan_Main:
c = 'M';
content += Multicell(row, col, rows, cols).ToString() + "\n";
break;
case wxGrid::CellSpan_Inside:
// Check if the offset to main cell is correct.
c = (GetCellSize(row + rows, col + cols, &rows, &cols)
== wxGrid::CellSpan_Main) ? 'm' : '?';
break;
}
s += c;
}
// If applicable draw annotated row edits.
if ( m_edit.count && m_edit.direction == wxGRID_ROW
&& row >= m_edit.pos && row < m_edit.pos + absEditCount)
{
s += (m_edit.count < 0 ? " ^" : " v");
if ( row == m_edit.pos )
s += action;
}
s += "\n";
}
// Draw annotated footer if columns edited.
if ( m_edit.count && m_edit.direction == wxGRID_COLUMN )
{
s += leftIndent;
for ( int col = 0; col < m_edit.pos + absEditCount; ++col )
{
if ( col < m_edit.pos )
s += " ";
else
s += (m_edit.count < 0 ? "<" : ">");
}
s += action + "\n";
}
s += "\n";
if ( content.empty() )
content = "Empty grid\n";
return content + s;
}
//
// GridAttrMatcher
//
GridAttrMatcher::GridAttrMatcher(const TestableGrid& grid) : m_grid(&grid)
{
m_expectedGridDesc = m_grid->ToString();
}
bool GridAttrMatcher::match(const TestableGrid& other) const
{
const int rows = wxMax(m_grid->GetNumberRows(), other.GetNumberRows());
const int cols = wxMax(m_grid->GetNumberCols(), other.GetNumberCols());
for ( int row = 0; row < rows; ++row )
{
for ( int col = 0; col < cols; ++col )
{
const bool hasAttr = m_grid->HasAttr(row, col);
if ( hasAttr != other.HasAttr(row, col) )
{
m_diffDesc.Printf("%s: attribute presence (%d, expected %d)",
CellCoordsToString(row, col),
!hasAttr, hasAttr);
return false;
}
int thisRows, thisCols;
const wxGrid::CellSpan thisSpan
= m_grid->GetCellSize(row, col, &thisRows, &thisCols);
int otherRows, otherCols;
(void) other.GetCellSize(row, col, &otherRows, &otherCols);
if ( thisRows != otherRows || thisCols != otherCols )
{
wxString mismatchKind;
switch ( thisSpan )
{
case wxGrid::CellSpan_None:
mismatchKind = "different cell size";
break;
case wxGrid::CellSpan_Main:
mismatchKind = "different multicell size";
break;
case wxGrid::CellSpan_Inside:
mismatchKind = "main offset mismatch";
break;
}
m_diffDesc.Printf( "%s: %s (%s, expected %s)",
CellCoordsToString(row, col),
mismatchKind,
CellSizeToString(otherRows, otherCols),
CellSizeToString(thisRows, thisCols)
);
return false;
}
}
}
return true;
}
std::string GridAttrMatcher::describe() const
{
std::string desc = (m_diffDesc.empty() ? "Matches" : "Doesn't match");
desc += " expected content:\n" + m_expectedGridDesc.ToStdString();
if ( !m_diffDesc.empty() )
desc += + "first difference at " + m_diffDesc.ToStdString();
return desc;
}
#endif //wxUSE_GRID