moved text matching code from expect() to V86.wait_until_vga_screen_contains()

All changes to V86.wait_until_vga_screen_contains() are backward compatible.
This commit is contained in:
Christian Schnell 2025-08-20 15:24:43 +02:00 committed by Fabian
parent b91afaa821
commit 00203987f4
2 changed files with 115 additions and 121 deletions

View file

@ -1358,52 +1358,126 @@ V86.prototype.automatically = function(steps)
run(steps);
};
V86.prototype.wait_until_vga_screen_contains = function(text)
/**
* Wait until expected text is present on the VGA text screen.
*
* Returns immediately if the expected text is already present on screen
* at the time this funtion is called.
*
* An optional timeout may be specified in `options.timeout_msec`, an Error
* is thrown if the timeout expires before the expected text could be
* detected. If no timeout is specified then this function only unblocks
* once text detection succeeds.
*
* Expected text (or texts, see below) must be of type string or RegExp.
*
* Two methods of text detection are supported depending on the type of the
* argument `expected`:
*
* 1. If `expected` is a string or RegExp then the given text string or
* regular expression must partially or fully match any line on screen.
*
* 2. If `expected` is an array of strings and/or RegExp objects then the
* list of expected lines must match exactly at "the bottom" of the
* screen. The "bottom" line is the first non-empty line starting from
* the screen's end.
* Expected lines specified as strings must match the entire line on
* screen, and regular expressions are matched against full screen
* lines but may match partially and use wildcards.
* Expected lines should not contain any trailing whitespace and/or
* newline characters. Expecting an empty line is valid.
*
* Returns the array of lines that matched on screen for both methods.
*
* @param {string|RegExp|Array<string|RegExp>} expected
* @param {{timeout_msec:(number|undefined)}=} options
*/
V86.prototype.wait_until_vga_screen_contains = async function(expected, options)
{
return new Promise(resolve =>
{
function test_line(line)
{
return typeof text === "string" ? line.includes(text) : text.test(line);
}
const match_multi = Array.isArray(expected);
const timeout_msec = options?.timeout_msec ? options.timeout_msec : 0;
const changed_rows = new Set();
const screen_put_char = args => changed_rows.add(args[0]);
const contains_expected = expected.test ? expected.test : line => line.includes(expected);
for(const line of this.screen_adapter.get_text_screen())
this.add_listener("screen-put-char", screen_put_char);
try
{
const screen_lines = [];
for(const screen_line of this.screen_adapter.get_text_screen())
{
if(test_line(line))
if(match_multi)
{
resolve(true);
return;
screen_lines.push(screen_line.trimRight());
}
else if(contains_expected(screen_line))
{
return [screen_line];
}
}
const changed_rows = new Set();
function put_char(args)
const tm_end = timeout_msec ? performance.now() + timeout_msec : 0;
while(true)
{
const [row, col, char] = args;
changed_rows.add(row);
}
await new Promise(resolve => setTimeout(resolve, 100));
if(tm_end && performance.now() >= tm_end)
{
throw new Error("Timeout of " + timeout_msec + " msec expired");
}
const check = () =>
{
for(const row of changed_rows)
{
const line = this.screen_adapter.get_text_row(row);
if(test_line(line))
const screen_line = this.screen_adapter.get_text_row(row);
if(match_multi)
{
this.remove_listener("screen-put-char", put_char);
resolve();
return;
screen_lines[row] = screen_line.trimRight();
}
else if(contains_expected(screen_line))
{
return [screen_line];
}
}
changed_rows.clear();
setTimeout(check, 100);
};
check();
this.add_listener("screen-put-char", put_char);
});
if(!match_multi)
{
continue;
}
let screen_height = screen_lines.length;
while(screen_height > 0 && screen_lines[screen_height - 1] === "")
{
screen_height--;
}
const screen_offset = screen_height - expected.length;
if(screen_offset < 0)
{
continue;
}
let matches = true;
for(let i = 0; i < expected.length && matches; i++)
{
if(expected[i].test)
{
matches = expected[i].test(screen_lines[screen_offset + i]);
}
else
{
matches = screen_lines[screen_offset + i] === expected[i];
}
}
if(matches)
{
return screen_lines.slice(screen_offset, screen_height);
}
}
}
finally
{
this.remove_listener("screen-put-char", screen_put_char);
}
};
/**

View file

@ -11,7 +11,7 @@ const { V86 } = await import(TEST_RELEASE_BUILD ? "../../build/libv86.mjs" : "..
process.on("unhandledRejection", exn => { throw exn; });
async function exec_test(test_name, v86_config, timeout_sec, test_function)
export async function exec_test(test_name, v86_config, timeout_sec, test_function)
{
console.log("Starting: " + test_name);
const tm_start = performance.now();
@ -26,7 +26,7 @@ async function exec_test(test_name, v86_config, timeout_sec, test_function)
}, timeout_sec * 1000);
try
{
await new Promise(resolve => { emulator.bus.register("emulator-started", () => { resolve(); }); });
await new Promise(resolve => emulator.bus.register("emulator-started", () => resolve()));
await test_function(emulator);
console.log("Done: " + test_name + " (" + ((performance.now() - tm_start) / 1000).toFixed(2) + " sec)");
}
@ -50,123 +50,43 @@ async function exec_test(test_name, v86_config, timeout_sec, test_function)
* Execute given CLI command and wait for its completion.
*
* Injects command into the guest's keyboard buffer, then waits for both the
* command and the expected response lines to show at "the bottom" of the
* VGA screen. The "bottom" line is the first non-empty line from the VGA
* text screen's bottom.
* command and the expected response lines to show at the bottom of the VGA
* screen.
*
* If command is empty then no command is executed and only the expected
* response lines are waited for. If command contains only whitespace and/or
* newline characters then it is send to the guest, but it does not become
* part of the expected response.
*
* Items in response_lines must be of type string or RegExp. A regular
* expression is matched against the complete screen line, whereas a string
* is matched against the entire screen line. Expected response lines should
* not contain any trailing whitespace and/or newline characters. Expecting
* an empty line is valid.
*
* Allowed character set for command and response_lines is the printable
* subset of 7-bit ASCII, use newline character "\n" to encode ENTER key.
* Allowed character set for command and expected is the printable subset
* of 7-bit ASCII, use newline character "\n" to encode ENTER key.
*
* Returns the matched response lines. Throws an Error if the given timeout
* elapsed before the expected response could be detected.
*
* @param {V86} emulator
* @param {string} command
* @param {Array[string|RegExp]} response_lines
* @param {number} response_timeout_msec
* @return {Array[string]}
* @param {Array<string|RegExp>} expected
* @param {number} timeout_msec
*/
async function expect(emulator, command, response_lines, response_timeout_msec)
export async function expect(emulator, command, expected, timeout_msec)
{
if(command)
{
// inject command characters into guest's keyboard buffer
for(const ch of command)
{
emulator.keyboard_send_text(ch);
await pause(10);
}
// trim trailing newline (and/or whitespace) from command
command = command.trimRight();
if(command)
{
// prepend command to expected response lines
response_lines = [command, ...response_lines];
expected = [new RegExp(RegExp.escape(command) + "$"), ...expected];
}
}
const changed_rows = new Set();
const screen_put_char = args => { changed_rows.add(args[0]); };
emulator.add_listener("screen-put-char", screen_put_char);
try
{
// initialize VGA screen buffer
const screen_lines = [];
for(const line of emulator.screen_adapter.get_text_screen())
{
screen_lines.push(line.trimRight());
}
const tm_end = performance.now() + response_timeout_msec;
while(performance.now() < tm_end)
{
await pause(100);
// update VGA screen buffer
for(const row of changed_rows)
{
screen_lines[row] = emulator.screen_adapter.get_text_row(row).trimRight();
}
changed_rows.clear();
let screen_bottom = screen_lines.length;
while(screen_bottom > 0 && screen_lines[screen_bottom - 1] === "")
{
screen_bottom--;
}
const screen_offset = screen_bottom - response_lines.length;
if(screen_offset < 0)
{
continue;
}
let matches = true;
for(let i = 0; i < response_lines.length && matches; i++)
{
if(i === 0 && command)
{
// match raw command against end of screen line
matches = screen_lines[screen_offset + i].endsWith(response_lines[i]);
}
else if(response_lines[i].test)
{
// match screen line against anything that implements test(), for example a RegExp
matches = response_lines[i].test(screen_lines[screen_offset + i]);
}
else
{
// match exact
matches = screen_lines[screen_offset + i] === response_lines[i];
}
}
if(matches)
{
return screen_lines.slice(screen_offset, screen_bottom);
}
}
throw new Error("Timeout in command \"" + command + "\"");
}
finally
{
emulator.remove_listener("screen-put-char", screen_put_char);
}
return await emulator.wait_until_vga_screen_contains(expected, {timeout_msec: timeout_msec});
}
// ---------------------------------------------------------------------------
const CONFIG_MSDOS622_HD = {
bios: { url: __dirname + "/../../bios/seabios.bin" },
vga_bios: { url: __dirname + "/../../bios/vgabios.bin" },