package com.example.aiapp import androidx.compose.ui.graphics.Color import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertTrue /** * What survives a terminal's escape sequences, and what the styling reads as. * * Asserted as the plain text and as the style over a named substring, rather than as span offsets, * so a failure prints the output that was got wrong instead of a pair of numbers. */ class AnsiTest { private val palette = AnsiPalette( colours = (0..15).map { Color(it, 0, 0) }, foreground = Color(1f, 1f, 1f), background = Color(0f, 0f, 0f), ) private fun styled(text: String) = ansiStyled(text, palette) /** The style covering the first character of [word], or null where nothing styles it. */ private fun styleOver(text: String, word: String) = styled(text).let { annotated -> val at = annotated.text.indexOf(word) assertTrue(at >= 0, "no \"$word\" in ${annotated.text}") annotated.spanStyles.firstOrNull { at >= it.start && at < it.end }?.item } private val esc = '\u001B' @Test fun `a colour becomes a span and the sequence itself disappears`() { val text = "plain ${esc}[31mred${esc}[0m plain" assertEquals("plain red plain", styled(text).text) assertEquals(palette.colours[1], styleOver(text, "red")?.color) assertNull(styleOver(text, "plain")) } @Test fun `bright, background and 256-colour forms all reach the same table`() { assertEquals(palette.colours[9], styleOver("${esc}[91mx", "x")?.color) assertEquals(palette.colours[4], styleOver("${esc}[44mx", "x")?.background) // The first sixteen of the 256-colour table are the palette's own, so a program that // spells a colour either way gets the same one. assertEquals(palette.colours[1], styleOver("${esc}[38;5;1mx", "x")?.color) // And past them, xterm's cube: 16 is its black corner, 231 its white one. assertEquals(Color(0, 0, 0), styleOver("${esc}[38;5;16mx", "x")?.color) assertEquals(Color(255, 255, 255), styleOver("${esc}[38;5;231mx", "x")?.color) assertEquals(Color(10, 20, 30), styleOver("${esc}[38;2;10;20;30mx", "x")?.color) } @Test fun `everything that is not styling is dropped rather than printed`() { // A cursor move, an erase, an OSC window title with its bell, and a bare two-character // escape. None of them mean anything in a scrolling document, and all of them would be // line noise if the escape alone were stripped and the body left behind. val text = "a${esc}[2Jb${esc}[Kc${esc}]0;a titled${esc}=e" assertEquals("abcde", styled(text).text) } @Test fun `a carriage return rewrites its line, as it does on a terminal`() { // What a progress bar looks like: every state it passed through, ending on the last. assertEquals("done\n", styled("10%\r50%\rdone\n").text) // The line before it is untouched. A Windows line ending rewrites nothing and is not // kept either: it is one line break, and passing the carriage return through would draw // a stray control character in the middle of the output. assertEquals("kept\nlast", styled("kept\r\nfirst\rlast").text) } @Test fun `a sequence cut off mid-stream takes no text with it`() { // Output still arriving ends anywhere, including inside an escape. The fragment goes and // the whole sequence arrives with the next delta. assertEquals("text ", styled("text ${esc}[3").text) } @Test fun `unstyled text costs no spans at all`() { assertEquals(0, styled("nothing to do here").spanStyles.size) assertEquals(0, styled("a${esc}[2Jb").spanStyles.size) } }