diff options
Diffstat (limited to 'addons/escoria-core/testing/ashes')
138 files changed, 3113 insertions, 0 deletions
diff --git a/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd new file mode 100644 index 0000000..e03d122 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd @@ -0,0 +1,1879 @@ +# GdUnit generated TestSuite +class_name ESCInterpreterTest +extends GdUnitTestSuite + +@warning_ignore("unused_parameter") +@warning_ignore("return_value_discarded") + +const MARK_SCENE := preload("res://game/characters/mark/mark.tscn") +const SIMPLE_CHOOSER_SCENE := preload("res://addons/escoria-dialog-simple/chooser/simple.tscn") + +var _terminate_on_errors_before: bool +var _command_directories_before: PackedStringArray +var _dialog_player_before + + +class DialogPlayerDouble extends ESCDialogPlayer: + var _choice_indexes: Array = [] + var _choices_consumed: int = 0 + var _dialogs_started: Array = [] + + + func _init(choice_indexes: Array = []): + _choice_indexes = choice_indexes.duplicate() + + + func start_dialog_choices(dialog: ESCDialog, _type: String = "simple"): + var option_keys: Array = [] + var option_texts: Array = [] + var option_source_indexes: Array = [] + for option in dialog.options: + option_keys.append(option.translation_key) + option_texts.append(option.option) + option_source_indexes.append(option.source_option_index) + + var timeout_option := dialog.get_timeout_option() + + _dialogs_started.append({ + "avatar": dialog.avatar, + "timeout": dialog.timeout, + "timeout_option": dialog.timeout_option, + "authored_options_count": dialog.authored_options_count, + "visible_timeout_option": timeout_option.option if timeout_option else null, + "options_size": dialog.options.size(), + "option_keys": option_keys, + "option_texts": option_texts, + "option_source_indexes": option_source_indexes + }) + + # Mirror the real dialog player closely enough for interpreter tests: + # only valid options are choosable, and scripted indexes deterministically + # drive nested dialog flows without needing UI interaction. + var valid_options: Array = [] + for option in dialog.options: + if option.is_valid(): + valid_options.append(option) + + var choice_index := 0 + if not _choice_indexes.is_empty(): + choice_index = int(_choice_indexes.pop_front()) + _choices_consumed += 1 + + call_deferred("_emit_choice", valid_options[choice_index] if choice_index < valid_options.size() else null) + + + func _emit_choice(option) -> void: + option_chosen.emit(option) + + + func get_choices_consumed() -> int: + return _choices_consumed + + + func get_remaining_choices() -> int: + return _choice_indexes.size() + + + func get_dialogs_started() -> Array: + return _dialogs_started.duplicate(true) + + +func before() -> void: + # Interpreter tests intentionally trigger runtime errors in a few cases, so + # they disable terminate-on-error and install a test-only command directory. + # The dialog player is also replaced on demand by a deterministic test + # double so dialog execution can run headlessly. + ESCInterpreterFactory.reset_interpreter() + escoria.event_manager.interrupt([], false) + escoria.event_manager.clear_event_queue() + _terminate_on_errors_before = ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.TERMINATE_ON_ERRORS + ) + _command_directories_before = PackedStringArray( + ESCProjectSettingsManager.get_setting( + ESCProjectSettingsManager.COMMAND_DIRECTORIES + ) + ) + _dialog_player_before = escoria.get("dialog_player") + + ESCProjectSettingsManager.set_setting( + ESCProjectSettingsManager.TERMINATE_ON_ERRORS, + false + ) + ESCProjectSettingsManager.set_setting( + ESCProjectSettingsManager.COMMAND_DIRECTORIES, + PackedStringArray([ + "res://addons/escoria-core/testing/ashes/interpreter/support/commands" + ] + Array(_command_directories_before)) + ) + escoria.command_registry.registry.clear() + escoria.set("dialog_player", null) + + +func after() -> void: + # Restore shared Escoria settings and globals so each test starts from the + # same baseline and does not leak command registry or dialog-player state. + ESCInterpreterFactory.reset_interpreter() + escoria.event_manager.interrupt([], false) + escoria.event_manager.clear_event_queue() + ESCProjectSettingsManager.set_setting( + ESCProjectSettingsManager.TERMINATE_ON_ERRORS, + _terminate_on_errors_before + ) + ESCProjectSettingsManager.set_setting( + ESCProjectSettingsManager.COMMAND_DIRECTORIES, + _command_directories_before + ) + escoria.command_registry.registry.clear() + escoria.set("dialog_player", _dialog_player_before) + + +func test_execute_block_restores_scope_after_break() -> void: + # This fixture creates a nested block-local `x`, exits that block with + # `break`, and then reads `x` afterward into a global. If `_execute_block()` + # fails to restore `_environment` on the early return, the later lookup will + # incorrectly resolve the inner `x` instead of the outer one. + var source := _load_fixture("block_scope_restores_after_break.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("block_scope_restores_after_break.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new([], {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + + await interpreter.interpret(statements) + + var globals := interpreter.get_global_values() + assert_bool(globals.has("result")).is_true() + assert_float(float(globals["result"])).is_equal(1.0) + + interpreter.cleanup() + + +func test_invalid_numeric_comparison_does_not_succeed_silently() -> void: + # This fixture performs an invalid numeric comparison. Invalid numeric + # operations should log an interpreter error and evaluate to `null` rather + # than continuing into a native GDScript operator error. + var source := _load_fixture("invalid_numeric_comparison.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("invalid_numeric_comparison.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new([], {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + + await interpreter.interpret(statements) + + var globals := interpreter.get_global_values() + assert_bool(globals.has("result")).is_true() + assert_bool(globals["result"] == null).is_true() + + interpreter.cleanup() + + +func test_print_with_zero_arguments_does_not_native_crash() -> void: + # This fixture calls the builtin `print()` with zero arguments. The current + # implementation only rejects calls with more than one argument, so this + # test captures the actual runtime behavior before we decide on a fix. + var source := _load_fixture("print_zero_args.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("print_zero_args.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new([], {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + + await interpreter.interpret(statements) + + interpreter.cleanup() + + +func test_print_with_two_arguments_does_not_native_crash() -> void: + # This fixture calls the builtin `print()` with too many arguments. The + # builtin should reject the call cleanly and avoid any native runtime error. + var source := _load_fixture("print_two_args.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("print_two_args.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new([], {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + + await interpreter.interpret(statements) + + interpreter.cleanup() + + +func test_stop_in_nested_block_prevents_later_statements() -> void: + # `stop` should unwind the current event immediately, even when it is issued + # from inside a nested block. The later assignment must therefore never run. + var outcome := await _interpret_fixture("stop_nested_block.esc") + var globals: Dictionary = outcome.globals + assert_bool(globals.has("result")).is_true() + assert_float(float(globals["result"])).is_equal(0.0) + + +func test_done_in_nested_dialog_block_prevents_later_statements() -> void: + # `done` should conclude the current dialog from inside a nested block, but + # it must not abort the rest of the event. The assignment after the dialog + # should still execute, proving the dialog exits cleanly without leaking a + # control-flow sentinel to the enclosing event body. + var outcome := await _interpret_fixture( + "done_nested_block_in_dialog.esc", + [0] + ) + var globals: Dictionary = outcome.globals + assert_bool(globals.has("result")).is_true() + assert_float(float(globals["result"])).is_equal(1.0) + + +func test_break_one_returns_to_parent_dialog_level() -> void: + # `break 1` should move dialog control back up one level, not continue the + # remainder of the current outer option body. The scripted choice sequence + # must therefore consume a third outer-dialog choice after the nested break; + # if the dialog ends early instead, the final string alone is ambiguous. + var outcome := await _interpret_fixture( + "nested_dialog_break_one.esc", + [0, 0, 1] + ) + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("start-done") + assert_int(int(outcome.dialog_choices_consumed)).is_equal(3) + assert_int(int(outcome.remaining_dialog_choices)).is_equal(0) + + +func test_done_exits_entire_nested_dialog_tree() -> void: + # `done` should conclude the entire dialog from any nesting depth. After the + # nested choice issues `done`, the outer option body must not continue and + # the outer dialog must not be shown again. Execution should resume only + # after the dialog statement, so the final value should reflect the initial + # value plus the post-dialog append. + var outcome := await _interpret_fixture( + "done_exits_entire_nested_dialog.esc", + [0, 0] + ) + var globals: Dictionary = outcome.globals + assert_bool(globals.has("result")).is_true() + assert_str(String(globals["result"])).is_equal("start-done") + + +func test_dialog_followup_runs_after_all_options_are_exhausted() -> void: + # When each option invalidates itself, the dialog should conclude once no + # valid options remain. Execution must then continue with the statement after + # the dialog block. + var outcome := await _interpret_fixture( + "dialog_exhausted_options_runs_followup.esc", + [0, 0] + ) + var globals: Dictionary = outcome.globals + assert_bool(globals.has("result")).is_true() + assert_str(String(globals["result"])).is_equal("start-say1-say2-finished") + assert_bool(bool(globals["option1_is_valid"])).is_false() + assert_bool(bool(globals["option2_is_valid"])).is_false() + assert_int(int(outcome.dialog_choices_consumed)).is_equal(2) + assert_int(int(outcome.remaining_dialog_choices)).is_equal(0) + + +func test_break_two_exits_two_dialog_levels() -> void: + # `break 2` from the innermost dialog should move back up two dialog levels + # to the outermost dialog in this fixture shape. As with `break 1`, the test + # also verifies that the final scripted outer choice was actually consumed, + # which proves the outer dialog stayed alive after the nested unwind. + var outcome := await _interpret_fixture( + "break_two_exits_two_dialog_levels.esc", + [0, 0, 0, 1] + ) + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("start-done") + assert_int(int(outcome.dialog_choices_consumed)).is_equal(4) + assert_int(int(outcome.remaining_dialog_choices)).is_equal(0) + + +func test_stop_in_dialog_option_prevents_later_event_statements() -> void: + # `stop` issued from a dialog option should abort the entire event, not just + # the current dialog. The assignment after the dialog must therefore never run. + var outcome := await _interpret_fixture( + "stop_in_dialog_option.esc", + [0] + ) + assert_bool(outcome.globals.has("result")).is_true() + assert_float(float(outcome.globals["result"])).is_equal(0.0) + + +func test_delayed_command_in_dialog_option_preserves_ordering() -> void: + # A blocking command inside a dialog option must complete before the option + # body resumes and before execution continues after the dialog. If it does + # not block correctly, the final append would observe `unset` instead of the + # command-written value. + var outcome := await _interpret_fixture( + "delayed_command_in_dialog_option.esc", + [0] + ) + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("cmd-after") + + +func test_delayed_command_in_loop_preserves_ordering() -> void: + # A blocking command inside a loop body must finish before the loop can + # continue to the next statement. The later append therefore proves the + # delayed command completed before the event resumed after the loop. + var outcome := await _interpret_fixture("delayed_command_in_loop.esc") + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("cmd-after") + assert_bool(outcome.globals.has("count")).is_true() + assert_float(float(outcome.globals["count"])).is_equal(1.0) + + +func test_interrupting_delayed_command_stops_event_progress() -> void: + # Interrupting an in-flight command should stop the surrounding event from + # resuming normal execution. The command records that it observed the + # interrupt, and the later append must never run. + var source := _load_fixture("delayed_command_interrupt.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("delayed_command_interrupt.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + var event_stmt: ESCGrammarStmts.Event = statements[0] + interpreter.call_deferred("interpret", statements) + await escoria.get_tree().create_timer(0.05).timeout + interpreter.interrupt() + await event_stmt.finished + + var globals := interpreter.get_global_values() + assert_bool(globals.has("result")).is_true() + assert_str(String(globals["result"])).is_equal("start") + assert_bool(globals.has("was_interrupted")).is_true() + assert_bool(bool(globals["was_interrupted"])).is_true() + assert_bool(event_stmt.is_interrupted()).is_true() + + interpreter.cleanup() + + +func test_interrupting_delayed_command_in_dialog_stops_event_progress() -> void: + # Interrupting an in-flight command inside a dialog option should abort the + # surrounding event just as cleanly as interruption in a plain block. The + # dialog must not resume and the post-dialog append must never run. + var source := _load_fixture("delayed_command_interrupt_in_dialog.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("delayed_command_interrupt_in_dialog.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + var dialog_player := DialogPlayerDouble.new([0]) + escoria.set("dialog_player", dialog_player) + + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + var event_stmt: ESCGrammarStmts.Event = statements[0] + interpreter.call_deferred("interpret", statements) + await escoria.get_tree().create_timer(0.05).timeout + interpreter.interrupt() + await event_stmt.finished + + var globals := interpreter.get_global_values() + assert_bool(globals.has("result")).is_true() + assert_str(String(globals["result"])).is_equal("start") + assert_bool(globals.has("was_interrupted")).is_true() + assert_bool(bool(globals["was_interrupted"])).is_true() + assert_bool(event_stmt.is_interrupted()).is_true() + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_interrupting_delayed_command_stops_multi_iteration_loop() -> void: + # Interrupting a delayed command in the first loop iteration should stop the + # loop itself, not just the current statement. The counter must therefore + # stay at zero and the post-loop append must never run. + var source := _load_fixture("delayed_command_interrupt_in_multi_loop.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("delayed_command_interrupt_in_multi_loop.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + var event_stmt: ESCGrammarStmts.Event = statements[0] + interpreter.call_deferred("interpret", statements) + await escoria.get_tree().create_timer(0.05).timeout + interpreter.interrupt() + await event_stmt.finished + + var globals := interpreter.get_global_values() + assert_bool(globals.has("result")).is_true() + assert_str(String(globals["result"])).is_equal("start") + assert_bool(globals.has("was_interrupted")).is_true() + assert_bool(bool(globals["was_interrupted"])).is_true() + assert_bool(globals.has("count")).is_true() + assert_float(float(globals["count"])).is_equal(0.0) + assert_bool(event_stmt.is_interrupted()).is_true() + + interpreter.cleanup() + + +func test_invalid_command_does_not_continue_event() -> void: + # A missing command should fail the current event cleanly instead of letting + # later statements continue as if the command had succeeded. + var outcome := await _interpret_fixture("invalid_command_does_not_continue.esc") + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("start") + + +func test_invalid_command_in_dialog_does_not_continue_event() -> void: + # A missing command inside a dialog option should still terminate the event + # cleanly. The assignment after the dialog must not run. + var outcome := await _interpret_fixture( + "invalid_command_in_dialog_stops_event.esc", + [0] + ) + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("start") + + +func test_invalid_command_in_loop_does_not_continue_event() -> void: + # A missing command inside a loop body should escape the loop as a terminal + # runtime error instead of allowing later loop or post-loop statements to run. + var outcome := await _interpret_fixture("invalid_command_in_loop_stops_event.esc") + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("start") + assert_bool(outcome.globals.has("count")).is_true() + assert_float(float(outcome.globals["count"])).is_equal(0.0) + + +func test_script_preserves_multi_target_use_events_without_creating_untargeted_use() -> void: + # Room05-style scripts can define the same action multiple times with + # different targets. Both targeted events must survive compilation, while an + # untargeted lookup for the same action must not accidentally match either. + var script_object := _compile_fixture_script("multi_target_use_events.esc") + + assert_bool(script_object.has_event_with_target("use", "r5_wrench")).is_true() + assert_bool(script_object.has_event_with_target("use", "r5_pen")).is_true() + assert_bool(script_object.has_event_with_target("use")).is_false() + + var wrench_event = script_object.get_event_with_target("use", "r5_wrench") + var pen_event = script_object.get_event_with_target("use", "r5_pen") + + assert_object(wrench_event).is_not_null() + assert_object(pen_event).is_not_null() + assert_object(wrench_event).is_not_same(pen_event) + assert_str(wrench_event.get_target_name()).is_equal("r5_wrench") + assert_str(pen_event.get_target_name()).is_equal("r5_pen") + assert_int(wrench_event.get_num_statements_in_block()).is_equal(1) + assert_int(pen_event.get_num_statements_in_block()).is_equal(1) + + +func test_dialog_option_scope_shadowing_restores_outer_local() -> void: + # A dialog option body introduces nested block scope. Assignments inside the + # option should see the inner shadowing variable, while execution after the + # dialog must still resolve the original outer local. + var outcome := await _interpret_fixture( + "dialog_scope_shadowing.esc", + [0] + ) + assert_bool(outcome.globals.has("result")).is_true() + assert_str(String(outcome.globals["result"])).is_equal("inner:outer") + + +func test_interrupting_background_channel_does_not_interrupt_front_channel() -> void: + # Background-channel interruption should only affect commands running on + # that channel. The front event must still complete normally while the + # background event should finish as interrupted once its in-flight command + # observes the interrupt. + var events = _load_fixture_events("channel_interrupt_isolation.esc") + assert_bool(events.has_event_with_target("front_event")).is_true() + assert_bool(events.has_event_with_target("background_event")).is_true() + escoria.globals_manager.set_global("channel_front_result", "start") + escoria.globals_manager.set_global("channel_background_result", "start") + escoria.globals_manager.set_global("channel_front_was_interrupted", false) + escoria.globals_manager.set_global("channel_background_was_interrupted", false) + + var signal_results := { + "front": null, + "background": null, + } + var on_front_finished := func(return_code, event_name) -> void: + if event_name == "front_event": + signal_results["front"] = [return_code, event_name] + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if event_name == "background_event" and finished_channel_name == "bg_test": + signal_results["background"] = [return_code, event_name, finished_channel_name] + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_event")) + escoria.event_manager.queue_background_event( + "bg_test", + events.get_event_with_target("background_event") + ) + + await escoria.get_tree().create_timer(0.05).timeout + escoria.event_manager.interrupt_channel("bg_test") + + while signal_results["front"] == null or signal_results["background"] == null: + await escoria.get_tree().process_frame + + var front_finished = signal_results["front"] + var background_finished = signal_results["background"] + + assert_int(int(front_finished[0])).is_equal(ESCExecution.RC_OK) + assert_int(int(background_finished[0])).is_equal(ESCExecution.RC_INTERRUPTED) + assert_str(String(escoria.globals_manager.get_global("channel_front_result"))).is_equal("front-cmd-after") + assert_bool(escoria.globals_manager.get_global("channel_front_was_interrupted") == true).is_false() + assert_str(String(escoria.globals_manager.get_global("channel_background_result"))).is_equal("start") + assert_bool(escoria.globals_manager.get_global("channel_background_was_interrupted") == true).is_true() + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_concurrent_channels_complete_without_interrupting_each_other() -> void: + # Two events on different channels should be able to run at the same time + # and still complete normally when no interrupt is issued. + var events = _load_fixture_events("channel_interrupt_isolation.esc") + assert_bool(events.has_event_with_target("front_event")).is_true() + assert_bool(events.has_event_with_target("background_event")).is_true() + escoria.globals_manager.set_global("channel_front_result", "start") + escoria.globals_manager.set_global("channel_background_result", "start") + escoria.globals_manager.set_global("channel_front_was_interrupted", false) + escoria.globals_manager.set_global("channel_background_was_interrupted", false) + + var signal_results := { + "front": null, + "background": null, + } + var on_front_finished := func(return_code, event_name) -> void: + if event_name == "front_event": + signal_results["front"] = [return_code, event_name] + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if event_name == "background_event" and finished_channel_name == "bg_test": + signal_results["background"] = [return_code, event_name, finished_channel_name] + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_event")) + escoria.event_manager.queue_background_event( + "bg_test", + events.get_event_with_target("background_event") + ) + + while signal_results["front"] == null or signal_results["background"] == null: + await escoria.get_tree().process_frame + + var front_finished = signal_results["front"] + var background_finished = signal_results["background"] + + assert_int(int(front_finished[0])).is_equal(ESCExecution.RC_OK) + assert_int(int(background_finished[0])).is_equal(ESCExecution.RC_OK) + assert_str(String(escoria.globals_manager.get_global("channel_front_result"))).is_equal("front-cmd-after") + assert_bool(escoria.globals_manager.get_global("channel_front_was_interrupted") == true).is_false() + assert_str(String(escoria.globals_manager.get_global("channel_background_result"))).is_equal("bg-cmd-after") + assert_bool(escoria.globals_manager.get_global("channel_background_was_interrupted") == true).is_false() + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_interrupting_channel_clears_queued_successor_events() -> void: + # Interrupting a channel should stop the currently running event and purge + # later queued events on that same channel before they ever start running. + var events = _load_fixture_events("channel_interrupt_clears_queue.esc") + assert_bool(events.has_event_with_target("first")).is_true() + assert_bool(events.has_event_with_target("second")).is_true() + escoria.globals_manager.set_global("bg_queue_interrupted", false) + + var background_finishes: Array = [] + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + background_finishes.append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("first")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("second")) + + await escoria.get_tree().create_timer(0.05).timeout + escoria.event_manager.interrupt_channel("bg_test") + + while background_finishes.is_empty(): + await escoria.get_tree().process_frame + + assert_int(background_finishes.size()).is_equal(1) + assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_INTERRUPTED) + assert_str(String(background_finishes[0][1])).is_equal("first") + assert_str(String(background_finishes[0][2])).is_equal("bg_test") + assert_str(String(escoria.globals_manager.get_global("bg_queue_result"))).is_equal("first") + assert_bool(escoria.globals_manager.get_global("bg_queue_interrupted") == true).is_true() + + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_concurrent_channels_do_not_block_other_channel_queues() -> void: + # Each channel should serialize only its own queue. A front-channel event + # must complete independently while a background channel works through two + # queued events in order. + var events = _load_fixture_events("channel_queue_progress_isolated.esc") + assert_bool(events.has_event_with_target("front_event")).is_true() + assert_bool(events.has_event_with_target("background_first")).is_true() + assert_bool(events.has_event_with_target("background_second")).is_true() + escoria.globals_manager.set_global("isolated_front_result", "start") + escoria.globals_manager.set_global("isolated_background_result", "start") + escoria.globals_manager.set_global("isolated_front_interrupted", false) + escoria.globals_manager.set_global("isolated_background_interrupted", false) + + var signal_results := { + "front": null, + "background_finishes": [], + } + var on_front_finished := func(return_code, event_name) -> void: + if event_name == "front_event": + signal_results["front"] = [return_code, event_name] + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + signal_results["background_finishes"].append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_event")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_first")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_second")) + + while signal_results["front"] == null or signal_results["background_finishes"].size() < 2: + await escoria.get_tree().process_frame + + var front_finished = signal_results["front"] + var background_finishes: Array = signal_results["background_finishes"] + + assert_int(int(front_finished[0])).is_equal(ESCExecution.RC_OK) + assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(background_finishes[0][1])).is_equal("background_first") + assert_int(int(background_finishes[1][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(background_finishes[1][1])).is_equal("background_second") + assert_str(String(escoria.globals_manager.get_global("isolated_front_result"))).is_equal("front-cmd-after") + assert_bool(escoria.globals_manager.get_global("isolated_front_interrupted") == true).is_false() + assert_str(String(escoria.globals_manager.get_global("isolated_background_result"))).is_equal("first-cmd-after-first-second") + assert_bool(escoria.globals_manager.get_global("isolated_background_interrupted") == true).is_false() + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_global_interrupt_stops_running_channels_and_clears_queues() -> void: + # A global interrupt should stop every currently running channel and purge + # any queued successor events before they start. This fixture queues one + # running and one queued event on both `_front` and `bg_test`. + var events = _load_fixture_events("global_interrupt_clears_all_channels.esc") + assert_bool(events.has_event_with_target("front_running")).is_true() + assert_bool(events.has_event_with_target("front_queued")).is_true() + assert_bool(events.has_event_with_target("background_running")).is_true() + assert_bool(events.has_event_with_target("background_queued")).is_true() + escoria.globals_manager.set_global("global_interrupt_front_result", "start") + escoria.globals_manager.set_global("global_interrupt_background_result", "start") + escoria.globals_manager.set_global("global_interrupt_front_was_interrupted", false) + escoria.globals_manager.set_global("global_interrupt_background_was_interrupted", false) + + var signal_results := { + "front_finishes": [], + "background_finishes": [], + } + var on_front_finished := func(return_code, event_name) -> void: + signal_results["front_finishes"].append([return_code, event_name]) + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + signal_results["background_finishes"].append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_running")) + escoria.event_manager.queue_event(events.get_event_with_target("front_queued")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_running")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_queued")) + + await escoria.get_tree().create_timer(0.05).timeout + escoria.event_manager.interrupt() + + while signal_results["front_finishes"].is_empty() or signal_results["background_finishes"].is_empty(): + await escoria.get_tree().process_frame + + await escoria.get_tree().create_timer(0.05).timeout + + assert_int(signal_results["front_finishes"].size()).is_equal(1) + assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED) + assert_str(String(signal_results["front_finishes"][0][1])).is_equal("front_running") + assert_int(signal_results["background_finishes"].size()).is_equal(1) + assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED) + assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_running") + assert_str(String(signal_results["background_finishes"][0][2])).is_equal("bg_test") + assert_str(String(escoria.globals_manager.get_global("global_interrupt_front_result"))).is_equal("start") + assert_bool(escoria.globals_manager.get_global("global_interrupt_front_was_interrupted") == true).is_true() + assert_str(String(escoria.globals_manager.get_global("global_interrupt_background_result"))).is_equal("start") + assert_bool(escoria.globals_manager.get_global("global_interrupt_background_was_interrupted") == true).is_true() + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_queue_event_block_waits_for_correct_background_event_under_concurrency() -> void: + # Blocking queue_event waits must ignore unrelated background completions, + # including earlier events on the same channel, and return only when the + # specifically requested background event finishes. + var script_object := _compile_fixture_script("queue_event_block_waits_for_correct_background_event.esc") + assert_bool(script_object.has_event_with_target("background_first")).is_true() + assert_bool(script_object.has_event_with_target("background_target")).is_true() + assert_bool(script_object.has_event_with_target("front_event")).is_true() + escoria.globals_manager.set_global("block_wait_result", "start") + escoria.globals_manager.set_global("block_wait_front_result", "start") + + escoria.event_manager.queue_background_event( + "bg_test", + script_object.get_event_with_target("background_first") + ) + escoria.event_manager.queue_event(script_object.get_event_with_target("front_event")) + + var block_rc: int = await escoria.event_manager.queue_event_from_esc( + script_object, + "background_target", + "bg_test", + true + ) + + assert_int(block_rc).is_equal(ESCExecution.RC_OK) + assert_str(String(escoria.globals_manager.get_global("block_wait_result"))).is_equal("first-target") + assert_str(String(escoria.globals_manager.get_global("block_wait_front_result"))).is_equal("front") + + +func test_queue_event_block_waits_for_correct_front_event_under_concurrency() -> void: + # Blocking front queue_event waits must ignore both background completions + # and earlier foreground events, returning only when the specifically + # requested front event finishes. + var script_object := _compile_fixture_script("queue_event_block_waits_for_correct_front_event.esc") + assert_bool(script_object.has_event_with_target("front_first")).is_true() + assert_bool(script_object.has_event_with_target("front_target")).is_true() + assert_bool(script_object.has_event_with_target("background_event")).is_true() + escoria.globals_manager.set_global("front_block_wait_result", "start") + escoria.globals_manager.set_global("front_block_wait_background_result", "start") + + escoria.event_manager.queue_event(script_object.get_event_with_target("front_first")) + escoria.event_manager.queue_background_event( + "bg_test", + script_object.get_event_with_target("background_event") + ) + + var block_rc: int = await escoria.event_manager.queue_event_from_esc( + script_object, + "front_target", + "_front", + true + ) + + assert_int(block_rc).is_equal(ESCExecution.RC_OK) + assert_str(String(escoria.globals_manager.get_global("front_block_wait_result"))).is_equal("first-target") + assert_str(String(escoria.globals_manager.get_global("front_block_wait_background_result"))).is_equal("background") + + +func test_sequential_front_events_preserve_globals_between_runtime_interpreters() -> void: + # Each event-manager run currently observes globals written by earlier runs. + # The ownership refactor must preserve that user-visible behavior even though + # the underlying interpreter instances will stop sharing a singleton slot. + var events = _load_fixture_events("interpreter_runtime_state_persists_between_events.esc") + assert_bool(events.has_event_with_target("first")).is_true() + assert_bool(events.has_event_with_target("second")).is_true() + + var front_finishes: Array = [] + var on_front_finished := func(return_code, event_name) -> void: + front_finishes.append([return_code, event_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("first")) + while front_finishes.size() < 1: + await escoria.get_tree().process_frame + + escoria.event_manager.queue_event(events.get_event_with_target("second")) + while front_finishes.size() < 2: + await escoria.get_tree().process_frame + + assert_int(front_finishes.size()).is_equal(2) + assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(front_finishes[0][1])).is_equal("first") + assert_int(int(front_finishes[1][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(front_finishes[1][1])).is_equal("second") + assert_str(String(escoria.globals_manager.get_global("interpreter_runtime_result"))).is_equal("first-second") + + escoria.event_manager.event_finished.disconnect(on_front_finished) + + +func test_background_event_sees_globals_written_by_previous_front_event() -> void: + # Front and background channels each create their own runtime interpreter. + # A background event queued after a front event must still observe globals + # written by that earlier front-channel execution. + var events = _load_fixture_events("interpreter_runtime_state_persists_between_events.esc") + assert_bool(events.has_event_with_target("background_seed")).is_true() + assert_bool(events.has_event_with_target("background_after")).is_true() + + var front_finishes: Array = [] + var background_finishes: Array = [] + var on_front_finished := func(return_code, event_name) -> void: + front_finishes.append([return_code, event_name]) + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + background_finishes.append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("background_seed")) + while front_finishes.size() < 1: + await escoria.get_tree().process_frame + + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_after")) + while background_finishes.size() < 1: + await escoria.get_tree().process_frame + + assert_int(front_finishes.size()).is_equal(1) + assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(front_finishes[0][1])).is_equal("background_seed") + assert_int(background_finishes.size()).is_equal(1) + assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(background_finishes[0][1])).is_equal("background_after") + assert_str(String(background_finishes[0][2])).is_equal("bg_test") + assert_str(String(escoria.globals_manager.get_global("interpreter_runtime_background_result"))).is_equal("first-background") + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_running_interpreter_sees_new_global_defined_by_other_channel() -> void: + # A runtime interpreter that is already running must observe globals declared + # by a concurrent channel after it has started. Otherwise overlapping events + # can regress from shared-global behavior to "Undefined variable" failures. + var events = _load_fixture_events("concurrent_new_global_visibility.esc") + assert_bool(events.has_event_with_target("front_waits_for_new_global")).is_true() + assert_bool(events.has_event_with_target("background_declares_new_global")).is_true() + + var front_finishes: Array = [] + var background_finishes: Array = [] + var on_front_finished := func(return_code, event_name) -> void: + front_finishes.append([return_code, event_name]) + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + background_finishes.append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_waits_for_new_global")) + await escoria.get_tree().create_timer(0.05).timeout + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_declares_new_global")) + + while front_finishes.is_empty() or background_finishes.is_empty(): + await escoria.get_tree().process_frame + + assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(str(front_finishes[0][1])).is_equal("front_waits_for_new_global") + assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(str(background_finishes[0][1])).is_equal("background_declares_new_global") + assert_str(str(escoria.globals_manager.get_global("concurrent_visibility_result"))).is_equal("from-background") + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_queue_background_event_allows_consecutive_duplicate_queue_entries() -> void: + # Queueing the same background event twice in a row should be allowed on a + # background channel. In practice these queues are populated via ASHES + # commands, so the second request should remain queued behind the first run. + var events = _load_fixture_events("background_duplicate_suppression.esc") + assert_bool(events.has_event_with_target("repeat_event")).is_true() + escoria.globals_manager.set_global("background_duplicate_count", 0) + escoria.globals_manager.set_global("background_duplicate_last", "start") + + var background_finishes: Array = [] + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + background_finishes.append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event")) + + while background_finishes.size() < 2: + await escoria.get_tree().process_frame + + assert_int(background_finishes.size()).is_equal(2) + assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(background_finishes[0][1])).is_equal("repeat_event") + assert_str(String(background_finishes[0][2])).is_equal("bg_test") + assert_int(int(background_finishes[1][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(background_finishes[1][1])).is_equal("repeat_event") + assert_str(String(background_finishes[1][2])).is_equal("bg_test") + assert_int(int(escoria.globals_manager.get_global("background_duplicate_count"))).is_equal(2) + assert_str(String(escoria.globals_manager.get_global("background_duplicate_last"))).is_equal("tick") + + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_queue_background_event_allows_same_event_after_current_run_starts() -> void: + # Once the first background event is already running, queueing the same event + # again on that channel should queue a second run behind it instead of being + # suppressed as a duplicate. + var events = _load_fixture_events("background_duplicate_suppression.esc") + assert_bool(events.has_event_with_target("repeat_event")).is_true() + escoria.globals_manager.set_global("background_duplicate_count", 0) + escoria.globals_manager.set_global("background_duplicate_last", "start") + + var background_finishes: Array = [] + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + background_finishes.append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event")) + await escoria.get_tree().create_timer(0.01).timeout + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("repeat_event")) + + while background_finishes.size() < 2: + await escoria.get_tree().process_frame + + assert_int(background_finishes.size()).is_equal(2) + assert_int(int(background_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(background_finishes[0][1])).is_equal("repeat_event") + assert_int(int(background_finishes[1][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(background_finishes[1][1])).is_equal("repeat_event") + assert_int(int(escoria.globals_manager.get_global("background_duplicate_count"))).is_equal(2) + assert_str(String(escoria.globals_manager.get_global("background_duplicate_last"))).is_equal("tick") + + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_global_interrupt_preserves_exception_events() -> void: + # `interrupt(exceptions=...)` should leave only the named running and queued + # events alive. Here the front running event and one queued background event + # are excepted, while the other front/background events must be cleared. + var events = _load_fixture_events("global_interrupt_preserves_exceptions.esc") + assert_bool(events.has_event_with_target("front_keep")).is_true() + assert_bool(events.has_event_with_target("front_drop")).is_true() + assert_bool(events.has_event_with_target("background_drop")).is_true() + assert_bool(events.has_event_with_target("background_keep")).is_true() + escoria.globals_manager.set_global("interrupt_except_front_result", "start") + escoria.globals_manager.set_global("interrupt_except_front_interrupted", false) + escoria.globals_manager.set_global("interrupt_except_background_result", "start") + escoria.globals_manager.set_global("interrupt_except_background_interrupted", false) + escoria.globals_manager.set_global("interrupt_except_background_keep_result", "start") + + var signal_results := { + "front_finishes": [], + "background_finishes": [], + } + var on_front_finished := func(return_code, event_name) -> void: + signal_results["front_finishes"].append([return_code, event_name]) + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + signal_results["background_finishes"].append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_keep")) + escoria.event_manager.queue_event(events.get_event_with_target("front_drop")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_drop")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_keep")) + + await escoria.get_tree().create_timer(0.05).timeout + escoria.event_manager.interrupt(PackedStringArray(["front_keep", "background_keep"])) + + while signal_results["front_finishes"].is_empty() or signal_results["background_finishes"].size() < 2: + await escoria.get_tree().process_frame + + assert_int(signal_results["front_finishes"].size()).is_equal(1) + assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(signal_results["front_finishes"][0][1])).is_equal("front_keep") + assert_int(signal_results["background_finishes"].size()).is_equal(2) + assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED) + assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_drop") + assert_int(int(signal_results["background_finishes"][1][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(signal_results["background_finishes"][1][1])).is_equal("background_keep") + assert_str(String(escoria.globals_manager.get_global("interrupt_except_front_result"))).is_equal("front-cmd-after") + assert_bool(escoria.globals_manager.get_global("interrupt_except_front_interrupted") == true).is_false() + assert_str(String(escoria.globals_manager.get_global("interrupt_except_background_result"))).is_equal("start") + assert_bool(escoria.globals_manager.get_global("interrupt_except_background_interrupted") == true).is_true() + assert_str(String(escoria.globals_manager.get_global("interrupt_except_background_keep_result"))).is_equal("bg-keep-after") + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_scheduled_front_event_runs_after_current_front_event_while_background_is_busy() -> void: + # Scheduled events always feed the `_front` queue. They should still dispatch + # while a background channel is busy, but must wait for the current front + # event to finish before the scheduled front event runs. + var events = _load_fixture_events("scheduled_front_event_with_busy_background.esc") + assert_bool(events.has_event_with_target("front_running")).is_true() + assert_bool(events.has_event_with_target("scheduled_front")).is_true() + assert_bool(events.has_event_with_target("background_running")).is_true() + escoria.globals_manager.set_global("scheduled_front_result", "start") + escoria.globals_manager.set_global("scheduled_front_interrupted", false) + escoria.globals_manager.set_global("scheduled_background_result", "start") + escoria.globals_manager.set_global("scheduled_background_interrupted", false) + + var signal_results := { + "front_finishes": [], + "background_finishes": [], + } + var on_front_finished := func(return_code, event_name) -> void: + signal_results["front_finishes"].append([return_code, event_name]) + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + signal_results["background_finishes"].append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_running")) + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_running")) + escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.05, "") + + while signal_results["front_finishes"].size() < 2 or signal_results["background_finishes"].is_empty(): + await escoria.get_tree().process_frame + + assert_int(signal_results["front_finishes"].size()).is_equal(2) + assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(signal_results["front_finishes"][0][1])).is_equal("front_running") + assert_int(int(signal_results["front_finishes"][1][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(signal_results["front_finishes"][1][1])).is_equal("scheduled_front") + assert_int(signal_results["background_finishes"].size()).is_equal(1) + assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_running") + assert_str(String(escoria.globals_manager.get_global("scheduled_front_result"))).is_equal("front-running-after-scheduled") + assert_bool(escoria.globals_manager.get_global("scheduled_front_interrupted") == true).is_false() + assert_str(String(escoria.globals_manager.get_global("scheduled_background_result"))).is_equal("background-running-after") + assert_bool(escoria.globals_manager.get_global("scheduled_background_interrupted") == true).is_false() + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_global_interrupt_preserves_scheduled_events_before_dispatch() -> void: + # Global interrupt should stop the currently running event, but it should + # not clear scheduled events that have not yet been promoted into the front + # queue. The scheduled front event should therefore still dispatch and + # complete normally after the interrupted event finishes. + var events = _load_fixture_events("scheduled_event_interrupted_before_dispatch.esc") + assert_bool(events.has_event_with_target("front_running")).is_true() + assert_bool(events.has_event_with_target("scheduled_front")).is_true() + escoria.globals_manager.set_global("scheduled_interrupt_result", "start") + escoria.globals_manager.set_global("scheduled_interrupt_was_interrupted", false) + + var front_finishes: Array = [] + var on_front_finished := func(return_code, event_name) -> void: + front_finishes.append([return_code, event_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + + escoria.event_manager.queue_event(events.get_event_with_target("front_running")) + escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.2, "") + + await escoria.get_tree().create_timer(0.05).timeout + escoria.event_manager.interrupt() + + while front_finishes.is_empty(): + await escoria.get_tree().process_frame + + await escoria.get_tree().create_timer(0.25).timeout + + assert_int(front_finishes.size()).is_equal(2) + assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_INTERRUPTED) + assert_str(String(front_finishes[0][1])).is_equal("front_running") + assert_int(int(front_finishes[1][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(front_finishes[1][1])).is_equal("scheduled_front") + assert_str(String(escoria.globals_manager.get_global("scheduled_interrupt_result"))).is_equal("start-scheduled") + assert_bool(escoria.globals_manager.get_global("scheduled_interrupt_was_interrupted") == true).is_true() + + escoria.event_manager.event_finished.disconnect(on_front_finished) + + +func test_clear_event_queue_preserves_scheduled_events() -> void: + # Clearing the event queue should only remove events that are already queued + # for execution. Scheduled events live in a separate list until `_process()` + # promotes them into the front queue, so they should still dispatch after + # `clear_event_queue()` is called. + var events = _load_fixture_events("scheduled_event_interrupted_before_dispatch.esc") + assert_bool(events.has_event_with_target("scheduled_front")).is_true() + escoria.globals_manager.set_global("scheduled_interrupt_result", "start") + + var front_finishes: Array = [] + var on_front_finished := func(return_code, event_name) -> void: + front_finishes.append([return_code, event_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + + escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.05, "") + escoria.event_manager.clear_event_queue() + + while front_finishes.is_empty(): + await escoria.get_tree().process_frame + + assert_int(front_finishes.size()).is_equal(1) + assert_int(int(front_finishes[0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(front_finishes[0][1])).is_equal("scheduled_front") + assert_str(String(escoria.globals_manager.get_global("scheduled_interrupt_result"))).is_equal("start-scheduled") + + escoria.event_manager.event_finished.disconnect(on_front_finished) + + +func test_interrupt_channel_does_not_affect_still_scheduled_front_events() -> void: + # A scheduled front event should be unaffected by `interrupt_channel()` for a + # background channel while it is still waiting in `scheduled_events`. Once it + # is later promoted into `_front`, it should run like any other front-queue + # event. + var events = _load_fixture_events("scheduled_front_event_with_busy_background.esc") + assert_bool(events.has_event_with_target("scheduled_front")).is_true() + assert_bool(events.has_event_with_target("background_running")).is_true() + escoria.globals_manager.set_global("scheduled_front_result", "start") + escoria.globals_manager.set_global("scheduled_front_interrupted", false) + escoria.globals_manager.set_global("scheduled_background_result", "start") + escoria.globals_manager.set_global("scheduled_background_interrupted", false) + + var signal_results := { + "front_finishes": [], + "background_finishes": [], + } + var on_front_finished := func(return_code, event_name) -> void: + signal_results["front_finishes"].append([return_code, event_name]) + var on_background_finished := func(return_code, event_name, finished_channel_name) -> void: + if finished_channel_name == "bg_test": + signal_results["background_finishes"].append([return_code, event_name, finished_channel_name]) + + escoria.event_manager.event_finished.connect(on_front_finished) + escoria.event_manager.background_event_finished.connect(on_background_finished) + + escoria.event_manager.queue_background_event("bg_test", events.get_event_with_target("background_running")) + escoria.event_manager.schedule_event(events.get_event_with_target("scheduled_front"), 0.2, "") + + await escoria.get_tree().create_timer(0.05).timeout + escoria.event_manager.interrupt_channel("bg_test") + + while signal_results["front_finishes"].is_empty() or signal_results["background_finishes"].is_empty(): + await escoria.get_tree().process_frame + + assert_int(signal_results["background_finishes"].size()).is_equal(1) + assert_int(int(signal_results["background_finishes"][0][0])).is_equal(ESCExecution.RC_INTERRUPTED) + assert_str(String(signal_results["background_finishes"][0][1])).is_equal("background_running") + assert_int(signal_results["front_finishes"].size()).is_equal(1) + assert_int(int(signal_results["front_finishes"][0][0])).is_equal(ESCExecution.RC_OK) + assert_str(String(signal_results["front_finishes"][0][1])).is_equal("scheduled_front") + assert_str(String(escoria.globals_manager.get_global("scheduled_front_result"))).is_equal("start-scheduled") + assert_bool(escoria.globals_manager.get_global("scheduled_front_interrupted") == true).is_false() + assert_str(String(escoria.globals_manager.get_global("scheduled_background_result"))).is_equal("start") + assert_bool(escoria.globals_manager.get_global("scheduled_background_interrupted") == true).is_true() + + escoria.event_manager.event_finished.disconnect(on_front_finished) + escoria.event_manager.background_event_finished.disconnect(on_background_finished) + + +func test_immediate_command_preserves_statement_ordering() -> void: + # A synchronous command should complete before the next statement runs. The + # trailing assignment observes the command's mutation and appends to it. + var outcome := await _interpret_fixture("immediate_command_ordering.esc") + var globals: Dictionary = outcome.globals + assert_bool(globals.has("result")).is_true() + assert_str(String(globals["result"])).is_equal("cmd-after") + + +func test_delayed_command_preserves_statement_ordering() -> void: + # An asynchronous command must also block statement sequencing. If the + # interpreter failed to await it, the trailing assignment would append to the + # pre-command value instead of the command-written one. + var outcome := await _interpret_fixture("delayed_command_ordering.esc") + var globals: Dictionary = outcome.globals + assert_bool(globals.has("result")).is_true() + assert_str(String(globals["result"])).is_equal("cmd-after") + + +func test_set_animations_updates_persisted_player_resource_on_repeat_calls() -> void: + # Room changes restore player animations from the reserved + # `ANIMATION_RESOURCES` global. Repeated `set_animations()` calls must update + # the stored path each time, otherwise the next room load re-applies a stale + # costume even if the live player was changed back already. + var previous_animations = escoria.globals_manager.get_global( + escoria.room_manager.GLOBAL_ANIMATION_RESOURCES + ) + var room := ESCRoom.new() + room.global_id = "test_room_issue_1226" + escoria.object_manager.set_current_room(room) + + var player := MARK_SCENE.instantiate() as ESCPlayer + player.global_id = "player" + escoria.object_manager.register_object( + ESCObject.new(player.global_id, player), + room, + true, + false + ) + escoria.globals_manager.set_global( + escoria.room_manager.GLOBAL_ANIMATION_RESOURCES, + {}, + true + ) + + var command := SetAnimationsCommand.new() + var jester_animations := "res://game/characters/mark/mark_animations_jester.tres" + var default_animations := "res://game/characters/mark/mark_animations.tres" + + assert_int(command.run([player.global_id, jester_animations])).is_equal(ESCExecution.RC_OK) + assert_int(command.run([player.global_id, default_animations])).is_equal(ESCExecution.RC_OK) + + var animations: Dictionary = escoria.globals_manager.get_global( + escoria.room_manager.GLOBAL_ANIMATION_RESOURCES + ) + assert_bool(animations.has(player.global_id)).is_true() + assert_str(String(animations[player.global_id])).is_equal(default_animations) + + var room_key := ESCRoomObjectsKey.new() + room_key.room_global_id = room.global_id + room_key.room_instance_id = room.get_instance_id() + escoria.object_manager.unregister_object_by_global_id(player.global_id, room_key) + escoria.globals_manager.set_global( + escoria.room_manager.GLOBAL_ANIMATION_RESOURCES, + previous_animations if previous_animations != null else {}, + true + ) + player.free() + room.free() + + +func test_set_speed_rejects_negative_values_but_accepts_zero() -> void: + var room := ESCRoom.new() + room.global_id = "test_room_set_speed_contract" + escoria.object_manager.set_current_room(room) + + var player := MARK_SCENE.instantiate() as ESCPlayer + player.global_id = "player" + escoria.object_manager.register_object( + ESCObject.new(player.global_id, player), + room, + true, + false + ) + + var command := SetSpeedCommand.new() + var initial_speed := player.speed + + assert_bool(command.validate([player.global_id, -1])).is_false() + assert_int(player.speed).is_equal(initial_speed) + + assert_bool(command.validate([player.global_id, 0])).is_true() + assert_int(command.run([player.global_id, 0])).is_equal(ESCExecution.RC_OK) + assert_int(player.speed).is_equal(0) + + var room_key := ESCRoomObjectsKey.new() + room_key.room_global_id = room.global_id + room_key.room_instance_id = room.get_instance_id() + escoria.object_manager.unregister_object_by_global_id(player.global_id, room_key) + player.free() + room.free() + + +func test_trigger_actions_select_targeted_events_for_activating_object() -> void: + var script_object := _compile_fixture_script("trigger_targeted_events_use_object_id.esc") + var room := ESCRoom.new() + room.global_id = "test_trigger_targeted_events_room" + escoria.object_manager.set_current_room(room) + + var trigger_node := Node2D.new() + var trigger_object := ESCObject.new("trigger_under_test", trigger_node) + trigger_object.events = script_object.events + escoria.object_manager.register_object(trigger_object, room, true, false) + + escoria.globals_manager.set_global("trigger_in_result", "unset", true) + escoria.globals_manager.set_global("trigger_out_result", "unset", true) + + var finished_events: Array = [] + var on_front_finished := func(return_code, event_name) -> void: + if return_code == ESCExecution.RC_OK: + finished_events.append(event_name) + + escoria.event_manager.event_finished.connect(on_front_finished) + + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_IN, + [trigger_object.global_id, "player", "trigger_in"] + ) + + if not await _wait_for_finished_events(finished_events, 1, "targeted trigger_in"): + escoria.event_manager.event_finished.disconnect(on_front_finished) + _cleanup_registered_object(trigger_object, trigger_node, room) + return + + assert_str(String(escoria.globals_manager.get_global("trigger_in_result"))).is_equal( + "player-in" + ) + assert_str(String(finished_events[0])).is_equal("trigger_in") + + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_OUT, + [trigger_object.global_id, "player", "trigger_out"] + ) + + if not await _wait_for_finished_events(finished_events, 2, "targeted trigger_out"): + escoria.event_manager.event_finished.disconnect(on_front_finished) + _cleanup_registered_object(trigger_object, trigger_node, room) + return + + assert_str(String(escoria.globals_manager.get_global("trigger_out_result"))).is_equal( + "player-out" + ) + assert_str(String(finished_events[1])).is_equal("trigger_out") + + escoria.event_manager.event_finished.disconnect(on_front_finished) + + _cleanup_registered_object(trigger_object, trigger_node, room) + + +func test_trigger_actions_fall_back_to_targetless_events() -> void: + var script_object := _compile_fixture_script("trigger_targeted_events_use_object_id.esc") + var room := ESCRoom.new() + room.global_id = "test_trigger_targetless_events_room" + escoria.object_manager.set_current_room(room) + + var trigger_node := Node2D.new() + var trigger_object := ESCObject.new("trigger_under_test", trigger_node) + trigger_object.events = script_object.events + escoria.object_manager.register_object(trigger_object, room, true, false) + + escoria.globals_manager.set_global("trigger_in_result", "unset", true) + escoria.globals_manager.set_global("trigger_out_result", "unset", true) + + var finished_events: Array = [] + var on_front_finished := func(return_code, event_name) -> void: + if return_code == ESCExecution.RC_OK: + finished_events.append(event_name) + + escoria.event_manager.event_finished.connect(on_front_finished) + + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_IN, + [trigger_object.global_id, "unexpected_actor", "trigger_in"] + ) + + if not await _wait_for_finished_events(finished_events, 1, "targetless trigger_in fallback"): + escoria.event_manager.event_finished.disconnect(on_front_finished) + _cleanup_registered_object(trigger_object, trigger_node, room) + return + + assert_str(String(escoria.globals_manager.get_global("trigger_in_result"))).is_equal( + "fallback-in" + ) + assert_str(String(finished_events[0])).is_equal("trigger_in") + + escoria.action_manager.do( + escoria.action_manager.ACTION.TRIGGER_OUT, + [trigger_object.global_id, "unexpected_actor", "trigger_out"] + ) + + if not await _wait_for_finished_events(finished_events, 2, "targetless trigger_out fallback"): + escoria.event_manager.event_finished.disconnect(on_front_finished) + _cleanup_registered_object(trigger_object, trigger_node, room) + return + + assert_str(String(escoria.globals_manager.get_global("trigger_out_result"))).is_equal( + "fallback-out" + ) + assert_str(String(finished_events[1])).is_equal("trigger_out") + + escoria.event_manager.event_finished.disconnect(on_front_finished) + + _cleanup_registered_object(trigger_object, trigger_node, room) + + +func _cleanup_registered_object(trigger_object: ESCObject, trigger_node: Node, room: ESCRoom) -> void: + var room_key := ESCRoomObjectsKey.new() + room_key.room_global_id = room.global_id + room_key.room_instance_id = room.get_instance_id() + escoria.object_manager.unregister_object_by_global_id(trigger_object.global_id, room_key) + trigger_node.free() + room.free() + + +func _wait_for_finished_events( + finished_events: Array, + expected_size: int, + context: String +) -> bool: + for _frame in range(120): + if finished_events.size() >= expected_size: + return true + + await escoria.get_tree().process_frame + + fail( + "Timed out waiting for %s; expected %d finished event(s), got %d." % [ + context, + expected_size, + finished_events.size() + ] + ) + return false + + +func _interpret_fixture(name: String, dialog_choices: Array = []) -> Dictionary: + # Shared interpreter harness for fixture-based tests. This intentionally + # runs the full scan -> parse -> resolve -> interpret pipeline so the tests + # cover the real runtime path rather than isolated visitor calls. + var source := _load_fixture(name) + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path(name)) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + resolver.resolve(statements) + var dialog_player = null + + if not dialog_choices.is_empty(): + # Install a temporary deterministic dialog player only for tests that + # need scripted choices, then clean it up afterward to avoid orphan nodes. + dialog_player = DialogPlayerDouble.new(dialog_choices) + escoria.set("dialog_player", dialog_player) + + await interpreter.interpret(statements) + + var globals := interpreter.get_global_values().duplicate(true) + var dialog_choices_consumed := 0 + var remaining_dialog_choices := 0 + + if dialog_player: + dialog_choices_consumed = dialog_player.get_choices_consumed() + remaining_dialog_choices = dialog_player.get_remaining_choices() + + _cleanup_dialog_test(interpreter, dialog_player) + + return { + "globals": globals, + "dialog_choices_consumed": dialog_choices_consumed, + "remaining_dialog_choices": remaining_dialog_choices, + } + + +func test_dialog_start_args_are_passed_to_runtime_dialog() -> void: + var script_object := _compile_fixture_script("dialog_start_args_reach_runtime.esc") + var dialog_player := DialogPlayerDouble.new([1]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(1) + + var started_dialogs := dialog_player.get_dialogs_started() + assert_int(started_dialogs.size()).is_equal(1) + assert_str(started_dialogs[0]["avatar"]).is_equal("res://game/dialog_avatars/player.tres") + assert_int(started_dialogs[0]["timeout"]).is_equal(5) + assert_int(started_dialogs[0]["timeout_option"]).is_equal(2) + assert_int(started_dialogs[0]["authored_options_count"]).is_equal(2) + assert_int(started_dialogs[0]["options_size"]).is_equal(2) + assert_str(started_dialogs[0]["visible_timeout_option"]).is_equal("Second option") + assert_array(started_dialogs[0]["option_source_indexes"]).contains_exactly([1, 2]) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_timeout_option_uses_authored_index_after_filtering() -> void: + var script_object := _compile_fixture_script("dialog_timeout_visible_authored_default_after_filtering.esc") + var dialog_player := DialogPlayerDouble.new([1]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(1) + + var started_dialogs := dialog_player.get_dialogs_started() + assert_int(started_dialogs.size()).is_equal(1) + assert_int(started_dialogs[0]["timeout"]).is_equal(5) + assert_int(started_dialogs[0]["timeout_option"]).is_equal(3) + assert_int(started_dialogs[0]["authored_options_count"]).is_equal(3) + assert_int(started_dialogs[0]["options_size"]).is_equal(2) + assert_array(started_dialogs[0]["option_source_indexes"]).contains_exactly([2, 3]) + assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Second option", "Third option"]) + assert_str(started_dialogs[0]["visible_timeout_option"]).is_equal("Third option") + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_timeout_option_hidden_after_filtering_is_not_available() -> void: + var script_object := _compile_fixture_script("dialog_timeout_hidden_authored_default_after_filtering.esc") + var dialog_player := DialogPlayerDouble.new([0]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(1) + + var started_dialogs := dialog_player.get_dialogs_started() + assert_int(started_dialogs.size()).is_equal(1) + assert_int(started_dialogs[0]["timeout"]).is_equal(5) + assert_int(started_dialogs[0]["timeout_option"]).is_equal(1) + assert_int(started_dialogs[0]["authored_options_count"]).is_equal(3) + assert_int(started_dialogs[0]["options_size"]).is_equal(2) + assert_array(started_dialogs[0]["option_source_indexes"]).contains_exactly([2, 3]) + assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Second option", "Third option"]) + assert_object(started_dialogs[0]["visible_timeout_option"]).is_null() + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_simple_chooser_does_not_start_timer_when_timeout_default_is_hidden() -> void: + var dialog := ESCDialog.new() + dialog.timeout = 5 + dialog.timeout_option = 1 + dialog.authored_options_count = 2 + dialog.options = [ + _make_dialog_option("Visible option", 2), + ] + + var chooser = SIMPLE_CHOOSER_SCENE.instantiate() + add_child(chooser) + chooser.set_dialog(dialog) + chooser.show_chooser() + + assert_bool(chooser.get_node("Timer").is_stopped()).is_true() + assert_float(chooser.get_node("TimerProgress").value).is_equal(0.0) + + chooser.queue_free() + + +func test_simple_chooser_starts_timer_when_timeout_default_is_visible() -> void: + var dialog := ESCDialog.new() + dialog.timeout = 5 + dialog.timeout_option = 2 + dialog.authored_options_count = 2 + dialog.options = [ + _make_dialog_option("First option", 1), + _make_dialog_option("Second option", 2), + ] + + var chooser = SIMPLE_CHOOSER_SCENE.instantiate() + add_child(chooser) + chooser.set_dialog(dialog) + chooser.show_chooser() + + assert_bool(chooser.get_node("Timer").is_stopped()).is_false() + + chooser.queue_free() + + +func test_dialog_start_invalid_avatar_type_returns_error() -> void: + var script_object := _compile_fixture_script("dialog_start_invalid_avatar_type.esc") + var dialog_player := DialogPlayerDouble.new([]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(0) + assert_int(dialog_player.get_dialogs_started().size()).is_equal(0) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_start_invalid_timeout_type_returns_error() -> void: + var script_object := _compile_fixture_script("dialog_start_invalid_timeout_type.esc") + var dialog_player := DialogPlayerDouble.new([]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(0) + assert_int(dialog_player.get_dialogs_started().size()).is_equal(0) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_option_translation_key_is_passed_to_runtime_dialog() -> void: + var script_object := _compile_fixture_script("dialog_option_translation_key.esc") + var dialog_player := DialogPlayerDouble.new([0]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(1) + + var started_dialogs := dialog_player.get_dialogs_started() + assert_int(started_dialogs.size()).is_equal(1) + assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["UNCLE_SVEN", ""]) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_start_invalid_timeout_option_type_returns_error() -> void: + var script_object := _compile_fixture_script("dialog_start_invalid_timeout_option_type.esc") + var dialog_player := DialogPlayerDouble.new([]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(0) + assert_int(dialog_player.get_dialogs_started().size()).is_equal(0) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_start_invalid_timeout_option_negative_returns_error() -> void: + var script_object := _compile_fixture_script("dialog_start_invalid_timeout_option_negative.esc") + var dialog_player := DialogPlayerDouble.new([]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + assert_int(dialog_player.get_choices_consumed()).is_equal(0) + assert_int(dialog_player.get_dialogs_started().size()).is_equal(0) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_option_translation_key_beats_legacy_prefix_fallback() -> void: + var script_object := _compile_fixture_script("dialog_option_translation_key_beats_legacy_prefix.esc") + var dialog_player := DialogPlayerDouble.new([0]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + + var started_dialogs := dialog_player.get_dialogs_started() + assert_int(started_dialogs.size()).is_equal(1) + assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["OUTER_KEY"]) + assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["INNER_KEY:Fallback text"]) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_option_translation_key_with_condition_survives_runtime_filtering() -> void: + var script_object := _compile_fixture_script("dialog_option_translation_key_with_condition.esc") + var dialog_player := DialogPlayerDouble.new([0]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + + var started_dialogs := dialog_player.get_dialogs_started() + assert_int(started_dialogs.size()).is_equal(1) + assert_int(started_dialogs[0]["options_size"]).is_equal(1) + assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["CONDITIONAL_KEY"]) + assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Conditional option"]) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func test_dialog_option_translation_key_with_expression_preserves_key_and_value() -> void: + var script_object := _compile_fixture_script("dialog_option_translation_key_with_expression.esc") + var dialog_player := DialogPlayerDouble.new([0]) + escoria.set("dialog_player", dialog_player) + var interpreter := ESCInterpreter.new(ESCCompiler.load_commands(), {}) + var resolver := ESCResolver.new(interpreter) + var event: ESCGrammarStmts.Event = script_object.get_event_with_target("talk") + + assert_object(event).is_not_null() + + resolver.resolve(event) + + await interpreter.interpret(event) + + var started_dialogs := dialog_player.get_dialogs_started() + assert_int(started_dialogs.size()).is_equal(1) + assert_array(started_dialogs[0]["option_keys"]).contains_exactly(["DYNAMIC_KEY"]) + assert_array(started_dialogs[0]["option_texts"]).contains_exactly(["Dynamic option"]) + + _cleanup_dialog_test(interpreter, dialog_player) + + +func _cleanup_dialog_test(interpreter: ESCInterpreter, dialog_player: DialogPlayerDouble) -> void: + interpreter.cleanup() + + if dialog_player: + escoria.set("dialog_player", null) + dialog_player.queue_free() + + +func test_compiler_rejects_exact_duplicate_event_targets() -> void: + var source := _load_fixture("duplicate_exact_target_event_overwrites.esc") + var compiler := ESCCompiler.new() + var script_object := compiler.compile(source) + + assert_bool(compiler.had_error).is_true() + assert_int(script_object.events.events.size()).is_equal(1) + assert_object(script_object.get_event_with_target("use", "wrench")).is_not_null() + + +func _load_fixture(name: String) -> String: + return FileAccess.get_file_as_string(_fixture_path(name)) + + +func _load_fixture_events(name: String): + var script_object := _compile_fixture_script(name) + return script_object.events + + +func _compile_fixture_script(name: String) -> ESCScript: + var source := _load_fixture(name) + var compiler := ESCCompiler.new() + var script_object := compiler.compile(source, _fixture_path(name)) + assert_bool(compiler.had_error).is_false() + return script_object + + +func _fixture_path(name: String) -> String: + return "res://addons/escoria-core/testing/ashes/interpreter/fixtures/%s" % name + + +func _make_dialog_option(text: String, source_option_index: int) -> ESCDialogOption: + var option := ESCDialogOption.new() + option.option = text + option.source_option_index = source_option_index + option.set_is_valid(true) + return option diff --git a/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd.uid b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd.uid new file mode 100644 index 0000000..f4c0246 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/esc_interpreter_test.gd.uid @@ -0,0 +1 @@ +uid://cpogk3hmwoaom diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc new file mode 100644 index 0000000..6ab1752 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc @@ -0,0 +1,3 @@ +:repeat_event + test_delayed_set_global("background_duplicate_last", "tick", 0.05) + inc_global("background_duplicate_count") diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc.uid new file mode 100644 index 0000000..eeed814 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/background_duplicate_suppression.esc.uid @@ -0,0 +1 @@ +uid://c5o8v1l4n3xmd diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc new file mode 100644 index 0000000..e4c2d34 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc @@ -0,0 +1,8 @@ +:test + global result = 0 + var x = 1 + while true: + if true: + var x = 2 + break + result = x diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc.uid new file mode 100644 index 0000000..1060cfd --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/block_scope_restores_after_break.esc.uid @@ -0,0 +1 @@ +uid://ccf4pbps7lq2k diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc new file mode 100644 index 0000000..cb170eb --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc @@ -0,0 +1,14 @@ +:test + global result = "start" + ?! + - "Outer" + ?! + - "Middle" + ?! + - "Break out" + break 2 + result = "after_inner" + result = "after_middle" + - "Exit outer" + break + result = result + "-done" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc.uid new file mode 100644 index 0000000..31dc1f6 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/break_two_exits_two_dialog_levels.esc.uid @@ -0,0 +1 @@ +uid://dorj4y3h76w2j diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc new file mode 100644 index 0000000..4e9f649 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc @@ -0,0 +1,7 @@ +:first + global bg_queue_result = "first" + test_interruptible_set_global("bg_queue_result", "running", "bg_queue_interrupted", 0.2) + bg_queue_result = bg_queue_result + "-after" + +:second + bg_queue_result = "second" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc.uid new file mode 100644 index 0000000..45a95b6 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_clears_queue.esc.uid @@ -0,0 +1 @@ +uid://dp5nx4h5bqgu1 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc new file mode 100644 index 0000000..e3130d4 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc @@ -0,0 +1,9 @@ +:front_event + global channel_front_result = "start" + test_interruptible_set_global("channel_front_result", "front-cmd", "channel_front_was_interrupted", 0.2) + channel_front_result = channel_front_result + "-after" + +:background_event + global channel_background_result = "start" + test_interruptible_set_global("channel_background_result", "bg-cmd", "channel_background_was_interrupted", 0.2) + channel_background_result = channel_background_result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc.uid new file mode 100644 index 0000000..5e3ad25 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_interrupt_isolation.esc.uid @@ -0,0 +1 @@ +uid://cx76t0u0r5f4f diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc new file mode 100644 index 0000000..31098f8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc @@ -0,0 +1,12 @@ +:front_event + global isolated_front_result = "start" + test_interruptible_set_global("isolated_front_result", "front-cmd", "isolated_front_interrupted", 0.2) + isolated_front_result = isolated_front_result + "-after" + +:background_first + global isolated_background_result = "start" + test_interruptible_set_global("isolated_background_result", "first-cmd", "isolated_background_interrupted", 0.2) + isolated_background_result = isolated_background_result + "-after-first" + +:background_second + isolated_background_result = isolated_background_result + "-second" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc.uid new file mode 100644 index 0000000..7961883 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/channel_queue_progress_isolated.esc.uid @@ -0,0 +1 @@ +uid://dzq6qk1ufhcv0 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc new file mode 100644 index 0000000..3df3b97 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc @@ -0,0 +1,7 @@ +:front_waits_for_new_global + global concurrent_visibility_result = "start" + test_delayed_set_global("concurrent_visibility_progress", "waiting", 0.2) + concurrent_visibility_result = shared_runtime_global + +:background_declares_new_global + global shared_runtime_global = "from-background" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc.uid new file mode 100644 index 0000000..f19670e --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/concurrent_new_global_visibility.esc.uid @@ -0,0 +1 @@ +uid://a226b7bb41b142b5812d0d3dbd389a31 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc new file mode 100644 index 0000000..2ae480f --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc @@ -0,0 +1,7 @@ +:test + global result = "unset" + ?! + - "Run delayed" + test_delayed_set_global("result", "cmd", 0.01) + break + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc.uid new file mode 100644 index 0000000..d89858e --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_dialog_option.esc.uid @@ -0,0 +1 @@ +uid://e0743a93c87c4db4af251658723ed469 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc new file mode 100644 index 0000000..e55b9a6 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc @@ -0,0 +1,7 @@ +:test + global result = "start" + global count = 0 + while count < 1: + test_delayed_set_global("result", "cmd", 0.01) + count = count + 1 + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc.uid new file mode 100644 index 0000000..e832298 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_in_loop.esc.uid @@ -0,0 +1 @@ +uid://c08224d490434f5dbe8529aa021c6041 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc new file mode 100644 index 0000000..5aa9aed --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc @@ -0,0 +1,5 @@ +:test + global result = "start" + global was_interrupted = false + test_interruptible_set_global("result", "cmd", "was_interrupted", 0.2) + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc.uid new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt.esc.uid diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc new file mode 100644 index 0000000..e9187b1 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc @@ -0,0 +1,7 @@ +:test + global result = "start" + global was_interrupted = false + ?! + - "Wait" + test_interruptible_set_global("result", "cmd", "was_interrupted", 0.2) + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc.uid new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_dialog.esc.uid diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc new file mode 100644 index 0000000..fc19824 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc @@ -0,0 +1,8 @@ +:test + global result = "start" + global was_interrupted = false + global count = 0 + while count < 3: + test_interruptible_set_global("result", "cmd", "was_interrupted", 0.2) + count = count + 1 + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc.uid new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_interrupt_in_multi_loop.esc.uid diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc new file mode 100644 index 0000000..52ef2a4 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc @@ -0,0 +1,4 @@ +:test + global result = "unset" + test_delayed_set_global("result", "cmd", 0.01) + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc.uid new file mode 100644 index 0000000..a014454 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/delayed_command_ordering.esc.uid @@ -0,0 +1 @@ +uid://bl7s16m8hlyj1 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc new file mode 100644 index 0000000..eaf762e --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc @@ -0,0 +1,14 @@ +:talk + global result = "start" + global option1_is_valid = true + global option2_is_valid = true + + ?! + - "option1" [option1_is_valid] + result = result + "-say1" + option1_is_valid = false + - "option2" [option2_is_valid] + result = result + "-say2" + option2_is_valid = false + + result = result + "-finished" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc.uid new file mode 100644 index 0000000..fac2cb3 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_exhausted_options_runs_followup.esc.uid @@ -0,0 +1 @@ +uid://cn1f4l17yg4x8 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc new file mode 100644 index 0000000..69f88db --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc @@ -0,0 +1,6 @@ +:talk + ?! + - UNCLE_SVEN:"Uncle Sven sends regards." + break + - "Fallback" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc.uid new file mode 100644 index 0000000..d371627 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key.esc.uid @@ -0,0 +1 @@ +uid://dcjcq0w2qj1h2 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc new file mode 100644 index 0000000..1be9d33 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc @@ -0,0 +1,4 @@ +:talk + ?! + - OUTER_KEY:"INNER_KEY:Fallback text" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc.uid new file mode 100644 index 0000000..aff1543 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_beats_legacy_prefix.esc.uid @@ -0,0 +1 @@ +uid://cb71rbl5a7hlj diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc new file mode 100644 index 0000000..718a5fc --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc @@ -0,0 +1,7 @@ +:talk + global show_first = true + ?! + - CONDITIONAL_KEY:"Conditional option" [show_first] + break + - HIDDEN_KEY:"Hidden option" [false] + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc.uid new file mode 100644 index 0000000..4f944b1 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_condition.esc.uid @@ -0,0 +1 @@ +uid://bnep8t7ivgu7f diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc new file mode 100644 index 0000000..587bd39 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc @@ -0,0 +1,5 @@ +:talk + var option_text = "Dynamic option" + ?! + - DYNAMIC_KEY:option_text + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc.uid new file mode 100644 index 0000000..56eee44 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_option_translation_key_with_expression.esc.uid @@ -0,0 +1 @@ +uid://bf67f4h73tba2 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc new file mode 100644 index 0000000..924cf52 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc @@ -0,0 +1,9 @@ +:test + global result = "start" + var x = "outer" + ?! + - "Use inner" + var x = "inner" + result = x + break + result = result + ":" + x diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc.uid new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_scope_shadowing.esc.uid diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc new file mode 100644 index 0000000..aab29c6 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc @@ -0,0 +1,6 @@ +:talk + ?! "res://game/dialog_avatars/player.tres" 5 2 + - "First option" + pass + - "Second option" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc.uid new file mode 100644 index 0000000..8a474a9 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_args_reach_runtime.esc.uid @@ -0,0 +1 @@ +uid://b3a4m3l6nr6m7 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc new file mode 100644 index 0000000..368ef0b --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc @@ -0,0 +1,4 @@ +:talk + ?! 123 5 2 + - "Only option" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc.uid new file mode 100644 index 0000000..5708f33 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_avatar_type.esc.uid @@ -0,0 +1 @@ +uid://chev11x0d0lg0 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc new file mode 100644 index 0000000..06cab55 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc @@ -0,0 +1,4 @@ +:talk + ?! "res://game/dialog_avatars/player.tres" 5 0 - 1 + - "Only option" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc.uid new file mode 100644 index 0000000..a7cf6b8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_negative.esc.uid @@ -0,0 +1 @@ +uid://db2w5uh1qvqst diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc new file mode 100644 index 0000000..bc71bc7 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc @@ -0,0 +1,4 @@ +:talk + ?! "res://game/dialog_avatars/player.tres" 5 "two" + - "Only option" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc.uid new file mode 100644 index 0000000..deb663d --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_option_type.esc.uid @@ -0,0 +1 @@ +uid://b5fstn4fo0igo diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc new file mode 100644 index 0000000..351be56 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc @@ -0,0 +1,4 @@ +:talk + ?! "res://game/dialog_avatars/player.tres" "five" 2 + - "Only option" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc.uid new file mode 100644 index 0000000..db53e65 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_start_invalid_timeout_type.esc.uid @@ -0,0 +1 @@ +uid://cy8r7l4o3dmn0 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_hidden_authored_default_after_filtering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_hidden_authored_default_after_filtering.esc new file mode 100644 index 0000000..80f8ef7 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_hidden_authored_default_after_filtering.esc @@ -0,0 +1,10 @@ +:talk + global show_first = false + + ?! "-" 5 1 + - "First option" [show_first] + break + - "Second option" + break + - "Third option" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_visible_authored_default_after_filtering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_visible_authored_default_after_filtering.esc new file mode 100644 index 0000000..0067327 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/dialog_timeout_visible_authored_default_after_filtering.esc @@ -0,0 +1,10 @@ +:talk + global show_first = false + + ?! "-" 5 3 + - "First option" [show_first] + break + - "Second option" + break + - "Third option" + break diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc new file mode 100644 index 0000000..770224a --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc @@ -0,0 +1,11 @@ +:test + global result = "start" + ?! + - "Outer" + ?! + - "Done" + done + result = "after_nested" + - "Exit outer" + break + result = result + "-done" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc.uid new file mode 100644 index 0000000..9b3eaf5 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_exits_entire_nested_dialog.esc.uid @@ -0,0 +1 @@ +uid://catfpqk5u8vj0 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc new file mode 100644 index 0000000..a37b050 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc @@ -0,0 +1,7 @@ +:test + global result = 0 + ?! + - "Outer" + if true: + done + result = 1 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc.uid new file mode 100644 index 0000000..8e5c281 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/done_nested_block_in_dialog.esc.uid @@ -0,0 +1 @@ +uid://bgxmdpje356jd diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc new file mode 100644 index 0000000..8461cff --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc @@ -0,0 +1,5 @@ +:use "wrench" + test_set_global("result", "first") + +:use "wrench" + test_set_global("result", "second") diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc.uid new file mode 100644 index 0000000..600a3bf --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/duplicate_exact_target_event_overwrites.esc.uid @@ -0,0 +1 @@ +uid://cfpnxq6v1bg1t diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc new file mode 100644 index 0000000..e11c818 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc @@ -0,0 +1,15 @@ +:front_running + global global_interrupt_front_result = "start" + test_interruptible_set_global("global_interrupt_front_result", "front-cmd", "global_interrupt_front_was_interrupted", 0.2) + global_interrupt_front_result = global_interrupt_front_result + "-after" + +:front_queued + global_interrupt_front_result = "front-queued" + +:background_running + global global_interrupt_background_result = "start" + test_interruptible_set_global("global_interrupt_background_result", "bg-cmd", "global_interrupt_background_was_interrupted", 0.2) + global_interrupt_background_result = global_interrupt_background_result + "-after" + +:background_queued + global_interrupt_background_result = "bg-queued" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc.uid new file mode 100644 index 0000000..5547f25 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_clears_all_channels.esc.uid @@ -0,0 +1 @@ +uid://cje3uy4r8lh1x diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc new file mode 100644 index 0000000..a9ffa26 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc @@ -0,0 +1,17 @@ +:front_keep + global interrupt_except_front_result = "start" + test_interruptible_set_global("interrupt_except_front_result", "front-cmd", "interrupt_except_front_interrupted", 0.2) + interrupt_except_front_result = interrupt_except_front_result + "-after" + +:front_drop + interrupt_except_front_result = "front-drop" + +:background_drop + global interrupt_except_background_result = "start" + test_interruptible_set_global("interrupt_except_background_result", "bg-drop", "interrupt_except_background_interrupted", 0.2) + interrupt_except_background_result = interrupt_except_background_result + "-after" + +:background_keep + global interrupt_except_background_keep_result = "start" + test_delayed_set_global("interrupt_except_background_keep_result", "bg-keep", 0.01) + interrupt_except_background_keep_result = interrupt_except_background_keep_result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc.uid new file mode 100644 index 0000000..dc38433 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/global_interrupt_preserves_exceptions.esc.uid @@ -0,0 +1 @@ +uid://ceu2x1lbwfxby diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc new file mode 100644 index 0000000..7d39c5f --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc @@ -0,0 +1,4 @@ +:test + global result = "unset" + test_set_global("result", "cmd") + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc.uid new file mode 100644 index 0000000..16cf665 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/immediate_command_ordering.esc.uid @@ -0,0 +1 @@ +uid://dmf5wqf50nfnr diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc new file mode 100644 index 0000000..fd1ba03 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc @@ -0,0 +1,11 @@ +:first + global interpreter_runtime_result = "first" + +:second + interpreter_runtime_result = interpreter_runtime_result + "-second" + +:background_seed + global interpreter_runtime_background_result = "first" + +:background_after + interpreter_runtime_background_result = interpreter_runtime_background_result + "-background" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc.uid new file mode 100644 index 0000000..cafce06 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/interpreter_runtime_state_persists_between_events.esc.uid @@ -0,0 +1 @@ +uid://clqs3e5r4yem1 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc new file mode 100644 index 0000000..4d92d26 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc @@ -0,0 +1,4 @@ +:test + global result = "start" + missing_test_command() + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc.uid new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_does_not_continue.esc.uid diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc new file mode 100644 index 0000000..2d59fb8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc @@ -0,0 +1,6 @@ +:test + global result = "start" + ?! + - "Break it" + missing_command() + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc.uid new file mode 100644 index 0000000..77a66d7 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_dialog_stops_event.esc.uid @@ -0,0 +1 @@ +uid://d4esrhlw7qhri diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc new file mode 100644 index 0000000..4ecf824 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc @@ -0,0 +1,7 @@ +:test + global result = "start" + global count = 0 + while count < 1: + missing_command() + count = count + 1 + result = result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc.uid new file mode 100644 index 0000000..ec48741 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_command_in_loop_stops_event.esc.uid @@ -0,0 +1 @@ +uid://ci5h4hpy7mjav diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc new file mode 100644 index 0000000..a81c4c0 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc @@ -0,0 +1,3 @@ +:test + global result = "unset" + result = "a" > 1 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc.uid new file mode 100644 index 0000000..67b9e76 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/invalid_numeric_comparison.esc.uid @@ -0,0 +1 @@ +uid://bo4clnuxdy778 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc new file mode 100644 index 0000000..10b036b --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc @@ -0,0 +1,8 @@ +:look + say("player", "It's a leaking pipe.") + +:use "r5_wrench" + say("player", "The leak's now fixed.") + +:use "r5_pen" + say("player", "I don't see how I can fix the pipe with a pen.") diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc.uid new file mode 100644 index 0000000..f77b460 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/multi_target_use_events.esc.uid @@ -0,0 +1 @@ +uid://cpdc2ha8v6svh diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc new file mode 100644 index 0000000..82d638d --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc @@ -0,0 +1,11 @@ +:test + global result = "start" + ?! + - "Outer" + ?! + - "Break nested" + break 1 + result = "after_nested" + - "Exit outer" + break + result = result + "-done" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc.uid new file mode 100644 index 0000000..8d392b8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/nested_dialog_break_one.esc.uid @@ -0,0 +1 @@ +uid://bfwg7dby8q4g3 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc new file mode 100644 index 0000000..07b5ccd --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc @@ -0,0 +1,2 @@ +:test + print("a", "b") diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc.uid new file mode 100644 index 0000000..cd16f5f --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_two_args.esc.uid @@ -0,0 +1 @@ +uid://d0cv0a5fo7hdh diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc new file mode 100644 index 0000000..3ff88bf --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc @@ -0,0 +1,2 @@ +:test + print() diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc.uid new file mode 100644 index 0000000..8dd21c3 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/print_zero_args.esc.uid @@ -0,0 +1 @@ +uid://cc274jba2egui diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc new file mode 100644 index 0000000..853a985 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc @@ -0,0 +1,10 @@ +:background_first + global block_wait_result = "start" + test_delayed_set_global("block_wait_result", "first", 0.05) + +:background_target + test_delayed_set_global("block_wait_result", "first-target", 0.2) + +:front_event + global block_wait_front_result = "start" + test_delayed_set_global("block_wait_front_result", "front", 0.01) diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc.uid new file mode 100644 index 0000000..928dcef --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_background_event.esc.uid @@ -0,0 +1 @@ +uid://4g0g2m5j3ve7u diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc new file mode 100644 index 0000000..9ea46b0 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc @@ -0,0 +1,10 @@ +:front_first + global front_block_wait_result = "start" + test_delayed_set_global("front_block_wait_result", "first", 0.05) + +:front_target + test_delayed_set_global("front_block_wait_result", "first-target", 0.2) + +:background_event + global front_block_wait_background_result = "start" + test_delayed_set_global("front_block_wait_background_result", "background", 0.01) diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc.uid new file mode 100644 index 0000000..b725b6c --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/queue_event_block_waits_for_correct_front_event.esc.uid @@ -0,0 +1 @@ +uid://c5q5hi1sp5u4h diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc new file mode 100644 index 0000000..475c523 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc @@ -0,0 +1,7 @@ +:front_running + global scheduled_interrupt_result = "start" + test_interruptible_set_global("scheduled_interrupt_result", "front-running", "scheduled_interrupt_was_interrupted", 0.2) + scheduled_interrupt_result = scheduled_interrupt_result + "-after" + +:scheduled_front + scheduled_interrupt_result = scheduled_interrupt_result + "-scheduled" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc.uid new file mode 100644 index 0000000..1ca4eb5 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_event_interrupted_before_dispatch.esc.uid @@ -0,0 +1 @@ +uid://dd1tktxwp4t0s diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc new file mode 100644 index 0000000..1f88f71 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc @@ -0,0 +1,12 @@ +:front_running + global scheduled_front_result = "start" + test_interruptible_set_global("scheduled_front_result", "front-running", "scheduled_front_interrupted", 0.2) + scheduled_front_result = scheduled_front_result + "-after" + +:scheduled_front + scheduled_front_result = scheduled_front_result + "-scheduled" + +:background_running + global scheduled_background_result = "start" + test_interruptible_set_global("scheduled_background_result", "background-running", "scheduled_background_interrupted", 0.2) + scheduled_background_result = scheduled_background_result + "-after" diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc.uid new file mode 100644 index 0000000..235d8c4 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/scheduled_front_event_with_busy_background.esc.uid @@ -0,0 +1 @@ +uid://cqt1mxg5k7q6m diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc new file mode 100644 index 0000000..7be3f85 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc @@ -0,0 +1,6 @@ +:test + global result = 0 + ?! + - "Stop" + stop + result = 1 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc.uid new file mode 100644 index 0000000..d979742 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_in_dialog_option.esc.uid @@ -0,0 +1 @@ +uid://148e3f0660144f8a9e00065bdde07cde diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc new file mode 100644 index 0000000..52ea799 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc @@ -0,0 +1,5 @@ +:test + global result = 0 + if true: + stop + result = 1 diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc.uid new file mode 100644 index 0000000..f005816 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/stop_nested_block.esc.uid @@ -0,0 +1 @@ +uid://ccw3t8r0s6n6r diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc new file mode 100644 index 0000000..728ca59 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc @@ -0,0 +1,11 @@ +:trigger_in "player" + test_set_global("trigger_in_result", "player-in") + +:trigger_in + test_set_global("trigger_in_result", "fallback-in") + +:trigger_out "player" + test_set_global("trigger_out_result", "player-out") + +:trigger_out + test_set_global("trigger_out_result", "fallback-out") diff --git a/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc.uid b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc.uid new file mode 100644 index 0000000..46d6dcb --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/fixtures/trigger_targeted_events_use_object_id.esc.uid @@ -0,0 +1 @@ +uid://ciepdqqwhcgtf diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd new file mode 100644 index 0000000..c0463b2 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd @@ -0,0 +1,16 @@ +class_name TestDelayedSetGlobalCommand +extends ESCBaseCommand + + +func configure() -> ESCCommandArgumentDescriptor: + return ESCCommandArgumentDescriptor.new( + 3, + [[TYPE_STRING], [TYPE_STRING], [TYPE_FLOAT, TYPE_INT]], + [null, null, null] + ) + + +func run(command_params: Array) -> int: + await escoria.get_tree().create_timer(float(command_params[2])).timeout + escoria.globals_manager.set_global(command_params[0], command_params[1]) + return ESCExecution.RC_OK diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd.uid b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd.uid new file mode 100644 index 0000000..f844e2a --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_delayed_set_global.gd.uid @@ -0,0 +1 @@ +uid://b21sl821n61w4 diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd new file mode 100644 index 0000000..2ea34a9 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd @@ -0,0 +1,32 @@ +class_name TestInterruptibleSetGlobalCommand +extends ESCBaseCommand + + +var _interrupted := false + + +func configure() -> ESCCommandArgumentDescriptor: + return ESCCommandArgumentDescriptor.new( + 4, + [[TYPE_STRING], [TYPE_STRING], [TYPE_STRING], [TYPE_FLOAT, TYPE_INT]], + [null, null, null, null] + ) + + +func run(command_params: Array) -> int: + _interrupted = false + + var deadline_usec := Time.get_ticks_usec() + int(float(command_params[3]) * 1000000.0) + while Time.get_ticks_usec() < deadline_usec and not _interrupted: + await escoria.get_tree().create_timer(0.01).timeout + + if _interrupted: + escoria.globals_manager.set_global(command_params[2], true) + return ESCExecution.RC_OK + + escoria.globals_manager.set_global(command_params[0], command_params[1]) + return ESCExecution.RC_OK + + +func interrupt(): + _interrupted = true diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd.uid b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd.uid new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_interruptible_set_global.gd.uid diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd new file mode 100644 index 0000000..41bd624 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd @@ -0,0 +1,15 @@ +class_name TestSetGlobalCommand +extends ESCBaseCommand + + +func configure() -> ESCCommandArgumentDescriptor: + return ESCCommandArgumentDescriptor.new( + 2, + [[TYPE_STRING], [TYPE_STRING]], + [null, null] + ) + + +func run(command_params: Array) -> int: + escoria.globals_manager.set_global(command_params[0], command_params[1]) + return ESCExecution.RC_OK diff --git a/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd.uid b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd.uid new file mode 100644 index 0000000..b273d48 --- /dev/null +++ b/addons/escoria-core/testing/ashes/interpreter/support/commands/test_set_global.gd.uid @@ -0,0 +1 @@ +uid://chb08gv4i2nlw diff --git a/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd new file mode 100644 index 0000000..8a2539c --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd @@ -0,0 +1,586 @@ +# GdUnit generated TestSuite +class_name ESCParserTest +extends GdUnitTestSuite + +@warning_ignore("unused_parameter") +@warning_ignore("return_value_discarded") + + +func test_done_statement_does_not_consume_tokens_on_partial_match() -> void: + # This fixture is intentionally valid up to the dialog option body: + # `done` is in a dialog context, but `done x` is invalid because the parser + # only accepts `done` followed immediately by a newline. + var source := _load_fixture("done_partial_match.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("done_partial_match.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var done_index := _find_token_index(tokens, ESCTokenType.TokenType.DONE) + var print_index := _find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "print") + assert_int(done_index).is_greater_equal(0) + assert_int(print_index).is_greater_equal(0) + + # Jump directly to the `done` token so this test exercises the statement + # parser in isolation. We also force dialog context because `done` is only + # legal inside dialogs. + parser._current = done_index + parser._dialog_level = 1 + + var statement = parser._statement() + + # We expect a parse error, but we specifically do not want the parser to + # consume the `done` token before it discovers that the following token is + # not a newline. If it advances here, later recovery code starts from the + # wrong token and can corrupt the remaining parse. + assert_object(statement).is_instanceof(ESCParseError) + assert_bool(compiler.had_error).is_true() + assert_int(parser._current).is_equal(done_index) + assert_int(parser._peek().get_type()).is_equal(ESCTokenType.TokenType.DONE) + assert_int(tokens[print_index].get_type()).is_equal(ESCTokenType.TokenType.IDENTIFIER) + assert_str(tokens[print_index].get_lexeme()).is_equal("print") + + # Tightening the regression: the next statement must still be present in the + # token stream after the failed `done` parse attempt. This protects against + # accidental over-consumption beyond the failing statement. + assert_int(_find_token_index_after(tokens, parser._current, ESCTokenType.TokenType.IDENTIFIER, "print")).is_equal(print_index) + + +func test_parser_synchronizes_after_invalid_done_and_keeps_following_event() -> void: + # This fixture places an invalid `done` at the start of an event body and + # then follows it with a valid statement and a second event. The purpose is + # to verify parser recovery at ASHES statement and event boundaries. + var source := _load_fixture("synchronize_after_invalid_done.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("synchronize_after_invalid_done.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + # The parser should report an error for `done x`, but it should still recover + # well enough to continue parsing later declarations. In particular, the + # following event `:second` must survive recovery. + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_parser_synchronizes_after_invalid_if_and_keeps_following_event() -> void: + # This fixture uses a malformed `if` statement inside an event body. The + # missing colon forces a real statement parse error after the parser has + # already committed to the `if` branch. Recovery must still preserve the + # following top-level event declaration. + var source := _load_fixture("synchronize_after_invalid_if.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("synchronize_after_invalid_if.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_dialog_option_missing_right_square_returns_parse_error() -> void: + # This fixture omits the closing `]` from a dialog option predicate. The + # parser should report a parse error here rather than silently building a + # malformed dialog option node and continuing as if the condition was valid. + # Recovery should still preserve the following top-level event declaration. + var source := _load_fixture("dialog_option_missing_right_square.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_option_missing_right_square.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + var found_second := false + for statement in statements: + if statement is ESCGrammarStmts.Event and statement.get_event_name() == "second": + found_second = true + break + + assert_bool(found_second).is_true() + + +func test_dialog_option_missing_indented_body_returns_parse_error() -> void: + # This fixture omits the required indented body after a dialog option. The + # parser should surface the missing block-start error instead of continuing + # into `_block()` and constructing a malformed option node. + var source := _load_fixture("dialog_option_missing_indented_body.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_option_missing_indented_body.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + var found_second := false + for statement in statements: + if statement is ESCGrammarStmts.Event and statement.get_event_name() == "second": + found_second = true + break + + assert_bool(found_second).is_true() + + +func test_dialog_start_arguments_without_parens_parse_and_are_preserved() -> void: + # The public docs show dialog-start arguments in space-separated form: + # `?! "res://avatar" 5 2`. This regression locks that syntax down so the + # parser preserves those arguments instead of requiring the older `?!(...)` + # form. + var source := _load_fixture("dialog_start_args_without_parens.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_start_args_without_parens.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + + var talk_event := statements[0] as ESCGrammarStmts.Event + var talk_block := talk_event.get_body() as ESCGrammarStmts.Block + assert_int(talk_block.get_statements().size()).is_equal(1) + assert_object(talk_block.get_statements()[0]).is_instanceof(ESCGrammarStmts.Dialog) + + var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog + var args := dialog.get_args() + assert_int(args.size()).is_equal(3) + assert_object(args[0]).is_instanceof(ESCGrammarExprs.Literal) + assert_str(args[0].get_value()).is_equal("res://game/dialog_avatars/player.tres") + assert_object(args[1]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[1].get_value()).is_equal(5.0) + assert_object(args[2]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[2].get_value()).is_equal(2.0) + + +func test_dialog_start_arguments_with_parens_parse_and_are_preserved() -> void: + # Preserve the older parenthesized positional form as well, so both + # positional dialog-start syntaxes remain supported. + var source := _load_fixture("dialog_start_args_with_parens.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_start_args_with_parens.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + + var talk_event := statements[0] as ESCGrammarStmts.Event + var talk_block := talk_event.get_body() as ESCGrammarStmts.Block + assert_int(talk_block.get_statements().size()).is_equal(1) + assert_object(talk_block.get_statements()[0]).is_instanceof(ESCGrammarStmts.Dialog) + + var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog + var args := dialog.get_args() + assert_int(args.size()).is_equal(3) + assert_object(args[0]).is_instanceof(ESCGrammarExprs.Literal) + assert_str(args[0].get_value()).is_equal("res://game/dialog_avatars/player.tres") + assert_object(args[1]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[1].get_value()).is_equal(5.0) + assert_object(args[2]).is_instanceof(ESCGrammarExprs.Literal) + assert_float(args[2].get_value()).is_equal(2.0) + + +func test_dialog_option_translation_key_is_parsed_explicitly() -> void: + var source := _load_fixture("dialog_option_translation_key.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_option_translation_key.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(1) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + + var talk_event := statements[0] as ESCGrammarStmts.Event + var talk_block := talk_event.get_body() as ESCGrammarStmts.Block + var dialog := talk_block.get_statements()[0] as ESCGrammarStmts.Dialog + var first_option := dialog.get_options()[0] as ESCGrammarStmts.DialogOption + var second_option := dialog.get_options()[1] as ESCGrammarStmts.DialogOption + + assert_str(first_option.get_translation_key()).is_equal("UNCLE_SVEN") + assert_str(second_option.get_translation_key()).is_equal("") + + +func test_grouping_missing_right_paren_returns_parse_error() -> void: + # This fixture omits the closing `)` from a grouped expression inside a + # function call. The parser must not silently accept the grouping and build + # a malformed AST; it should report the error and still preserve the + # following event declaration after recovery. + var source := _load_fixture("grouping_missing_right_paren.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("grouping_missing_right_paren.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_var_grouping_missing_right_paren_returns_parse_error() -> void: + # This fixture omits the closing `)` from a grouped expression in a variable + # initializer. Unlike the call-expression regression above, this path goes + # straight through `_primary()`, so the parser must reject the malformed + # grouping instead of silently constructing a `Grouping` node. + var source := _load_fixture("var_grouping_missing_right_paren.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("var_grouping_missing_right_paren.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_var_declaration_missing_newline_returns_parse_error() -> void: + # This fixture continues a variable declaration onto the same line with a + # second statement. The parser should not silently accept the declaration + # when the required trailing newline is missing, and recovery should still + # preserve the following event. + var source := _load_fixture("var_missing_newline.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("var_missing_newline.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_global_declaration_missing_newline_returns_parse_error() -> void: + # This fixture continues a global declaration onto the same line with a + # second statement. The parser should reject the malformed declaration + # instead of constructing a partial `Global` node, and recovery should still + # preserve the following event. + var source := _load_fixture("global_missing_newline.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("global_missing_newline.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(2) + assert_object(statements[0]).is_not_null() + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_not_null() + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_while_error_does_not_leak_loop_level_into_later_event() -> void: + # This fixture forces `_while_statement()` to fail after incrementing + # `_loop_level`. The later `break` in `:second` is top-level within that + # event body and must still be rejected. If the loop counter leaks across the + # error path, the parser will incorrectly accept it as a valid `Break`. + var source := _load_fixture("while_error_then_top_level_break.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("while_error_then_top_level_break.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(3) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + assert_object(statements[2]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[2].get_event_name()).is_equal("third") + + var second_body: Array = statements[1].get_body().get_statements() + assert_int(second_body.size()).is_equal(0) + + +func test_dialog_error_does_not_leak_dialog_level_into_later_event() -> void: + # This fixture forces `_dialog_statement()` to fail after incrementing + # `_dialog_level`. The later `done` in `:second` is top-level within that + # event body and must still be rejected. If the dialog counter leaks across + # the error path, the parser will incorrectly accept it as a valid `Done`. + var source := _load_fixture("dialog_error_then_top_level_done.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("dialog_error_then_top_level_done.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_true() + assert_int(statements.size()).is_greater_equal(3) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + assert_object(statements[2]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[2].get_event_name()).is_equal("third") + + var second_body: Array = statements[1].get_body().get_statements() + assert_int(second_body.size()).is_equal(0) + + +func test_happy_path_full_language_fixture_parses_without_errors() -> void: + # This smoke fixture exercises a broad cross-section of ASHES syntax on the + # parser happy path: event flags, targets, globals, locals, branching, + # looping, built-ins, dialog, nested dialog, conditional dialog options, + # `break`, `done`, `in inventory`, `is active`, and `is "state"`. + var source := _load_fixture("happy_path_full_language.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("happy_path_full_language.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("setup") + assert_int(statements[0].get_num_statements_in_block()).is_equal(6) + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("look") + assert_str(statements[1].get_target_name()).is_equal("worker") + assert_int(statements[1].get_num_statements_in_block()).is_equal(3) + + +func test_keyword_prefixed_names_scan_as_identifiers() -> void: + # ASHES keywords should only match on exact identifier text. Names that + # merely start with keyword text must still scan as ordinary identifiers. + var source := _load_fixture("identifier_keyword_prefixes.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("identifier_keyword_prefixes.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "breakfast")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "donee")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "elseify")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "notable")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.BREAK, "break")).is_equal(-1) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.DONE, "done")).is_equal(-1) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.ELSE, "else")).is_equal(-1) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.NOT, "not")).is_equal(-1) + assert_int(statements.size()).is_equal(1) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + +func test_hyphenated_global_identifier_scans_as_single_identifier() -> void: + # `$`-prefixed global-ID shorthand may contain hyphens, but ordinary `-` + # characters must still scan as subtraction operators outside that prefix. + var source := _load_fixture("hyphenated_global_identifier.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("hyphenated_global_identifier.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.IDENTIFIER, "$closet-door")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.MINUS, "-")).is_greater_equal(0) + assert_int(_find_token_index(tokens, ESCTokenType.TokenType.NUMBER, "1")).is_greater_equal(0) + assert_int(statements.size()).is_equal(1) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + + +func test_blank_lines_in_nested_blocks_do_not_break_later_events() -> void: + # Blank lines inside nested blocks should not corrupt INDENT/DEDENT handling. + # The parser must still recover the later top-level event after the nested + # block ends. + var source := _load_fixture("blank_lines_in_nested_blocks.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("blank_lines_in_nested_blocks.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + + +func test_blank_lines_before_dedent_do_not_hide_block_termination() -> void: + # Blank lines immediately before a block-closing dedent should not be eaten + # in a way that hides the dedent from `_block()`. The outer statement after + # the nested block, plus the later top-level event, must still parse. + var source := _load_fixture("blank_lines_before_dedent.esc") + var compiler := ESCCompiler.new() + var scanner := ESCScanner.new() + scanner.set_source(source) + scanner.set_filename(_fixture_path("blank_lines_before_dedent.esc")) + + var tokens: Array = scanner.scan_tokens() + var parser := ESCParser.new() + parser.init(compiler, tokens, "") + var statements := parser.parse() + + assert_bool(compiler.had_error).is_false() + assert_int(statements.size()).is_equal(2) + assert_object(statements[0]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[0].get_event_name()).is_equal("test") + assert_object(statements[1]).is_instanceof(ESCGrammarStmts.Event) + assert_str(statements[1].get_event_name()).is_equal("second") + assert_int(statements[0].get_num_statements_in_block()).is_equal(2) + + +func _load_fixture(name: String) -> String: + return FileAccess.get_file_as_string(_fixture_path(name)) + + +func _fixture_path(name: String) -> String: + return "res://addons/escoria-core/testing/ashes/parser/fixtures/%s" % name + + +func _find_token_index(tokens: Array, token_type: int, lexeme: String = "") -> int: + for index in range(tokens.size()): + var token = tokens[index] + if token.get_type() == token_type and (lexeme.is_empty() or token.get_lexeme() == lexeme): + return index + + return -1 + + +func _find_token_index_after(tokens: Array, current_index: int, token_type: int, lexeme: String = "") -> int: + for index in range(current_index + 1, tokens.size()): + var token = tokens[index] + if token.get_type() == token_type and (lexeme.is_empty() or token.get_lexeme() == lexeme): + return index + + return -1 diff --git a/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid new file mode 100644 index 0000000..e97a2bb --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/esc_parser_test.gd.uid @@ -0,0 +1 @@ +uid://di2o7dahssqsc diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc new file mode 100644 index 0000000..0ba7826 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc @@ -0,0 +1,9 @@ +:test + if true: + print("inside") + + + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid new file mode 100644 index 0000000..4287f2f --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_before_dedent.esc.uid @@ -0,0 +1 @@ +uid://dc3kyx3rnh13d diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc new file mode 100644 index 0000000..f1bf7dc --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc @@ -0,0 +1,11 @@ +:test + if true: + var value = 1 + + if true: + print(value) + + print("done") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid new file mode 100644 index 0000000..5d9dc6a --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/blank_lines_in_nested_blocks.esc.uid @@ -0,0 +1 @@ +uid://c6pkm7n2t4wyf diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc new file mode 100644 index 0000000..a6aa528 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc @@ -0,0 +1,10 @@ +:test + ?!(1, + - "Option" + print("broken") + +:second + done + +:third + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid new file mode 100644 index 0000000..13ee072 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_error_then_top_level_done.esc.uid @@ -0,0 +1 @@ +uid://f4q8q8et7mbo2 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc new file mode 100644 index 0000000..51c525c --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc @@ -0,0 +1,7 @@ +:test + ?! + - "Option" + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid new file mode 100644 index 0000000..7bd0fe8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_indented_body.esc.uid @@ -0,0 +1 @@ +uid://blnqmv4wjsoi8 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc new file mode 100644 index 0000000..32f407a --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_missing_right_square.esc @@ -0,0 +1,9 @@ +:test + ?! + - "Option" [some_flag < 3 + print("after") + - "Second option" + print("ok") + +:second + print("after_dialog") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc new file mode 100644 index 0000000..69f88db --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc @@ -0,0 +1,6 @@ +:talk + ?! + - UNCLE_SVEN:"Uncle Sven sends regards." + break + - "Fallback" + break diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid new file mode 100644 index 0000000..7ac79a6 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_option_translation_key.esc.uid @@ -0,0 +1 @@ +uid://doadggrcx5l6l diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc new file mode 100644 index 0000000..0b3c59e --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc @@ -0,0 +1,9 @@ +:talk + ?!("res://game/dialog_avatars/player.tres", 5, 2) + - "First option" + say($player, "First option") + - "Second option" + say($player, "Second option") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid new file mode 100644 index 0000000..076398a --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_with_parens.esc.uid @@ -0,0 +1 @@ +uid://d0xvqlf18ax11 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc new file mode 100644 index 0000000..cdb2090 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc @@ -0,0 +1,9 @@ +:talk + ?! "res://game/dialog_avatars/player.tres" 5 2 + - "First option" + say($player, "First option") + - "Second option" + say($player, "Second option") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid new file mode 100644 index 0000000..d0dd054 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/dialog_start_args_without_parens.esc.uid @@ -0,0 +1 @@ +uid://cb2w2jdpxy8sr diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc b/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc new file mode 100644 index 0000000..a36a85b --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/done_partial_match.esc @@ -0,0 +1,5 @@ +:test + ?! + - "Option" + done x + print("after") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc new file mode 100644 index 0000000..f51f093 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc @@ -0,0 +1,5 @@ +:test + global value = 1 print("oops") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid new file mode 100644 index 0000000..bcb4fd1 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/global_missing_newline.esc.uid @@ -0,0 +1 @@ +uid://di8w10jb2c7u4 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc new file mode 100644 index 0000000..97e2e1e --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc @@ -0,0 +1,5 @@ +:test + print((1 + 2) + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid new file mode 100644 index 0000000..a872107 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/grouping_missing_right_paren.esc.uid @@ -0,0 +1 @@ +uid://dnj6r3c3d8m5x diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc new file mode 100644 index 0000000..0a82acd --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc @@ -0,0 +1,41 @@ +:setup | NO_UI | NO_SAVE + global has_met_worker = false + global ask_count = 0 + var local_count = 0 + var target_name = $worker + + if !has_met_worker and ESC_LAST_SCENE == "room1": + print("first visit") + elif $worker is active: + print("worker active") + else: + print("fallback") + + while local_count < 3: + local_count = local_count + 1 + if local_count == 2: + break + +:look "worker" + if $badge in inventory: + print("has badge") + elif $worker is "busy": + print("busy") + else: + print("idle") + + ?! + - "Who are you?" [!has_met_worker] + print("ask name") + has_met_worker = true + - "Tell me more." [ask_count < 2] + ask_count = ask_count + 1 + ?! + - "Enough." + break 1 + - "I am done." + done + - "Bye." + break + + print("after dialog") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid new file mode 100644 index 0000000..d6713b5 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/happy_path_full_language.esc.uid @@ -0,0 +1 @@ +uid://cbv6x0w8g17ap diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc new file mode 100644 index 0000000..0c86561 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc @@ -0,0 +1,2 @@ +:test + var total = $closet-door - 1 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid new file mode 100644 index 0000000..f2e55d8 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/hyphenated_global_identifier.esc.uid @@ -0,0 +1 @@ +uid://b2mxj0r3fk4yy diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc new file mode 100644 index 0000000..dd86b8f --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc @@ -0,0 +1,6 @@ +:test + var breakfast = 1 + var donee = 2 + var elseify = 3 + var notable = 4 + print(breakfast + donee + elseify + notable) diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid new file mode 100644 index 0000000..3bdfa1b --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/identifier_keyword_prefixes.esc.uid @@ -0,0 +1 @@ +uid://d1s8r2n3kq1vh diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc new file mode 100644 index 0000000..0203977 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_done.esc @@ -0,0 +1,6 @@ +:test + done x + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc new file mode 100644 index 0000000..d871c98 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/synchronize_after_invalid_if.esc @@ -0,0 +1,7 @@ +:test + if some_flag + print("broken") + print("after") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc new file mode 100644 index 0000000..05fd749 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc @@ -0,0 +1,5 @@ +:test + var x = (1 + 2 + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid new file mode 100644 index 0000000..567b162 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_grouping_missing_right_paren.esc.uid @@ -0,0 +1 @@ +uid://b2d1d5qj4o8t7 diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc new file mode 100644 index 0000000..c62d37d --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc @@ -0,0 +1,5 @@ +:test + var x = 1 print("bad") + +:second + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid new file mode 100644 index 0000000..f65cfb9 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/var_missing_newline.esc.uid @@ -0,0 +1 @@ +uid://dssmdd6b0cwut diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc new file mode 100644 index 0000000..dd6ef8b --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc @@ -0,0 +1,9 @@ +:test + while true + print("broken") + +:second + break + +:third + print("ok") diff --git a/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid new file mode 100644 index 0000000..1a1f894 --- /dev/null +++ b/addons/escoria-core/testing/ashes/parser/fixtures/while_error_then_top_level_break.esc.uid @@ -0,0 +1 @@ +uid://dgo0jap62bmth |
