/* vi: set ts=2 shiftwidth=2 expandtab: * * Copyright (C) 2003-2007 Simon Baldwin and Mark J. Tilford * * This program is free software; you can redistribute it and/or modify * it under the terms of version 2 of the GNU General Public License * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ /* * Module notes: * * o The script file structure is intentionally simple, but might be too * simple for some purposes. */ #include #include #include #include #include "scare.h" #include "sxprotos.h" /* Assorted definitions and constants. */ static const sc_int LINE_BUFFER_SIZE = 256; static const sc_char NUL = '\0'; static const sc_char COMMENT = '#'; static const sc_char COMMAND = '>'; static const sc_char DEBUG_COMMAND = '~'; /* Verbosity, and references to the game and script being processed. */ static sc_bool scr_is_verbose = FALSE; static sc_game scr_game = NULL; static sx_script scr_script = NULL; /* Count of errors registered for the script. */ static sc_int scr_errors = 0; /* * Current expected output, and game accumulated output, used by the * expectation checking function. */ static sc_char *scr_expectation = NULL, *scr_game_output = NULL; /* * scr_set_verbose() * * Set error reporting for expectation errors detected in the script. */ void scr_set_verbose (sc_bool flag) { scr_is_verbose = flag; } /* * scr_test_message() * scr_test_failed() * * Simple common message and test case failure handling functions. The second * is used by the serialization helper, so is not static. */ static void scr_test_message (const sc_char *format, const sc_char *string) { if (scr_is_verbose) { sx_trace ("--- "); sx_trace (format, string); sx_trace ("\n"); } } void scr_test_failed (const sc_char *format, const sc_char *string) { scr_test_message (format, string); scr_errors++; } /* * scr_get_next_section() * * Return the next line and expectation from the script file. The file has * a simple format: lines beginning '#' are comments, otherwise the file * is composed of sections. The first section line is one that starts with * either '>' or '~'. This is the next command, and the following lines, up * to the next '>' or '~' section start, are concatenated into the expectation * for the command. Expectations are glob patterns. Commands starting with * '>' are fed to the game; those starting with '~' are taken to be SCARE * debugger commands. Before the game is running, debugger commands are valid. * The first non-debugger command will start up the game. An empty debugger * command ("~") following any introductory debugger commands both starts the * game and sets an expectation for the game's intro text. After the game has * completed (or quit), only debugger commands are valid; others are ignored. * * Returns TRUE if a line is returned, FALSE at end-of-file. Expectation may * be NULL if this paragraph doesn't have one; line may not be (if TRUE is * returned). Line and expectation are allocated, and the caller needs to * free them. */ static sc_bool scr_get_next_section (sx_script script, sc_char **command, sc_char **expectation) { sc_char *buffer, *first_line, *other_lines; /* Allocate a temporary read buffer, clear initial lines. */ buffer = sx_malloc (LINE_BUFFER_SIZE); first_line = other_lines = NULL; /* Read until end of file, or we have a section or single line. */ while (!feof (script)) { fpos_t marker; /* Record script position, and read the next line; loop if comment. */ fgetpos (script, &marker); if (fgets (buffer, LINE_BUFFER_SIZE, script)) { if (buffer[0] == COMMENT) continue; /* If not blank, set to first_line or append to other_lines. */ buffer = sx_trim_string (buffer); if (strspn (buffer, "\t\n\v\f\r ") < strlen (buffer)) { if (first_line) { /* If we found the start of the next section, we're done. */ if (buffer[0] == COMMAND || buffer[0] == DEBUG_COMMAND) { fsetpos (script, &marker); break; } /* Concatenate the remaining section lines. */ if (other_lines) { other_lines = sx_realloc (other_lines, strlen (other_lines) + 1 + strlen (buffer) + 1); strcat (other_lines, " "); strcat (other_lines, buffer); } else { other_lines = sx_malloc (strlen (buffer) + 1); strcpy (other_lines, buffer); } } else { first_line = sx_malloc (strlen (buffer) + 1); strcpy (first_line, buffer); } } } } /* Free the temporary read buffer, and return anything read. */ sx_free (buffer); if (first_line) { *command = sx_normalize_string (first_line); *expectation = other_lines ? sx_normalize_string (other_lines) : NULL; } return first_line != NULL; } /* * scr_expect() * scr_verify_expectation() * * Set an expectation, and compare the expectation, if any, with the * accumulated game output, using glob matching. scr_verify_expectation() * increments the error count if the expectation isn't met, and reports the * error if required. It then frees both the expectation and accumulated * input. */ static void scr_expect (sc_char *expectation) { /* * Save the expectation, and set up collection of game output if needed. * Setting scr_expectation to expectation takes ownership of the allocation * for this string. */ scr_expectation = expectation; if (expectation) { scr_game_output = sx_malloc (1); strcpy (scr_game_output, ""); } } static void scr_verify_expectation (void) { /* Compare expected with actual, and handle any error detected. */ if (scr_expectation && scr_game_output) { scr_game_output = sx_normalize_string (scr_game_output); if (!glob_match (scr_expectation, scr_game_output)) { scr_test_failed ("Expectation error:", ""); scr_test_message (" Received: \"%s\"", scr_game_output); scr_test_message (" Expected: \"%s\"", scr_expectation); } } /* Dispose of the expectation and accumulated game output. */ sx_free (scr_expectation); scr_expectation = NULL; sx_free (scr_game_output); scr_game_output = NULL; } /* * scr_execute_debugger_command() * * Convenience interface for immediate execution of debugger commands. This * function directly calls the debugger interface, and because it's immediate, * can also verify the expectation before returning to the caller. Note that * this function calls scr_expect(), and so takes ownership of expectation. * * Also, it turns on the game debugger, and it's the caller's responsibility * to turn it off when it's no longer needed. */ static void scr_execute_debugger_command (const sc_char *command, sc_char *expectation) { sc_bool status; /* Set up the expectation. */ scr_expect (expectation); /* * Execute the command via the debugger interface. The "+1" on command * skips the leading '~' read in from the game script. */ sc_set_game_debugger_enabled (scr_game, TRUE); status = sc_run_game_debugger_command (scr_game, command + 1); if (!status) { scr_test_failed ("Script error:" " debug command \"%s\" is not valid", command); } /* Check expectations immediately. */ scr_verify_expectation (); } /* * scr_read_line_callback() * * Check any expectations set for the last line. Consult the script for the * next line to feed to the game, and any expectation for the game output * for that line. If there is an expectation, save it and set scr_game_output * to "" so that accumulation begins. Then pass the next line of data back * to the game. */ static sc_bool scr_read_line_callback (sc_char *buffer, sc_int length) { sc_char *command, *expectation; assert (buffer && length > 0); /* Check pending expectation, and clear settings for the next line. */ scr_verify_expectation (); /* Get the next line-expectation pair from the script stream. */ if (scr_get_next_section (scr_script, &command, &expectation)) { /* If prefixed with '~', execute as a debug command. */ if (command[0] == DEBUG_COMMAND) { /* The debugger persists where debug commands are adjacent. */ scr_execute_debugger_command (command, expectation); sx_free (command); /* * Returning FALSE here causes the game to re-prompt. We could * loop (or tail recurse) ourselves, but returning is simpler. */ return FALSE; } else sc_set_game_debugger_enabled (scr_game, FALSE); /* If prefixed with '>', return the data as a game command. */ if (command[0] == COMMAND) { /* Set up the expectation. */ scr_expect (expectation); /* Copy out the line to the return buffer, and free the line. */ strncpy (buffer, command + 1, length); buffer[length - 1] = NUL; sx_free (command); return TRUE; } /* Neither a '~' nor a '>' command. */ scr_test_failed ("Script error:" " command \"%s\" is not valid, ignored", command); return FALSE; } /* * We reached the end of the script, so if the game is still running, try * to quit it. This call should not return, but should instead longjump * so as to appear as if the call to sc_interpret_game() returned. */ if (sc_is_game_running (scr_game)) { sc_quit_game (scr_game); /* Not expected to reach here; return FALSE to placate the compiler. */ sx_fatal ("scr_read_line_callback: unable to quit cleanly\n"); return FALSE; } /* * If the game's not running, why is it asking for input? The only poss- * ibility might be the end-of-game debug dialog, but debugging's normally * off. For now, just return "quit" until something finally gives up. */ assert (length > 4); strcpy (buffer, "quit"); return TRUE; } /* * scr_print_string_callback() * * Handler function for game output. Accumulates strings received from the * game into scr_game_output, unless no expectation is set, in which case * the current game output will be NULL, and we can simply save the effort. */ static void scr_print_string_callback (const sc_char *string) { assert (string); if (scr_game_output) { scr_game_output = sx_realloc (scr_game_output, strlen (scr_game_output) + strlen (string) + 1); strcat (scr_game_output, string); } } /* * scr_start_script() * * Set up game monitoring so that each request for a line from the game * enters this module. For each request, we grab the next "send" and * "expect" pair from the script, satisfy the request with the send data, * and match against the expectations on next request or on finalization. */ void scr_start_script (sc_game game, sx_script script) { sc_char *command, *expectation; fpos_t marker; assert (game && script); /* Save the game and stream, and clear the errors count. */ assert (!scr_game && !scr_script); scr_game = game; scr_script = script; scr_errors = 0; /* Set up our callback functions to catch game i/o. */ stub_attach_handlers (scr_read_line_callback, scr_print_string_callback, file_open_file_callback, file_read_file_callback, file_write_file_callback, file_close_file_callback); /* * Handle any initial debugging commands, terminating on either a non- * debugging one or an expectation for the game intro. */ rewind (scr_script); fgetpos (scr_script, &marker); while (scr_get_next_section (scr_script, &command, &expectation)) { /* If prefixed with '~', it's a debug command or intro expectation. */ if (command[0] == DEBUG_COMMAND) { if (command[1] == NUL) { /* It's an intro expectation - set and break loop. */ scr_expect (expectation); sx_free (command); break; } else { /* It's a full debug command - execute it as one. */ scr_execute_debugger_command (command, expectation); sx_free (command); } } else { /* * It's an ordinary section - rewind so that it's the first one * handled in the callback, and break loop. */ fsetpos (scr_script, &marker); sx_free (command); sx_free (expectation); break; } /* Note script position before reading the next section. */ fgetpos (scr_script, &marker); } /* Ensure the game debugger is off after this section. */ sc_set_game_debugger_enabled (scr_game, FALSE); } /* * scr_finalize_script() * * Match any final received string against a possible expectation, and then * clear local records of the game, stream, and error count. Returns the * count of errors detected during the script. */ sc_int scr_finalize_script (void) { sc_char *command, *expectation; sc_int errors; /* Check pending expectation, and clear settings. */ scr_verify_expectation (); /* Drain the remainder of the script, ignoring non-debugging commands. */ while (scr_get_next_section (scr_script, &command, &expectation)) { /* If prefixed with '~', action it; it's a debug command. */ if (command[0] == DEBUG_COMMAND) { scr_execute_debugger_command (command, expectation); sx_free (command); } else { /* Complain about script entries ignored because the game ended. */ scr_test_failed ("Script error:" " game completed, command \"%s\" ignored", command); sx_free (command); sx_free (expectation); } } /* Ensure the game debugger is off after this section. */ sc_set_game_debugger_enabled (scr_game, FALSE); /* * Remove our callback functions from the stubs, and "close" any retained * stream data from game save/load tests. */ stub_detach_handlers (); file_cleanup (); /* Clear local records of game stream and errors count. */ errors = scr_errors; scr_game = NULL; scr_script = NULL; scr_errors = 0; return errors; }