Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 52 additions & 7 deletions src/XTerm.NET.Tests/ChecksumReportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,57 @@ public void A_rects_checksum_is_the_sum_of_its_characters()
}

[Fact]
public void A_cell_nothing_ever_wrote_counts_as_a_space()
public void A_run_of_blanks_counts_once_for_the_first_cell()
{
// Erased and never-written alike: DEC terminals trim trailing blanks and esctest's client
// side reasons that away, but only if blanks come back as spaces rather than zeros.
// DEC terminals trim the blanks at the end of a row rather than counting them, and the
// first cell of the area is the documented exception -- it counts whatever it holds, which
// is what lets esctest read a written space back as 0x20 one cell at a time.
var terminal = NewTerminal();

Assert.Equal(Report(2, 3 * 0x20), Reply(terminal, $"{Esc}[2;0;2;1;2;3*y"));
Assert.Equal(Report(2, 0x20), Reply(terminal, $"{Esc}[2;0;2;1;2;3*y"));
}

[Fact]
public void A_single_blank_cell_is_a_space()
{
// The shape esctest reads content back in: one cell at a time, expecting the character it
// put there. Trimming that unconditionally would answer zero for every space on screen.
var terminal = NewTerminal();

Assert.Equal(Report(8, 0x20), Reply(terminal, $"{Esc}[8;0;2;1;2;1*y"));
}

[Fact]
public void A_blank_between_two_characters_counts()
{
// Only TRAILING blanks are trimmed. One with text still to come on its row is interior,
// and vttest computes its expectation the same way.
var terminal = NewTerminal();
terminal.Write("a b");

Assert.Equal(Report(9, Sum("a b")), Reply(terminal, $"{Esc}[9;0;1;1;1;3*y"));
}

[Fact]
public void Blanks_trailing_a_row_are_trimmed()
{
// The same three cells as above with the tail cut off: 'a', a blank, and nothing after it
// on the row.
var terminal = NewTerminal();
terminal.Write("a b");

Assert.Equal(Report(10, 'a'), Reply(terminal, $"{Esc}[10;0;1;1;1;2*y"));
}

[Fact]
public void A_rows_trailing_blanks_do_not_carry_into_the_next()
{
// Trimming is per row: the blanks after "hi" end with row 1 rather than being revived by
// the "there" on row 2.
var terminal = NewTerminal(cols: 8, rows: 2);
terminal.Write($"hi{Esc}[2;1Hthere");

Assert.Equal(Report(11, Sum("hi") + Sum("there")), Reply(terminal, $"{Esc}[11*y"));
}

[Fact]
Expand All @@ -76,8 +120,9 @@ public void Coordinates_are_clamped_to_the_screen()
var terminal = NewTerminal(cols: 10, rows: 3);
terminal.Write("AB");

// A rect hanging off every edge still answers, for what the screen actually holds.
Assert.Equal(Report(4, Sum("AB") + (3 * 10 - 2) * 0x20),
// A rect hanging off every edge still answers, for what the screen actually holds -- the
// blanks after "AB" trail their row and the two rows below it, so none of them count.
Assert.Equal(Report(4, Sum("AB")),
Reply(terminal, $"{Esc}[4;0;1;1;99;99*y"));
}

Expand All @@ -87,7 +132,7 @@ public void Omitted_coordinates_mean_the_whole_screen()
var terminal = NewTerminal(cols: 4, rows: 2);
terminal.Write("hi");

Assert.Equal(Report(5, Sum("hi") + 6 * 0x20), Reply(terminal, $"{Esc}[5*y"));
Assert.Equal(Report(5, Sum("hi")), Reply(terminal, $"{Esc}[5*y"));
}

[Fact]
Expand Down
3 changes: 1 addition & 2 deletions src/XTerm.NET.Tests/Common/CsiCommandExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,9 @@ public void ToCsiCommand_BareQ_IsDecllAndReturnsUnknown()

/// <summary>
/// '&lt;' and '=' were never stripped, so they are recognised only where the map lists them --
/// the Kitty keyboard pop and set forms, and nothing else.
/// the Kitty keyboard pop and set forms, the tertiary DA, and nothing else.
/// </summary>
[Theory]
[InlineData("=c")]
[InlineData("<c")]
[InlineData("=S")]
[InlineData("<m")]
Expand Down
63 changes: 63 additions & 0 deletions src/XTerm.NET.Tests/DeviceReportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,67 @@ public void Decxcpr_ReportsThePageAndHonoursOriginMode()
terminal.Write(Esc + "[?6n");
Assert.Equal(Esc + "[?2;4;1R", replies[^1]);
}

