Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,9 @@ jobs:
- name: Run tests
shell: bash
timeout-minutes: 5
env:
SENTRY_TEST: 1
SENTRY_TEST_INCLUDE: "res://test/suites/"
run: |
# Exit status codes: 0 - success, 100 - failures, 101 - warnings, 104 - tests not found, 105 - didn't run.
${GODOT} --headless --path project/
${GODOT} --headless --path project/ -- run-tests "res://test/suites/"

- name: Run isolated tests
if: success() || failure()
Expand Down
52 changes: 52 additions & 0 deletions project/cli/android_cli_adapter.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
class_name AndroidCLIAdapter
extends RefCounted
## Adapter class that converts Android intent extras into CLI-style arguments.
##
## Expects Android intent extras as key-value pairs: [br]
## command: name of the command to run. [br]
## arg0: first argument to the command. [br]
## arg1: second argument, etc... [br]



# Reads Android intent extras and returns CLI-style arguments as PackedStringArray.
# Supports --es command and --es arg0, arg1, arg2...
static func get_command_argv() -> PackedStringArray:
var rv := PackedStringArray()
var extras: Dictionary = _get_android_intent_extras()

if extras.has("command"):
rv.append(extras["command"])
for i in range(10):
var key := "arg%d" % i
if extras.has(key):
rv.append(extras[key])
return rv


# Returns intent extras from AndroidRuntime (strings-only).
static func _get_android_intent_extras() -> Dictionary:
if not Engine.has_singleton("AndroidRuntime"):
return {}

var android_runtime = Engine.get_singleton("AndroidRuntime")
var activity = android_runtime.getActivity()
if not activity:
return {}
var intent = activity.getIntent()
if not intent:
return {}
var extras = intent.getExtras()
if not extras:
return {}

# Convert Java map to Godot Dictionary
var keys = extras.keySet().toArray()
var rv: Dictionary = {}
for i in range(keys.size()):
var key: String = keys[i].toString()
var raw_value = extras.get(key)
var value: String = raw_value.toString() if raw_value != null else ""
rv[key] = value

return rv
1 change: 1 addition & 0 deletions project/cli/android_cli_adapter.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bb5p3f8q35xjy
156 changes: 156 additions & 0 deletions project/cli/cli_commands.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
class_name CLICommands
extends Node
## Contains command functions that can be executed via CLI.
##
## A command must return a POSIX-compliant integer exit code, where 0 indicates success.
## Exit codes are typically in the range of 0–125.[br][br]
##
## Usage:[br]
## godot --headless --path ./project -- COMMAND [ARGS...]
##
## [br][br]
## Use "godot --headless --path ./project -- help" to list available commands.


var exit_code: int

var _parser := CLIParser.new()


func _ready() -> void:
_register_commands()


## Checks and executes CLI commands if found.
## Returns true if a command was executed (caller should handle app exit).
func check_and_execute_cli() -> bool:
var executed: bool = await _parser.check_and_execute_cli()
exit_code = _parser.exit_code
return executed


## Registers all available commands with the parser.
func _register_commands() -> void:
_parser.add_command("help", _cmd_help, "Show available commands")
_parser.add_command("crash-capture", _cmd_crash_capture, "Generate a controlled crash for testing")
_parser.add_command("message-capture", _cmd_message_capture, "Capture a test message to Sentry")
_parser.add_command("run-tests", _cmd_run_tests, "Run unit tests")


## Shows available commands and their arguments.
func _cmd_help() -> int:
print(_parser.generate_help())
return 0


## Generates a controlled crash for testing.
func _cmd_crash_capture() -> int:
_init_sentry()
_add_integration_test_context("crash-capture")

await get_tree().create_timer(0.5).timeout

print("Triggering controlled crash...")

# NOTE: Borrowing UUID generation from SentryUser class.
var uuid_gen := SentryUser.new()
uuid_gen.generate_new_id()
SentrySDK.set_tag("test.crash_id", uuid_gen.id)

_print_test_result("crash-capture", true, "Pre-crash setup complete")
SentrySDK.add_breadcrumb(SentryBreadcrumb.create("About to trigger controlled crash"))

# Use the same crash method as the demo
SentrySDK._demo_helper_crash_app()
return 0


## Captures a test message to Sentry.
func _cmd_message_capture(p_message: String = "Integration test message", p_level: String = "info") -> int:
_init_sentry()
_add_integration_test_context("message-capture")

await get_tree().create_timer(0.5).timeout

print("Capturing message: '%s' with level: %s" % [p_message, p_level])

var level: SentrySDK.Level
match p_level.to_lower():
"debug": level = SentrySDK.LEVEL_DEBUG
"info": level = SentrySDK.LEVEL_INFO
"warning", "warn": level = SentrySDK.LEVEL_WARNING
"error": level = SentrySDK.LEVEL_ERROR
"fatal": level = SentrySDK.LEVEL_FATAL
_:
printerr("Warning: Unknown level '%s', using INFO" % p_level)
level = SentrySDK.LEVEL_INFO

var event_id := SentrySDK.capture_message(p_message, level)
print("EVENT_CAPTURED: ", event_id)
_print_test_result("message-capture", true, "Test complete")
return 0


func _cmd_run_tests(tests: String = "res://test/suites/") -> int:
if FileAccess.file_exists("res://test/util/test_runner.gd"):
print(">>> Initializing testing")
await get_tree().process_frame

var included_paths: PackedStringArray = tests.split(";", false)
if included_paths.is_empty():
printerr("No test path provided.")
return 1
print(" -- Tests included: ", included_paths)

# Add test runner node.
print(" -- Adding test runner...")
var test_runner: Node = load("res://test/util/test_runner.gd").new()
get_tree().root.add_child(test_runner)
for path in included_paths:
test_runner.include_tests(path)

# Wait for completion.
await test_runner.finished
print(">>> Test run complete with code: ", str(test_runner.result_code))

return test_runner.result_code
else:
printerr("Error: Test runner not found")
return 1


## Initializes Sentry for integration testing.
func _init_sentry() -> void:
print("Initializing Sentry...")

SentrySDK.init(func(options: SentryOptions) -> void:
options.debug = true
options.release = "[email protected]"
options.environment = "integration-test"
)

# Wait for Sentry to initialize
await get_tree().create_timer(0.3).timeout


## Add additional context for integration tests.
func _add_integration_test_context(p_command: String) -> void:
SentrySDK.add_breadcrumb(SentryBreadcrumb.create("Integration test started"))

var user := SentryUser.new()
user.id = "test-user-123"
user.username = "ps-tester"
SentrySDK.set_user(user)

SentrySDK.set_tag("test.suite", "integration")
SentrySDK.set_tag("test.type", p_command)

SentrySDK.add_breadcrumb(SentryBreadcrumb.create("Context configuration finished"))


func _print_test_result(test_name: String, success: bool, message: String) -> void:
print("TEST_RESULT: {\"test\":\"%s\",\"success\":%s,\"message\":\"%s\"}" % [
test_name,
success,
message
])
1 change: 1 addition & 0 deletions project/cli/cli_commands.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bg6afsun3ekyl
Loading
Loading