test(vt): cover wide-pair wrap and ICH/DCH repair branches
Add five table cases to TestWideAndZeroWidthRunes covering branches added during self-review that had no direct coverage: - wrap abandoning a continuation cell: asserts both halves of the pair are cleared and the damage rect extends left over the orphaned wide half. - ICH at a continuation splitting a pair mid-row; ICH pushing a pair's continuation off the right edge. - DCH landing on each half of a pair, asserting repairRow blanks orphaned halves and damage extends one column left on a left-half split. RED verified by temporarily neutralizing the cx damage extension, the repairRow calls, and the leftSplit dmgX extension: all five cases fail; restored code passes.
- commit
- 4ac84f440a4482d201a19ff80c21fb229899cf14
- author
- brice · · 2026-07-02 09:45 UTC
- parent
- e3beccd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +130 -0
M pkg/vt/screen_test.go +130 -0
| @@ -1462,6 +1462,136 @@ func TestWideAndZeroWidthRunes(t *testing.T) { | |||
| 1462 | 1462 | assertCell(t, s, 3, 0, 'B') | |
| 1463 | 1463 | }, | |
| 1464 | 1464 | }, | |
| 1465 | + | { | |
| 1466 | + | name: "wrap abandoning a continuation cell clears its pair and extends damage", | |
| 1467 | + | run: func(t *testing.T) { | |
| 1468 | + | s := NewScreen(4, 2) | |
| 1469 | + | s.Write([]byte("AB你")) // A(0) B(1) 你(2) cont(3) | |
| 1470 | + | s.Write([]byte("\x1b[1;4H")) // cursor to col 3: the continuation cell | |
| 1471 | + | s.ClearDamage() | |
| 1472 | + | // A wide rune at the last column wraps; the abandoned last cell | |
| 1473 | + | // is a continuation, so its wide left half (col 2) must be | |
| 1474 | + | // cleared too and the damage must span both columns. | |
| 1475 | + | s.Write([]byte("好")) | |
| 1476 | + | ||
| 1477 | + | assertCell(t, s, 0, 0, 'A') | |
| 1478 | + | assertCell(t, s, 1, 0, 'B') | |
| 1479 | + | assertBlank(t, s, 2, 0) // orphaned wide left cleared | |
| 1480 | + | assertBlank(t, s, 3, 0) // abandoned continuation cleared | |
| 1481 | + | assertCell(t, s, 0, 1, '好') | |
| 1482 | + | assertContinuation(t, s, 1, 1) | |
| 1483 | + | if s.Col != 2 || s.Row != 1 { | |
| 1484 | + | t.Errorf("cursor at col=%d row=%d, want col=2 row=1", s.Col, s.Row) | |
| 1485 | + | } | |
| 1486 | + | ||
| 1487 | + | d := s.Damage() | |
| 1488 | + | if len(d) == 0 { | |
| 1489 | + | t.Fatal("expected damage") | |
| 1490 | + | } | |
| 1491 | + | // First damage item covers the cleared pair on row 0. | |
| 1492 | + | if d[0].X != 2 || d[0].Y != 0 || d[0].Width != 2 { | |
| 1493 | + | t.Errorf("wrap damage = {X:%d Y:%d W:%d}, want {X:2 Y:0 W:2}", d[0].X, d[0].Y, d[0].Width) | |
| 1494 | + | } | |
| 1495 | + | }, | |
| 1496 | + | }, | |
| 1497 | + | { | |
| 1498 | + | name: "insert chars at a continuation splits the pair and repairs orphans", | |
| 1499 | + | run: func(t *testing.T) { | |
| 1500 | + | s := NewScreen(6, 2) | |
| 1501 | + | s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3) | |
| 1502 | + | s.Write([]byte("\x1b[1;4H")) // cursor to col 3: continuation of 好 | |
| 1503 | + | s.ClearDamage() | |
| 1504 | + | s.Write([]byte("\x1b[1@")) // ICH 1: shift right from col 3 | |
| 1505 | + | ||
| 1506 | + | // The shift splits 好/cont: 好 stays at col 2, its continuation | |
| 1507 | + | // moves to col 4. Both orphans must be repaired to blanks. | |
| 1508 | + | assertCell(t, s, 0, 0, '你') | |
| 1509 | + | assertContinuation(t, s, 1, 0) | |
| 1510 | + | assertBlank(t, s, 2, 0) // 好 orphaned by the split → cleared | |
| 1511 | + | assertBlank(t, s, 3, 0) // inserted blank | |
| 1512 | + | assertBlank(t, s, 4, 0) // shifted continuation orphaned → cleared | |
| 1513 | + | assertBlank(t, s, 5, 0) | |
| 1514 | + | ||
| 1515 | + | d := s.Damage() | |
| 1516 | + | if len(d) == 0 { | |
| 1517 | + | t.Fatal("expected damage") | |
| 1518 | + | } | |
| 1519 | + | // Damage must extend one column left to cover the repaired 好. | |
| 1520 | + | if d[0].X != 2 || d[0].Width != 4 { | |
| 1521 | + | t.Errorf("ICH damage = {X:%d W:%d}, want {X:2 W:4}", d[0].X, d[0].Width) | |
| 1522 | + | } | |
| 1523 | + | }, | |
| 1524 | + | }, | |
| 1525 | + | { | |
| 1526 | + | name: "insert chars shifting a wide pair off the right edge repairs the left half", | |
| 1527 | + | run: func(t *testing.T) { | |
| 1528 | + | s := NewScreen(4, 2) | |
| 1529 | + | s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3) | |
| 1530 | + | s.Write([]byte("\x1b[1;1H")) // cursor home | |
| 1531 | + | s.Write([]byte("\x1b[1@")) // ICH 1: shift the row right by 1 | |
| 1532 | + | ||
| 1533 | + | // 好 lands on the last column with its continuation pushed off | |
| 1534 | + | // the edge; the orphaned wide left must be blanked. | |
| 1535 | + | assertBlank(t, s, 0, 0) | |
| 1536 | + | assertCell(t, s, 1, 0, '你') | |
| 1537 | + | assertContinuation(t, s, 2, 0) | |
| 1538 | + | assertBlank(t, s, 3, 0) // 好 lost its continuation → cleared | |
| 1539 | + | }, | |
| 1540 | + | }, | |
| 1541 | + | { | |
| 1542 | + | name: "delete char at the left half of a wide pair repairs the orphan", | |
| 1543 | + | run: func(t *testing.T) { | |
| 1544 | + | s := NewScreen(6, 2) | |
| 1545 | + | s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5) | |
| 1546 | + | s.Write([]byte("\x1b[1;1H")) // cursor to col 0: wide left of 你 | |
| 1547 | + | s.ClearDamage() | |
| 1548 | + | s.Write([]byte("\x1b[1P")) // DCH 1 | |
| 1549 | + | ||
| 1550 | + | // 你's continuation shifts to col 0 with no left half → repaired. | |
| 1551 | + | assertBlank(t, s, 0, 0) | |
| 1552 | + | assertCell(t, s, 1, 0, '好') | |
| 1553 | + | assertContinuation(t, s, 2, 0) | |
| 1554 | + | assertCell(t, s, 3, 0, 'A') | |
| 1555 | + | assertCell(t, s, 4, 0, 'B') | |
| 1556 | + | assertBlank(t, s, 5, 0) | |
| 1557 | + | ||
| 1558 | + | d := s.Damage() | |
| 1559 | + | if len(d) == 0 { | |
| 1560 | + | t.Fatal("expected damage") | |
| 1561 | + | } | |
| 1562 | + | if d[0].X != 0 || d[0].Width != 6 { | |
| 1563 | + | t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width) | |
| 1564 | + | } | |
| 1565 | + | }, | |
| 1566 | + | }, | |
| 1567 | + | { | |
| 1568 | + | name: "delete char at a continuation repairs the wide left and extends damage", | |
| 1569 | + | run: func(t *testing.T) { | |
| 1570 | + | s := NewScreen(6, 2) | |
| 1571 | + | s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5) | |
| 1572 | + | s.Write([]byte("\x1b[1;2H")) // cursor to col 1: continuation of 你 | |
| 1573 | + | s.ClearDamage() | |
| 1574 | + | s.Write([]byte("\x1b[1P")) // DCH 1: delete the continuation | |
| 1575 | + | ||
| 1576 | + | // 你 at col 0 loses its continuation → repaired to blank. | |
| 1577 | + | assertBlank(t, s, 0, 0) | |
| 1578 | + | assertCell(t, s, 1, 0, '好') | |
| 1579 | + | assertContinuation(t, s, 2, 0) | |
| 1580 | + | assertCell(t, s, 3, 0, 'A') | |
| 1581 | + | assertCell(t, s, 4, 0, 'B') | |
| 1582 | + | assertBlank(t, s, 5, 0) | |
| 1583 | + | ||
| 1584 | + | d := s.Damage() | |
| 1585 | + | if len(d) == 0 { | |
| 1586 | + | t.Fatal("expected damage") | |
| 1587 | + | } | |
| 1588 | + | // Damage must extend one column left (to col 0) to cover the | |
| 1589 | + | // repaired wide left half. | |
| 1590 | + | if d[0].X != 0 || d[0].Width != 6 { | |
| 1591 | + | t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width) | |
| 1592 | + | } | |
| 1593 | + | }, | |
| 1594 | + | }, | |
| 1465 | 1595 | { | |
| 1466 | 1596 | name: "wide char on a width-1 screen degrades to a single cell", | |
| 1467 | 1597 | run: func(t *testing.T) { | |
| @@ -1462,6 +1462,136 @@ func TestWideAndZeroWidthRunes(t *testing.T) { | |||||
| 1462 | assertCell(t, s, 3, 0, 'B') | 1462 | assertCell(t, s, 3, 0, 'B') | ||
| 1463 | }, | 1463 | }, | ||
| 1464 | }, | 1464 | }, | ||
| 1465 | + | { | |||
| 1466 | + | name: "wrap abandoning a continuation cell clears its pair and extends damage", | |||
| 1467 | + | run: func(t *testing.T) { | |||
| 1468 | + | s := NewScreen(4, 2) | |||
| 1469 | + | s.Write([]byte("AB你")) // A(0) B(1) 你(2) cont(3) | |||
| 1470 | + | s.Write([]byte("\x1b[1;4H")) // cursor to col 3: the continuation cell | |||
| 1471 | + | s.ClearDamage() | |||
| 1472 | + | // A wide rune at the last column wraps; the abandoned last cell | |||
| 1473 | + | // is a continuation, so its wide left half (col 2) must be | |||
| 1474 | + | // cleared too and the damage must span both columns. | |||
| 1475 | + | s.Write([]byte("好")) | |||
| 1476 | + | ||||
| 1477 | + | assertCell(t, s, 0, 0, 'A') | |||
| 1478 | + | assertCell(t, s, 1, 0, 'B') | |||
| 1479 | + | assertBlank(t, s, 2, 0) // orphaned wide left cleared | |||
| 1480 | + | assertBlank(t, s, 3, 0) // abandoned continuation cleared | |||
| 1481 | + | assertCell(t, s, 0, 1, '好') | |||
| 1482 | + | assertContinuation(t, s, 1, 1) | |||
| 1483 | + | if s.Col != 2 || s.Row != 1 { | |||
| 1484 | + | t.Errorf("cursor at col=%d row=%d, want col=2 row=1", s.Col, s.Row) | |||
| 1485 | + | } | |||
| 1486 | + | ||||
| 1487 | + | d := s.Damage() | |||
| 1488 | + | if len(d) == 0 { | |||
| 1489 | + | t.Fatal("expected damage") | |||
| 1490 | + | } | |||
| 1491 | + | // First damage item covers the cleared pair on row 0. | |||
| 1492 | + | if d[0].X != 2 || d[0].Y != 0 || d[0].Width != 2 { | |||
| 1493 | + | t.Errorf("wrap damage = {X:%d Y:%d W:%d}, want {X:2 Y:0 W:2}", d[0].X, d[0].Y, d[0].Width) | |||
| 1494 | + | } | |||
| 1495 | + | }, | |||
| 1496 | + | }, | |||
| 1497 | + | { | |||
| 1498 | + | name: "insert chars at a continuation splits the pair and repairs orphans", | |||
| 1499 | + | run: func(t *testing.T) { | |||
| 1500 | + | s := NewScreen(6, 2) | |||
| 1501 | + | s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3) | |||
| 1502 | + | s.Write([]byte("\x1b[1;4H")) // cursor to col 3: continuation of 好 | |||
| 1503 | + | s.ClearDamage() | |||
| 1504 | + | s.Write([]byte("\x1b[1@")) // ICH 1: shift right from col 3 | |||
| 1505 | + | ||||
| 1506 | + | // The shift splits 好/cont: 好 stays at col 2, its continuation | |||
| 1507 | + | // moves to col 4. Both orphans must be repaired to blanks. | |||
| 1508 | + | assertCell(t, s, 0, 0, '你') | |||
| 1509 | + | assertContinuation(t, s, 1, 0) | |||
| 1510 | + | assertBlank(t, s, 2, 0) // 好 orphaned by the split → cleared | |||
| 1511 | + | assertBlank(t, s, 3, 0) // inserted blank | |||
| 1512 | + | assertBlank(t, s, 4, 0) // shifted continuation orphaned → cleared | |||
| 1513 | + | assertBlank(t, s, 5, 0) | |||
| 1514 | + | ||||
| 1515 | + | d := s.Damage() | |||
| 1516 | + | if len(d) == 0 { | |||
| 1517 | + | t.Fatal("expected damage") | |||
| 1518 | + | } | |||
| 1519 | + | // Damage must extend one column left to cover the repaired 好. | |||
| 1520 | + | if d[0].X != 2 || d[0].Width != 4 { | |||
| 1521 | + | t.Errorf("ICH damage = {X:%d W:%d}, want {X:2 W:4}", d[0].X, d[0].Width) | |||
| 1522 | + | } | |||
| 1523 | + | }, | |||
| 1524 | + | }, | |||
| 1525 | + | { | |||
| 1526 | + | name: "insert chars shifting a wide pair off the right edge repairs the left half", | |||
| 1527 | + | run: func(t *testing.T) { | |||
| 1528 | + | s := NewScreen(4, 2) | |||
| 1529 | + | s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3) | |||
| 1530 | + | s.Write([]byte("\x1b[1;1H")) // cursor home | |||
| 1531 | + | s.Write([]byte("\x1b[1@")) // ICH 1: shift the row right by 1 | |||
| 1532 | + | ||||
| 1533 | + | // 好 lands on the last column with its continuation pushed off | |||
| 1534 | + | // the edge; the orphaned wide left must be blanked. | |||
| 1535 | + | assertBlank(t, s, 0, 0) | |||
| 1536 | + | assertCell(t, s, 1, 0, '你') | |||
| 1537 | + | assertContinuation(t, s, 2, 0) | |||
| 1538 | + | assertBlank(t, s, 3, 0) // 好 lost its continuation → cleared | |||
| 1539 | + | }, | |||
| 1540 | + | }, | |||
| 1541 | + | { | |||
| 1542 | + | name: "delete char at the left half of a wide pair repairs the orphan", | |||
| 1543 | + | run: func(t *testing.T) { | |||
| 1544 | + | s := NewScreen(6, 2) | |||
| 1545 | + | s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5) | |||
| 1546 | + | s.Write([]byte("\x1b[1;1H")) // cursor to col 0: wide left of 你 | |||
| 1547 | + | s.ClearDamage() | |||
| 1548 | + | s.Write([]byte("\x1b[1P")) // DCH 1 | |||
| 1549 | + | ||||
| 1550 | + | // 你's continuation shifts to col 0 with no left half → repaired. | |||
| 1551 | + | assertBlank(t, s, 0, 0) | |||
| 1552 | + | assertCell(t, s, 1, 0, '好') | |||
| 1553 | + | assertContinuation(t, s, 2, 0) | |||
| 1554 | + | assertCell(t, s, 3, 0, 'A') | |||
| 1555 | + | assertCell(t, s, 4, 0, 'B') | |||
| 1556 | + | assertBlank(t, s, 5, 0) | |||
| 1557 | + | ||||
| 1558 | + | d := s.Damage() | |||
| 1559 | + | if len(d) == 0 { | |||
| 1560 | + | t.Fatal("expected damage") | |||
| 1561 | + | } | |||
| 1562 | + | if d[0].X != 0 || d[0].Width != 6 { | |||
| 1563 | + | t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width) | |||
| 1564 | + | } | |||
| 1565 | + | }, | |||
| 1566 | + | }, | |||
| 1567 | + | { | |||
| 1568 | + | name: "delete char at a continuation repairs the wide left and extends damage", | |||
| 1569 | + | run: func(t *testing.T) { | |||
| 1570 | + | s := NewScreen(6, 2) | |||
| 1571 | + | s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5) | |||
| 1572 | + | s.Write([]byte("\x1b[1;2H")) // cursor to col 1: continuation of 你 | |||
| 1573 | + | s.ClearDamage() | |||
| 1574 | + | s.Write([]byte("\x1b[1P")) // DCH 1: delete the continuation | |||
| 1575 | + | ||||
| 1576 | + | // 你 at col 0 loses its continuation → repaired to blank. | |||
| 1577 | + | assertBlank(t, s, 0, 0) | |||
| 1578 | + | assertCell(t, s, 1, 0, '好') | |||
| 1579 | + | assertContinuation(t, s, 2, 0) | |||
| 1580 | + | assertCell(t, s, 3, 0, 'A') | |||
| 1581 | + | assertCell(t, s, 4, 0, 'B') | |||
| 1582 | + | assertBlank(t, s, 5, 0) | |||
| 1583 | + | ||||
| 1584 | + | d := s.Damage() | |||
| 1585 | + | if len(d) == 0 { | |||
| 1586 | + | t.Fatal("expected damage") | |||
| 1587 | + | } | |||
| 1588 | + | // Damage must extend one column left (to col 0) to cover the | |||
| 1589 | + | // repaired wide left half. | |||
| 1590 | + | if d[0].X != 0 || d[0].Width != 6 { | |||
| 1591 | + | t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width) | |||
| 1592 | + | } | |||
| 1593 | + | }, | |||
| 1594 | + | }, | |||
| 1465 | { | 1595 | { | ||
| 1466 | name: "wide char on a width-1 screen degrades to a single cell", | 1596 | name: "wide char on a width-1 screen degrades to a single cell", | ||
| 1467 | run: func(t *testing.T) { | 1597 | run: func(t *testing.T) { | ||