[Fact]
public void TertiaryDa_ReportsAUnitIdOfZeros()
{
var (terminal, replies) = Create();
terminal.Write(Esc + "[=c");
Assert.Equal(Esc + "P!|00000000" + Esc + "\\", Assert.Single(replies));
}

/// <summary>
/// DECRQDE, vttest menu 11.2.5 -> 6. The window IS the page, so the corner is 1;1 and there is
/// one page; the size is the same one CSI 18 t already reports in the dtterm dialect.
/// </summary>
[Fact]
public void Decrqde_ReportsTheDisplayedExtent()
{
var (terminal, replies) = Create();
terminal.Write(Esc + "[\"v");
Assert.Equal(Esc + "[24;80;1;1;1\"w", Assert.Single(replies));
}

[Fact]
public void Decrqde_IsSilentBelowVt300()
{
var (terminal, replies) = Create();
terminal.Write(Esc + "[62\"p"); // DECSCL: VT200
replies.Clear();
terminal.Write(Esc + "[\"v");
Assert.Empty(replies);
}

/// <summary>
/// DECRQUPSS, vttest menu 11.2.5 -> 5. A UTF-8 terminal's supplemental set is ISO Latin-1,
/// a 96-character set, which is the Ps = 1 form and the designator 'A'.
/// </summary>
[Fact]
public void Decrqupss_ReportsIsoLatin1()
{
var (terminal, replies) = Create();
terminal.Write(Esc + "[&u");
Assert.Equal(Esc + "P1!uA" + Esc + "\\", Assert.Single(replies));
}

/// <summary>
/// DECRQTSR, vttest menu 11.2.5 -> 4 -> 2. There is no DECRSTS to consume a terminal state
/// report, so the answer is the invalid-request form rather than a payload nothing can restore
/// -- and rather than the silence a client blocks on, which is how DECRQSS already declines.
/// </summary>
[Fact]
public void Decrqtsr_DeclinesInsteadOfStayingSilent()
{
var (terminal, replies) = Create();
terminal.Write(Esc + "[1$u");
Assert.Equal(Esc + "P0$s" + Esc + "\\", Assert.Single(replies));
}

[Fact]
public void Decrqtsr_WithNoParameterAsksForNothing()
{
var (terminal, replies) = Create();
terminal.Write(Esc + "[$u");
Assert.Empty(replies);
}
}
49 changes: 45 additions & 4 deletions src/XTerm.NET.Tests/InputHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1171,7 +1171,7 @@ public void HandleCsi_DA_IgnoresNonZeroParameter(string identifier)
}

[Fact]
public void HandleCsi_DA_Tertiary_IsNotAnswered()
public void HandleCsi_DA_Tertiary_ReportsAZeroUnitId()
{
// Arrange
var terminal = CreateTerminal();
Expand All @@ -1185,9 +1185,50 @@ public void HandleCsi_DA_Tertiary_IsNotAnswered()
// Act - "=c" is the tertiary DA, asking for a unit ID
handler.HandleCsi("=c", params_);

// Assert - there is no unit ID to report, and terminals without DECRPTUI stay quiet.
// Answering a question nobody asked is worse than silence: the program would read a DA
// reply where it expected DECRPTUI, while still waiting for the reply it did ask for.
// Assert - DECRPTUI, with the site code and serial number as the zeros xterm reports.
// There is no unit to identify, but vttest and anything else that asks decodes this and
// waits forever for the silence it used to get.
Assert.Equal("\u001bP!|00000000\u001b\\", receivedData);
}

[Fact]
public void HandleCsi_DA_Tertiary_IsSilentBelowVt400()
{
// Arrange
var terminal = CreateTerminal();
var handler = new InputHandler(terminal);
terminal.ConformanceLevel = 62; // VT200
var params_ = new Params();
params_.AddParam(0);

string? receivedData = null;
terminal.DataReceived += (_, e) => receivedData = e.Data;

// Act
handler.HandleCsi("=c", params_);

// Assert - DECRPTUI is a VT420 control, and a program that put the terminal back to a
// VT200 level with DECSCL gets the silence a terminal of that vintage would have given it.
Assert.Null(receivedData);
}

[Fact]
public void HandleCsi_DA_Tertiary_IgnoresAReply()
{
// Arrange
var terminal = CreateTerminal();
var handler = new InputHandler(terminal);
var params_ = new Params();
params_.AddParam(1);

string? receivedData = null;
terminal.DataReceived += (_, e) => receivedData = e.Data;

// Act
handler.HandleCsi("=c", params_);

// Assert - a non-zero parameter is another terminal's reply arriving on our input, and
// answering it starts a ping-pong. The primary and secondary DA already refuse it.
Assert.Null(receivedData);
}

Expand Down
89 changes: 89 additions & 0 deletions src/XTerm.NET.Tests/OscSequenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -980,4 +980,93 @@ public void OscColorQueries_Sequential_AllRespond()
Assert.Equal(3, responses.Count);
Assert.All(responses, r => Assert.Contains("rgb:", r));
}

/// <summary>
/// OSC 50 ; ? -- vttest menu 11.8.4.2. The emulator has no font; whatever draws its cells does,
/// so the question goes to the host the way OSC 52's does.
/// </summary>
[Fact]
public void OscFontQuery_AnswersWithTheNameTheHostGives()
{
var terminal = CreateTerminal();
var responses = new List<string>();
terminal.DataReceived += (_, e) => responses.Add(e.Data);
terminal.FontQueryRequested += (_, e) =>
{
e.FontName = "Cascadia Mono";
e.Handled = true;
};

terminal.Write("\x1B]50;?\x1B\\");

Assert.Equal("\x1B]50;Cascadia Mono\x1B\\", Assert.Single(responses));
}

/// <summary>
/// The whole point of handling this at all: a host that will not say still produces a REPLY.
/// xterm answers a font query it cannot satisfy with a nameless OSC 50, and vttest reads
/// exactly that -- it skips entries whose reply carries no name. Silence leaves the asking
/// program waiting forever.
/// </summary>
[Fact]
public void OscFontQuery_AnswersNamelesslyWhenNoHostDoes()
{
var terminal = CreateTerminal();
var responses = new List<string>();
terminal.DataReceived += (_, e) => responses.Add(e.Data);

terminal.Write("\x1B]50;?\x1B\\");

Assert.Equal("\x1B]50\x1B\\", Assert.Single(responses));
}

[Fact]
public void OscFontQuery_AnswersNamelesslyForAMenuIndex()
{
// There is no font menu to index, so every indexed form is declined -- and never put to
// the host, which has no menu either.
var terminal = CreateTerminal();
var responses = new List<string>();
var asked = false;
terminal.DataReceived += (_, e) => responses.Add(e.Data);
terminal.FontQueryRequested += (_, _) => asked = true;

terminal.Write("\x1B]50;?#2\x1B\\");
terminal.Write("\x1B]50;?+1\x1B\\");

Assert.False(asked);
Assert.Equal(new[] { "\x1B]50\x1B\\", "\x1B]50\x1B\\" }, responses);
}

[Fact]
public void OscFontQuery_TerminatesTheReplyTheWayTheRequestWasTerminated()
{
var terminal = CreateTerminal();
var responses = new List<string>();
terminal.DataReceived += (_, e) => responses.Add(e.Data);

terminal.Write("\x1B]50;?\x07");

Assert.Equal("\x1B]50\x07", Assert.Single(responses));
}

[Fact]
public void OscFontSet_IsLeftToTheHostAndReportedUnrecognised()
{
// Setting a font is the host's business, so the terminal answers nothing and says it did
// not act -- a listener on OscReceived can, without having to work out whether it already
// had been.
var terminal = CreateTerminal();
var responses = new List<string>();
TerminalEvents.OscReceivedEventArgs? received = null;
terminal.DataReceived += (_, e) => responses.Add(e.Data);
terminal.OscReceived += (_, e) => received = e;

terminal.Write("\x1B]50;9x15\x1B\\");

Assert.Empty(responses);
Assert.NotNull(received);
Assert.False(received!.Recognized);
Assert.Equal("9x15", received.Data);
}
}
Loading
Loading