diff --git a/addons/gd-plug/plug.gd b/addons/gd-plug/plug.gd new file mode 100644 index 0000000..14d2cdd --- /dev/null +++ b/addons/gd-plug/plug.gd @@ -0,0 +1,1163 @@ +@tool +extends SceneTree + +signal updated(plugin) + +const VERSION = "0.2.5" +const DEFAULT_PLUGIN_URL = "https://git::@github.com/%s.git" +const DEFAULT_PLUG_DIR = "res://.plugged" +const DEFAULT_CONFIG_PATH = DEFAULT_PLUG_DIR + "/index.cfg" +const DEFAULT_USER_PLUG_SCRIPT_PATH = "res://plug.gd" +const DEFAULT_BASE_PLUG_SCRIPT_PATH = "res://addons/gd-plug/plug.gd" + +const ENV_PRODUCTION = "production" +const ENV_TEST = "test" +const ENV_FORCE = "force" +const ENV_KEEP_IMPORT_FILE = "keep_import_file" +const ENV_KEEP_IMPORT_RESOURCE_FILE = "keep_import_resource_file" + +const MSG_PLUG_START_ASSERTION = "_plug_start() must be called first" + +var project_dir +var installation_config = ConfigFile.new() +var logger = _Logger.new() + +var _installed_plugins +var _plugged_plugins = {} + +var _threads = [] +var _mutex = Mutex.new() +var _start_time = 0 +var threadpool = _ThreadPool.new(logger) + + +func _init(): + threadpool.connect("all_thread_finished", request_quit) + project_dir = DirAccess.open("res://") + +func _initialize(): + var args = OS.get_cmdline_args() + # Trim unwanted args passed to godot executable + for arg in Array(args): + args.remove_at(0) + if "plug.gd" in arg: + break + + var help = false + var help_config = false + for arg in args: + # NOTE: "--key" or "-key" will always be consumed by godot executable, see https://github.com/godotengine/godot/issues/8721 + var key = arg.to_lower() + match key: + "help": + help = true + "help-config": + help_config = true + "detail": + logger.log_format = _Logger.DEFAULT_LOG_FORMAT_DETAIL + "debug", "d": + logger.log_level = _Logger.LogLevel.DEBUG + "quiet", "q", "silent": + logger.log_level = _Logger.LogLevel.NONE + "production": + OS.set_environment(ENV_PRODUCTION, "true") + "test": + OS.set_environment(ENV_TEST, "true") + "force": + OS.set_environment(ENV_FORCE, "true") + "keep-import-file": + OS.set_environment(ENV_KEEP_IMPORT_FILE, "true") + "keep-import-resource-file": + OS.set_environment(ENV_KEEP_IMPORT_RESOURCE_FILE, "true") + + logger.debug("cmdline_args: %s" % args) + _start_time = Time.get_ticks_msec() + _plug_start() + if help_config: + show_config_syntax() + elif help or args.size() == 0: + show_syntax() + else: + _plugging() + match args[0]: + "init": + _plug_init() + "install", "update": + _plug_install() + "uninstall": + _plug_uninstall() + "clean": + _plug_clean() + "upgrade": + _plug_upgrade() + "status": + _plug_status() + "version": + logger.info(VERSION) + _: + logger.error("Unknown command %s" % args[0]) + show_syntax() + # NOTE: Do no put anything after this line except request_quit(), as _plug_*() may call request_quit() + request_quit() + +func show_syntax(): + logger.info("gd-plug - Minimal plugin manager for Godot") + logger.info("") + logger.info("Usage: godot --headless -s plug.gd action [options...]") + logger.info("") + logger.info("Actions:") + var actions = { + "init": "Initialize current project by creating plug.gd at root", + "status": "Check the status of plugins(installed, added or removed), execute this command whenever in doubts", + "install(alias update)": "Install or update plugins based on plug.gd", + "uninstall": "Uninstall all plugins, regardless of plug.gd", + "clean": "Clean unused files/folders from /.plugged", + "upgrade": "Upgrade addons/gd-plug/plug.gd to the latest version", + "version": "Print current version of gd-plug", + } + logger.indent() + logger.table_start() + for action_name in actions: + logger.table_row([action_name, actions[action_name]]) + logger.table_end() + logger.dedent() + + logger.info("") + logger.info("Options:") + var options = { + "production": "Install only plugins not marked as dev, or uninstall already installed dev plugins", + "test": "Testing mode, no files will actually be installed/uninstalled", + "force": "Force gd-plug to overwrite destination files when running install command. *WARNING: Check README for more details*", + "keep-import-file": "Keep \".import\" files generated by plugin, when run uninstall command", + "keep-import-resource-file": "Keep files located in \".import\" that generated by plugin, when run uninstall command", + "debug(alias d)": "Print debug message", + "detail": "Print with datetime and log level, \"[{time}] [{level}] {msg}\"", + "quiet(alias q, silent)": "Disable logging", + "help": "Show this help", + "help-config": "plug.gd configuration documentation" + } + logger.indent() + logger.table_start() + for option_name in options: + logger.table_row([option_name, options[option_name]]) + logger.table_end() + logger.dedent() + logger.info("") + +func show_config_syntax(): + logger.info("Configs: plug(src, args={})") + logger.info("") + logger.info("Sources:") + logger.indent() + logger.info("Github repo: \"username/repo\", for example, \"imjp94/gd-plug\"") + logger.info("or") + logger.info("Any valid git url, for example, \"git@github.com:username/repo.git\"") + logger.dedent() + logger.info("") + logger.info("Arguments:") + var arguments = { + "include": "Array of strings that define what files or directory to include. Only \"addons/\" will be included if omitted", + "exclude": "Array of strings that define what files or directory to exclude", + "branch": "Name of branch to freeze to", + "tag": "Name of tag to freeze to", + "commit": "Commit hash string to freeze to, must be full length 40 digits commit-hash, for example, 7a642f90d3fb88976dd913051de994e58e838d1a", + "dev": "Boolean to mark the plugin as dev or not, plugin marked as dev will not be installed when production command given", + "on-updated": "Post update hook, a function name declared in plug.gd that will be called whenever the plugin installed/updated" + } + logger.indent() + logger.table_start() + for argument_name in arguments: + logger.table_row([argument_name, arguments[argument_name]]) + logger.table_end() + logger.dedent() + logger.info("") + +func _process(delta): + threadpool.process(delta) + +func _finalize(): + _plug_end() + threadpool.stop() + logger.info("Finished, elapsed %.3fs" % ((Time.get_ticks_msec() - _start_time) / 1000.0)) + +func _on_updated(plugin): + pass + +func _plugging(): + pass + +func request_quit(exit_code=-1): + if threadpool.is_all_thread_finished() and threadpool.is_all_task_finished(): + quit(exit_code) + return true + logger.debug("Request quit declined, threadpool is still running") + return false + +# Index installed plugins, or create directory "plugged" if not exists +func _plug_start(): + logger.debug("Plug start") + if not project_dir.dir_exists(DEFAULT_PLUG_DIR): + if project_dir.make_dir(ProjectSettings.globalize_path(DEFAULT_PLUG_DIR)) == OK: + logger.debug("Make dir %s for plugin installation") + if installation_config.load(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Installation config loaded") + else: + logger.debug("Installation config not found") + _installed_plugins = installation_config.get_value("plugin", "installed", {}) + +# Install plugin or uninstall plugin if unlisted +func _plug_end(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + var test = !OS.get_environment(ENV_TEST).is_empty() + if not test: + installation_config.set_value("plugin", "installed", _installed_plugins) + if installation_config.save(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Plugged config saved") + else: + logger.error("Failed to save plugged config") + else: + logger.warn("Skipped saving of plugged config in test mode") + _installed_plugins = null + +func _plug_init(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + logger.info("Init gd-plug...") + if FileAccess.file_exists(DEFAULT_USER_PLUG_SCRIPT_PATH): + logger.warn("%s already exists!" % DEFAULT_USER_PLUG_SCRIPT_PATH) + else: + var file = FileAccess.open(DEFAULT_USER_PLUG_SCRIPT_PATH, FileAccess.WRITE) + file.store_string(INIT_PLUG_SCRIPT) + file.close() + logger.info("Created %s" % DEFAULT_USER_PLUG_SCRIPT_PATH) + +func _plug_install(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installing...") + for plugin in _plugged_plugins.values(): + var installed = plugin.name in _installed_plugins + if installed: + var installed_plugin = get_installed_plugin(plugin.name) + if (installed_plugin.dev or plugin.dev) and OS.get_environment(ENV_PRODUCTION): + logger.info("Remove dev plugin for production: %s" % plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin)) + else: + threadpool.enqueue_task(update_plugin.bind(plugin)) + else: + threadpool.enqueue_task(install_plugin.bind(plugin)) + + var removed_plugins = [] + for plugin in _installed_plugins.values(): + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + if removed_plugins: + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + for plugin in removed_plugins: + threadpool.enqueue_task(uninstall_plugin.bind(plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_uninstall(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Uninstalling...") + for plugin in _installed_plugins.values(): + var installed_plugin = get_installed_plugin(plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_clean(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Cleaning...") + var plugged_dir = DirAccess.open(DEFAULT_PLUG_DIR) + plugged_dir.include_hidden = true + plugged_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plugged_dir.get_next() + while not file.is_empty(): + if plugged_dir.current_is_dir(): + if not (file in _installed_plugins): + logger.info("Remove %s" % file) + threadpool.enqueue_task(directory_delete_recursively.bind(plugged_dir.get_current_dir() + "/" + file)) + file = plugged_dir.get_next() + plugged_dir.list_dir_end() + threadpool.active = true + +func _plug_upgrade(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Upgrading gd-plug...") + plug("imjp94/gd-plug") + var gd_plug = _plugged_plugins["gd-plug"] + OS.set_environment(ENV_FORCE, "true") # Required to overwrite res://addons/gd-plug/plug.gd + threadpool.enqueue_task(install_plugin.bind(gd_plug)) + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + threadpool.enqueue_task(directory_delete_recursively.bind(gd_plug.plug_dir)) + threadpool.active = true + +func _plug_status(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installed %d plugin%s" % [_installed_plugins.size(), "s" if _installed_plugins.size() > 1 else ""]) + var new_plugins = _plugged_plugins.duplicate() + var has_checking_plugin = false + var removed_plugins = [] + for plugin in _installed_plugins.values(): + logger.info("- {name} - {url}".format(plugin)) + new_plugins.erase(plugin.name) + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + else: + threadpool.enqueue_task(check_plugin.bind(_plugged_plugins[plugin.name])) + has_checking_plugin = true + if has_checking_plugin: + logger.info("\n", true) + threadpool.disconnect("all_thread_finished", request_quit) + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + threadpool.connect("all_thread_finished", request_quit) + logger.debug("Finished checking plugins, ready to proceed") + if new_plugins: + logger.info("\nPlugged %d plugin%s" % [new_plugins.size(), "s" if new_plugins.size() > 1 else ""]) + for plugin in new_plugins.values(): + var is_new = not (plugin.name in _installed_plugins) + if is_new: + logger.info("- {name} - {url}".format(plugin)) + if removed_plugins: + logger.info("\nUnplugged %d plugin%s" % [removed_plugins.size(), "s" if removed_plugins.size() > 1 else ""]) + for plugin in removed_plugins: + logger.info("- %s removed" % plugin.name) + var plug_directory = DirAccess.open(DEFAULT_PLUG_DIR) + var orphan_dirs = [] + if plug_directory.get_open_error() == OK: + plug_directory.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plug_directory.get_next() + while not file.is_empty(): + if plug_directory.current_is_dir(): + if not (file in _installed_plugins): + orphan_dirs.append(file) + file = plug_directory.get_next() + plug_directory.list_dir_end() + if orphan_dirs: + logger.info("\nOrphan directory, %d found in %s, execute \"clean\" command to remove" % [orphan_dirs.size(), DEFAULT_PLUG_DIR]) + for dir in orphan_dirs: + logger.info("- %s" % dir) + threadpool.active = true + + if has_checking_plugin: + request_quit() + +# Index & validate plugin +func plug(repo, args={}): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + repo = repo.strip_edges() + var plugin_name = get_plugin_name_from_repo(repo) + if plugin_name in _plugged_plugins: + logger.info("Plugin already plugged: %s" % plugin_name) + return + var plugin = {} + plugin.name = plugin_name + plugin.url = "" + if ":" in repo: + plugin.url = repo + elif repo.find("/") == repo.rfind("/"): + plugin.url = DEFAULT_PLUGIN_URL % repo + else: + logger.error("Invalid repo: %s" % repo) + plugin.plug_dir = DEFAULT_PLUG_DIR + "/" + plugin.name + + var is_valid = true + plugin.include = args.get("include", []) + is_valid = is_valid and validate_var_type(plugin, "include", TYPE_ARRAY, "Array") + plugin.exclude = args.get("exclude", []) + is_valid = is_valid and validate_var_type(plugin, "exclude", TYPE_ARRAY, "Array") + plugin.branch = args.get("branch", "") + is_valid = is_valid and validate_var_type(plugin, "branch", TYPE_STRING, "String") + plugin.tag = args.get("tag", "") + is_valid = is_valid and validate_var_type(plugin, "tag", TYPE_STRING, "String") + plugin.commit = args.get("commit", "") + is_valid = is_valid and validate_var_type(plugin, "commit", TYPE_STRING, "String") + if not plugin.commit.is_empty(): + var is_valid_commit = plugin.commit.length() == 40 + if not is_valid_commit: + logger.error("Expected full length 40 digits commit-hash string, given %s" % plugin.commit) + is_valid = is_valid and is_valid_commit + plugin.dev = args.get("dev", false) + is_valid = is_valid and validate_var_type(plugin, "dev", TYPE_BOOL, "Boolean") + plugin.on_updated = args.get("on_updated", "") + is_valid = is_valid and validate_var_type(plugin, "on_updated", TYPE_STRING, "String") + plugin.install_root = args.get("install_root", "") + is_valid = is_valid and validate_var_type(plugin, "install_root", TYPE_STRING, "String") + + if is_valid: + _plugged_plugins[plugin.name] = plugin + logger.debug("Plug: %s" % plugin) + else: + logger.error("Failed to plug %s, validation error" % plugin.name) + +func install_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var can_install = OS.get_environment(ENV_PRODUCTION).is_empty() if plugin.dev else true + if can_install: + logger.info("Installing plugin %s..." % plugin.name) + var result = is_plugin_downloaded(plugin) + if result != OK: + result = download(plugin) + else: + logger.info("Plugin already downloaded") + + if result == OK: + install(plugin) + else: + logger.error("Failed to install plugin %s with error code %d" % [plugin.name, result]) + +func uninstall_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + logger.info("Uninstalling plugin %s..." % plugin.name) + uninstall(plugin) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + +func update_plugin(plugin, checking=false): + if not (plugin.name in _installed_plugins): + logger.info("%s new plugin" % plugin.name) + return true + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + var installed_plugin = get_installed_plugin(plugin.name) + var changes = compare_plugins(plugin, installed_plugin) + var should_clone = false + var should_pull = false + var should_reinstall = false + + if plugin.tag or plugin.commit: + for rev in ["tag", "commit"]: + var freeze_at = plugin[rev] + if freeze_at: + logger.info("%s frozen at %s \"%s\"" % [plugin.name, rev, freeze_at]) + break + else: + var ahead_behind = [] + if git.fetch("origin " + plugin.branch if plugin.branch else "origin").exit == OK: + ahead_behind = git.get_commit_comparison("HEAD", "origin/" + plugin.branch if plugin.branch else "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + if is_commit_behind: + logger.info("%s %d commits behind, update required" % [plugin.name, ahead_behind[1]]) + should_pull = true + else: + logger.info("%s up to date" % plugin.name) + + if changes: + logger.info("%s changed %s" % [plugin.name, changes]) + should_reinstall = true + if "url" in changes or "branch" in changes or "tag" in changes or "commit" in changes: + logger.info("%s repository setting changed, update required" % plugin.name) + should_clone = true + + if not checking: + if should_clone: + logger.info("%s cloning from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + uninstall(get_installed_plugin(plugin.name)) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + if download(plugin) == OK: + install(plugin) + elif should_pull: + logger.info("%s pulling updates from %s..." % [plugin.name, plugin.url]) + uninstall(get_installed_plugin(plugin.name)) + if git.pull().exit == OK: + install(plugin) + elif should_reinstall: + logger.info("%s reinstalling..." % plugin.name) + uninstall(get_installed_plugin(plugin.name)) + install(plugin) + +func check_plugin(plugin): + update_plugin(plugin, true) + +func download(plugin): + logger.info("Downloading %s from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + var global_dest_dir = ProjectSettings.globalize_path(plugin.plug_dir) + if project_dir.dir_exists(plugin.plug_dir): + directory_delete_recursively(plugin.plug_dir) + project_dir.make_dir(plugin.plug_dir) + var result = _GitExecutable.new(global_dest_dir, logger).clone(plugin.url, global_dest_dir, {"branch": plugin.branch, "tag": plugin.tag, "commit": plugin.commit}) + if result.exit == OK: + logger.info("Successfully download %s" % [plugin.name]) + else: + logger.info("Failed to download %s" % plugin.name) + # Make sure plug_dir is clean when failed + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + project_dir.remove(plugin.plug_dir) # Remove empty directory + return result.exit + +func install(plugin): + var include = plugin.get("include", []) + if include.is_empty(): # Auto include "addons/" folder if not explicitly specified + include = ["addons/"] + if OS.get_environment(ENV_FORCE).is_empty() and OS.get_environment(ENV_TEST).is_empty(): + var is_exists = false + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": true, "silent_test": true}) + for dest_file in dest_files: + if project_dir.file_exists(dest_file): + logger.warn("%s attempting to overwrite file %s" % [plugin.name, dest_file]) + is_exists = true + if is_exists: + logger.warn("Installation of %s terminated to avoid overwriting user files, you may disable safe mode with command \"force\"" % plugin.name) + return ERR_ALREADY_EXISTS + + logger.info("Installing files for %s..." % plugin.name) + var test = !OS.get_environment(ENV_TEST).is_empty() + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": test}) + plugin.dest_files = dest_files + logger.info("Installed %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "", plugin.name]) + if plugin.name != "gd-plug": + set_installed_plugin(plugin) + if plugin.on_updated: + if has_method(plugin.on_updated): + logger.info("Execute post-update function for %s" % plugin.name) + _on_updated(plugin) + call(plugin.on_updated, plugin.duplicate()) + emit_signal("updated", plugin) + return OK + +func uninstall(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var keep_import_file = !OS.get_environment(ENV_KEEP_IMPORT_FILE).is_empty() + var keep_import_resource_file = !OS.get_environment(ENV_KEEP_IMPORT_RESOURCE_FILE).is_empty() + var dest_files = plugin.get("dest_files", []) + logger.info("Uninstalling %d file%s for %s..." % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + directory_remove_batch(dest_files, {"test": test, "keep_import_file": keep_import_file, "keep_import_resource_file": keep_import_resource_file}) + logger.info("Uninstalled %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + remove_installed_plugin(plugin.name) + +func is_plugin_downloaded(plugin): + if not project_dir.dir_exists(plugin.plug_dir + "/.git"): + return + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + return git.is_up_to_date(plugin) + +# Get installed plugin, thread safe +func get_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var installed_plugin = _installed_plugins[plugin_name] + _mutex.unlock() + return installed_plugin + +# Set installed plugin, thread safe +func set_installed_plugin(plugin): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + _installed_plugins[plugin.name] = plugin + _mutex.unlock() + +# Remove installed plugin, thread safe +func remove_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var result = _installed_plugins.erase(plugin_name) + _mutex.unlock() + return result + +func directory_copy_recursively(from, to, args={}): + var include = args.get("include", []) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(from) + dir.include_hidden = true + var dest_files = [] + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + var dest = to + ("/" if to != "res://" else "") + file_name + + if dir.current_is_dir(): + dest_files += directory_copy_recursively(source, dest, args) + else: + for include_key in include: + if include_key in source: + var is_excluded = false + for exclude_key in exclude: + if exclude_key in source: + is_excluded = true + break + if not is_excluded: + if test: + if not silent_test: logger.warn("[TEST] Writing to %s" % dest) + else: + dir.make_dir_recursive(to) + if dir.copy(source, dest) == OK: + logger.debug("Copy from %s to %s" % [source, dest]) + dest_files.append(dest) + break + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % from) + + return dest_files + +func directory_delete_recursively(dir_path, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(dir_path) + dir.include_hidden = true + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + + if dir.current_is_dir(): + var sub_dir = directory_delete_recursively(source, args) + if remove_empty_directory: + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if source.get_file() == ".git": + var empty_dir_path = ProjectSettings.globalize_path(source) + var exit = FAILED + match OS.get_name(): + "Windows": + empty_dir_path = "\"%s\"" % empty_dir_path + empty_dir_path = empty_dir_path.replace("/", "\\") + var cmd = "rd /s /q %s" % empty_dir_path + exit = OS.execute("cmd", ["/C", cmd]) + "X11", "OSX", "Server": + empty_dir_path = "\'%s\'" % empty_dir_path + var cmd = "rm -rf %s" % empty_dir_path + exit = OS.execute("bash", ["-c", cmd]) + # Hacks to remove .git, as git pack files stop it from being removed + # See https://stackoverflow.com/questions/1213430/how-to-fully-delete-a-git-repository-created-with-init + if exit == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + logger.debug("Failed to remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if dir.remove(sub_dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + var excluded = false + for exclude_key in exclude: + if source in exclude_key: + excluded = true + break + if not excluded: + if test: + if not silent_test: logger.warn("[TEST] Remove file: %s" % source) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: %s" % source) + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % dir_path) + + if remove_empty_directory: + dir.remove(dir.get_current_dir()) + + return dir + +func directory_remove_batch(files, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var keep_import_file = args.get("keep_import_file", false) + var keep_import_resource_file = args.get("keep_import_resource_file", false) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dirs = {} + for file in files: + var file_dir = file.get_base_dir() + var file_name =file.get_file() + var dir = dirs.get(file_dir) + + if not dir: + dir = DirAccess.open(file_dir) + dirs[file_dir] = dir + + if file.ends_with(".import"): + if not keep_import_file: + _remove_import_file(dir, file, keep_import_resource_file, test, silent_test) + else: + if test: + if not silent_test: logger.warn("[TEST] Remove file: " + file) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: " + file) + if not keep_import_file: + _remove_import_file(dir, file + ".import", keep_import_resource_file, test, silent_test) + + for dir in dirs.values(): + var slash_count = dir.get_current_dir().count("/") - 2 # Deduct 2 slash from "res://" + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % dir.get_current_dir()) + else: + if dir.remove(dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % dir.get_current_dir()) + # Dumb method to clean empty ancestor directories + logger.debug("Removing emoty ancestor directory for %s..." % dir.get_current_dir()) + var current_dir = dir.get_current_dir() + for i in slash_count: + current_dir = current_dir.get_base_dir() + var d = DirAccess.open(current_dir) + if d.get_open_error() == OK: + if test: + if not silent_test: logger.warn("[TEST] Remove empty ancestor directory: %s" % d.get_current_dir()) + else: + if d.remove(d.get_current_dir()) == OK: + logger.debug("Remove empty ancestor directory: %s" % d.get_current_dir()) + +func _remove_import_file(dir, file, keep_import_resource_file=false, test=false, silent_test=false): + if not dir.file_exists(file): + return + + if not keep_import_resource_file: + var import_config = ConfigFile.new() + if import_config.load(file) == OK: + var metadata = import_config.get_value("remap", "metadata", {}) + var imported_formats = metadata.get("imported_formats", []) + if imported_formats: + for format in imported_formats: + _remove_import_resource_file(dir, import_config, "." + format, test) + else: + _remove_import_resource_file(dir, import_config, "", test) + if test: + if not silent_test: logger.warn("[TEST] Remove import file: " + file) + else: + if dir.remove(file) == OK: + logger.debug("Remove import file: " + file) + else: + # TODO: Sometimes Directory.remove() unable to remove random .import file and return error code 1(Generic Error) + # Maybe enforce the removal from shell? + logger.warn("Failed to remove import file: " + file) + +func _remove_import_resource_file(dir, import_config, import_format="", test=false): + var import_resource_file = import_config.get_value("remap", "path" + import_format, "") + var checksum_file = import_resource_file.trim_suffix("." + import_resource_file.get_extension()) + ".md5" if import_resource_file else "" + if import_resource_file: + if dir.file_exists(import_resource_file): + if test: + logger.info("[IMPORT] Remove import resource file: " + import_resource_file) + else: + if dir.remove(import_resource_file) == OK: + logger.debug("Remove import resource file: " + import_resource_file) + if checksum_file: + checksum_file = checksum_file.replace(import_format, "") + if dir.file_exists(checksum_file): + if test: + logger.info("[IMPORT] Remove import checksum file: " + checksum_file) + else: + if dir.remove(checksum_file) == OK: + logger.debug("Remove import checksum file: " + checksum_file) + +func compare_plugins(p1, p2): + var changed_keys = [] + for key in p1.keys(): + var v1 = p1[key] + var v2 = p2[key] + if v1 != v2: + changed_keys.append(key) + return changed_keys + +func get_plugin_name_from_repo(repo): + repo = repo.replace(".git", "").trim_suffix("/") + return repo.get_file() + +func validate_var_type(obj, var_name, type, type_string): + var value = obj.get(var_name) + var is_valid = typeof(value) == type + if not is_valid: + logger.error("Expected variable \"%s\" to be %s, given %s" % [var_name, type_string, value]) + return is_valid + +const INIT_PLUG_SCRIPT = \ +"""extends "res://addons/gd-plug/plug.gd" + +func _plugging(): + # Declare plugins with plug(repo, args) + # For example, clone from github repo("user/repo_name") + # plug("imjp94/gd-YAFSM") # By default, gd-plug will only install anything from "addons/" directory + # Or you can explicitly specify which file/directory to include + # plug("imjp94/gd-YAFSM", {"include": ["addons/"]}) # By default, gd-plug will only install anything from "addons/" directory + pass +""" + +class _GitExecutable extends RefCounted: + var cwd = "" + var logger + + func _init(p_cwd, p_logger): + cwd = p_cwd + logger = p_logger + + func _execute(command, output=[], read_stderr=false): + var cmd = "cd '%s' && %s" % [cwd, command] + # NOTE: OS.execute() seems to ignore read_stderr + var exit = FAILED + match OS.get_name(): + "Windows": + cmd = cmd.replace("\'", "\"") # cmd doesn't accept single-quotes + cmd = cmd if read_stderr else "%s 2> nul" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("cmd", ["/C", cmd], output, read_stderr) + "macOS", "Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD": + cmd if read_stderr else "%s 2>/dev/null" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("bash", ["-c", cmd], output, read_stderr) + var unhandled_os: + logger.error("Unexpected OS: %s" % unhandled_os) + logger.debug("Execution ended(code:%d): %s" % [exit, output]) + return exit + + func init(): + logger.debug("Initializing git at %s..." % cwd) + var output = [] + var exit = _execute("git init", output) + logger.debug("Successfully init" if exit == OK else "Failed to init") + return {"exit": exit, "output": output} + + func clone(src, dest, args={}): + logger.debug("Cloning from %s to %s..." % [src, dest]) + var output = [] + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + var command = "git clone --depth=1 --progress '%s' '%s'" % [src, dest] + if branch or tag: + command = "git clone --depth=1 --single-branch --branch %s '%s' '%s'" % [branch if branch else tag, src, dest] + elif commit: + return clone_commit(src, dest, commit) + var exit = _execute(command, output) + logger.debug("Successfully cloned from %s" % src if exit == OK else "Failed to clone from %s" % src) + return {"exit": exit, "output": output} + + func clone_commit(src, dest, commit): + var output = [] + if commit.length() < 40: + logger.error("Expected full length 40 digits commit-hash to clone specific commit, given {%s}" % commit) + return {"exit": FAILED, "output": output} + + logger.debug("Cloning from %s to %s @ %s..." % [src, dest, commit]) + var result = init() + if result.exit == OK: + result = remote_add("origin", src) + if result.exit == OK: + result = fetch("%s %s" % ["origin", commit]) + if result.exit == OK: + result = reset("--hard", "FETCH_HEAD") + return result + + func fetch(rm="--all"): + logger.debug("Fetching %s..." % rm.replace("--", "")) + var output = [] + var exit = _execute("git fetch %s" % rm, output) + logger.debug("Successfully fetched" if exit == OK else "Failed to fetch") + return {"exit": exit, "output": output} + + func pull(): + logger.debug("Pulling...") + var output = [] + var exit = _execute("git pull --rebase", output) + logger.debug("Successfully pulled" if exit == OK else "Failed to pull") + return {"exit": exit, "output": output} + + func remote_add(name, src): + logger.debug("Adding remote %s@%s..." % [name, src]) + var output = [] + var exit = _execute("git remote add %s '%s'" % [name, src], output) + logger.debug("Successfully added remote" if exit == OK else "Failed to add remote") + return {"exit": exit, "output": output} + + func reset(mode, to): + logger.debug("Resetting %s %s..." % [mode, to]) + var output = [] + var exit = _execute("git reset %s %s" % [mode, to], output) + logger.debug("Successfully reset" if exit == OK else "Failed to reset") + return {"exit": exit, "output": output} + + func get_commit_comparison(branch_a, branch_b): + var output = [] + var exit = _execute("git rev-list --count --left-right %s...%s" % [branch_a, branch_b], output) + var raw_ahead_behind = output[0].split("\t") + var ahead_behind = [] + for msg in raw_ahead_behind: + ahead_behind.append(msg.to_int()) + return ahead_behind if exit == OK else [] + + func get_current_branch(): + var output = [] + var exit = _execute("git rev-parse --abbrev-ref HEAD", output) + return output[0] if exit == OK else "" + + func get_current_tag(): + var output = [] + var exit = _execute("git describe --tags --exact-match", output) + return output[0] if exit == OK else "" + + func get_current_commit(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return output[0] if exit == OK else "" + + func is_detached_head(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return (!!output[0]) if exit == OK else true + + func is_up_to_date(args={}): + if fetch().exit == OK: + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + + if branch: + if branch == get_current_branch(): + return FAILED if is_detached_head() else OK + elif tag: + if tag == get_current_tag(): + return OK + elif commit: + if commit == get_current_commit(): + return OK + + var ahead_behind = get_commit_comparison("HEAD", "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + return FAILED if is_commit_behind else OK + return FAILED + +class _ThreadPool extends RefCounted: + signal all_thread_finished() + + var active = true + + var _threads = [] + var _finished_threads = [] + var _mutex = Mutex.new() + var _tasks = [] + var logger + + func _init(p_logger): + logger = p_logger + _threads.resize(OS.get_processor_count()) + + func _execute_task(task): + var thread = _get_thread() + var can_execute = thread + if can_execute: + task.thread = weakref(thread) + var callable = task.get("callable") + thread.start(_execute.bind(task), task.priority) + logger.debug("Execute task %s.%s() " % [callable.get_object(), callable.get_method()]) + return can_execute + + func _execute(args): + var callable = args.get("callable") + callable.call() + _mutex.lock() + var thread = args.thread.get_ref() + _threads[_threads.find(thread)] = null + _finished_threads.append(thread) + var all_finished = is_all_thread_finished() + _mutex.unlock() + + logger.debug("Execution finished %s.%s() " % [callable.get_object(), callable.get_method()]) + if all_finished: + logger.debug("All thread finished") + emit_signal("all_thread_finished") + + func _flush_tasks(): + if _tasks.size() == 0: + return + + var executed = true + while executed: + var task = _tasks.pop_front() + if task != null: + executed = _execute_task(task) + if not executed: + _tasks.push_front(task) + else: + executed = false + + func _flush_threads(): + for i in _finished_threads.size(): + var thread = _finished_threads.pop_front() + if not thread.is_alive(): + thread.wait_to_finish() + + func enqueue_task(callable, priority=1): + enqueue({"callable": callable, "priority": priority}) + + func enqueue(task): + var can_execute = false + if active: + can_execute = _execute_task(task) + if not can_execute: + _tasks.append(task) + + func process(delta): + if active: + _flush_tasks() + _flush_threads() + + func stop(): + _tasks.clear() + _flush_threads() + + func _get_thread(): + var thread + for i in OS.get_processor_count(): + var t = _threads[i] + if t: + if not t.is_started(): + thread = t + break + else: + thread = Thread.new() + _threads[i] = thread + break + return thread + + func is_all_thread_finished(): + for i in _threads.size(): + if _threads[i]: + return false + return true + + func is_all_task_finished(): + for i in _tasks.size(): + if _tasks[i]: + return false + return true + +class _Logger extends RefCounted: + enum LogLevel { + ALL, DEBUG, INFO, WARN, ERROR, NONE + } + const DEFAULT_LOG_FORMAT_DETAIL = "[{time}] [{level}] {msg}" + const DEFAULT_LOG_FORMAT_NORMAL = "{msg}" + + var log_level = LogLevel.INFO + var log_format = DEFAULT_LOG_FORMAT_NORMAL + var log_time_format = "{year}/{month}/{day} {hour}:{minute}:{second}" + var indent_level = 0 + var is_locked = false + + var _rows + var _max_column_length = [] + var _max_column_size = 0 + + func debug(msg, raw=false): + _log(LogLevel.DEBUG, msg, raw) + + func info(msg, raw=false): + _log(LogLevel.INFO, msg, raw) + + func warn(msg, raw=false): + _log(LogLevel.WARN, msg, raw) + + func error(msg, raw=false): + _log(LogLevel.ERROR, msg, raw) + + func _log(level, msg, raw=false): + if is_locked: + return + + if typeof(msg) != TYPE_STRING: + msg = str(msg) + if log_level <= level: + match level: + LogLevel.WARN: + push_warning(format_log(level, msg)) + LogLevel.ERROR: + push_error(format_log(level, msg)) + _: + if raw: + printraw(format_log(level, msg)) + else: + print(format_log(level, msg)) + + func format_log(level, msg): + return log_format.format({ + "time": log_time_format.format(get_formatted_datatime()), + "level": LogLevel.keys()[level], + "msg": msg.indent(" ".repeat(indent_level)) + }) + + func indent(): + indent_level += 1 + + func dedent(): + indent_level -= 1 + max(indent_level, 0) + + func lock(): + is_locked = true + + func unlock(): + is_locked = false + + func table_start(): + _rows = [] + + func table_end(): + assert(_rows != null, "Expected table_start() to be called first") + for columns in _rows: + var text = "" + for i in columns.size(): + var column = columns[i] + var max_tab_count = ceil(float(_max_column_length[i]) / 4.0) + var tab_count = max_tab_count - ceil(float(column.length()) / 4.0) + var extra_spaces = ceil(float(column.length()) / 4.0) * 4 - column.length() + if i < _max_column_size - 1: + text += column + " ".repeat(extra_spaces) + " ".repeat(tab_count) + else: + text += column + info(text) + + _rows.clear() + _rows = null + _max_column_length.clear() + _max_column_size = 0 + + func table_row(columns=[]): + assert(_rows != null, "Expected table_start() to be called first") + _rows.append(columns) + _max_column_size = max(_max_column_size, columns.size()) + for i in columns.size(): + var column = columns[i] + if _max_column_length.size() >= i + 1: + var max_column_length = _max_column_length[i] + _max_column_length[i] = max(max_column_length, column.length()) + else: + _max_column_length.append(column.length()) + + func get_formatted_datatime(): + var datetime = Time.get_datetime_dict_from_system() + datetime.year = "%04d" % datetime.year + datetime.month = "%02d" % datetime.month + datetime.day = "%02d" % datetime.day + datetime.hour = "%02d" % datetime.hour + datetime.minute = "%02d" % datetime.minute + datetime.second = "%02d" % datetime.second + return datetime diff --git a/addons/gdUnit4/LICENSE b/addons/gdUnit4/LICENSE new file mode 100644 index 0000000..8c60d13 --- /dev/null +++ b/addons/gdUnit4/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mike Schulze + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/gdUnit4/bin/GdUnitBuildTool.gd b/addons/gdUnit4/bin/GdUnitBuildTool.gd new file mode 100644 index 0000000..7070992 --- /dev/null +++ b/addons/gdUnit4/bin/GdUnitBuildTool.gd @@ -0,0 +1,110 @@ +#!/usr/bin/env -S godot -s +extends SceneTree + +enum { + INIT, + PROCESSING, + EXIT +} + +const RETURN_SUCCESS = 0 +const RETURN_ERROR = 100 +const RETURN_WARNING = 101 + +var _console := CmdConsole.new() +var _cmd_options: = CmdOptions.new([ + CmdOption.new( + "-scp, --src_class_path", + "-scp ", + "The full class path of the source file.", + TYPE_STRING + ), + CmdOption.new( + "-scl, --src_class_line", + "-scl ", + "The selected line number to generate test case.", + TYPE_INT + ) +]) + +var _status := INIT +var _source_file :String = "" +var _source_line :int = -1 + + +func _init(): + var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitBuildTool.gd") + var result := cmd_parser.parse(OS.get_cmdline_args()) + if result.is_error(): + show_options() + exit(RETURN_ERROR, result.error_message()); + return + var cmd_options = result.value() + for cmd in cmd_options: + if cmd.name() == '-scp': + _source_file = cmd.arguments()[0] + _source_file = ProjectSettings.localize_path(ProjectSettings.localize_path(_source_file)) + if cmd.name() == '-scl': + _source_line = int(cmd.arguments()[0]) + # verify required arguments + if _source_file == "": + exit(RETURN_ERROR, "missing required argument -scp ") + return + if _source_line == -1: + exit(RETURN_ERROR, "missing required argument -scl ") + return + _status = PROCESSING + + +func _idle(_delta): + if _status == PROCESSING: + var script := ResourceLoader.load(_source_file) as Script + if script == null: + exit(RETURN_ERROR, "Can't load source file %s!" % _source_file) + var result := GdUnitTestSuiteBuilder.create(script, _source_line) + if result.is_error(): + print_json_error(result.error_message()) + exit(RETURN_ERROR, result.error_message()) + return + _console.prints_color("Added testcase: %s" % result.value(), Color.CORNFLOWER_BLUE) + print_json_result(result.value()) + exit(RETURN_SUCCESS) + + +func exit(code :int, message :String = "") -> void: + _status = EXIT + if code == RETURN_ERROR: + if not message.is_empty(): + _console.prints_error(message) + _console.prints_error("Abnormal exit with %d" % code) + else: + _console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) + quit(code) + + +func print_json_result(result :Dictionary) -> void: + # convert back to system path + var path = ProjectSettings.globalize_path(result["path"]); + var json = 'JSON_RESULT:{"TestCases" : [{"line":%d, "path": "%s"}]}' % [result["line"], path] + prints(json) + + +func print_json_error(error :String) -> void: + prints('JSON_RESULT:{"Error" : "%s"}' % error) + + +func show_options() -> void: + _console.prints_color(" Usage:", Color.DARK_SALMON) + _console.prints_color(" build -scp -scl ", Color.DARK_SALMON) + _console.prints_color("-- Options ---------------------------------------------------------------------------------------", + Color.DARK_SALMON).new_line() + for option in _cmd_options.default_options(): + descripe_option(option) + + +func descripe_option(cmd_option :CmdOption) -> void: + _console.print_color(" %-40s" % str(cmd_option.commands()), Color.CORNFLOWER_BLUE) + _console.prints_color(cmd_option.description(), Color.LIGHT_GREEN) + if not cmd_option.help().is_empty(): + _console.prints_color("%-4s %s" % ["", cmd_option.help()], Color.DARK_TURQUOISE) + _console.new_line() diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd new file mode 100644 index 0000000..677ed99 --- /dev/null +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd @@ -0,0 +1,600 @@ +#!/usr/bin/env -S godot -s +extends SceneTree + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +#warning-ignore-all:return_value_discarded +class CLIRunner: + extends Node + + enum { + READY, + INIT, + RUN, + STOP, + EXIT + } + + const DEFAULT_REPORT_COUNT = 20 + const RETURN_SUCCESS = 0 + const RETURN_ERROR = 100 + const RETURN_ERROR_HEADLESS_NOT_SUPPORTED = 103 + const RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED = 104 + const RETURN_WARNING = 101 + + var _state = READY + var _test_suites_to_process: Array + var _executor + var _cs_executor + var _report: GdUnitHtmlReport + var _report_dir: String + var _report_max: int = DEFAULT_REPORT_COUNT + var _headless_mode_ignore := false + var _runner_config := GdUnitRunnerConfig.new() + var _console := CmdConsole.new() + var _cmd_options := CmdOptions.new([ + CmdOption.new( + "-a, --add", + "-a ", + "Adds the given test suite or directory to the execution pipeline.", + TYPE_STRING + ), + CmdOption.new( + "-i, --ignore", + "-i ", + "Adds the given test suite or test case to the ignore list.", + TYPE_STRING + ), + CmdOption.new( + "-c, --continue", + "", + """By default GdUnit will abort checked first test failure to be fail fast, + instead of stop after first failure you can use this option to run the complete test set.""".dedent() + ), + CmdOption.new( + "-conf, --config", + "-conf [testconfiguration.cfg]", + "Run all tests by given test configuration. Default is 'GdUnitRunner.cfg'", + TYPE_STRING, + true + ), + CmdOption.new( + "-help", "", + "Shows this help message." + ), + CmdOption.new("--help-advanced", "", + "Shows advanced options." + ) + ], + [ + # advanced options + CmdOption.new( + "-rd, --report-directory", + "-rd ", + "Specifies the output directory in which the reports are to be written. The default is res://reports/.", + TYPE_STRING, + true + ), + CmdOption.new( + "-rc, --report-count", + "-rc ", + "Specifies how many reports are saved before they are deleted. The default is %s." % str(DEFAULT_REPORT_COUNT), + TYPE_INT, + true + ), + #CmdOption.new("--list-suites", "--list-suites [directory]", "Lists all test suites located in the given directory.", TYPE_STRING), + #CmdOption.new("--describe-suite", "--describe-suite ", "Shows the description of selected test suite.", TYPE_STRING), + CmdOption.new( + "--info", "", + "Shows the GdUnit version info" + ), + CmdOption.new( + "--selftest", "", + "Runs the GdUnit self test" + ), + CmdOption.new( + "--ignoreHeadlessMode", + "--ignoreHeadlessMode", + "By default, running GdUnit4 in headless mode is not allowed. You can switch off the headless mode check by set this property." + ), + ]) + + + func _ready(): + _state = INIT + _report_dir = GdUnitFileAccess.current_dir() + "reports" + _executor = load("res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd").new() + # stop checked first test failure to fail fast + _executor.fail_fast(true) + if GdUnit4CSharpApiLoader.is_mono_supported(): + prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version()) + _cs_executor = GdUnit4CSharpApiLoader.create_executor(self) + var err := GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + if err != OK: + prints("gdUnitSignals failed") + push_error("Error checked startup, can't connect executor for 'send_event'") + quit(RETURN_ERROR) + + + func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + prints("Finallize .. done") + + + func _process(_delta :float) -> void: + match _state: + INIT: + init_gd_unit() + _state = RUN + RUN: + # all test suites executed + if _test_suites_to_process.is_empty(): + _state = STOP + else: + set_process(false) + # process next test suite + var test_suite := _test_suites_to_process.pop_front() as Node + if _cs_executor != null and _cs_executor.IsExecutable(test_suite): + _cs_executor.Execute(test_suite) + await _cs_executor.ExecutionCompleted + else: + await _executor.execute(test_suite) + set_process(true) + STOP: + _state = EXIT + _on_gdunit_event(GdUnitStop.new()) + quit(report_exit_code(_report)) + + + func quit(code: int) -> void: + _cs_executor = null + GdUnitTools.dispose_all() + await GdUnitMemoryObserver.gc_on_guarded_instances() + await get_tree().physics_frame + get_tree().quit(code) + + + func set_report_dir(path: String) -> void: + _report_dir = ProjectSettings.globalize_path(GdUnitFileAccess.make_qualified_path(path)) + _console.prints_color( + "Set write reports to %s" % _report_dir, + Color.DEEP_SKY_BLUE + ) + + + func set_report_count(count: String) -> void: + var report_count := count.to_int() + if report_count < 1: + _console.prints_error( + "Invalid report history count '%s' set back to default %d" + % [count, DEFAULT_REPORT_COUNT] + ) + _report_max = DEFAULT_REPORT_COUNT + else: + _console.prints_color( + "Set report history count to %s" % count, + Color.DEEP_SKY_BLUE + ) + _report_max = report_count + + + func disable_fail_fast() -> void: + _console.prints_color( + "Disabled fail fast!", + Color.DEEP_SKY_BLUE + ) + _executor.fail_fast(false) + + + func run_self_test() -> void: + _console.prints_color( + "Run GdUnit4 self tests.", + Color.DEEP_SKY_BLUE + ) + disable_fail_fast() + _runner_config.self_test() + + + func show_version() -> void: + _console.prints_color( + "Godot %s" % Engine.get_version_info().get("string"), + Color.DARK_SALMON + ) + var config := ConfigFile.new() + config.load("addons/gdUnit4/plugin.cfg") + _console.prints_color( + "GdUnit4 %s" % config.get_value("plugin", "version"), + Color.DARK_SALMON + ) + quit(RETURN_SUCCESS) + + + func check_headless_mode() -> void: + _headless_mode_ignore = true + + + func show_options(show_advanced: bool = false) -> void: + _console.prints_color( + """ + Usage: + runtest -a + runtest -a -i + """.dedent(), + Color.DARK_SALMON + ).prints_color( + "-- Options ---------------------------------------------------------------------------------------", + Color.DARK_SALMON + ).new_line() + for option in _cmd_options.default_options(): + descripe_option(option) + if show_advanced: + _console.prints_color( + "-- Advanced options --------------------------------------------------------------------------", + Color.DARK_SALMON + ).new_line() + for option in _cmd_options.advanced_options(): + descripe_option(option) + + + func descripe_option(cmd_option: CmdOption) -> void: + _console.print_color( + " %-40s" % str(cmd_option.commands()), + Color.CORNFLOWER_BLUE + ) + _console.prints_color( + cmd_option.description(), + Color.LIGHT_GREEN + ) + if not cmd_option.help().is_empty(): + _console.prints_color( + "%-4s %s" % ["", cmd_option.help()], + Color.DARK_TURQUOISE + ) + _console.new_line() + + + func load_test_config(path := GdUnitRunnerConfig.CONFIG_FILE) -> void: + _console.print_color( + "Loading test configuration %s\n" % path, + Color.CORNFLOWER_BLUE + ) + _runner_config.load_config(path) + + + func show_help() -> void: + show_options() + quit(RETURN_SUCCESS) + + + func show_advanced_help() -> void: + show_options(true) + quit(RETURN_SUCCESS) + + + func init_gd_unit() -> void: + _console.prints_color( + """ + -------------------------------------------------------------------------------------------------- + GdUnit4 Comandline Tool + --------------------------------------------------------------------------------------------------""".dedent(), + Color.DARK_SALMON + ).new_line() + + var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") + var result := cmd_parser.parse(OS.get_cmdline_args()) + if result.is_error(): + show_options() + _console.prints_error(result.error_message()) + _console.prints_error("Abnormal exit with %d" % RETURN_ERROR) + _state = STOP + quit(RETURN_ERROR) + return + if result.is_empty(): + show_help() + return + # build runner config by given commands + result = ( + CmdCommandHandler.new(_cmd_options) + .register_cb("-help", Callable(self, "show_help")) + .register_cb("--help-advanced", Callable(self, "show_advanced_help")) + .register_cb("-a", Callable(_runner_config, "add_test_suite")) + .register_cbv("-a", Callable(_runner_config, "add_test_suites")) + .register_cb("-i", Callable(_runner_config, "skip_test_suite")) + .register_cbv("-i", Callable(_runner_config, "skip_test_suites")) + .register_cb("-rd", set_report_dir) + .register_cb("-rc", set_report_count) + .register_cb("--selftest", run_self_test) + .register_cb("-c", disable_fail_fast) + .register_cb("-conf", load_test_config) + .register_cb("--info", show_version) + .register_cb("--ignoreHeadlessMode", check_headless_mode) + .execute(result.value()) + ) + if result.is_error(): + _console.prints_error(result.error_message()) + _state = STOP + quit(RETURN_ERROR) + + if DisplayServer.get_name() == "headless": + if _headless_mode_ignore: + _console.prints_warning(""" + Headless mode is ignored by option '--ignoreHeadlessMode'" + + Please note that tests that use UI interaction do not work correctly in headless mode. + Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore + have no effect in the test! + """.dedent() + ).new_line() + else: + _console.prints_error(""" + Headless mode is not supported! + + Please note that tests that use UI interaction do not work correctly in headless mode. + Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore + have no effect in the test! + + You can run with '--ignoreHeadlessMode' to swtich off this check. + """.dedent() + ).prints_error( + "Abnormal exit with %d" % RETURN_ERROR_HEADLESS_NOT_SUPPORTED + ) + quit(RETURN_ERROR_HEADLESS_NOT_SUPPORTED) + return + + _test_suites_to_process = load_testsuites(_runner_config) + if _test_suites_to_process.is_empty(): + _console.prints_warning("No test suites found, abort test run!") + _console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) + _state = STOP + quit(RETURN_SUCCESS) + var total_test_count := _collect_test_case_count(_test_suites_to_process) + _on_gdunit_event(GdUnitInit.new(_test_suites_to_process.size(), total_test_count)) + + + func load_testsuites(config: GdUnitRunnerConfig) -> Array[Node]: + var test_suites_to_process: Array[Node] = [] + var to_execute := config.to_execute() + # scan for the requested test suites + var ts_scanner := GdUnitTestSuiteScanner.new() + for as_resource_path in to_execute.keys(): + var selected_tests: PackedStringArray = to_execute.get(as_resource_path) + var scaned_suites := ts_scanner.scan(as_resource_path) + skip_test_case(scaned_suites, selected_tests) + test_suites_to_process.append_array(scaned_suites) + skip_suites(test_suites_to_process, config) + return test_suites_to_process + + + func skip_test_case(test_suites: Array, test_case_names: Array) -> void: + if test_case_names.is_empty(): + return + for test_suite in test_suites: + for test_case in test_suite.get_children(): + if not test_case_names.has(test_case.get_name()): + test_suite.remove_child(test_case) + test_case.free() + + + func skip_suites(test_suites: Array, config: GdUnitRunnerConfig) -> void: + var skipped := config.skipped() + for test_suite in test_suites: + skip_suite(test_suite, skipped) + + + func skip_suite(test_suite: Node, skipped: Dictionary) -> void: + var skipped_suites := skipped.keys() + if skipped_suites.is_empty(): + return + var suite_name := test_suite.get_name() + # skipp c# testsuites for now + if test_suite.get_script() == null: + return + var test_suite_path: String = ( + test_suite.get_meta("ResourcePath") if test_suite.get_script() == null + else test_suite.get_script().resource_path + ) + for suite_to_skip in skipped_suites: + # if suite skipped by path or name + if ( + suite_to_skip == test_suite_path + or (suite_to_skip.is_valid_filename() and suite_to_skip == suite_name) + ): + var skipped_tests: Array = skipped.get(suite_to_skip) + # if no tests skipped test the complete suite is skipped + if skipped_tests.is_empty(): + _console.prints_warning("Skip test suite %s:%s" % suite_to_skip) + test_suite.skip(true) + else: + # skip tests + for test_to_skip in skipped_tests: + var test_case: _TestCase = test_suite.find_child(test_to_skip, true, false) + if test_case: + test_case.skip(true) + _console.prints_warning("Skip test case %s:%s" % [suite_to_skip, test_to_skip]) + else: + _console.prints_error( + "Can't skip test '%s' checked test suite '%s', no test with given name exists!" + % [test_to_skip, suite_to_skip] + ) + + + func _collect_test_case_count(test_suites: Array) -> int: + var total: int = 0 + for test_suite in test_suites: + total += (test_suite as Node).get_child_count() + return total + + + # gdlint: disable=function-name + func PublishEvent(data: Dictionary) -> void: + _on_gdunit_event(GdUnitEvent.new().deserialize(data)) + + + func _on_gdunit_event(event: GdUnitEvent): + match event.type(): + GdUnitEvent.INIT: + _report = GdUnitHtmlReport.new(_report_dir) + GdUnitEvent.STOP: + if _report == null: + _report = GdUnitHtmlReport.new(_report_dir) + var report_path := _report.write() + _report.delete_history(_report_max) + JUnitXmlReport.new(_report._report_path, _report.iteration()).write(_report) + _console.prints_color( + "Total test suites: %s" % _report.suite_count(), + Color.DARK_SALMON + ).prints_color( + "Total test cases: %s" % _report.test_count(), + Color.DARK_SALMON + ).prints_color( + "Total time: %s" % LocalTime.elapsed(_report.duration()), + Color.DARK_SALMON + ).prints_color( + "Open Report at: file://%s" % report_path, + Color.CORNFLOWER_BLUE + ) + GdUnitEvent.TESTSUITE_BEFORE: + _report.add_testsuite_report( + GdUnitTestSuiteReport.new(event.resource_path(), event.suite_name()) + ) + GdUnitEvent.TESTSUITE_AFTER: + _report.update_test_suite_report( + event.resource_path(), + event.elapsed_time(), + event.is_error(), + event.is_failed(), + event.is_warning(), + event.is_skipped(), + event.skipped_count(), + event.failed_count(), + event.orphan_nodes(), + event.reports() + ) + GdUnitEvent.TESTCASE_BEFORE: + _report.add_testcase_report( + event.resource_path(), + GdUnitTestCaseReport.new( + event.resource_path(), + event.suite_name(), + event.test_name() + ) + ) + GdUnitEvent.TESTCASE_AFTER: + var test_report := GdUnitTestCaseReport.new( + event.resource_path(), + event.suite_name(), + event.test_name(), + event.is_error(), + event.is_failed(), + event.failed_count(), + event.orphan_nodes(), + event.is_skipped(), + event.reports(), + event.elapsed_time() + ) + _report.update_testcase_report(event.resource_path(), test_report) + print_status(event) + + + func report_exit_code(report: GdUnitHtmlReport) -> int: + if report.error_count() + report.failure_count() > 0: + _console.prints_color("Exit code: %d" % RETURN_ERROR, Color.FIREBRICK) + return RETURN_ERROR + if report.orphan_count() > 0: + _console.prints_color("Exit code: %d" % RETURN_WARNING, Color.GOLDENROD) + return RETURN_WARNING + _console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) + return RETURN_SUCCESS + + + func print_status(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.TESTSUITE_BEFORE: + _console.prints_color( + "Run Test Suite %s " % event.resource_path(), + Color.ANTIQUE_WHITE + ) + GdUnitEvent.TESTCASE_BEFORE: + _console.print_color( + " Run Test: %s > %s :" % [event.resource_path(), event.test_name()], + Color.ANTIQUE_WHITE + ).prints_color( + "STARTED", + Color.FOREST_GREEN + ).save_cursor() + GdUnitEvent.TESTCASE_AFTER: + #_console.restore_cursor() + _console.print_color( + " Run Test: %s > %s :" % [event.resource_path(), event.test_name()], + Color.ANTIQUE_WHITE + ) + _print_status(event) + _print_failure_report(event.reports()) + GdUnitEvent.TESTSUITE_AFTER: + _print_failure_report(event.reports()) + _print_status(event) + _console.prints_color( + "Statistics: | %d tests cases | %d error | %d failed | %d skipped | %d orphans |\n" + % [ + _report.test_count(), + _report.error_count(), + _report.failure_count(), + _report.skipped_count(), + _report.orphan_count() + ], + Color.ANTIQUE_WHITE + ) + + + func _print_failure_report(reports: Array) -> void: + for report in reports: + if ( + report.is_failure() + or report.is_error() + or report.is_warning() + or report.is_skipped() + ): + _console.prints_color( + " Report:", + Color.DARK_TURQUOISE, CmdConsole.BOLD | CmdConsole.UNDERLINE + ) + var text = GdUnitTools.richtext_normalize(str(report)) + for line in text.split("\n"): + _console.prints_color(" %s" % line, Color.DARK_TURQUOISE) + _console.new_line() + + + func _print_status(event: GdUnitEvent) -> void: + if event.is_skipped(): + _console.print_color("SKIPPED", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.ITALIC) + elif event.is_failed() or event.is_error(): + _console.print_color("FAILED", Color.FIREBRICK, CmdConsole.BOLD) + elif event.orphan_nodes() > 0: + _console.print_color("PASSED", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.UNDERLINE) + else: + _console.print_color("PASSED", Color.FOREST_GREEN, CmdConsole.BOLD) + _console.prints_color( + " %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE + ) + + +var _cli_runner: CLIRunner + + +func _initialize(): + if Engine.get_version_info().hex < 0x40100: + prints("GdUnit4 requires a minimum of Godot 4.1.x Version!") + quit(CLIRunner.RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED) + return + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + _cli_runner = CLIRunner.new() + root.add_child(_cli_runner) + + +# do not use print statements on _finalize it results in random crashes +#func _finalize(): +# prints("Finallize ..") +# prints("-Orphan nodes report-----------------------") +# Window.print_orphan_nodes() +# prints("Finallize .. done") diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd new file mode 100644 index 0000000..f7bb475 --- /dev/null +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd @@ -0,0 +1,141 @@ +#!/usr/bin/env -S godot -s +extends MainLoop + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +# gdlint: disable=max-line-length +const NO_LOG_TEMPLATE = """ + + + + + + Logging + + + +
+

No logging available!

+
+

For logging to occur, you must check Enable File Logging in Project Settings.

+

You can enable Logging Project Settings > Logging > File Logging > Enable File Logging in the Project Settings.

+
+ +""" + +#warning-ignore-all:return_value_discarded +var _cmd_options := CmdOptions.new([ + CmdOption.new( + "-rd, --report-directory", + "-rd ", + "Specifies the output directory in which the reports are to be written. The default is res://reports/.", + TYPE_STRING, + true + ) + ]) + +var _report_root_path: String + + +func _init(): + _report_root_path = GdUnitFileAccess.current_dir() + "reports" + + +func _process(_delta): + # check if reports exists + if not reports_available(): + prints("no reports found") + return true + # scan for latest report path + var iteration := GdUnitFileAccess.find_last_path_index( + _report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX + ) + var report_path = "%s/%s%d" % [_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX, iteration] + # only process if godot logging is enabled + if not GdUnitSettings.is_log_enabled(): + _patch_report(report_path, "") + return true + # parse possible custom report path, + var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") + # ignore erros and exit quitly + if cmd_parser.parse(OS.get_cmdline_args(), true).is_error(): + return true + CmdCommandHandler.new(_cmd_options).register_cb("-rd", set_report_directory) + # scan for latest godot log and copy to report + var godot_log := _scan_latest_godot_log() + var result := _copy_and_pach(godot_log, report_path) + if result.is_error(): + push_error(result.error_message()) + return true + _patch_report(report_path, godot_log) + return true + + +func set_report_directory(path: String) -> void: + _report_root_path = path + + +func _scan_latest_godot_log() -> String: + var path := GdUnitSettings.get_log_path().get_base_dir() + var files_sorted := Array() + for file in GdUnitFileAccess.scan_dir(path): + var file_name := "%s/%s" % [path, file] + files_sorted.append(file_name) + # sort by name, the name contains the timestamp so we sort at the end by timestamp + files_sorted.sort() + return files_sorted[-1] + + +func _patch_report(report_path: String, godot_log: String) -> void: + var index_file := FileAccess.open("%s/index.html" % report_path, FileAccess.READ_WRITE) + if index_file == null: + push_error( + "Can't add log path to index.html. Error: %s" + % error_string(FileAccess.get_open_error()) + ) + return + # if no log file available than add a information howto enable it + if godot_log.is_empty(): + FileAccess.open( + "%s/logging_not_available.html" % report_path, + FileAccess.WRITE).store_string(NO_LOG_TEMPLATE) + var log_file = "logging_not_available.html" if godot_log.is_empty() else godot_log.get_file() + var content := index_file.get_as_text().replace("${log_file}", log_file) + # overide it + index_file.seek(0) + index_file.store_string(content) + + +func _copy_and_pach(from_file: String, to_dir: String) -> GdUnitResult: + var result := GdUnitFileAccess.copy_file(from_file, to_dir) + if result.is_error(): + return result + var file := FileAccess.open(from_file, FileAccess.READ) + if file == null: + return GdUnitResult.error( + "Can't find file '%s'. Error: %s" + % [from_file, error_string(FileAccess.get_open_error())] + ) + var content := file.get_as_text() + # patch out console format codes + for color_index in range(0, 256): + var to_replace := "[38;5;%dm" % color_index + content = content.replace(to_replace, "") + content = content\ + .replace("", "")\ + .replace(CmdConsole.__CSI_BOLD, "")\ + .replace(CmdConsole.__CSI_ITALIC, "")\ + .replace(CmdConsole.__CSI_UNDERLINE, "") + var to_file := to_dir + "/" + from_file.get_file() + file = FileAccess.open(to_file, FileAccess.WRITE) + if file == null: + return GdUnitResult.error( + "Can't open to write '%s'. Error: %s" + % [to_file, error_string(FileAccess.get_open_error())] + ) + file.store_string(content) + return GdUnitResult.empty() + + +func reports_available() -> bool: + return DirAccess.dir_exists_absolute(_report_root_path) diff --git a/addons/gdUnit4/bin/ProjectScanner.gd b/addons/gdUnit4/bin/ProjectScanner.gd new file mode 100644 index 0000000..c2fc0fd --- /dev/null +++ b/addons/gdUnit4/bin/ProjectScanner.gd @@ -0,0 +1,99 @@ +#!/usr/bin/env -S godot -s +@tool +extends SceneTree + +const CmdConsole = preload("res://addons/gdUnit4/src/cmd/CmdConsole.gd") + + +func _initialize(): + set_auto_accept_quit(false) + var scanner := SourceScanner.new(self) + root.add_child(scanner) + + +# gdlint: disable=trailing-whitespace +class SourceScanner extends Node: + + enum { + INIT, + STARTUP, + SCAN, + QUIT, + DONE + } + + var _state = INIT + var _console := CmdConsole.new() + var _elapsed_time := 0.0 + var _plugin: EditorPlugin + var _fs: EditorFileSystem + var _scene: SceneTree + + + func _init(scene :SceneTree) -> void: + _scene = scene + _console.prints_color(""" + ======================================================================== + Running project scan:""".dedent(), + Color.CORNFLOWER_BLUE + ) + _state = INIT + + + func _process(delta :float) -> void: + _elapsed_time += delta + set_process(false) + await_inital_scan() + await scan_project() + set_process(true) + + + # !! don't use any await in this phase otherwise the editor will be instable !! + func await_inital_scan() -> void: + if _state == INIT: + _console.prints_color("Wait initial scanning ...", Color.DARK_GREEN) + _plugin = EditorPlugin.new() + _fs = _plugin.get_editor_interface().get_resource_filesystem() + _plugin.get_editor_interface().set_plugin_enabled("gdUnit4", false) + _state = STARTUP + + if _state == STARTUP: + if _fs.is_scanning(): + _console.progressBar(_fs.get_scanning_progress() * 100 as int) + # we wait 10s in addition to be on the save site the scanning is done + if _elapsed_time > 10.0: + _console.progressBar(100) + _console.new_line() + _console.prints_color("initial scanning ... done", Color.DARK_GREEN) + _state = SCAN + + + func scan_project() -> void: + if _state != SCAN: + return + _console.prints_color("Scan project: ", Color.SANDY_BROWN) + await get_tree().process_frame + _fs.scan_sources() + await get_tree().create_timer(5).timeout + _console.prints_color("Scan: ", Color.SANDY_BROWN) + _console.progressBar(0) + await get_tree().process_frame + _fs.scan() + while _fs.is_scanning(): + await get_tree().process_frame + _console.progressBar(_fs.get_scanning_progress() * 100 as int) + await get_tree().create_timer(10).timeout + _console.progressBar(100) + _console.new_line() + _plugin.free() + _console.prints_color(""" + Scan project done. + ========================================================================""".dedent(), + Color.CORNFLOWER_BLUE + ) + await get_tree().process_frame + await get_tree().physics_frame + queue_free() + # force quit editor + _state = DONE + _scene.quit(0) diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg new file mode 100644 index 0000000..fce580f --- /dev/null +++ b/addons/gdUnit4/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="gdUnit4" +description="Unit Testing Framework for Godot Scripts" +author="Mike Schulze" +version="4.2.4" +script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd new file mode 100644 index 0000000..8aedfc0 --- /dev/null +++ b/addons/gdUnit4/plugin.gd @@ -0,0 +1,47 @@ +@tool +extends EditorPlugin + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _gd_inspector :Node +var _server_node :Node +var _gd_console :Node + + +func _enter_tree() -> void: + if Engine.get_version_info().hex < 0x40100: + prints("GdUnit4 plugin requires a minimum of Godot 4.1.x Version!") + return + Engine.set_meta("GdUnitEditorPlugin", self) + GdUnitSettings.setup() + # install the GdUnit inspector + _gd_inspector = load("res://addons/gdUnit4/src/ui/GdUnitInspector.tscn").instantiate() + add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector) + # install the GdUnit Console + _gd_console = load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn").instantiate() + add_control_to_bottom_panel(_gd_console, "gdUnitConsole") + _server_node = load("res://addons/gdUnit4/src/network/GdUnitServer.tscn").instantiate() + add_child(_server_node) + prints("Loading GdUnit4 Plugin success") + if GdUnitSettings.is_update_notification_enabled(): + var update_tool :Node = load("res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn").instantiate() + Engine.get_main_loop().root.call_deferred("add_child", update_tool) + if GdUnit4CSharpApiLoader.is_mono_supported(): + prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version()) + + +func _exit_tree() -> void: + if is_instance_valid(_gd_inspector): + remove_control_from_docks(_gd_inspector) + _gd_inspector.free() + if is_instance_valid(_gd_console): + remove_control_from_bottom_panel(_gd_console) + _gd_console.free() + if is_instance_valid(_server_node): + remove_child(_server_node) + _server_node.free() + GdUnitTools.dispose_all() + if Engine.has_meta("GdUnitEditorPlugin"): + Engine.remove_meta("GdUnitEditorPlugin") + if Engine.get_version_info().hex < 0x40100 or Engine.get_version_info().hex > 0x40101: + prints("Unload GdUnit4 Plugin success") diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd new file mode 100644 index 0000000..0c8fbc5 --- /dev/null +++ b/addons/gdUnit4/runtest.cmd @@ -0,0 +1,25 @@ +@ECHO OFF +CLS + +IF NOT DEFINED GODOT_BIN ( + ECHO "GODOT_BIN is not set." + ECHO "Please set the environment variable 'setx GODOT_BIN '" + EXIT /b -1 +) + +REM scan if Godot mono used and compile c# classes +for /f "tokens=5 delims=. " %%i in ('%GODOT_BIN% --version') do set GODOT_TYPE=%%i +IF "%GODOT_TYPE%" == "mono" ( + ECHO "Godot mono detected" + ECHO Compiling c# classes ... Please Wait + dotnet build --debug + ECHO done %errorlevel% +) + +%GODOT_BIN% -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd %* +SET exit_code=%errorlevel% +%GODOT_BIN% --headless --quiet -s -d res://addons/gdUnit4/bin/GdUnitCopyLog.gd %* + +ECHO %exit_code% + +EXIT /B %exit_code% diff --git a/addons/gdUnit4/runtest.sh b/addons/gdUnit4/runtest.sh new file mode 100644 index 0000000..83ed272 --- /dev/null +++ b/addons/gdUnit4/runtest.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +if [ -z "$GODOT_BIN" ]; then + GODOT_BIN=$(which godot) +fi + +if [ -z "$GODOT_BIN" ]; then + echo "'GODOT_BIN' is not set." + echo "Please set the environment variable 'export GODOT_BIN=/Applications/Godot.app/Contents/MacOS/Godot'" + exit 1 +fi + +"$GODOT_BIN" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd $* +exit_code=$? +echo "Run tests ends with $exit_code" + +"$GODOT_BIN" --headless --path . --quiet -s -d res://addons/gdUnit4/bin/GdUnitCopyLog.gd $* > /dev/null +exit_code2=$? +exit $exit_code diff --git a/addons/gdUnit4/src/Comparator.gd b/addons/gdUnit4/src/Comparator.gd new file mode 100644 index 0000000..096088a --- /dev/null +++ b/addons/gdUnit4/src/Comparator.gd @@ -0,0 +1,12 @@ +class_name Comparator +extends Resource + +enum { + EQUAL, + LESS_THAN, + LESS_EQUAL, + GREATER_THAN, + GREATER_EQUAL, + BETWEEN_EQUAL, + NOT_BETWEEN_EQUAL, +} diff --git a/addons/gdUnit4/src/Fuzzers.gd b/addons/gdUnit4/src/Fuzzers.gd new file mode 100644 index 0000000..8affdbf --- /dev/null +++ b/addons/gdUnit4/src/Fuzzers.gd @@ -0,0 +1,34 @@ +## A fuzzer implementation to provide default implementation +class_name Fuzzers +extends Resource + + +## Generates an random string with min/max length and given charset +static func rand_str(min_length: int, max_length, charset := StringFuzzer.DEFAULT_CHARSET) -> Fuzzer: + return StringFuzzer.new(min_length, max_length, charset) + + +## Generates an random integer in a range form to +static func rangei(from: int, to: int) -> Fuzzer: + return IntFuzzer.new(from, to) + +## Generates a randon float within in a given range +static func rangef(from: float, to: float) -> Fuzzer: + return FloatFuzzer.new(from, to) + +## Generates an random Vector2 in a range form to +static func rangev2(from: Vector2, to: Vector2) -> Fuzzer: + return Vector2Fuzzer.new(from, to) + + +## Generates an random Vector3 in a range form to +static func rangev3(from: Vector3, to: Vector3) -> Fuzzer: + return Vector3Fuzzer.new(from, to) + +## Generates an integer in a range form to that can be divided exactly by 2 +static func eveni(from: int, to: int) -> Fuzzer: + return IntFuzzer.new(from, to, IntFuzzer.EVEN) + +## Generates an integer in a range form to that cannot be divided exactly by 2 +static func oddi(from: int, to: int) -> Fuzzer: + return IntFuzzer.new(from, to, IntFuzzer.ODD) diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd b/addons/gdUnit4/src/GdUnitArrayAssert.gd new file mode 100644 index 0000000..31cb088 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd @@ -0,0 +1,160 @@ +## An Assertion Tool to verify array values +class_name GdUnitArrayAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +func is_null() -> GdUnitArrayAssert: + return self + + +## Verifies that the current value is not null. +func is_not_null() -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array is equal to the given one. +@warning_ignore("unused_parameter") +func is_equal(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array is equal to the given one, ignoring case considerations. +@warning_ignore("unused_parameter") +func is_equal_ignoring_case(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array is not equal to the given one. +@warning_ignore("unused_parameter") +func is_not_equal(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array is not equal to the given one, ignoring case considerations. +@warning_ignore("unused_parameter") +func is_not_equal_ignoring_case(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array is empty, it has a size of 0. +func is_empty() -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array is not empty, it has a size of minimum 1. +func is_not_empty() -> GdUnitArrayAssert: + return self + +## Verifies that the current Array is the same. [br] +## Compares the current by object reference equals +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_same(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array is NOT the same. [br] +## Compares the current by object reference equals +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_not_same(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array has a size of given value. +@warning_ignore("unused_parameter") +func has_size(expectd: int) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array contains the given values, in any order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same] +@warning_ignore("unused_parameter") +func contains(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly] +@warning_ignore("unused_parameter") +func contains_exactly(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly_in_any_order] +@warning_ignore("unused_parameter") +func contains_exactly_in_any_order(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array contains the given values, in any order.[br] +## The values are compared by object reference, for deep parameter comparision use [method contains] +@warning_ignore("unused_parameter") +func contains_same(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br] +## The values are compared by object reference, for deep parameter comparision use [method contains_exactly] +@warning_ignore("unused_parameter") +func contains_same_exactly(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br] +## The values are compared by object reference, for deep parameter comparision use [method contains_exactly_in_any_order] +@warning_ignore("unused_parameter") +func contains_same_exactly_in_any_order(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array do NOT contains the given values, in any order.[br] +## The values are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same] +## [b]Example:[/b] +## [codeblock] +## # will succeed +## assert_array([1, 2, 3, 4, 5]).not_contains([6]) +## # will fail +## assert_array([1, 2, 3, 4, 5]).not_contains([2, 6]) +## [/codeblock] +@warning_ignore("unused_parameter") +func not_contains(expected) -> GdUnitArrayAssert: + return self + + +## Verifies that the current Array do NOT contains the given values, in any order.[br] +## The values are compared by object reference, for deep parameter comparision use [method not_contains] +## [b]Example:[/b] +## [codeblock] +## # will succeed +## assert_array([1, 2, 3, 4, 5]).not_contains([6]) +## # will fail +## assert_array([1, 2, 3, 4, 5]).not_contains([2, 6]) +## [/codeblock] +@warning_ignore("unused_parameter") +func not_contains_same(expected) -> GdUnitArrayAssert: + return self + + +## Extracts all values by given function name and optional arguments into a new ArrayAssert. +## If the elements not accessible by `func_name` the value is converted to `"n.a"`, expecting null values +@warning_ignore("unused_parameter") +func extract(func_name: String, args := Array()) -> GdUnitArrayAssert: + return self + + +## Extracts all values by given extractor's into a new ArrayAssert. +## If the elements not extractable than the value is converted to `"n.a"`, expecting null values +@warning_ignore("unused_parameter") +func extractv( + extractor0 :GdUnitValueExtractor, + extractor1 :GdUnitValueExtractor = null, + extractor2 :GdUnitValueExtractor = null, + extractor3 :GdUnitValueExtractor = null, + extractor4 :GdUnitValueExtractor = null, + extractor5 :GdUnitValueExtractor = null, + extractor6 :GdUnitValueExtractor = null, + extractor7 :GdUnitValueExtractor = null, + extractor8 :GdUnitValueExtractor = null, + extractor9 :GdUnitValueExtractor = null) -> GdUnitArrayAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd new file mode 100644 index 0000000..1674d26 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitAssert.gd @@ -0,0 +1,35 @@ +## Base interface of all GdUnit asserts +class_name GdUnitAssert +extends RefCounted + + +## Verifies that the current value is null. +func is_null(): + return self + + +## Verifies that the current value is not null. +func is_not_null(): + return self + + +## Verifies that the current value is equal to expected one. +@warning_ignore("unused_parameter") +func is_equal(expected): + return self + + +## Verifies that the current value is not equal to expected one. +@warning_ignore("unused_parameter") +func is_not_equal(expected): + return self + + +func test_fail(): + return self + + +## Overrides the default failure message by given custom message. +@warning_ignore("unused_parameter") +func override_failure_message(message :String): + return self diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd new file mode 100644 index 0000000..a9b5bb1 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd @@ -0,0 +1,69 @@ +class_name GdUnitAwaiter +extends RefCounted + +const GdUnitAssertImpl = preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + + +# Waits for a specified signal in an interval of 50ms sent from the , and terminates with an error after the specified timeout has elapsed. +# source: the object from which the signal is emitted +# signal_name: signal name +# args: the expected signal arguments as an array +# timeout: the timeout in ms, default is set to 2000ms +func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: + # fail fast if the given source instance invalid + var assert_that := GdUnitAssertImpl.new(signal_name) + var line_number := GdUnitAssertions.get_line_number() + if not is_instance_valid(source): + assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await Engine.get_main_loop().process_frame + # fail fast if the given source instance invalid + if not is_instance_valid(source): + assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await await_idle_frame() + var awaiter := GdUnitSignalAwaiter.new(timeout_millis) + var value :Variant = await awaiter.on_signal(source, signal_name, args) + if awaiter.is_interrupted(): + var failure = "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis] + assert_that.report_error(failure, line_number) + return value + + +# Waits for a specified signal sent from the between idle frames and aborts with an error after the specified timeout has elapsed +# source: the object from which the signal is emitted +# signal_name: signal name +# args: the expected signal arguments as an array +# timeout: the timeout in ms, default is set to 2000ms +func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: + var line_number := GdUnitAssertions.get_line_number() + # fail fast if the given source instance invalid + if not is_instance_valid(source): + GdUnitAssertImpl.new(signal_name)\ + .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await await_idle_frame() + var awaiter := GdUnitSignalAwaiter.new(timeout_millis, true) + var value :Variant = await awaiter.on_signal(source, signal_name, args) + if awaiter.is_interrupted(): + var failure = "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis] + GdUnitAssertImpl.new(signal_name).report_error(failure, line_number) + return value + + +# Waits for for a given amount of milliseconds +# example: +# # waits for 100ms +# await GdUnitAwaiter.await_millis(myNode, 100).completed +# use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out +func await_millis(milliSec :int) -> void: + var timer :Timer = Timer.new() + timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id()) + Engine.get_main_loop().root.add_child(timer) + timer.add_to_group("GdUnitTimers") + timer.set_one_shot(true) + timer.start(milliSec / 1000.0) + await timer.timeout + timer.queue_free() + + +# Waits until the next idle frame +func await_idle_frame() -> void: + await Engine.get_main_loop().process_frame diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd b/addons/gdUnit4/src/GdUnitBoolAssert.gd new file mode 100644 index 0000000..7e62dbe --- /dev/null +++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd @@ -0,0 +1,41 @@ +## An Assertion Tool to verify boolean values +class_name GdUnitBoolAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +func is_null() -> GdUnitBoolAssert: + return self + + +## Verifies that the current value is not null. +func is_not_null() -> GdUnitBoolAssert: + return self + + +## Verifies that the current value is equal to the given one. +@warning_ignore("unused_parameter") +func is_equal(expected) -> GdUnitBoolAssert: + return self + + +## Verifies that the current value is not equal to the given one. +@warning_ignore("unused_parameter") +func is_not_equal(expected) -> GdUnitBoolAssert: + return self + + +## Verifies that the current value is true. +func is_true() -> GdUnitBoolAssert: + return self + + +## Verifies that the current value is false. +func is_false() -> GdUnitBoolAssert: + return self + + +## Overrides the default failure message by given custom message. +@warning_ignore("unused_parameter") +func override_failure_message(message :String) -> GdUnitBoolAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitConstants.gd b/addons/gdUnit4/src/GdUnitConstants.gd new file mode 100644 index 0000000..0445894 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitConstants.gd @@ -0,0 +1,6 @@ +class_name GdUnitConstants +extends RefCounted + +const NO_ARG :Variant = "<--null-->" + +const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures" diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd new file mode 100644 index 0000000..092a794 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd @@ -0,0 +1,105 @@ +## An Assertion Tool to verify dictionary +class_name GdUnitDictionaryAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +func is_null() -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current value is not null. +func is_not_null() -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary is equal to the given one, ignoring order. +@warning_ignore("unused_parameter") +func is_equal(expected) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary is not equal to the given one, ignoring order. +@warning_ignore("unused_parameter") +func is_not_equal(expected) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary is empty, it has a size of 0. +func is_empty() -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary is not empty, it has a size of minimum 1. +func is_not_empty() -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary is the same. [br] +## Compares the current by object reference equals +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_same(expected) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary is NOT the same. [br] +## Compares the current by object reference equals +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_not_same(expected) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary has a size of given value. +@warning_ignore("unused_parameter") +func has_size(expected: int) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary contains the given key(s).[br] +## The keys are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_keys] +@warning_ignore("unused_parameter") +func contains_keys(expected :Array) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary contains the given key and value.[br] +## The key and value are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_key_value] +@warning_ignore("unused_parameter") +func contains_key_value(key, value) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary not contains the given key(s).[br] +## This function is [b]deprecated[/b] you have to use [method not_contains_keys] instead +@warning_ignore("unused_parameter") +func contains_not_keys(expected :Array) -> GdUnitDictionaryAssert: + push_warning("Deprecated: 'contains_not_keys' is deprectated and will be removed soon, use `not_contains_keys` instead!") + return not_contains_keys(expected) + + +## Verifies that the current dictionary not contains the given key(s).[br] +## The keys are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same_keys] +@warning_ignore("unused_parameter") +func not_contains_keys(expected :Array) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary contains the given key(s).[br] +## The keys are compared by object reference, for deep parameter comparision use [method contains_keys] +@warning_ignore("unused_parameter") +func contains_same_keys(expected :Array) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary contains the given key and value.[br] +## The key and value are compared by object reference, for deep parameter comparision use [method contains_key_value] +@warning_ignore("unused_parameter") +func contains_same_key_value(key, value) -> GdUnitDictionaryAssert: + return self + + +## Verifies that the current dictionary not contains the given key(s). +## The keys are compared by object reference, for deep parameter comparision use [method not_contains_keys] +@warning_ignore("unused_parameter") +func not_contains_same_keys(expected :Array) -> GdUnitDictionaryAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd b/addons/gdUnit4/src/GdUnitFailureAssert.gd new file mode 100644 index 0000000..64ff965 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd @@ -0,0 +1,31 @@ +## An assertion tool to verify GDUnit asserts. +## This assert is for internal use only, to verify that failed asserts work as expected. +class_name GdUnitFailureAssert +extends GdUnitAssert + + +## Verifies if the executed assert was successful +func is_success() -> GdUnitFailureAssert: + return self + +## Verifies if the executed assert has failed +func is_failed() -> GdUnitFailureAssert: + return self + + +## Verifies the failure line is equal to expected one. +@warning_ignore("unused_parameter") +func has_line(expected :int) -> GdUnitFailureAssert: + return self + + +## Verifies the failure message is equal to expected one. +@warning_ignore("unused_parameter") +func has_message(expected: String) -> GdUnitFailureAssert: + return self + + +## Verifies that the failure message starts with the expected message. +@warning_ignore("unused_parameter") +func starts_with_message(expected: String) -> GdUnitFailureAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd b/addons/gdUnit4/src/GdUnitFileAssert.gd new file mode 100644 index 0000000..21bf21a --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFileAssert.gd @@ -0,0 +1,19 @@ +class_name GdUnitFileAssert +extends GdUnitAssert + + +func is_file() -> GdUnitFileAssert: + return self + + +func exists() -> GdUnitFileAssert: + return self + + +func is_script() -> GdUnitFileAssert: + return self + + +@warning_ignore("unused_parameter") +func contains_exactly(expected_rows :Array) -> GdUnitFileAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd b/addons/gdUnit4/src/GdUnitFloatAssert.gd new file mode 100644 index 0000000..8f24f56 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd @@ -0,0 +1,83 @@ +## An Assertion Tool to verify float values +class_name GdUnitFloatAssert +extends GdUnitAssert + + +## Verifies that the current value is equal to expected one. +@warning_ignore("unused_parameter") +func is_equal(expected :float) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is not equal to expected one. +@warning_ignore("unused_parameter") +func is_not_equal(expected :float) -> GdUnitFloatAssert: + return self + + +## Verifies that the current and expected value are approximately equal. +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_equal_approx(expected :float, approx :float) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is less than the given one. +@warning_ignore("unused_parameter") +func is_less(expected :float) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is less than or equal the given one. +@warning_ignore("unused_parameter") +func is_less_equal(expected :float) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is greater than the given one. +@warning_ignore("unused_parameter") +func is_greater(expected :float) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is greater than or equal the given one. +@warning_ignore("unused_parameter") +func is_greater_equal(expected :float) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is negative. +func is_negative() -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is not negative. +func is_not_negative() -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is equal to zero. +func is_zero() -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is not equal to zero. +func is_not_zero() -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is in the given set of values. +@warning_ignore("unused_parameter") +func is_in(expected :Array) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is not in the given set of values. +@warning_ignore("unused_parameter") +func is_not_in(expected :Array) -> GdUnitFloatAssert: + return self + + +## Verifies that the current value is between the given boundaries (inclusive). +@warning_ignore("unused_parameter") +func is_between(from :float, to :float) -> GdUnitFloatAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd b/addons/gdUnit4/src/GdUnitFuncAssert.gd new file mode 100644 index 0000000..77c4609 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd @@ -0,0 +1,56 @@ +## An Assertion Tool to verify function callback values +class_name GdUnitFuncAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +func is_null() -> GdUnitFuncAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies that the current value is not null. +func is_not_null() -> GdUnitFuncAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies that the current value is equal to the given one. +@warning_ignore("unused_parameter") +func is_equal(expected) -> GdUnitFuncAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies that the current value is not equal to the given one. +@warning_ignore("unused_parameter") +func is_not_equal(expected) -> GdUnitFuncAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies that the current value is true. +func is_true() -> GdUnitFuncAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies that the current value is false. +func is_false() -> GdUnitFuncAssert: + await Engine.get_main_loop().process_frame + return self + + +## Overrides the default failure message by given custom message. +@warning_ignore("unused_parameter") +func override_failure_message(message :String) -> GdUnitFuncAssert: + return self + + +## Sets the timeout in ms to wait the function returnd the expected value, if the time over a failure is emitted.[br] +## e.g.[br] +## do wait until 5s the function `is_state` is returns 10 [br] +## [code]assert_func(instance, "is_state").wait_until(5000).is_equal(10)[/code] +@warning_ignore("unused_parameter") +func wait_until(timeout :int) -> GdUnitFuncAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd new file mode 100644 index 0000000..d689625 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd @@ -0,0 +1,46 @@ +## An assertion tool to verify for Godot runtime errors like assert() and push notifications like push_error(). +class_name GdUnitGodotErrorAssert +extends GdUnitAssert + + +## Verifies if the executed code runs without any runtime errors +## Usage: +## [codeblock] +## await assert_error().is_success() +## [/codeblock] +func is_success() -> GdUnitGodotErrorAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies if the executed code runs into a runtime error +## Usage: +## [codeblock] +## await assert_error().is_runtime_error() +## [/codeblock] +@warning_ignore("unused_parameter") +func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies if the executed code has a push_warning() used +## Usage: +## [codeblock] +## await assert_error().is_push_warning() +## [/codeblock] +@warning_ignore("unused_parameter") +func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies if the executed code has a push_error() used +## Usage: +## [codeblock] +## await assert_error().is_push_error() +## [/codeblock] +@warning_ignore("unused_parameter") +func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert: + await Engine.get_main_loop().process_frame + return self diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd b/addons/gdUnit4/src/GdUnitIntAssert.gd new file mode 100644 index 0000000..584788f --- /dev/null +++ b/addons/gdUnit4/src/GdUnitIntAssert.gd @@ -0,0 +1,87 @@ +## An Assertion Tool to verify integer values +class_name GdUnitIntAssert +extends GdUnitAssert + + +## Verifies that the current value is equal to expected one. +@warning_ignore("unused_parameter") +func is_equal(expected :int) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is not equal to expected one. +@warning_ignore("unused_parameter") +func is_not_equal(expected :int) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is less than the given one. +@warning_ignore("unused_parameter") +func is_less(expected :int) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is less than or equal the given one. +@warning_ignore("unused_parameter") +func is_less_equal(expected :int) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is greater than the given one. +@warning_ignore("unused_parameter") +func is_greater(expected :int) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is greater than or equal the given one. +@warning_ignore("unused_parameter") +func is_greater_equal(expected :int) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is even. +func is_even() -> GdUnitIntAssert: + return self + + +## Verifies that the current value is odd. +func is_odd() -> GdUnitIntAssert: + return self + + +## Verifies that the current value is negative. +func is_negative() -> GdUnitIntAssert: + return self + + +## Verifies that the current value is not negative. +func is_not_negative() -> GdUnitIntAssert: + return self + + +## Verifies that the current value is equal to zero. +func is_zero() -> GdUnitIntAssert: + return self + + +## Verifies that the current value is not equal to zero. +func is_not_zero() -> GdUnitIntAssert: + return self + + +## Verifies that the current value is in the given set of values. +@warning_ignore("unused_parameter") +func is_in(expected :Array) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is not in the given set of values. +@warning_ignore("unused_parameter") +func is_not_in(expected :Array) -> GdUnitIntAssert: + return self + + +## Verifies that the current value is between the given boundaries (inclusive). +@warning_ignore("unused_parameter") +func is_between(from :int, to :int) -> GdUnitIntAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd b/addons/gdUnit4/src/GdUnitObjectAssert.gd new file mode 100644 index 0000000..d73bfd5 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd @@ -0,0 +1,49 @@ +## An Assertion Tool to verify Object values +class_name GdUnitObjectAssert +extends GdUnitAssert + + +## Verifies that the current value is equal to expected one. +@warning_ignore("unused_parameter") +func is_equal(expected) -> GdUnitObjectAssert: + return self + + +## Verifies that the current value is not equal to expected one. +@warning_ignore("unused_parameter") +func is_not_equal(expected) -> GdUnitObjectAssert: + return self + + +## Verifies that the current value is null. +func is_null() -> GdUnitObjectAssert: + return self + + +## Verifies that the current value is not null. +func is_not_null() -> GdUnitObjectAssert: + return self + + +## Verifies that the current value is the same as the given one. +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_same(expected) -> GdUnitObjectAssert: + return self + + +## Verifies that the current value is not the same as the given one. +@warning_ignore("unused_parameter") +func is_not_same(expected) -> GdUnitObjectAssert: + return self + + +## Verifies that the current value is an instance of the given type. +@warning_ignore("unused_parameter") +func is_instanceof(expected :Object) -> GdUnitObjectAssert: + return self + + +## Verifies that the current value is not an instance of the given type. +@warning_ignore("unused_parameter") +func is_not_instanceof(expected) -> GdUnitObjectAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd b/addons/gdUnit4/src/GdUnitResultAssert.gd new file mode 100644 index 0000000..4123d66 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitResultAssert.gd @@ -0,0 +1,45 @@ +## An Assertion Tool to verify Results +class_name GdUnitResultAssert +extends GdUnitAssert + + +## Verifies that the current value is null. +func is_null() -> GdUnitResultAssert: + return self + + +## Verifies that the current value is not null. +func is_not_null() -> GdUnitResultAssert: + return self + + +## Verifies that the result is ends up with empty +func is_empty() -> GdUnitResultAssert: + return self + + +## Verifies that the result is ends up with success +func is_success() -> GdUnitResultAssert: + return self + + +## Verifies that the result is ends up with warning +func is_warning() -> GdUnitResultAssert: + return self + + +## Verifies that the result is ends up with error +func is_error() -> GdUnitResultAssert: + return self + + +## Verifies that the result contains the given message +@warning_ignore("unused_parameter") +func contains_message(expected :String) -> GdUnitResultAssert: + return self + + +## Verifies that the result contains the given value +@warning_ignore("unused_parameter") +func is_value(expected) -> GdUnitResultAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd b/addons/gdUnit4/src/GdUnitSceneRunner.gd new file mode 100644 index 0000000..07a6a9f --- /dev/null +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd @@ -0,0 +1,225 @@ +## The scene runner for GdUnit to simmulate scene interactions +class_name GdUnitSceneRunner +extends RefCounted + +const NO_ARG = GdUnitConstants.NO_ARG + + +## Sets the mouse cursor to given position relative to the viewport. +@warning_ignore("unused_parameter") +func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner: + return self + + +## Gets the current mouse position of the current viewport +func get_mouse_position() -> Vector2: + return Vector2.ZERO + + +## Gets the current global mouse position of the current window +func get_global_mouse_position() -> Vector2: + return Vector2.ZERO + + +## Simulates that a key has been pressed.[br] +## [member key_code] : the key code e.g. [constant KEY_ENTER][br] +## [member shift_pressed] : false by default set to true if simmulate shift is press[br] +## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] +@warning_ignore("unused_parameter") +func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + return self + + +## Simulates that a key is pressed.[br] +## [member key_code] : the key code e.g. [constant KEY_ENTER][br] +## [member shift_pressed] : false by default set to true if simmulate shift is press[br] +## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] +@warning_ignore("unused_parameter") +func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + return self + + +## Simulates that a key has been released.[br] +## [member key_code] : the key code e.g. [constant KEY_ENTER][br] +## [member shift_pressed] : false by default set to true if simmulate shift is press[br] +## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] +@warning_ignore("unused_parameter") +func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + return self + + +## Simulates a mouse moved to final position.[br] +## [member pos] : The final mouse position +@warning_ignore("unused_parameter") +func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner: + return self + + +## Simulates a mouse move to the relative coordinates (offset).[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br] +## [br] +## [member relative] : The relative position, indicating the mouse position offset.[br] +## [member time] : The time to move the mouse by the relative position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_move_mouse(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_mouse_move_relative(Vector2(100,100)) +## [/codeblock] +@warning_ignore("unused_parameter") +func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + await Engine.get_main_loop().process_frame + return self + + +## Simulates a mouse move to the absolute coordinates.[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br] +## [br] +## [member position] : The final position of the mouse.[br] +## [member time] : The time to move the mouse to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_move_mouse(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_mouse_move_absolute(Vector2(100,100)) +## [/codeblock] +@warning_ignore("unused_parameter") +func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + await Engine.get_main_loop().process_frame + return self + + +## Simulates a mouse button pressed.[br] +## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +@warning_ignore("unused_parameter") +func simulate_mouse_button_pressed(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: + return self + + +## Simulates a mouse button press (holding)[br] +## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +@warning_ignore("unused_parameter") +func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: + return self + + +## Simulates a mouse button released.[br] +## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +@warning_ignore("unused_parameter") +func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner: + return self + + +## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br] +## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life, +## whilst a value of 0.5 means the game moves at half the regular speed. +@warning_ignore("unused_parameter") +func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner: + return self + + +## Simulates scene processing for a certain number of frames.[br] +## [member frames] : amount of frames to process[br] +## [member delta_milli] : the time delta between a frame in milliseconds +@warning_ignore("unused_parameter") +func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: + await Engine.get_main_loop().process_frame + return self + + +## Simulates scene processing until the given signal is emitted by the scene.[br] +## [member signal_name] : the signal to stop the simulation[br] +## [member args] : optional signal arguments to be matched for stop[br] +@warning_ignore("unused_parameter") +func simulate_until_signal(signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner: + await Engine.get_main_loop().process_frame + return self + + +## Simulates scene processing until the given signal is emitted by the given object.[br] +## [member source] : the object that should emit the signal[br] +## [member signal_name] : the signal to stop the simulation[br] +## [member args] : optional signal arguments to be matched for stop +@warning_ignore("unused_parameter") +func simulate_until_object_signal(source :Object, signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner: + await Engine.get_main_loop().process_frame + return self + + +## Waits for the function return value until specified timeout or fails.[br] +## [member args] : optional function arguments +@warning_ignore("unused_parameter") +func await_func(func_name :String, args := []) -> GdUnitFuncAssert: + return null + + +## Waits for the function return value of specified source until specified timeout or fails.[br] +## [member source : the object where implements the function[br] +## [member args] : optional function arguments +@warning_ignore("unused_parameter") +func await_func_on(source :Object, func_name :String, args := []) -> GdUnitFuncAssert: + return null + + +## Waits for given signal is emited by the scene until a specified timeout to fail.[br] +## [member signal_name] : signal name[br] +## [member args] : the expected signal arguments as an array[br] +## [member timeout] : the timeout in ms, default is set to 2000ms +@warning_ignore("unused_parameter") +func await_signal(signal_name :String, args := [], timeout := 2000 ): + await Engine.get_main_loop().process_frame + pass + + +## Waits for given signal is emited by the until a specified timeout to fail.[br] +## [member source] : the object from which the signal is emitted[br] +## [member signal_name] : signal name[br] +## [member args] : the expected signal arguments as an array[br] +## [member timeout] : the timeout in ms, default is set to 2000ms +@warning_ignore("unused_parameter") +func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ): + pass + + +## maximizes the window to bring the scene visible +func maximize_view() -> GdUnitSceneRunner: + return self + + +## Return the current value of the property with the name .[br] +## [member name] : name of property[br] +## [member return] : the value of the property +@warning_ignore("unused_parameter") +func get_property(name :String) -> Variant: + return null + +## Set the value of the property with the name .[br] +## [member name] : name of property[br] +## [member value] : value of property[br] +## [member return] : true|false depending on valid property name. +@warning_ignore("unused_parameter") +func set_property(name :String, value :Variant) -> bool: + return false + + +## executes the function specified by in the scene and returns the result.[br] +## [member name] : the name of the function to execute[br] +## [member args] : optional function arguments[br] +## [member return] : the function result +@warning_ignore("unused_parameter") +func invoke(name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG): + pass + + +## Searches for the specified node with the name in the current scene and returns it, otherwise null.[br] +## [member name] : the name of the node to find[br] +## [member recursive] : enables/disables seraching recursive[br] +## [member return] : the node if find otherwise null +@warning_ignore("unused_parameter") +func find_child(name :String, recursive :bool = true, owned :bool = false) -> Node: + return null + + +## Access to current running scene +func scene() -> Node: + return null diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd b/addons/gdUnit4/src/GdUnitSignalAssert.gd new file mode 100644 index 0000000..8150df2 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd @@ -0,0 +1,38 @@ +## An Assertion Tool to verify for emitted signals until a waiting time +class_name GdUnitSignalAssert +extends GdUnitAssert + + +## Verifies that given signal is emitted until waiting time +@warning_ignore("unused_parameter") +func is_emitted(name :String, args := []) -> GdUnitSignalAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies that given signal is NOT emitted until waiting time +@warning_ignore("unused_parameter") +func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: + await Engine.get_main_loop().process_frame + return self + + +## Verifies the signal exists checked the emitter +@warning_ignore("unused_parameter") +func is_signal_exists(name :String) -> GdUnitSignalAssert: + return self + + +## Overrides the default failure message by given custom message. +@warning_ignore("unused_parameter") +func override_failure_message(message :String) -> GdUnitSignalAssert: + return self + + +## Sets the assert signal timeout in ms, if the time over a failure is reported.[br] +## e.g.[br] +## do wait until 5s the instance has emitted the signal `signal_a`[br] +## [code]assert_signal(instance).wait_until(5000).is_emitted("signal_a")[/code] +@warning_ignore("unused_parameter") +func wait_until(timeout :int) -> GdUnitSignalAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd b/addons/gdUnit4/src/GdUnitStringAssert.gd new file mode 100644 index 0000000..91e5bf0 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitStringAssert.gd @@ -0,0 +1,79 @@ +## An Assertion Tool to verify String values +class_name GdUnitStringAssert +extends GdUnitAssert + + +## Verifies that the current String is equal to the given one. +@warning_ignore("unused_parameter") +func is_equal(expected) -> GdUnitStringAssert: + return self + + +## Verifies that the current String is equal to the given one, ignoring case considerations. +@warning_ignore("unused_parameter") +func is_equal_ignoring_case(expected) -> GdUnitStringAssert: + return self + + +## Verifies that the current String is not equal to the given one. +@warning_ignore("unused_parameter") +func is_not_equal(expected) -> GdUnitStringAssert: + return self + + +## Verifies that the current String is not equal to the given one, ignoring case considerations. +@warning_ignore("unused_parameter") +func is_not_equal_ignoring_case(expected) -> GdUnitStringAssert: + return self + + +## Verifies that the current String is empty, it has a length of 0. +func is_empty() -> GdUnitStringAssert: + return self + + +## Verifies that the current String is not empty, it has a length of minimum 1. +func is_not_empty() -> GdUnitStringAssert: + return self + + +## Verifies that the current String contains the given String. +@warning_ignore("unused_parameter") +func contains(expected: String) -> GdUnitStringAssert: + return self + + +## Verifies that the current String does not contain the given String. +@warning_ignore("unused_parameter") +func not_contains(expected: String) -> GdUnitStringAssert: + return self + + +## Verifies that the current String does not contain the given String, ignoring case considerations. +@warning_ignore("unused_parameter") +func contains_ignoring_case(expected: String) -> GdUnitStringAssert: + return self + + +## Verifies that the current String does not contain the given String, ignoring case considerations. +@warning_ignore("unused_parameter") +func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert: + return self + + +## Verifies that the current String starts with the given prefix. +@warning_ignore("unused_parameter") +func starts_with(expected: String) -> GdUnitStringAssert: + return self + + +## Verifies that the current String ends with the given suffix. +@warning_ignore("unused_parameter") +func ends_with(expected: String) -> GdUnitStringAssert: + return self + + +## Verifies that the current String has the expected length by used comparator. +@warning_ignore("unused_parameter") +func has_length(lenght: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert: + return self diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd b/addons/gdUnit4/src/GdUnitTestSuite.gd new file mode 100644 index 0000000..5c1f898 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd @@ -0,0 +1,620 @@ +## The main class for all GdUnit test suites[br] +## This class is the main class to implement your unit tests[br] +## You have to extend and implement your test cases as described[br] +## e.g MyTests.gd [br] +## [codeblock] +## extends GdUnitTestSuite +## # testcase +## func test_case_a(): +## assert_that("value").is_equal("value") +## [/codeblock] +## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/ + +@icon("res://addons/gdUnit4/src/ui/assets/TestSuite.svg") +class_name GdUnitTestSuite +extends Node + +const NO_ARG :Variant = GdUnitConstants.NO_ARG + +### internal runtime variables that must not be overwritten!!! +@warning_ignore("unused_private_class_variable") +var __is_skipped := false +@warning_ignore("unused_private_class_variable") +var __skip_reason :String = "Unknow." +var __active_test_case :String +var __awaiter := __gdunit_awaiter() +# holds the actual execution context +var __execution_context :RefCounted + + +### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" +### in order to noticeably reduce the loading time of the test suite. +# We go this hard way to increase the loading performance to avoid reparsing all the used scripts +# for more detailed info -> https://github.com/godotengine/godot/issues/67400 +func __lazy_load(script_path :String) -> GDScript: + return GdUnitAssertions.__lazy_load(script_path) + + +func __gdunit_assert() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + + +func __gdunit_tools() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func __gdunit_file_access() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/core/GdUnitFileAccess.gd") + + +func __gdunit_awaiter() -> Object: + return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new() + + +func __gdunit_argument_matchers() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd") + + +func __gdunit_object_interactions() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/core/GdUnitObjectInteractions.gd") + + +## This function is called before a test suite starts[br] +## You can overwrite to prepare test data or initalizize necessary variables +func before() -> void: + pass + + +## This function is called at least when a test suite is finished[br] +## You can overwrite to cleanup data created during test running +func after() -> void: + pass + + +## This function is called before a test case starts[br] +## You can overwrite to prepare test case specific data +func before_test() -> void: + pass + + +## This function is called after the test case is finished[br] +## You can overwrite to cleanup your test case specific data +func after_test() -> void: + pass + + +func is_failure(_expected_failure :String = NO_ARG) -> bool: + return Engine.get_meta("GD_TEST_FAILURE") if Engine.has_meta("GD_TEST_FAILURE") else false + + +func set_active_test_case(test_case :String) -> void: + __active_test_case = test_case + + +# === Tools ==================================================================== +# Mapps Godot error number to a readable error message. See at ERROR +# https://docs.godotengine.org/de/stable/classes/class_@globalscope.html#enum-globalscope-error +func error_as_string(error_number :int) -> String: + return error_string(error_number) + + +## A litle helper to auto freeing your created objects after test execution +func auto_free(obj :Variant) -> Variant: + if __execution_context != null: + return __execution_context.register_auto_free(obj) + else: + if is_instance_valid(obj): + obj.queue_free() + return obj + + +@warning_ignore("native_method_override") +func add_child(node :Node, force_readable_name := false, internal := Node.INTERNAL_MODE_DISABLED) -> void: + super.add_child(node, force_readable_name, internal) + if __execution_context != null: + __execution_context.orphan_monitor_start() + + +## Discard the error message triggered by a timeout (interruption).[br] +## By default, an interrupted test is reported as an error.[br] +## This function allows you to change the message to Success when an interrupted error is reported. +func discard_error_interupted_by_timeout() -> void: + __gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case) + + +## Creates a new directory under the temporary directory *user://tmp*[br] +## Useful for storing data during test execution. [br] +## The directory is automatically deleted after test suite execution +func create_temp_dir(relative_path :String) -> String: + return __gdunit_file_access().create_temp_dir(relative_path) + + +## Deletes the temporary base directory[br] +## Is called automatically after each execution of the test suite +func clean_temp_dir() -> void: + __gdunit_file_access().clear_tmp() + + +## Creates a new file under the temporary directory *user://tmp* + [br] +## with given name and given file (default = File.WRITE)[br] +## If success the returned File is automatically closed after the execution of the test suite +func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: + return __gdunit_file_access().create_temp_file(relative_path, file_name, mode) + + +## Reads a resource by given path into a PackedStringArray. +func resource_as_array(resource_path :String) -> PackedStringArray: + return __gdunit_file_access().resource_as_array(resource_path) + + +## Reads a resource by given path and returned the content as String. +func resource_as_string(resource_path :String) -> String: + return __gdunit_file_access().resource_as_string(resource_path) + + +## Reads a resource by given path and return Variand translated by str_to_var +func resource_as_var(resource_path :String) -> Variant: + return str_to_var(__gdunit_file_access().resource_as_string(resource_path)) + + +## clears the debuger error list[br] +## PROTOTYPE!!!! Don't use it for now +func clear_push_errors() -> void: + __gdunit_tools().clear_push_errors() + + +## Waits for given signal is emited by the until a specified timeout to fail[br] +## source: the object from which the signal is emitted[br] +## signal_name: signal name[br] +## args: the expected signal arguments as an array[br] +## timeout: the timeout in ms, default is set to 2000ms +func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout :int = 2000) -> Variant: + return await __awaiter.await_signal_on(source, signal_name, args, timeout) + + +## Waits until the next idle frame +func await_idle_frame() -> void: + await __awaiter.await_idle_frame() + + +## Waits for for a given amount of milliseconds[br] +## example:[br] +## [codeblock] +## # waits for 100ms +## await await_millis(myNode, 100).completed +## [/codeblock][br] +## use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out +func await_millis(timeout :int) -> void: + await __awaiter.await_millis(timeout) + + +## Creates a new scene runner to allow simulate interactions checked a scene.[br] +## The runner will manage the scene instance and release after the runner is released[br] +## example:[br] +## [codeblock] +## # creates a runner by using a instanciated scene +## var scene = load("res://foo/my_scne.tscn").instantiate() +## var runner := scene_runner(scene) +## +## # or simply creates a runner by using the scene resource path +## var runner := scene_runner("res://foo/my_scne.tscn") +## [/codeblock] +func scene_runner(scene :Variant, verbose := false) -> GdUnitSceneRunner: + return auto_free(__lazy_load("res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd").new(scene, verbose)) + + +# === Mocking & Spy =========================================================== + +## do return a default value for primitive types or null +const RETURN_DEFAULTS = GdUnitMock.RETURN_DEFAULTS +## do call the real implementation +const CALL_REAL_FUNC = GdUnitMock.CALL_REAL_FUNC +## do return a default value for primitive types and a fully mocked value for Object types +## builds full deep mocked object +const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB + + +## Creates a mock for given class name +func mock(clazz :Variant, mock_mode := RETURN_DEFAULTS) -> Object: + return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode) + + +## Creates a spy checked given object instance +func spy(instance :Variant) -> Object: + return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance) + + +## Configures a return value for the specified function and used arguments.[br] +## [b]Example: +## [codeblock] +## # overrides the return value of myMock.is_selected() to false +## do_return(false).on(myMock).is_selected() +## [/codeblock] +func do_return(value :Variant) -> GdUnitMock: + return GdUnitMock.new(value) + + +## Verifies certain behavior happened at least once or exact number of times +func verify(obj :Variant, times := 1) -> Variant: + return __gdunit_object_interactions().verify(obj, times) + + +## Verifies no interactions is happen checked this mock or spy +func verify_no_interactions(obj :Variant) -> GdUnitAssert: + return __gdunit_object_interactions().verify_no_interactions(obj) + + +## Verifies the given mock or spy has any unverified interaction. +func verify_no_more_interactions(obj :Variant) -> GdUnitAssert: + return __gdunit_object_interactions().verify_no_more_interactions(obj) + + +## Resets the saved function call counters checked a mock or spy +func reset(obj :Variant) -> void: + __gdunit_object_interactions().reset(obj) + + +## Starts monitoring the specified source to collect all transmitted signals.[br] +## The collected signals can then be checked with 'assert_signal'.[br] +## By default, the specified source is automatically released when the test ends. +## You can control this behavior by setting auto_free to false if you do not want the source to be automatically freed.[br] +## Usage: +## [codeblock] +## var emitter := monitor_signals(MyEmitter.new()) +## # call the function to send the signal +## emitter.do_it() +## # verify the signial is emitted +## await assert_signal(emitter).is_emitted('my_signal') +## [/codeblock] +func monitor_signals(source :Object, _auto_free := true) -> Object: + __lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\ + .get_current_context()\ + .get_signal_collector()\ + .register_emitter(source) + return auto_free(source) if _auto_free else source + + +# === Argument matchers ======================================================== +## Argument matcher to match any argument +func any() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().any() + + +## Argument matcher to match any boolean value +func any_bool() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_BOOL) + + +## Argument matcher to match any integer value +func any_int() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_INT) + + +## Argument matcher to match any float value +func any_float() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_FLOAT) + + +## Argument matcher to match any string value +func any_string() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_STRING) + + +## Argument matcher to match any Color value +func any_color() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_COLOR) + + +## Argument matcher to match any Vector typed value +func any_vector() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_types([ + TYPE_VECTOR2, + TYPE_VECTOR2I, + TYPE_VECTOR3, + TYPE_VECTOR3I, + TYPE_VECTOR4, + TYPE_VECTOR4I, + ]) + + +## Argument matcher to match any Vector2 value +func any_vector2() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_VECTOR2) + + +## Argument matcher to match any Vector2i value +func any_vector2i() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I) + + +## Argument matcher to match any Vector3 value +func any_vector3() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_VECTOR3) + + +## Argument matcher to match any Vector3i value +func any_vector3i() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I) + + +## Argument matcher to match any Vector4 value +func any_vector4() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_VECTOR4) + + +## Argument matcher to match any Vector3i value +func any_vector4i() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I) + + +## Argument matcher to match any Rect2 value +func any_rect2() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_RECT2) + + +## Argument matcher to match any Plane value +func any_plane() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PLANE) + + +## Argument matcher to match any Quaternion value +func any_quat() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_QUATERNION) + + +## Argument matcher to match any AABB value +func any_aabb() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_AABB) + + +## Argument matcher to match any Basis value +func any_basis() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_BASIS) + + +## Argument matcher to match any Transform2D value +func any_transform_2d() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D) + + +## Argument matcher to match any Transform3D value +func any_transform_3d() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D) + + +## Argument matcher to match any NodePath value +func any_node_path() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH) + + +## Argument matcher to match any RID value +func any_rid() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_RID) + + +## Argument matcher to match any Object value +func any_object() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_OBJECT) + + +## Argument matcher to match any Dictionary value +func any_dictionary() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY) + + +## Argument matcher to match any Array value +func any_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_ARRAY) + + +## Argument matcher to match any PackedByteArray value +func any_packed_byte_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY) + + +## Argument matcher to match any PackedInt32Array value +func any_packed_int32_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY) + + +## Argument matcher to match any PackedInt64Array value +func any_packed_int64_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT64_ARRAY) + + +## Argument matcher to match any PackedFloat32Array value +func any_packed_float32_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY) + + +## Argument matcher to match any PackedFloat64Array value +func any_packed_float64_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT64_ARRAY) + + +## Argument matcher to match any PackedStringArray value +func any_packed_string_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY) + + +## Argument matcher to match any PackedVector2Array value +func any_packed_vector2_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY) + + +## Argument matcher to match any PackedVector3Array value +func any_packed_vector3_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY) + + +## Argument matcher to match any PackedColorArray value +func any_packed_color_array() -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY) + + +## Argument matcher to match any instance of given class +func any_class(clazz :Object) -> GdUnitArgumentMatcher: + return __gdunit_argument_matchers().any_class(clazz) + + +# === value extract utils ====================================================== +## Builds an extractor by given function name and optional arguments +func extr(func_name :String, args := Array()) -> GdUnitValueExtractor: + return __lazy_load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd").new(func_name, args) + + +## Constructs a tuple by given arguments +func tuple(arg0 :Variant, + arg1 :Variant=NO_ARG, + arg2 :Variant=NO_ARG, + arg3 :Variant=NO_ARG, + arg4 :Variant=NO_ARG, + arg5 :Variant=NO_ARG, + arg6 :Variant=NO_ARG, + arg7 :Variant=NO_ARG, + arg8 :Variant=NO_ARG, + arg9 :Variant=NO_ARG) -> GdUnitTuple: + return GdUnitTuple.new(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) + + +# === Asserts ================================================================== + +## The common assertion tool to verify values. +## It checks the given value by type to fit to the best assert +func assert_that(current :Variant) -> GdUnitAssert: + match typeof(current): + TYPE_BOOL: + return assert_bool(current) + TYPE_INT: + return assert_int(current) + TYPE_FLOAT: + return assert_float(current) + TYPE_STRING: + return assert_str(current) + TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I: + return assert_vector(current) + TYPE_DICTIONARY: + return assert_dict(current) + TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\ + TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY: + return assert_array(current) + TYPE_OBJECT, TYPE_NIL: + return assert_object(current) + _: + return __gdunit_assert().new(current) + + +## An assertion tool to verify boolean values. +func assert_bool(current :Variant) -> GdUnitBoolAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd").new(current) + + +## An assertion tool to verify String values. +func assert_str(current :Variant) -> GdUnitStringAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd").new(current) + + +## An assertion tool to verify integer values. +func assert_int(current :Variant) -> GdUnitIntAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd").new(current) + + +## An assertion tool to verify float values. +func assert_float(current :Variant) -> GdUnitFloatAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd").new(current) + + +## An assertion tool to verify Vector values.[br] +## This assertion supports all vector types.[br] +## Usage: +## [codeblock] +## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001)) +## [/codeblock] +func assert_vector(current :Variant) -> GdUnitVectorAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current) + + +## An assertion tool to verify arrays. +func assert_array(current :Variant) -> GdUnitArrayAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current) + + +## An assertion tool to verify dictionaries. +func assert_dict(current :Variant) -> GdUnitDictionaryAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd").new(current) + + +## An assertion tool to verify FileAccess. +func assert_file(current :Variant) -> GdUnitFileAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd").new(current) + + +## An assertion tool to verify Objects. +func assert_object(current :Variant) -> GdUnitObjectAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd").new(current) + + +func assert_result(current :Variant) -> GdUnitResultAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd").new(current) + + +## An assertion tool that waits until a certain time for an expected function return value +func assert_func(instance :Object, func_name :String, args := Array()) -> GdUnitFuncAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args) + + +## An Assertion Tool to verify for emitted signals until a certain time. +func assert_signal(instance :Object) -> GdUnitSignalAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance) + + +## An assertion tool to test for failing assertions.[br] +## This assert is only designed for internal use to verify failing asserts working as expected.[br] +## Usage: +## [codeblock] +## assert_failure(func(): assert_bool(true).is_not_equal(true)) \ +## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") +## [/codeblock] +func assert_failure(assertion :Callable) -> GdUnitFailureAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion) + + +## An assertion tool to test for failing assertions.[br] +## This assert is only designed for internal use to verify failing asserts working as expected.[br] +## Usage: +## [codeblock] +## await assert_failure_await(func(): assert_bool(true).is_not_equal(true)) \ +## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") +## [/codeblock] +func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert: + return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion) + + +## An assertion tool to verify for Godot errors.[br] +## You can use to verify for certain Godot erros like failing assertions, push_error, push_warn.[br] +## Usage: +## [codeblock] +## # tests no error was occured during execution the code +## await assert_error(func (): return 0 )\ +## .is_success() +## +## # tests an push_error('test error') was occured during execution the code +## await assert_error(func (): push_error('test error') )\ +## .is_push_error('test error') +## [/codeblock] +func assert_error(current :Callable) -> GdUnitGodotErrorAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current) + + +func assert_not_yet_implemented() -> void: + __gdunit_assert().new(null).test_fail() + + +func fail(message :String) -> void: + __gdunit_assert().new(null).report_error(message) + + +# --- internal stuff do not override!!! +func ResourcePath() -> String: + return get_script().resource_path diff --git a/addons/gdUnit4/src/GdUnitTuple.gd b/addons/gdUnit4/src/GdUnitTuple.gd new file mode 100644 index 0000000..6c91002 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitTuple.gd @@ -0,0 +1,28 @@ +## A tuple implementation to hold two or many values +class_name GdUnitTuple +extends RefCounted + +const NO_ARG :Variant = GdUnitConstants.NO_ARG + +var __values :Array = Array() + + +func _init(arg0:Variant, + arg1 :Variant=NO_ARG, + arg2 :Variant=NO_ARG, + arg3 :Variant=NO_ARG, + arg4 :Variant=NO_ARG, + arg5 :Variant=NO_ARG, + arg6 :Variant=NO_ARG, + arg7 :Variant=NO_ARG, + arg8 :Variant=NO_ARG, + arg9 :Variant=NO_ARG) -> void: + __values = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + + +func values() -> Array: + return __values + + +func _to_string() -> String: + return "tuple(%s)" % str(__values) diff --git a/addons/gdUnit4/src/GdUnitValueExtractor.gd b/addons/gdUnit4/src/GdUnitValueExtractor.gd new file mode 100644 index 0000000..be702cf --- /dev/null +++ b/addons/gdUnit4/src/GdUnitValueExtractor.gd @@ -0,0 +1,9 @@ +## This is the base interface for value extraction +class_name GdUnitValueExtractor +extends RefCounted + + +## Extracts a value by given implementation +func extract_value(value): + push_error("Uninplemented func 'extract_value'") + return value diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd b/addons/gdUnit4/src/GdUnitVectorAssert.gd new file mode 100644 index 0000000..915fd3b --- /dev/null +++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd @@ -0,0 +1,57 @@ +## An Assertion Tool to verify Vector values +class_name GdUnitVectorAssert +extends GdUnitAssert + + +## Verifies that the current value is equal to expected one. +@warning_ignore("unused_parameter") +func is_equal(expected :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current value is not equal to expected one. +@warning_ignore("unused_parameter") +func is_not_equal(expected :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current and expected value are approximately equal. +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_equal_approx(expected :Variant, approx :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current value is less than the given one. +@warning_ignore("unused_parameter") +func is_less(expected :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current value is less than or equal the given one. +@warning_ignore("unused_parameter") +func is_less_equal(expected :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current value is greater than the given one. +@warning_ignore("unused_parameter") +func is_greater(expected :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current value is greater than or equal the given one. +@warning_ignore("unused_parameter") +func is_greater_equal(expected :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current value is between the given boundaries (inclusive). +@warning_ignore("unused_parameter") +func is_between(from :Variant, to :Variant) -> GdUnitVectorAssert: + return self + + +## Verifies that the current value is not between the given boundaries (inclusive). +@warning_ignore("unused_parameter") +func is_not_between(from :Variant, to :Variant) -> GdUnitVectorAssert: + return self diff --git a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd new file mode 100644 index 0000000..27242e8 --- /dev/null +++ b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd @@ -0,0 +1,25 @@ +# a value provider unsing a callback to get `next` value from a certain function +class_name CallBackValueProvider +extends ValueProvider + +var _cb :Callable +var _args :Array + + +func _init(instance :Object, func_name :String, args :Array = Array(), force_error := true): + _cb = Callable(instance, func_name); + _args = args + if force_error and not _cb.is_valid(): + push_error("Can't find function '%s' checked instance %s" % [func_name, instance]) + + +func get_value() -> Variant: + if not _cb.is_valid(): + return null + if _args.is_empty(): + return await _cb.call() + return await _cb.callv(_args) + + +func dispose(): + _cb = Callable() diff --git a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd new file mode 100644 index 0000000..036a482 --- /dev/null +++ b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd @@ -0,0 +1,12 @@ +# default value provider, simple returns the initial value +class_name DefaultValueProvider +extends ValueProvider + +var _value + +func _init(value): + _value = value + + +func get_value(): + return _value diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd new file mode 100644 index 0000000..3411f42 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd @@ -0,0 +1,608 @@ +class_name GdAssertMessages +extends Resource + +const WARN_COLOR = "#EFF883" +const ERROR_COLOR = "#CD5C5C" +const VALUE_COLOR = "#1E90FF" +const SUB_COLOR := Color(1, 0, 0, .3) +const ADD_COLOR := Color(0, 1, 0, .3) + + +static func format_dict(value :Dictionary) -> String: + if value.is_empty(): + return "{ }" + var as_rows := var_to_str(value).split("\n") + for index in range( 1, as_rows.size()-1): + as_rows[index] = " " + as_rows[index] + as_rows[-1] = " " + as_rows[-1] + return "\n".join(as_rows) + + +# improved version of InputEvent as text +static func input_event_as_text(event :InputEvent) -> String: + var text := "" + if event is InputEventKey: + text += "InputEventKey : key='%s', pressed=%s, keycode=%d, physical_keycode=%s" % [ + event.as_text(), event.pressed, event.keycode, event.physical_keycode] + else: + text += event.as_text() + if event is InputEventMouse: + text += ", global_position %s" % event.global_position + if event is InputEventWithModifiers: + text += ", shift=%s, alt=%s, control=%s, meta=%s, command=%s" % [ + event.shift_pressed, event.alt_pressed, event.ctrl_pressed, event.meta_pressed, event.command_or_control_autoremap] + return text + + +static func _colored_string_div(characters :String) -> String: + return colored_array_div(characters.to_ascii_buffer()) + + +static func colored_array_div(characters :PackedByteArray) -> String: + if characters.is_empty(): + return "" + var result = PackedByteArray() + var index = 0 + var missing_chars := PackedByteArray() + var additional_chars := PackedByteArray() + + while index < characters.size(): + var character := characters[index] + match character: + GdDiffTool.DIV_ADD: + index += 1 + additional_chars.append(characters[index]) + GdDiffTool.DIV_SUB: + index += 1 + missing_chars.append(characters[index]) + _: + if not missing_chars.is_empty(): + result.append_array(format_chars(missing_chars, SUB_COLOR)) + missing_chars = PackedByteArray() + if not additional_chars.is_empty(): + result.append_array(format_chars(additional_chars, ADD_COLOR)) + additional_chars = PackedByteArray() + result.append(character) + index += 1 + + result.append_array(format_chars(missing_chars, SUB_COLOR)) + result.append_array(format_chars(additional_chars, ADD_COLOR)) + return result.get_string_from_utf8() + + +static func _typed_value(value) -> String: + return GdDefaultValueDecoder.decode(value) + + +static func _warning(error :String) -> String: + return "[color=%s]%s[/color]" % [WARN_COLOR, error] + + +static func _error(error :String) -> String: + return "[color=%s]%s[/color]" % [ERROR_COLOR, error] + + +static func _nerror(number) -> String: + match typeof(number): + TYPE_INT: + return "[color=%s]%d[/color]" % [ERROR_COLOR, number] + TYPE_FLOAT: + return "[color=%s]%f[/color]" % [ERROR_COLOR, number] + _: + return "[color=%s]%s[/color]" % [ERROR_COLOR, str(number)] + + +static func _colored_value(value, _delimiter ="\n") -> String: + match typeof(value): + TYPE_STRING, TYPE_STRING_NAME: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _colored_string_div(value)] + TYPE_INT: + return "'[color=%s]%d[/color]'" % [VALUE_COLOR, value] + TYPE_FLOAT: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)] + TYPE_COLOR: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)] + TYPE_OBJECT: + if value == null: + return "'[color=%s][/color]'" % [VALUE_COLOR] + if value is InputEvent: + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, input_event_as_text(value)] + if value.has_method("_to_string"): + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, str(value)] + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value.get_class()] + TYPE_DICTIONARY: + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, format_dict(value)] + _: + if GdArrayTools.is_array_type(value): + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)] + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, value] + + +static func _index_report_as_table(index_reports :Array) -> String: + var table := "[table=3]$cells[/table]" + var header := "[cell][right][b]$text[/b][/right]\t[/cell]" + var cell := "[cell][right]$text[/right]\t[/cell]" + var cells := header.replace("$text", "Index") + header.replace("$text", "Current") + header.replace("$text", "Expected") + for report in index_reports: + var index :String = str(report["index"]) + var current :String = str(report["current"]) + var expected :String = str(report["expected"]) + cells += cell.replace("$text", index) + cell.replace("$text", current) + cell.replace("$text", expected) + return table.replace("$cells", cells) + + +static func orphan_detected_on_suite_setup(count :int): + return "%s\n Detected <%d> orphan nodes during test suite setup stage! [b]Check before() and after()![/b]" % [ + _warning("WARNING:"), count] + + +static func orphan_detected_on_test_setup(count :int): + return "%s\n Detected <%d> orphan nodes during test setup! [b]Check before_test() and after_test()![/b]" % [ + _warning("WARNING:"), count] + + +static func orphan_detected_on_test(count :int): + return "%s\n Detected <%d> orphan nodes during test execution!" % [ + _warning("WARNING:"), count] + + +static func fuzzer_interuped(iterations: int, error: String) -> String: + return "%s %s %s\n %s" % [ + _error("Found an error after"), + _colored_value(iterations + 1), + _error("test iterations"), + error] + + +static func test_timeout(timeout :int) -> String: + return "%s\n %s" % [_error("Timeout !"), _colored_value("Test timed out after %s" % LocalTime.elapsed(timeout))] + + +# gdlint:disable = mixed-tabs-and-spaces +static func test_suite_skipped(hint :String, skip_count) -> String: + return """ + %s + Tests skipped: %s + Reason: %s + """.dedent().trim_prefix("\n")\ + % [_error("Entire test-suite is skipped!"), _colored_value(skip_count), _colored_value(hint)] + + +static func test_skipped(hint :String) -> String: + return """ + %s + Reason: %s + """.dedent().trim_prefix("\n")\ + % [_error("This test is skipped!"), _colored_value(hint)] + + +static func error_not_implemented() -> String: + return _error("Test not implemented!") + + +static func error_is_null(current) -> String: + return "%s %s but was %s" % [_error("Expecting:"), _colored_value(null), _colored_value(current)] + + +static func error_is_not_null() -> String: + return "%s %s" % [_error("Expecting: not to be"), _colored_value(null)] + + +static func error_equal(current, expected, index_reports :Array = []) -> String: + var report = """ + %s + %s + but was + %s""".dedent().trim_prefix("\n") % [_error("Expecting:"), _colored_value(expected, true), _colored_value(current, true)] + if not index_reports.is_empty(): + report += "\n\n%s\n%s" % [_error("Differences found:"), _index_report_as_table(index_reports)] + return report + + +static func error_not_equal(current, expected) -> String: + return "%s\n %s\n not equal to\n %s" % [_error("Expecting:"), _colored_value(expected, true), _colored_value(current, true)] + + +static func error_not_equal_case_insensetiv(current, expected) -> String: + return "%s\n %s\n not equal to (case insensitiv)\n %s" % [ + _error("Expecting:"), _colored_value(expected, true), _colored_value(current, true)] + + +static func error_is_empty(current) -> String: + return "%s\n must be empty but was\n %s" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_not_empty() -> String: + return "%s\n must not be empty" % [_error("Expecting:")] + + +static func error_is_same(current, expected) -> String: + return "%s\n %s\n to refer to the same object\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +@warning_ignore("unused_parameter") +static func error_not_same(_current, expected) -> String: + return "%s\n %s" % [_error("Expecting not same:"), _colored_value(expected)] + + +static func error_not_same_error(current, expected) -> String: + return "%s\n %s\n but was\n %s" % [_error("Expecting error message:"), _colored_value(expected), _colored_value(current)] + + +static func error_is_instanceof(current: GdUnitResult, expected :GdUnitResult) -> String: + return "%s\n %s\n But it was %s" % [_error("Expected instance of:"),\ + _colored_value(expected.or_else(null)), _colored_value(current.or_else(null))] + + +# -- Boolean Assert specific messages ----------------------------------------------------- +static func error_is_true(current) -> String: + return "%s %s but is %s" % [_error("Expecting:"), _colored_value(true), _colored_value(current)] + + +static func error_is_false(current) -> String: + return "%s %s but is %s" % [_error("Expecting:"), _colored_value(false), _colored_value(current)] + + +# - Integer/Float Assert specific messages ----------------------------------------------------- + +static func error_is_even(current) -> String: + return "%s\n %s must be even" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_odd(current) -> String: + return "%s\n %s must be odd" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_negative(current) -> String: + return "%s\n %s be negative" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_not_negative(current) -> String: + return "%s\n %s be not negative" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_zero(current) -> String: + return "%s\n equal to 0 but is %s" % [_error("Expecting:"), _colored_value(current)] + + +static func error_is_not_zero() -> String: + return "%s\n not equal to 0" % [_error("Expecting:")] + + +static func error_is_wrong_type(current_type :Variant.Type, expected_type :Variant.Type) -> String: + return "%s\n Expecting type %s but is %s" % [ + _error("Unexpected type comparison:"), + _colored_value(GdObjects.type_as_string(current_type)), + _colored_value(GdObjects.type_as_string(expected_type))] + + +static func error_is_value(operation, current, expected, expected2=null) -> String: + match operation: + Comparator.EQUAL: + return "%s\n %s but was '%s'" % [_error("Expecting:"), _colored_value(expected), _nerror(current)] + Comparator.LESS_THAN: + return "%s\n %s but was '%s'" % [_error("Expecting to be less than:"), _colored_value(expected), _nerror(current)] + Comparator.LESS_EQUAL: + return "%s\n %s but was '%s'" % [_error("Expecting to be less than or equal:"), _colored_value(expected), _nerror(current)] + Comparator.GREATER_THAN: + return "%s\n %s but was '%s'" % [_error("Expecting to be greater than:"), _colored_value(expected), _nerror(current)] + Comparator.GREATER_EQUAL: + return "%s\n %s but was '%s'" % [_error("Expecting to be greater than or equal:"), _colored_value(expected), _nerror(current)] + Comparator.BETWEEN_EQUAL: + return "%s\n %s\n in range between\n %s <> %s" % [ + _error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)] + Comparator.NOT_BETWEEN_EQUAL: + return "%s\n %s\n not in range between\n %s <> %s" % [ + _error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)] + return "TODO create expected message" + + +static func error_is_in(current, expected :Array) -> String: + return "%s\n %s\n is in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))] + + +static func error_is_not_in(current, expected :Array) -> String: + return "%s\n %s\n is not in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))] + + +# - StringAssert --------------------------------------------------------------------------------- +static func error_equal_ignoring_case(current, expected) -> String: + return "%s\n %s\n but was\n %s (ignoring case)" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +static func error_contains(current, expected) -> String: + return "%s\n %s\n do contains\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_not_contains(current, expected) -> String: + return "%s\n %s\n not do contain\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_contains_ignoring_case(current, expected) -> String: + return "%s\n %s\n contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_not_contains_ignoring_case(current, expected) -> String: + return "%s\n %s\n not do contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_starts_with(current, expected) -> String: + return "%s\n %s\n to start with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_ends_with(current, expected) -> String: + return "%s\n %s\n to end with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)] + + +static func error_has_length(current, expected: int, compare_operator) -> String: + var current_length = current.length() if current != null else null + match compare_operator: + Comparator.EQUAL: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size:"), _colored_value(expected), _nerror(current_length), _colored_value(current)] + Comparator.LESS_THAN: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be less than:"), _colored_value(expected), _nerror(current_length), _colored_value(current)] + Comparator.LESS_EQUAL: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be less than or equal:"), _colored_value(expected), + _nerror(current_length), _colored_value(current)] + Comparator.GREATER_THAN: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be greater than:"), _colored_value(expected), + _nerror(current_length), _colored_value(current)] + Comparator.GREATER_EQUAL: + return "%s\n %s but was '%s' in\n %s" % [ + _error("Expecting size to be greater than or equal:"), _colored_value(expected), + _nerror(current_length), _colored_value(current)] + return "TODO create expected message" + + +# - ArrayAssert specific messgaes --------------------------------------------------- + +static func error_arr_contains(current, expected :Array, not_expect :Array, not_found :Array, by_reference :bool) -> String: + var failure_message = "Expecting contains SAME elements:" if by_reference else "Expecting contains elements:" + var error := "%s\n %s\n do contains (in any order)\n %s" % [ + _error(failure_message), _colored_value(current, ", "), _colored_value(expected, ", ")] + if not not_expect.is_empty(): + error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect, ", ") + if not not_found.is_empty(): + var prefix = "but" if not_expect.is_empty() else "and" + error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found, ", ")] + return error + + +static func error_arr_contains_exactly(current, expected, not_expect, not_found, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure_message = ( + "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME exactly elements:" + ) + if not_expect.is_empty() and not_found.is_empty(): + var diff := _find_first_diff(current, expected) + return "%s\n %s\n do contains (in same order)\n %s\n but has different order %s" % [ + _error(failure_message), _colored_value(current, ", "), _colored_value(expected, ", "), diff] + + var error := "%s\n %s\n do contains (in same order)\n %s" % [ + _error(failure_message), _colored_value(current, ", "), _colored_value(expected, ", ")] + if not not_expect.is_empty(): + error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect, ", ") + if not not_found.is_empty(): + var prefix = "but" if not_expect.is_empty() else "and" + error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found, ", ")] + return error + + +static func error_arr_contains_exactly_in_any_order( + current, + expected :Array, + not_expect :Array, + not_found :Array, + compare_mode :GdObjects.COMPARE_MODE) -> String: + + var failure_message = ( + "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME exactly elements:" + ) + var error := "%s\n %s\n do contains exactly (in any order)\n %s" % [ + _error(failure_message), _colored_value(current, ", "), _colored_value(expected, ", ")] + if not not_expect.is_empty(): + error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect, ", ") + if not not_found.is_empty(): + var prefix = "but" if not_expect.is_empty() else "and" + error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found, ", ")] + return error + + +static func error_arr_not_contains(current :Array, expected :Array, found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure_message = "Expecting:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST else "Expecting SAME:" + var error := "%s\n %s\n do not contains\n %s" % [ + _error(failure_message), _colored_value(current, ", "), _colored_value(expected, ", ")] + if not found.is_empty(): + error += "\n but found elements:\n %s" % _colored_value(found, ", ") + return error + + +# - DictionaryAssert specific messages ---------------------------------------------- +static func error_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure := ( + "Expecting contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME keys:" + ) + return "%s\n %s\n to contains:\n %s\n but can't find key's:\n %s" % [ + _error(failure), _colored_value(current, ", "), _colored_value(expected, ", "), _colored_value(keys_not_found, ", ")] + + +static func error_not_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure := ( + "Expecting NOT contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting NOT contains SAME keys" + ) + return "%s\n %s\n do not contains:\n %s\n but contains key's:\n %s" % [ + _error(failure), _colored_value(current, ", "), _colored_value(expected, ", "), _colored_value(keys_not_found, ", ")] + + +static func error_contains_key_value(key, value, current_value, compare_mode :GdObjects.COMPARE_MODE) -> String: + var failure := ( + "Expecting contains key and value:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST + else "Expecting contains SAME key and value:" + ) + return "%s\n %s : %s\n but contains\n %s : %s" % [ + _error(failure), _colored_value(key), _colored_value(value), _colored_value(key), _colored_value(current_value)] + + +# - ResultAssert specific errors ---------------------------------------------------- +static func error_result_is_empty(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.EMPTY) + + +static func error_result_is_success(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.SUCCESS) + + +static func error_result_is_warning(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.WARN) + + +static func error_result_is_error(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.ERROR) + + +static func error_result_has_message(current :String, expected :String) -> String: + return "%s\n %s\n but was\n %s." % [_error("Expecting:"), _colored_value(expected), _colored_value(current)] + + +static func error_result_has_message_on_success(expected :String) -> String: + return "%s\n %s\n but the GdUnitResult is a success." % [_error("Expecting:"), _colored_value(expected)] + + +static func error_result_is_value(current, expected) -> String: + return "%s\n %s\n but was\n %s." % [_error("Expecting to contain same value:"), _colored_value(expected), _colored_value(current)] + + +static func _result_error_message(current :GdUnitResult, expected_type :int) -> String: + if current == null: + return _error("Expecting the result must be a %s but was ." % result_type(expected_type)) + if current.is_success(): + return _error("Expecting the result must be a %s but was SUCCESS." % result_type(expected_type)) + var error = "Expecting the result must be a %s but was %s:" % [result_type(expected_type), result_type(current._state)] + return "%s\n %s" % [_error(error), _colored_value(result_message(current))] + + +static func error_interrupted(func_name :String, expected, elapsed :String) -> String: + func_name = humanized(func_name) + if expected == null: + return "%s %s but timed out after %s" % [_error("Expected:"), func_name, elapsed] + return "%s %s %s but timed out after %s" % [_error("Expected:"), func_name, _colored_value(expected), elapsed] + + +static func error_wait_signal(signal_name :String, args :Array, elapsed :String) -> String: + if args.is_empty(): + return "%s %s but timed out after %s" % [ + _error("Expecting emit signal:"), _colored_value(signal_name + "()"), elapsed] + return "%s %s but timed out after %s" % [ + _error("Expecting emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed] + + +static func error_signal_emitted(signal_name :String, args :Array, elapsed :String) -> String: + if args.is_empty(): + return "%s %s but is emitted after %s" % [ + _error("Expecting do not emit signal:"), _colored_value(signal_name + "()"), elapsed] + return "%s %s but is emitted after %s" % [ + _error("Expecting do not emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed] + +static func error_await_signal_on_invalid_instance(source, signal_name :String, args :Array) -> String: + return "%s\n await_signal_on(%s, %s, %s)" % [ + _error("Invalid source! Can't await on signal:"), _colored_value(source), signal_name, args] + +static func result_type(type :int) -> String: + match type: + GdUnitResult.SUCCESS: return "SUCCESS" + GdUnitResult.WARN: return "WARNING" + GdUnitResult.ERROR: return "ERROR" + GdUnitResult.EMPTY: return "EMPTY" + return "UNKNOWN" + + +static func result_message(result :GdUnitResult) -> String: + match result._state: + GdUnitResult.SUCCESS: return "" + GdUnitResult.WARN: return result.warn_message() + GdUnitResult.ERROR: return result.error_message() + GdUnitResult.EMPTY: return "" + return "UNKNOWN" +# ----------------------------------------------------------------------------------- + +# - Spy|Mock specific errors ---------------------------------------------------- +static func error_no_more_interactions(summary :Dictionary) -> String: + var interactions := PackedStringArray() + for args in summary.keys(): + var times :int = summary[args] + interactions.append(_format_arguments(args, times)) + return "%s\n%s\n%s" % [_error("Expecting no more interactions!"), _error("But found interactions on:"), "\n".join(interactions)] + + +static func error_validate_interactions(current_interactions :Dictionary, expected_interactions :Dictionary) -> String: + var interactions := PackedStringArray() + for args in current_interactions.keys(): + var times :int = current_interactions[args] + interactions.append(_format_arguments(args, times)) + var expected_interaction := _format_arguments(expected_interactions.keys()[0], expected_interactions.values()[0]) + return "%s\n%s\n%s\n%s" % [ + _error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(interactions)] + + +static func _format_arguments(args :Array, times :int) -> String: + var fname :String = args[0] + var fargs := args.slice(1) as Array + var typed_args := _to_typed_args(fargs) + var fsignature := _colored_value("%s(%s)" % [fname, ", ".join(typed_args)]) + return " %s %d time's" % [fsignature, times] + + +static func _to_typed_args(args :Array) -> PackedStringArray: + var typed := PackedStringArray() + for arg in args: + typed.append(_format_arg(arg) + " :" + GdObjects.type_as_string(typeof(arg))) + return typed + + +static func _format_arg(arg) -> String: + if arg is InputEvent: + return input_event_as_text(arg) + return str(arg) + + +static func _find_first_diff( left :Array, right :Array) -> String: + for index in left.size(): + var l = left[index] + var r = "" if index >= right.size() else right[index] + if not GdObjects.equals(l, r): + return "at position %s\n '%s' vs '%s'" % [_colored_value(index), _typed_value(l), _typed_value(r)] + return "" + + +static func error_has_size(current, expected: int) -> String: + var current_size = null if current == null else current.size() + return "%s\n %s\n but was\n %s" % [_error("Expecting size:"), _colored_value(expected), _colored_value(current_size)] + + +static func error_contains_exactly(current: Array, expected: Array) -> String: + return "%s\n %s\n but was\n %s" % [_error("Expecting exactly equal:"), _colored_value(expected), _colored_value(current)] + + +static func format_chars(characters :PackedByteArray, type :Color) -> PackedByteArray: + if characters.size() == 0:# or characters[0] == 10: + return characters + var result := PackedByteArray() + var message := "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [ + type.to_html(), characters.get_string_from_utf8().replace("\n", "")] + result.append_array(message.to_utf8_buffer()) + return result + + +static func format_invalid(value :String) -> String: + return "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [SUB_COLOR.to_html(), value] + + +static func humanized(value :String) -> String: + return value.replace("_", " ") diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd b/addons/gdUnit4/src/asserts/GdAssertReports.gd new file mode 100644 index 0000000..1ac9e04 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd @@ -0,0 +1,55 @@ +class_name GdAssertReports +extends RefCounted + +const LAST_ERROR = "last_assert_error_message" +const LAST_ERROR_LINE = "last_assert_error_line" + + +static func report_success() -> void: + GdUnitSignals.instance().gdunit_set_test_failed.emit(false) + GdAssertReports.set_last_error_line_number(-1) + Engine.remove_meta(LAST_ERROR) + + +static func report_warning(message :String, line_number :int) -> void: + GdUnitSignals.instance().gdunit_set_test_failed.emit(false) + send_report(GdUnitReport.new().create(GdUnitReport.WARN, line_number, message)) + + +static func report_error(message:String, line_number :int) -> void: + GdUnitSignals.instance().gdunit_set_test_failed.emit(true) + GdAssertReports.set_last_error_line_number(line_number) + Engine.set_meta(LAST_ERROR, message) + # if we expect to fail we handle as success test + if _do_expect_assert_failing(): + return + send_report(GdUnitReport.new().create(GdUnitReport.FAILURE, line_number, message)) + + +static func reset_last_error_line_number() -> void: + Engine.remove_meta(LAST_ERROR_LINE) + + +static func set_last_error_line_number(line_number :int) -> void: + Engine.set_meta(LAST_ERROR_LINE, line_number) + + +static func get_last_error_line_number() -> int: + if Engine.has_meta(LAST_ERROR_LINE): + return Engine.get_meta(LAST_ERROR_LINE) + return -1 + + +static func _do_expect_assert_failing() -> bool: + if Engine.has_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES): + return Engine.get_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES) + return false + + +static func current_failure() -> String: + return Engine.get_meta(LAST_ERROR) + + +static func send_report(report :GdUnitReport) -> void: + var execution_context_id := GdUnitThreadManager.get_current_context().get_execution_context_id() + GdUnitSignals.instance().gdunit_report.emit(execution_context_id, report) diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd new file mode 100644 index 0000000..cc8c9da --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd @@ -0,0 +1,349 @@ +extends GdUnitArrayAssert + + +var _base :GdUnitAssert +var _current_value_provider :ValueProvider + + +func _init(current): + _current_value_provider = DefaultValueProvider.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not _validate_value_type(current): + report_error("GdUnitArrayAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func report_success() -> GdUnitArrayAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitArrayAssert: + _base.report_error(error) + return self + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitArrayAssert: + _base.override_failure_message(message) + return self + + +func _validate_value_type(value) -> bool: + return value == null or GdArrayTools.is_array_type(value) + + +func current_value() -> Variant: + return _current_value_provider.get_value() + + +func max_length(left, right) -> int: + var ls = str(left).length() + var rs = str(right).length() + return rs if ls < rs else ls + + +func _array_equals_div(current, expected, case_sensitive :bool = false) -> Array: + var current_value := PackedStringArray(Array(current)) + var expected_value := PackedStringArray(Array(expected)) + var index_report := Array() + for index in current_value.size(): + var c := current_value[index] + if index < expected_value.size(): + var e := expected_value[index] + if not GdObjects.equals(c, e, case_sensitive): + var length := max_length(c, e) + current_value[index] = GdAssertMessages.format_invalid(c.lpad(length)) + expected_value[index] = e.lpad(length) + index_report.push_back({"index" : index, "current" :c, "expected": e}) + else: + current_value[index] = GdAssertMessages.format_invalid(c) + index_report.push_back({"index" : index, "current" :c, "expected": ""}) + + for index in range(current.size(), expected_value.size()): + var value := expected_value[index] + expected_value[index] = GdAssertMessages.format_invalid(value) + index_report.push_back({"index" : index, "current" : "", "expected": value}) + return [current_value, expected_value, index_report] + + +func _array_div(compare_mode :GdObjects.COMPARE_MODE, left :Array[Variant], right :Array[Variant], _same_order := false) -> Array[Variant]: + var not_expect := left.duplicate(true) + var not_found := right.duplicate(true) + for index_c in left.size(): + var c = left[index_c] + for index_e in right.size(): + var e = right[index_e] + if GdObjects.equals(c, e, false, compare_mode): + GdArrayTools.erase_value(not_expect, e) + GdArrayTools.erase_value(not_found, c) + break + return [not_expect, not_found] + + +func _contains(expected, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var by_reference := compare_mode == GdObjects.COMPARE_MODE.OBJECT_REFERENCE + var current_value = current_value() + if current_value == null: + return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], expected, by_reference)) + var diffs := _array_div(compare_mode, current_value, expected) + #var not_expect := diffs[0] as Array + var not_found := diffs[1] as Array + if not not_found.is_empty(): + return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], not_found, by_reference)) + return report_success() + + +func _contains_exactly(expected, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current_value = current_value() + if current_value == null: + return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], expected, compare_mode)) + # has same content in same order + if GdObjects.equals(Array(current_value), Array(expected), false, compare_mode): + return report_success() + # check has same elements but in different order + if GdObjects.equals_sorted(Array(current_value), Array(expected), false, compare_mode): + return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], [], compare_mode)) + # find the difference + var diffs := _array_div(compare_mode, current_value, expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + var not_expect := diffs[0] as Array[Variant] + var not_found := diffs[1] as Array[Variant] + return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, not_expect, not_found, compare_mode)) + + +func _contains_exactly_in_any_order(expected, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current_value = current_value() + if current_value == null: + return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode)) + # find the difference + var diffs := _array_div(compare_mode, current_value, expected, false) + var not_expect := diffs[0] as Array + var not_found := diffs[1] as Array + if not_expect.is_empty() and not_found.is_empty(): + return report_success() + return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, not_expect, not_found, compare_mode)) + + +func _not_contains(expected, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current_value = current_value() + if current_value == null: + return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode)) + var diffs := _array_div(compare_mode, current_value, expected) + var found := diffs[0] as Array + if found.size() == current_value.size(): + return report_success() + var diffs2 := _array_div(compare_mode, expected, diffs[1]) + return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected, diffs2[0], compare_mode)) + + +func is_null() -> GdUnitArrayAssert: + _base.is_null() + return self + + +func is_not_null() -> GdUnitArrayAssert: + _base.is_not_null() + return self + + +# Verifies that the current String is equal to the given one. +func is_equal(expected) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current_value = current_value() + if current_value == null and expected != null: + return report_error(GdAssertMessages.error_equal(null, expected)) + if not GdObjects.equals(current_value, expected): + var diff := _array_equals_div(current_value, expected) + var expected_as_list = GdArrayTools.as_string(diff[0], false) + var current_as_list = GdArrayTools.as_string(diff[1], false) + var index_report = diff[2] + return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report)) + return report_success() + + +# Verifies that the current Array is equal to the given one, ignoring case considerations. +func is_equal_ignoring_case(expected) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current_value = current_value() + if current_value == null and expected != null: + return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected))) + if not GdObjects.equals(current_value, expected, true): + var diff := _array_equals_div(current_value, expected, true) + var expected_as_list := GdArrayTools.as_string(diff[0]) + var current_as_list := GdArrayTools.as_string(diff[1]) + var index_report = diff[2] + return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report)) + return report_success() + + +func is_not_equal(expected) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current_value = current_value() + if GdObjects.equals(current_value, expected): + return report_error(GdAssertMessages.error_not_equal(current_value, expected)) + return report_success() + + +func is_not_equal_ignoring_case(expected) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current_value = current_value() + if GdObjects.equals(current_value, expected, true): + var c := GdArrayTools.as_string(current_value) + var e := GdArrayTools.as_string(expected) + return report_error(GdAssertMessages.error_not_equal_case_insensetiv(c, e)) + return report_success() + + +func is_empty() -> GdUnitArrayAssert: + var current_value = current_value() + if current_value == null or current_value.size() > 0: + return report_error(GdAssertMessages.error_is_empty(current_value)) + return report_success() + + +func is_not_empty() -> GdUnitArrayAssert: + var current_value = current_value() + if current_value != null and current_value.size() == 0: + return report_error(GdAssertMessages.error_is_not_empty()) + return report_success() + + +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_same(expected) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current = current_value() + if not is_same(current, expected): + report_error(GdAssertMessages.error_is_same(current, expected)) + return self + + +func is_not_same(expected) -> GdUnitArrayAssert: + if not _validate_value_type(expected): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) + var current = current_value() + if is_same(current, expected): + report_error(GdAssertMessages.error_not_same(current, expected)) + return self + + +func has_size(expected: int) -> GdUnitArrayAssert: + var current_value = current_value() + if current_value == null or current_value.size() != expected: + return report_error(GdAssertMessages.error_has_size(current_value, expected)) + return report_success() + + +func contains(expected) -> GdUnitArrayAssert: + return _contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + + +func contains_exactly(expected) -> GdUnitArrayAssert: + return _contains_exactly(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + + +func contains_exactly_in_any_order(expected) -> GdUnitArrayAssert: + return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + + +func contains_same(expected) -> GdUnitArrayAssert: + return _contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) + + +func contains_same_exactly(expected) -> GdUnitArrayAssert: + return _contains_exactly(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) + + +func contains_same_exactly_in_any_order(expected) -> GdUnitArrayAssert: + return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) + + +func not_contains(expected) -> GdUnitArrayAssert: + return _not_contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + + +func not_contains_same(expected) -> GdUnitArrayAssert: + return _not_contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) + + +func is_instanceof(expected) -> GdUnitAssert: + _base.is_instanceof(expected) + return self + + +func extract(func_name :String, args := Array()) -> GdUnitArrayAssert: + var extracted_elements := Array() + var extractor :GdUnitValueExtractor = ResourceLoader.load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd", + "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(func_name, args) + var current = current_value() + if current == null: + _current_value_provider = DefaultValueProvider.new(null) + else: + for element in current: + extracted_elements.append(extractor.extract_value(element)) + _current_value_provider = DefaultValueProvider.new(extracted_elements) + return self + + +func extractv( + extr0 :GdUnitValueExtractor, + extr1 :GdUnitValueExtractor = null, + extr2 :GdUnitValueExtractor = null, + extr3 :GdUnitValueExtractor = null, + extr4 :GdUnitValueExtractor = null, + extr5 :GdUnitValueExtractor = null, + extr6 :GdUnitValueExtractor = null, + extr7 :GdUnitValueExtractor = null, + extr8 :GdUnitValueExtractor = null, + extr9 :GdUnitValueExtractor = null) -> GdUnitArrayAssert: + var extractors :Variant = GdArrayTools.filter_value([extr0, extr1, extr2, extr3, extr4, extr5, extr6, extr7, extr8, extr9], null) + var extracted_elements := Array() + var current = current_value() + if current == null: + _current_value_provider = DefaultValueProvider.new(null) + else: + for element in current_value(): + var ev :Array[Variant] = [ + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG, + GdUnitTuple.NO_ARG + ] + for index in extractors.size(): + var extractor :GdUnitValueExtractor = extractors[index] + ev[index] = extractor.extract_value(element) + if extractors.size() > 1: + extracted_elements.append(GdUnitTuple.new(ev[0], ev[1], ev[2], ev[3], ev[4], ev[5], ev[6], ev[7], ev[8], ev[9])) + else: + extracted_elements.append(ev[0]) + _current_value_provider = DefaultValueProvider.new(extracted_elements) + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd new file mode 100644 index 0000000..30694b0 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd @@ -0,0 +1,71 @@ +extends GdUnitAssert + + +var _current :Variant +var _current_error_message :String = "" +var _custom_failure_message :String = "" + + +func _init(current :Variant) -> void: + _current = current + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + GdAssertReports.reset_last_error_line_number() + + +func failure_message() -> String: + return _current_error_message + + +func current_value() -> Variant: + return _current + + +func report_success() -> GdUnitAssert: + GdAssertReports.report_success() + return self + + +func report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() + GdAssertReports.set_last_error_line_number(line_number) + _current_error_message = error_message if _custom_failure_message.is_empty() else _custom_failure_message + GdAssertReports.report_error(_current_error_message, line_number) + return self + + +func test_fail(): + return report_error(GdAssertMessages.error_not_implemented()) + + +func override_failure_message(message :String): + _custom_failure_message = message + return self + + +func is_equal(expected) -> GdUnitAssert: + var current = current_value() + if not GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_equal(current, expected)) + return report_success() + + +func is_not_equal(expected) -> GdUnitAssert: + var current = current_value() + if GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() + + +func is_null() -> GdUnitAssert: + var current = current_value() + if current != null: + return report_error(GdAssertMessages.error_is_null(current)) + return report_success() + + +func is_not_null() -> GdUnitAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_is_not_null()) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd new file mode 100644 index 0000000..a452791 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd @@ -0,0 +1,63 @@ +# Preloads all GdUnit assertions +class_name GdUnitAssertions +extends RefCounted + + +func _init() -> void: + # preload all gdunit assertions to speedup testsuite loading time + # gdlint:disable=private-method-call + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd") + + +### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" +### in order to noticeably reduce the loading time of the test suite. +# We go this hard way to increase the loading performance to avoid reparsing all the used scripts +# for more detailed info -> https://github.com/godotengine/godot/issues/67400 +# gdlint:disable=function-name +static func __lazy_load(script_path :String) -> GDScript: + return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) + + +static func validate_value_type(value, type :Variant.Type) -> bool: + return value == null or typeof(value) == type + +# Scans the current stack trace for the root cause to extract the line number +static func get_line_number() -> int: + var stack_trace := get_stack() + if stack_trace == null or stack_trace.is_empty(): + return -1 + for index in stack_trace.size(): + var stack_info :Dictionary = stack_trace[index] + var function :String = stack_info.get("function") + # we catch helper asserts to skip over to return the correct line number + if function.begins_with("assert_"): + continue + if function.begins_with("test_"): + return stack_info.get("line") + var source :String = stack_info.get("source") + if source.is_empty() \ + or source.begins_with("user://") \ + or source.ends_with("GdUnitAssert.gd") \ + or source.ends_with("GdUnitAssertions.gd") \ + or source.ends_with("AssertImpl.gd") \ + or source.ends_with("GdUnitTestSuite.gd") \ + or source.ends_with("GdUnitSceneRunnerImpl.gd") \ + or source.ends_with("GdUnitObjectInteractions.gd") \ + or source.ends_with("GdUnitAwaiter.gd"): + continue + return stack_info.get("line") + return -1 diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd new file mode 100644 index 0000000..c2cdd34 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd @@ -0,0 +1,76 @@ +extends GdUnitBoolAssert + +var _base: GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_BOOL): + report_error("GdUnitBoolAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value(): + return _base.current_value() + + +func report_success() -> GdUnitBoolAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitBoolAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitBoolAssert: + _base.override_failure_message(message) + return self + + +# Verifies that the current value is null. +func is_null() -> GdUnitBoolAssert: + _base.is_null() + return self + + +# Verifies that the current value is not null. +func is_not_null() -> GdUnitBoolAssert: + _base.is_not_null() + return self + + +func is_equal(expected) -> GdUnitBoolAssert: + _base.is_equal(expected) + return self + + +func is_not_equal(expected) -> GdUnitBoolAssert: + _base.is_not_equal(expected) + return self + + +func is_true() -> GdUnitBoolAssert: + if current_value() != true: + return report_error(GdAssertMessages.error_is_true(current_value())) + return report_success() + + +func is_false() -> GdUnitBoolAssert: + if current_value() == true || current_value() == null: + return report_error(GdAssertMessages.error_is_false(current_value())) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd new file mode 100644 index 0000000..1d9dab2 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd @@ -0,0 +1,182 @@ +extends GdUnitDictionaryAssert + +var _base :GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_DICTIONARY): + report_error("GdUnitDictionaryAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func report_success() -> GdUnitDictionaryAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitDictionaryAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitDictionaryAssert: + _base.override_failure_message(message) + return self + + +func current_value() -> Variant: + return _base.current_value() + + +func is_null() -> GdUnitDictionaryAssert: + _base.is_null() + return self + + +func is_not_null() -> GdUnitDictionaryAssert: + _base.is_not_null() + return self + + +func is_equal(expected) -> GdUnitDictionaryAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_equal(null, GdAssertMessages.format_dict(expected))) + if not GdObjects.equals(current, expected): + var c := GdAssertMessages.format_dict(current) + var e := GdAssertMessages.format_dict(expected) + var diff := GdDiffTool.string_diff(c, e) + var curent_diff := GdAssertMessages.colored_array_div(diff[1]) + return report_error(GdAssertMessages.error_equal(curent_diff, e)) + return report_success() + + +func is_not_equal(expected) -> GdUnitDictionaryAssert: + var current = current_value() + if GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() + + +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_same(expected) -> GdUnitDictionaryAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_equal(null, GdAssertMessages.format_dict(expected))) + if not is_same(current, expected): + var c := GdAssertMessages.format_dict(current) + var e := GdAssertMessages.format_dict(expected) + var diff := GdDiffTool.string_diff(c, e) + var curent_diff := GdAssertMessages.colored_array_div(diff[1]) + return report_error(GdAssertMessages.error_is_same(curent_diff, e)) + return report_success() + + +@warning_ignore("unused_parameter", "shadowed_global_identifier") +func is_not_same(expected) -> GdUnitDictionaryAssert: + var current = current_value() + if is_same(current, expected): + return report_error(GdAssertMessages.error_not_same(current, expected)) + return report_success() + + +func is_empty() -> GdUnitDictionaryAssert: + var current = current_value() + if current == null or not current.is_empty(): + return report_error(GdAssertMessages.error_is_empty(current)) + return report_success() + + +func is_not_empty() -> GdUnitDictionaryAssert: + var current = current_value() + if current == null or current.is_empty(): + return report_error(GdAssertMessages.error_is_not_empty()) + return report_success() + + +func has_size(expected: int) -> GdUnitDictionaryAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_is_not_null()) + if current.size() != expected: + return report_error(GdAssertMessages.error_has_size(current, expected)) + return report_success() + + +func _contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_is_not_null()) + # find expected keys + var keys_not_found :Array = expected.filter(_filter_by_key.bind(current.keys(), compare_mode)) + if not keys_not_found.is_empty(): + return report_error(GdAssertMessages.error_contains_keys(current.keys(), expected, keys_not_found, compare_mode)) + return report_success() + + +func _contains_key_value(key, value, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert: + var current = current_value() + var expected := [key] + if current == null: + return report_error(GdAssertMessages.error_is_not_null()) + var keys_not_found :Array = expected.filter(_filter_by_key.bind(current.keys(), compare_mode)) + if not keys_not_found.is_empty(): + return report_error(GdAssertMessages.error_contains_key_value(key, value, current.keys(), compare_mode)) + if not GdObjects.equals(current[key], value, false, compare_mode): + return report_error(GdAssertMessages.error_contains_key_value(key, value, current[key], compare_mode)) + return report_success() + + +func _not_contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_is_not_null()) + var keys_found :Array = current.keys().filter(_filter_by_key.bind(expected, compare_mode, true)) + if not keys_found.is_empty(): + return report_error(GdAssertMessages.error_not_contains_keys(current.keys(), expected, keys_found, compare_mode)) + return report_success() + + +func contains_keys(expected :Array) -> GdUnitDictionaryAssert: + return _contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + + +func contains_key_value(key, value) -> GdUnitDictionaryAssert: + return _contains_key_value(key, value, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + + +func not_contains_keys(expected :Array) -> GdUnitDictionaryAssert: + return _not_contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + + +func contains_same_keys(expected :Array) -> GdUnitDictionaryAssert: + return _contains_keys(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) + + +func contains_same_key_value(key, value) -> GdUnitDictionaryAssert: + return _contains_key_value(key, value, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) + + +func not_contains_same_keys(expected :Array) -> GdUnitDictionaryAssert: + return _not_contains_keys(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) + + +func _filter_by_key(element :Variant, values :Array, compare_mode :GdObjects.COMPARE_MODE, is_not :bool = false) -> bool: + for key in values: + if GdObjects.equals(key, element, false, compare_mode): + return is_not + return !is_not diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd new file mode 100644 index 0000000..bcb949f --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd @@ -0,0 +1,110 @@ +extends GdUnitFailureAssert + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _is_failed := false +var _failure_message :String + + +static func _set_do_expect_fail(enabled :bool = true): + Engine.set_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES, enabled) + + +func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAssert: + # do not report any failure from the original assertion we want to test + _set_do_expect_fail(true) + var thread_context := GdUnitThreadManager.get_current_context() + thread_context.set_assert(null) + GdUnitSignals.instance().gdunit_set_test_failed.connect(_on_test_failed) + # execute the given assertion as callable + if do_await: + await assertion.call() + else: + assertion.call() + _set_do_expect_fail(false) + # get the assert instance from current tread context + var current_assert := thread_context.get_assert() + if not is_instance_of(current_assert, GdUnitAssert): + _is_failed = true + _failure_message = "Invalid Callable! It must be a callable of 'GdUnitAssert'" + return self + _failure_message = current_assert.failure_message() + return self + + +func execute(assertion :Callable) -> GdUnitFailureAssert: + execute_and_await(assertion, false) + return self + + +func _on_test_failed(value :bool) -> void: + _is_failed = value + + +@warning_ignore("unused_parameter") +func is_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +@warning_ignore("unused_parameter") +func is_not_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +func is_null() -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +func is_not_null() -> GdUnitFailureAssert: + return _report_error("Not implemented") + + +func is_success() -> GdUnitFailureAssert: + if _is_failed: + return _report_error("Expect: assertion ends successfully.") + return self + + +func is_failed() -> GdUnitFailureAssert: + if not _is_failed: + return _report_error("Expect: assertion fails.") + return self + + +func has_line(expected :int) -> GdUnitFailureAssert: + var current := GdAssertReports.get_last_error_line_number() + if current != expected: + return _report_error("Expect: to failed on line '%d'\n but was '%d'." % [expected, current]) + return self + + +func has_message(expected :String) -> GdUnitFailureAssert: + is_failed() + var expected_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(expected)) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) + if current_error != expected_error: + var diffs := GdDiffTool.string_diff(current_error, expected_error) + var current := GdAssertMessages.colored_array_div(diffs[1]) + _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return self + + +func starts_with_message(expected :String) -> GdUnitFailureAssert: + var expected_error := GdUnitTools.normalize_text(expected) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) + if current_error.find(expected_error) != 0: + var diffs := GdDiffTool.string_diff(current_error, expected_error) + var current := GdAssertMessages.colored_array_div(diffs[1]) + _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return self + + +func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() + GdAssertReports.report_error(error_message, line_number) + return self + + +func _report_success() -> GdUnitFailureAssert: + GdAssertReports.report_success() + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd new file mode 100644 index 0000000..b23212a --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd @@ -0,0 +1,95 @@ +extends GdUnitFileAssert + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _base: GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_STRING): + report_error("GdUnitFileAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> String: + return _base.current_value() as String + + +func report_success() -> GdUnitFileAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitFileAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitFileAssert: + _base.override_failure_message(message) + return self + + +func is_equal(expected) -> GdUnitFileAssert: + _base.is_equal(expected) + return self + + +func is_not_equal(expected) -> GdUnitFileAssert: + _base.is_not_equal(expected) + return self + + +func is_file() -> GdUnitFileAssert: + var current := current_value() + if FileAccess.open(current, FileAccess.READ) == null: + return report_error("Is not a file '%s', error code %s" % [current, FileAccess.get_open_error()]) + return report_success() + + +func exists() -> GdUnitFileAssert: + var current := current_value() + if not FileAccess.file_exists(current): + return report_error("The file '%s' not exists" %current) + return report_success() + + +func is_script() -> GdUnitFileAssert: + var current := current_value() + if FileAccess.open(current, FileAccess.READ) == null: + return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()]) + + var script = load(current) + if not script is GDScript: + return report_error("The file '%s' is not a GdScript" % current) + return report_success() + + +func contains_exactly(expected_rows :Array) -> GdUnitFileAssert: + var current := current_value() + if FileAccess.open(current, FileAccess.READ) == null: + return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()]) + + var script = load(current) + if script is GDScript: + var instance = script.new() + var source_code = GdScriptParser.to_unix_format(instance.get_script().source_code) + GdUnitTools.free_instance(instance) + var rows := Array(source_code.split("\n")) + ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(rows).contains_exactly(expected_rows) + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd new file mode 100644 index 0000000..c0cd4b4 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd @@ -0,0 +1,144 @@ +extends GdUnitFloatAssert + +var _base: GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_FLOAT): + report_error("GdUnitFloatAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value(): + return _base.current_value() + + +func report_success() -> GdUnitFloatAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitFloatAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitFloatAssert: + _base.override_failure_message(message) + return self + + +func is_null() -> GdUnitFloatAssert: + _base.is_null() + return self + + +func is_not_null() -> GdUnitFloatAssert: + _base.is_not_null() + return self + + +func is_equal(expected :float) -> GdUnitFloatAssert: + _base.is_equal(expected) + return self + + +func is_not_equal(expected :float) -> GdUnitFloatAssert: + _base.is_not_equal(expected) + return self + + +@warning_ignore("shadowed_global_identifier") +func is_equal_approx(expected :float, approx :float) -> GdUnitFloatAssert: + return is_between(expected-approx, expected+approx) + + +func is_less(expected :float) -> GdUnitFloatAssert: + var current = current_value() + if current == null or current >= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected)) + return report_success() + + +func is_less_equal(expected :float) -> GdUnitFloatAssert: + var current = current_value() + if current == null or current > expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected)) + return report_success() + + +func is_greater(expected :float) -> GdUnitFloatAssert: + var current = current_value() + if current == null or current <= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected)) + return report_success() + + +func is_greater_equal(expected :float) -> GdUnitFloatAssert: + var current = current_value() + if current == null or current < expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected)) + return report_success() + + +func is_negative() -> GdUnitFloatAssert: + var current = current_value() + if current == null or current >= 0.0: + return report_error(GdAssertMessages.error_is_negative(current)) + return report_success() + + +func is_not_negative() -> GdUnitFloatAssert: + var current = current_value() + if current == null or current < 0.0: + return report_error(GdAssertMessages.error_is_not_negative(current)) + return report_success() + + +func is_zero() -> GdUnitFloatAssert: + var current = current_value() + if current == null or not is_equal_approx(0.00000000, current): + return report_error(GdAssertMessages.error_is_zero(current)) + return report_success() + + +func is_not_zero() -> GdUnitFloatAssert: + var current = current_value() + if current == null or is_equal_approx(0.00000000, current): + return report_error(GdAssertMessages.error_is_not_zero()) + return report_success() + + +func is_in(expected :Array) -> GdUnitFloatAssert: + var current = current_value() + if not expected.has(current): + return report_error(GdAssertMessages.error_is_in(current, expected)) + return report_success() + + +func is_not_in(expected :Array) -> GdUnitFloatAssert: + var current = current_value() + if expected.has(current): + return report_error(GdAssertMessages.error_is_not_in(current, expected)) + return report_success() + + +func is_between(from :float, to :float) -> GdUnitFloatAssert: + var current = current_value() + if current == null or current < from or current > to: + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd new file mode 100644 index 0000000..4902ba0 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd @@ -0,0 +1,159 @@ +extends GdUnitFuncAssert + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const DEFAULT_TIMEOUT := 2000 + + +var _current_value_provider :ValueProvider +var _current_error_message :String = "" +var _custom_failure_message :String = "" +var _line_number := -1 +var _timeout := DEFAULT_TIMEOUT +var _interrupted := false +var _sleep_timer :Timer = null + + +func _init(instance :Object, func_name :String, args := Array()): + _line_number = GdUnitAssertions.get_line_number() + GdAssertReports.reset_last_error_line_number() + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + # verify at first the function name exists + if not instance.has_method(func_name): + report_error("The function '%s' do not exists checked instance '%s'." % [func_name, instance]) + _interrupted = true + else: + _current_value_provider = CallBackValueProvider.new(instance, func_name, args) + + +func _notification(_what): + if is_instance_valid(_current_value_provider): + _current_value_provider.dispose() + _current_value_provider = null + if is_instance_valid(_sleep_timer): + Engine.get_main_loop().root.remove_child(_sleep_timer) + _sleep_timer.stop() + _sleep_timer.free() + _sleep_timer = null + + +func report_success() -> GdUnitFuncAssert: + GdAssertReports.report_success() + return self + + +func report_error(error_message :String) -> GdUnitFuncAssert: + _current_error_message = error_message if _custom_failure_message == "" else _custom_failure_message + GdAssertReports.report_error(_current_error_message, _line_number) + return self + + +func failure_message() -> String: + return _current_error_message + + +func send_report(report :GdUnitReport)-> void: + GdUnitSignals.instance().gdunit_report.emit(report) + + +func override_failure_message(message :String) -> GdUnitFuncAssert: + _custom_failure_message = message + return self + + +func wait_until(timeout := 2000) -> GdUnitFuncAssert: + if timeout <= 0: + push_warning("Invalid timeout param, alloed timeouts must be grater than 0. Use default timeout instead") + _timeout = DEFAULT_TIMEOUT + else: + _timeout = timeout + return self + + +func is_null() -> GdUnitFuncAssert: + await _validate_callback(cb_is_null) + return self + + +func is_not_null() -> GdUnitFuncAssert: + await _validate_callback(cb_is_not_null) + return self + + +func is_false() -> GdUnitFuncAssert: + await _validate_callback(cb_is_false) + return self + + +func is_true() -> GdUnitFuncAssert: + await _validate_callback(cb_is_true) + return self + + +func is_equal(expected) -> GdUnitFuncAssert: + await _validate_callback(cb_is_equal, expected) + return self + + +func is_not_equal(expected) -> GdUnitFuncAssert: + await _validate_callback(cb_is_not_equal, expected) + return self + + +# we need actually to define this Callable as functions otherwise we results into leaked scripts here +# this is actually a Godot bug and needs this kind of workaround +func cb_is_null(c, _e): return c == null +func cb_is_not_null(c, _e): return c != null +func cb_is_false(c, _e): return c == false +func cb_is_true(c, _e): return c == true +func cb_is_equal(c, e): return GdObjects.equals(c,e) +func cb_is_not_equal(c, e): return not GdObjects.equals(c, e) + + +func _validate_callback(predicate :Callable, expected = null): + if _interrupted: + return + GdUnitMemoryObserver.guard_instance(self) + var time_scale = Engine.get_time_scale() + var timer := Timer.new() + timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id()) + Engine.get_main_loop().root.add_child(timer) + timer.add_to_group("GdUnitTimers") + timer.timeout.connect(func do_interrupt(): + _interrupted = true + , CONNECT_DEFERRED) + timer.set_one_shot(true) + timer.start((_timeout/1000.0)*time_scale) + _sleep_timer = Timer.new() + _sleep_timer.set_name("gdunit_funcassert_sleep_timer_%d" % _sleep_timer.get_instance_id() ) + Engine.get_main_loop().root.add_child(_sleep_timer) + + while true: + var current = await next_current_value() + # is interupted or predicate success + if _interrupted or predicate.call(current, expected): + break + if is_instance_valid(_sleep_timer): + _sleep_timer.start(0.05) + await _sleep_timer.timeout + + _sleep_timer.stop() + await Engine.get_main_loop().process_frame + if _interrupted: + # https://github.com/godotengine/godot/issues/73052 + #var predicate_name = predicate.get_method() + var predicate_name :String = str(predicate).split('::')[1] + report_error(GdAssertMessages.error_interrupted(predicate_name.strip_edges().trim_prefix("cb_"), expected, LocalTime.elapsed(_timeout))) + else: + report_success() + _sleep_timer.free() + timer.free() + GdUnitMemoryObserver.unguard_instance(self) + + +func next_current_value() -> Variant: + @warning_ignore("redundant_await") + if is_instance_valid(_current_value_provider): + return await _current_value_provider.get_value() + return "invalid value" diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd new file mode 100644 index 0000000..dcd6a9b --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd @@ -0,0 +1,106 @@ +extends GdUnitGodotErrorAssert + +var _current_error_message :String +var _callable :Callable + + +func _init(callable :Callable): + # we only support Godot 4.1.x+ because of await issue https://github.com/godotengine/godot/issues/80292 + assert(Engine.get_version_info().hex >= 0x40100, + "This assertion is not supported for Godot 4.0.x. Please upgrade to the minimum version Godot 4.1.0!") + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + GdAssertReports.reset_last_error_line_number() + _callable = callable + + +func _execute() -> Array[ErrorLogEntry]: + # execute the given code and monitor for runtime errors + if _callable == null or not _callable.is_valid(): + _report_error("Invalid Callable '%s'" % _callable) + else: + await _callable.call() + return await _error_monitor().scan(true) + + +func _error_monitor() -> GodotGdErrorMonitor: + return GdUnitThreadManager.get_current_context().get_execution_context().error_monitor + + +func failure_message() -> String: + return _current_error_message + + +func _report_success() -> GdUnitAssert: + GdAssertReports.report_success() + return self + + +func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() + _current_error_message = error_message + GdAssertReports.report_error(error_message, line_number) + return self + + +func _has_log_entry(log_entries :Array[ErrorLogEntry], type :ErrorLogEntry.TYPE, error :String) -> bool: + for entry in log_entries: + if entry._type == type and entry._message == error: + # Erase the log entry we already handled it by this assertion, otherwise it will report at twice + _error_monitor().erase_log_entry(entry) + return true + return false + + +func _to_list(log_entries :Array[ErrorLogEntry]) -> String: + if log_entries.is_empty(): + return "no errors" + if log_entries.size() == 1: + return log_entries[0]._message + var value := "" + for entry in log_entries: + value += "'%s'\n" % entry._message + return value + + +func is_success() -> GdUnitGodotErrorAssert: + var log_entries := await _execute() + if log_entries.is_empty(): + return _report_success() + return _report_error(""" + Expecting: no error's are ocured. + but found: '%s' + """.dedent().trim_prefix("\n") % _to_list(log_entries)) + + +func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: + var log_entries := await _execute() + if _has_log_entry(log_entries, ErrorLogEntry.TYPE.SCRIPT_ERROR, expected_error): + return _report_success() + return _report_error(""" + Expecting: a runtime error is triggered. + message: '%s' + found: %s + """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)]) + + +func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: + var log_entries := await _execute() + if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_WARNING, expected_warning): + return _report_success() + return _report_error(""" + Expecting: push_warning() is called. + message: '%s' + found: %s + """.dedent().trim_prefix("\n") % [expected_warning, _to_list(log_entries)]) + + +func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert: + var log_entries := await _execute() + if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_ERROR, expected_error): + return _report_success() + return _report_error(""" + Expecting: push_error() is called. + message: '%s' + found: %s + """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)]) diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd new file mode 100644 index 0000000..6fc16e1 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd @@ -0,0 +1,153 @@ +extends GdUnitIntAssert + +var _base: GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not GdUnitAssertions.validate_value_type(current, TYPE_INT): + report_error("GdUnitIntAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitIntAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitIntAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitIntAssert: + _base.override_failure_message(message) + return self + + +func is_null() -> GdUnitIntAssert: + _base.is_null() + return self + + +func is_not_null() -> GdUnitIntAssert: + _base.is_not_null() + return self + + +func is_equal(expected :int) -> GdUnitIntAssert: + _base.is_equal(expected) + return self + + +func is_not_equal(expected :int) -> GdUnitIntAssert: + _base.is_not_equal(expected) + return self + + +func is_less(expected :int) -> GdUnitIntAssert: + var current = current_value() + if current == null or current >= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected)) + return report_success() + + +func is_less_equal(expected :int) -> GdUnitIntAssert: + var current = current_value() + if current == null or current > expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected)) + return report_success() + + +func is_greater(expected :int) -> GdUnitIntAssert: + var current = current_value() + if current == null or current <= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected)) + return report_success() + + +func is_greater_equal(expected :int) -> GdUnitIntAssert: + var current = current_value() + if current == null or current < expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected)) + return report_success() + + +func is_even() -> GdUnitIntAssert: + var current = current_value() + if current == null or current % 2 != 0: + return report_error(GdAssertMessages.error_is_even(current)) + return report_success() + + +func is_odd() -> GdUnitIntAssert: + var current = current_value() + if current == null or current % 2 == 0: + return report_error(GdAssertMessages.error_is_odd(current)) + return report_success() + + +func is_negative() -> GdUnitIntAssert: + var current = current_value() + if current == null or current >= 0: + return report_error(GdAssertMessages.error_is_negative(current)) + return report_success() + + +func is_not_negative() -> GdUnitIntAssert: + var current = current_value() + if current == null or current < 0: + return report_error(GdAssertMessages.error_is_not_negative(current)) + return report_success() + + +func is_zero() -> GdUnitIntAssert: + var current = current_value() + if current != 0: + return report_error(GdAssertMessages.error_is_zero(current)) + return report_success() + + +func is_not_zero() -> GdUnitIntAssert: + var current = current_value() + if current == 0: + return report_error(GdAssertMessages.error_is_not_zero()) + return report_success() + + +func is_in(expected :Array) -> GdUnitIntAssert: + var current = current_value() + if not expected.has(current): + return report_error(GdAssertMessages.error_is_in(current, expected)) + return report_success() + + +func is_not_in(expected :Array) -> GdUnitIntAssert: + var current = current_value() + if expected.has(current): + return report_error(GdAssertMessages.error_is_not_in(current, expected)) + return report_success() + + +func is_between(from :int, to :int) -> GdUnitIntAssert: + var current = current_value() + if current == null or current < from or current > to: + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd new file mode 100644 index 0000000..99a2a21 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd @@ -0,0 +1,109 @@ +extends GdUnitObjectAssert + +var _base :GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if (current != null + and (GdUnitAssertions.validate_value_type(current, TYPE_BOOL) + or GdUnitAssertions.validate_value_type(current, TYPE_INT) + or GdUnitAssertions.validate_value_type(current, TYPE_FLOAT) + or GdUnitAssertions.validate_value_type(current, TYPE_STRING))): + report_error("GdUnitObjectAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitObjectAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitObjectAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitObjectAssert: + _base.override_failure_message(message) + return self + + +func is_equal(expected) -> GdUnitObjectAssert: + _base.is_equal(expected) + return self + + +func is_not_equal(expected) -> GdUnitObjectAssert: + _base.is_not_equal(expected) + return self + + +func is_null() -> GdUnitObjectAssert: + _base.is_null() + return self + + +func is_not_null() -> GdUnitObjectAssert: + _base.is_not_null() + return self + + +@warning_ignore("shadowed_global_identifier") +func is_same(expected) -> GdUnitObjectAssert: + var current :Variant = current_value() + if not is_same(current, expected): + report_error(GdAssertMessages.error_is_same(current, expected)) + return self + report_success() + return self + + +func is_not_same(expected) -> GdUnitObjectAssert: + var current = current_value() + if is_same(current, expected): + report_error(GdAssertMessages.error_not_same(current, expected)) + return self + report_success() + return self + + +func is_instanceof(type :Object) -> GdUnitObjectAssert: + var current = current_value() + if current == null or not is_instance_of(current, type): + var result_expected: = GdObjects.extract_class_name(type) + var result_current: = GdObjects.extract_class_name(current) + report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected)) + return self + report_success() + return self + + +func is_not_instanceof(type) -> GdUnitObjectAssert: + var current :Variant = current_value() + if is_instance_of(current, type): + var result: = GdObjects.extract_class_name(type) + if result.is_success(): + report_error("Expected not be a instance of <%s>" % result.value()) + else: + push_error("Internal ERROR: %s" % result.error_message()) + return self + report_success() + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd new file mode 100644 index 0000000..9598b3e --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -0,0 +1,121 @@ +extends GdUnitResultAssert + +var _base :GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not validate_value_type(current): + report_error("GdUnitResultAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func validate_value_type(value) -> bool: + return value == null or value is GdUnitResult + + +func current_value() -> GdUnitResult: + return _base.current_value() as GdUnitResult + + +func report_success() -> GdUnitResultAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitResultAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitResultAssert: + _base.override_failure_message(message) + return self + + +func is_null() -> GdUnitResultAssert: + _base.is_null() + return self + +func is_not_null() -> GdUnitResultAssert: + _base.is_not_null() + return self + + +func is_empty() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_empty(): + report_error(GdAssertMessages.error_result_is_empty(result)) + else: + report_success() + return self + + +func is_success() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_success(): + report_error(GdAssertMessages.error_result_is_success(result)) + else: + report_success() + return self + + +func is_warning() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_warn(): + report_error(GdAssertMessages.error_result_is_warning(result)) + else: + report_success() + return self + + +func is_error() -> GdUnitResultAssert: + var result := current_value() + if result == null or not result.is_error(): + report_error(GdAssertMessages.error_result_is_error(result)) + else: + report_success() + return self + + +func contains_message(expected :String) -> GdUnitResultAssert: + var result := current_value() + if result == null: + report_error(GdAssertMessages.error_result_has_message("", expected)) + return self + if result.is_success(): + report_error(GdAssertMessages.error_result_has_message_on_success(expected)) + elif result.is_error() and result.error_message() != expected: + report_error(GdAssertMessages.error_result_has_message(result.error_message(), expected)) + elif result.is_warn() and result.warn_message() != expected: + report_error(GdAssertMessages.error_result_has_message(result.warn_message(), expected)) + else: + report_success() + return self + + +func is_value(expected) -> GdUnitResultAssert: + var result := current_value() + var value = null if result == null else result.value() + if not GdObjects.equals(value, expected): + report_error(GdAssertMessages.error_result_is_value(value, expected)) + else: + report_success() + return self + + +func is_equal(expected) -> GdUnitResultAssert: + return is_value(expected) diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd new file mode 100644 index 0000000..4ef2eeb --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd @@ -0,0 +1,110 @@ +extends GdUnitSignalAssert + +const DEFAULT_TIMEOUT := 2000 + +var _signal_collector :GdUnitSignalCollector +var _emitter :Object +var _current_error_message :String = "" +var _custom_failure_message :String = "" +var _line_number := -1 +var _timeout := DEFAULT_TIMEOUT +var _interrupted := false + + +func _init(emitter :Object): + # save the actual assert instance on the current thread context + var context := GdUnitThreadManager.get_current_context() + context.set_assert(self) + _signal_collector = context.get_signal_collector() + _line_number = GdUnitAssertions.get_line_number() + _emitter = emitter + GdAssertReports.reset_last_error_line_number() + + +func report_success() -> GdUnitAssert: + GdAssertReports.report_success() + return self + + +func report_warning(message :String) -> GdUnitAssert: + GdAssertReports.report_warning(message, GdUnitAssertions.get_line_number()) + return self + + +func report_error(error_message :String) -> GdUnitAssert: + _current_error_message = error_message if _custom_failure_message == "" else _custom_failure_message + GdAssertReports.report_error(_current_error_message, _line_number) + return self + + +func failure_message() -> String: + return _current_error_message + + +func send_report(report :GdUnitReport)-> void: + GdUnitSignals.instance().gdunit_report.emit(report) + + +func override_failure_message(message :String) -> GdUnitSignalAssert: + _custom_failure_message = message + return self + + +func wait_until(timeout := 2000) -> GdUnitSignalAssert: + if timeout <= 0: + report_warning("Invalid timeout parameter, allowed timeouts must be greater than 0, use default timeout instead!") + _timeout = DEFAULT_TIMEOUT + else: + _timeout = timeout + return self + + +# Verifies the signal exists checked the emitter +func is_signal_exists(signal_name :String) -> GdUnitSignalAssert: + if not _emitter.has_signal(signal_name): + report_error("The signal '%s' not exists checked object '%s'." % [signal_name, _emitter.get_class()]) + return self + + +# Verifies that given signal is emitted until waiting time +func is_emitted(name :String, args := []) -> GdUnitSignalAssert: + _line_number = GdUnitAssertions.get_line_number() + return await _wail_until_signal(name, args, false) + + +# Verifies that given signal is NOT emitted until waiting time +func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: + _line_number = GdUnitAssertions.get_line_number() + return await _wail_until_signal(name, args, true) + + +func _wail_until_signal(signal_name :String, expected_args :Array, expect_not_emitted: bool) -> GdUnitSignalAssert: + if _emitter == null: + report_error("Can't wait for signal checked a NULL object.") + return self + # first verify the signal is defined + if not _emitter.has_signal(signal_name): + report_error("Can't wait for non-existion signal '%s' checked object '%s'." % [signal_name,_emitter.get_class()]) + return self + _signal_collector.register_emitter(_emitter) + var time_scale = Engine.get_time_scale() + var timer := Timer.new() + Engine.get_main_loop().root.add_child(timer) + timer.add_to_group("GdUnitTimers") + timer.set_one_shot(true) + timer.timeout.connect(func on_timeout(): _interrupted = true) + timer.start((_timeout/1000.0)*time_scale) + var is_signal_emitted = false + while not _interrupted and not is_signal_emitted: + await Engine.get_main_loop().process_frame + if is_instance_valid(_emitter): + is_signal_emitted = _signal_collector.match(_emitter, signal_name, expected_args) + if is_signal_emitted and expect_not_emitted: + report_error(GdAssertMessages.error_signal_emitted(signal_name, expected_args, LocalTime.elapsed(int(_timeout-timer.time_left*1000)))) + + if _interrupted and not expect_not_emitted: + report_error(GdAssertMessages.error_wait_signal(signal_name, expected_args, LocalTime.elapsed(_timeout))) + timer.free() + if is_instance_valid(_emitter): + _signal_collector.reset_received_signals(_emitter, signal_name, expected_args) + return self diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd new file mode 100644 index 0000000..8c1814f --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd @@ -0,0 +1,176 @@ +extends GdUnitStringAssert + +var _base :GdUnitAssert + + +func _init(current): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if current != null and typeof(current) != TYPE_STRING and typeof(current) != TYPE_STRING_NAME: + report_error("GdUnitStringAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func failure_message() -> String: + return _base._current_error_message + + +func current_value(): + var current = _base.current_value() + if current == null: + return null + return current as String + + +func report_success() -> GdUnitStringAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitStringAssert: + _base.report_error(error) + return self + + +func override_failure_message(message :String) -> GdUnitStringAssert: + _base.override_failure_message(message) + return self + + +func is_null() -> GdUnitStringAssert: + _base.is_null() + return self + + +func is_not_null() -> GdUnitStringAssert: + _base.is_not_null() + return self + + +func is_equal(expected) -> GdUnitStringAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_equal(current, expected)) + if not GdObjects.equals(current, expected): + var diffs := GdDiffTool.string_diff(current, expected) + var formatted_current := GdAssertMessages.colored_array_div(diffs[1]) + return report_error(GdAssertMessages.error_equal(formatted_current, expected)) + return report_success() + + +func is_equal_ignoring_case(expected) -> GdUnitStringAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_equal_ignoring_case(current, expected)) + if not GdObjects.equals(current, expected, true): + var diffs := GdDiffTool.string_diff(current, expected) + var formatted_current := GdAssertMessages.colored_array_div(diffs[1]) + return report_error(GdAssertMessages.error_equal_ignoring_case(formatted_current, expected)) + return report_success() + + +func is_not_equal(expected) -> GdUnitStringAssert: + var current = current_value() + if GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() + + +func is_not_equal_ignoring_case(expected) -> GdUnitStringAssert: + var current = current_value() + if GdObjects.equals(current, expected, true): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() + + +func is_empty() -> GdUnitStringAssert: + var current = current_value() + if current == null or not current.is_empty(): + return report_error(GdAssertMessages.error_is_empty(current)) + return report_success() + + +func is_not_empty() -> GdUnitStringAssert: + var current = current_value() + if current == null or current.is_empty(): + return report_error(GdAssertMessages.error_is_not_empty()) + return report_success() + + +func contains(expected :String) -> GdUnitStringAssert: + var current = current_value() + if current == null or current.find(expected) == -1: + return report_error(GdAssertMessages.error_contains(current, expected)) + return report_success() + + +func not_contains(expected :String) -> GdUnitStringAssert: + var current = current_value() + if current != null and current.find(expected) != -1: + return report_error(GdAssertMessages.error_not_contains(current, expected)) + return report_success() + + +func contains_ignoring_case(expected :String) -> GdUnitStringAssert: + var current = current_value() + if current == null or current.findn(expected) == -1: + return report_error(GdAssertMessages.error_contains_ignoring_case(current, expected)) + return report_success() + + +func not_contains_ignoring_case(expected :String) -> GdUnitStringAssert: + var current = current_value() + if current != null and current.findn(expected) != -1: + return report_error(GdAssertMessages.error_not_contains_ignoring_case(current, expected)) + return report_success() + + +func starts_with(expected :String) -> GdUnitStringAssert: + var current = current_value() + if current == null or current.find(expected) != 0: + return report_error(GdAssertMessages.error_starts_with(current, expected)) + return report_success() + + +func ends_with(expected :String) -> GdUnitStringAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_ends_with(current, expected)) + var find = current.length() - expected.length() + if current.rfind(expected) != find: + return report_error(GdAssertMessages.error_ends_with(current, expected)) + return report_success() + + +# gdlint:disable=max-returns +func has_length(expected :int, comparator :int = Comparator.EQUAL) -> GdUnitStringAssert: + var current = current_value() + if current == null: + return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + match comparator: + Comparator.EQUAL: + if current.length() != expected: + return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + Comparator.LESS_THAN: + if current.length() >= expected: + return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + Comparator.LESS_EQUAL: + if current.length() > expected: + return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + Comparator.GREATER_THAN: + if current.length() <= expected: + return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + Comparator.GREATER_EQUAL: + if current.length() < expected: + return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + _: + return report_error("Comparator '%d' not implemented!" % comparator) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd new file mode 100644 index 0000000..9fbe25c --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd @@ -0,0 +1,172 @@ +extends GdUnitVectorAssert + +var _base: GdUnitAssert +var _current_type :int + + +func _init(current :Variant): + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", + ResourceLoader.CACHE_MODE_REUSE).new(current) + # save the actual assert instance on the current thread context + GdUnitThreadManager.get_current_context().set_assert(self) + if not _validate_value_type(current): + report_error("GdUnitVectorAssert error, the type <%s> is not supported." % GdObjects.typeof_as_string(current)) + _current_type = typeof(current) + + +func _notification(event): + if event == NOTIFICATION_PREDELETE: + if _base != null: + _base.notification(event) + _base = null + + +func _validate_value_type(value) -> bool: + return ( + value == null + or typeof(value) in [ + TYPE_VECTOR2, + TYPE_VECTOR2I, + TYPE_VECTOR3, + TYPE_VECTOR3I, + TYPE_VECTOR4, + TYPE_VECTOR4I + ] + ) + + +func _validate_is_vector_type(value :Variant) -> bool: + var type := typeof(value) + if type == _current_type or _current_type == TYPE_NIL: + return true + report_error(GdAssertMessages.error_is_wrong_type(_current_type, type)) + return false + + +func current_value() -> Variant: + return _base.current_value() + + +func report_success() -> GdUnitVectorAssert: + _base.report_success() + return self + + +func report_error(error :String) -> GdUnitVectorAssert: + _base.report_error(error) + return self + + +func failure_message() -> String: + return _base._current_error_message + + +func override_failure_message(message :String) -> GdUnitVectorAssert: + _base.override_failure_message(message) + return self + + +func is_null() -> GdUnitVectorAssert: + _base.is_null() + return self + + +func is_not_null() -> GdUnitVectorAssert: + _base.is_not_null() + return self + + +func is_equal(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + _base.is_equal(expected) + return self + + +func is_not_equal(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + _base.is_not_equal(expected) + return self + + +@warning_ignore("shadowed_global_identifier") +func is_equal_approx(expected :Variant, approx :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected) or not _validate_is_vector_type(approx): + return self + var current = current_value() + var from = expected - approx + var to = expected + approx + if current == null or (not _is_equal_approx(current, from, to)): + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() + + +func _is_equal_approx(current, from, to) -> bool: + match typeof(current): + TYPE_VECTOR2, TYPE_VECTOR2I: + return ((current.x >= from.x and current.y >= from.y) + and (current.x <= to.x and current.y <= to.y)) + TYPE_VECTOR3, TYPE_VECTOR3I: + return ((current.x >= from.x and current.y >= from.y and current.z >= from.z) + and (current.x <= to.x and current.y <= to.y and current.z <= to.z)) + TYPE_VECTOR4, TYPE_VECTOR4I: + return ((current.x >= from.x and current.y >= from.y and current.z >= from.z and current.w >= from.w) + and (current.x <= to.x and current.y <= to.y and current.z <= to.z and current.w <= to.w)) + _: + push_error("Missing implementation '_is_equal_approx' for vector type %s" % typeof(current)) + return false + + +func is_less(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current = current_value() + if current == null or current >= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected)) + return report_success() + + +func is_less_equal(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current = current_value() + if current == null or current > expected: + return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected)) + return report_success() + + +func is_greater(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current = current_value() + if current == null or current <= expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected)) + return report_success() + + +func is_greater_equal(expected :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(expected): + return self + var current = current_value() + if current == null or current < expected: + return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected)) + return report_success() + + +func is_between(from :Variant, to :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(from) or not _validate_is_vector_type(to): + return self + var current = current_value() + if current == null or not (current >= from and current <= to): + return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to)) + return report_success() + + +func is_not_between(from :Variant, to :Variant) -> GdUnitVectorAssert: + if not _validate_is_vector_type(from) or not _validate_is_vector_type(to): + return self + var current = current_value() + if (current != null and current >= from and current <= to): + return report_error(GdAssertMessages.error_is_value(Comparator.NOT_BETWEEN_EQUAL, current, from, to)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd b/addons/gdUnit4/src/asserts/ValueProvider.gd new file mode 100644 index 0000000..5150f4c --- /dev/null +++ b/addons/gdUnit4/src/asserts/ValueProvider.gd @@ -0,0 +1,6 @@ +# base interface for assert value provider +class_name ValueProvider +extends RefCounted + +func get_value(): + pass diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd new file mode 100644 index 0000000..350e53c --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd @@ -0,0 +1,61 @@ +class_name CmdArgumentParser +extends RefCounted + +var _options :CmdOptions +var _tool_name :String +var _parsed_commands :Dictionary = Dictionary() + + +func _init(p_options :CmdOptions, p_tool_name :String): + _options = p_options + _tool_name = p_tool_name + + +func parse(args :Array, ignore_unknown_cmd := false) -> GdUnitResult: + _parsed_commands.clear() + + # parse until first program argument + while not args.is_empty(): + var arg :String = args.pop_front() + if arg.find(_tool_name) != -1: + break + + if args.is_empty(): + return GdUnitResult.empty() + + # now parse all arguments + while not args.is_empty(): + var cmd :String = args.pop_front() + var option := _options.get_option(cmd) + + if option: + if _parse_cmd_arguments(option, args) == -1: + return GdUnitResult.error("The '%s' command requires an argument!" % option.short_command()) + elif not ignore_unknown_cmd: + return GdUnitResult.error("Unknown '%s' command!" % cmd) + return GdUnitResult.success(_parsed_commands.values()) + + +func options() -> CmdOptions: + return _options + + +func _parse_cmd_arguments(option :CmdOption, args :Array) -> int: + var command_name := option.short_command() + var command :CmdCommand = _parsed_commands.get(command_name, CmdCommand.new(command_name)) + + if option.has_argument(): + if not option.is_argument_optional() and args.is_empty(): + return -1 + if _is_next_value_argument(args): + command.add_argument(args.pop_front()) + elif not option.is_argument_optional(): + return -1 + _parsed_commands[command_name] = command + return 0 + + +func _is_next_value_argument(args :Array) -> bool: + if args.is_empty(): + return false + return _options.get_option(args[0]) == null diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd b/addons/gdUnit4/src/cmd/CmdCommand.gd new file mode 100644 index 0000000..c9c3414 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdCommand.gd @@ -0,0 +1,26 @@ +class_name CmdCommand +extends RefCounted + +var _name: String +var _arguments: PackedStringArray + + +func _init(p_name: String, p_arguments: = []): + _name = p_name + _arguments = PackedStringArray(p_arguments) + + +func name() -> String: + return _name + + +func arguments() -> PackedStringArray: + return _arguments + + +func add_argument(arg: String) -> void: + _arguments.append(arg) + + +func _to_string(): + return "%s:%s" % [_name, ", ".join(_arguments)] diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd new file mode 100644 index 0000000..3cc10cd --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd @@ -0,0 +1,105 @@ +class_name CmdCommandHandler +extends RefCounted + +const CB_SINGLE_ARG = 0 +const CB_MULTI_ARGS = 1 + +var _cmd_options :CmdOptions +# holds the command callbacks by key::String and value: [, ]:Array +var _command_cbs :Dictionary + +const NO_CB := Callable() + +# we only able to check cb function name since Godot 3.3.x +var _enhanced_fr_test := false + + +func _init(cmd_options: CmdOptions): + _cmd_options = cmd_options + var major: int = Engine.get_version_info()["major"] + var minor: int = Engine.get_version_info()["minor"] + if major == 3 and minor == 3: + _enhanced_fr_test = true + + +# register a callback function for given command +# cmd_name short name of the command +# fr_arg a funcref to a function with a single argument +func register_cb(cmd_name: String, cb: Callable = NO_CB) -> CmdCommandHandler: + var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB]) + if registered_cb[CB_SINGLE_ARG]: + push_error("A function for command '%s' is already registered!" % cmd_name) + return self + registered_cb[CB_SINGLE_ARG] = cb + _command_cbs[cmd_name] = registered_cb + return self + + +# register a callback function for given command +# cb a funcref to a function with a variable number of arguments but expects all parameters to be passed via a single Array. +func register_cbv(cmd_name: String, cb: Callable) -> CmdCommandHandler: + var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB]) + if registered_cb[CB_MULTI_ARGS]: + push_error("A function for command '%s' is already registered!" % cmd_name) + return self + registered_cb[CB_MULTI_ARGS] = cb + _command_cbs[cmd_name] = registered_cb + return self + + +func _validate() -> GdUnitResult: + var errors: = PackedStringArray() + var registered_cbs: = Dictionary() + + for cmd_name in _command_cbs.keys(): + var cb: Callable = _command_cbs[cmd_name][CB_SINGLE_ARG] if _command_cbs[cmd_name][CB_SINGLE_ARG] else _command_cbs[cmd_name][CB_MULTI_ARGS] + if cb != NO_CB and not cb.is_valid(): + errors.append("Invalid function reference for command '%s', Check the function reference!" % cmd_name) + if _cmd_options.get_option(cmd_name) == null: + errors.append("The command '%s' is unknown, verify your CmdOptions!" % cmd_name) + # verify for multiple registered command callbacks + if _enhanced_fr_test and cb != NO_CB: + var cb_method: = cb.get_method() + if registered_cbs.has(cb_method): + var already_registered_cmd = registered_cbs[cb_method] + errors.append("The function reference '%s' already registerd for command '%s'!" % [cb_method, already_registered_cmd]) + else: + registered_cbs[cb_method] = cmd_name + if errors.is_empty(): + return GdUnitResult.success(true) + else: + return GdUnitResult.error("\n".join(errors)) + + +func execute(commands :Array) -> GdUnitResult: + var result := _validate() + if result.is_error(): + return result + for index in commands.size(): + var cmd :CmdCommand = commands[index] + assert(cmd is CmdCommand) #,"commands contains invalid command object '%s'" % cmd) + var cmd_name := cmd.name() + if _command_cbs.has(cmd_name): + var cb_s :Callable = _command_cbs.get(cmd_name)[CB_SINGLE_ARG] + var arguments := cmd.arguments() + var cmd_option := _cmd_options.get_option(cmd_name) + var argument = arguments[0] if arguments.size() > 0 else null + match cmd_option.type(): + TYPE_BOOL: + argument = true if argument == "true" else false + if cb_s and arguments.size() == 0: + cb_s.call() + elif cb_s: + cb_s.call(argument) + else: + var cb_m :Callable = _command_cbs.get(cmd_name)[CB_MULTI_ARGS] + # we need to find the method and determin the arguments to call the right function + for m in cb_m.get_object().get_method_list(): + if m["name"] == cb_m.get_method(): + if m["args"].size() > 1: + cb_m.callv(arguments) + break + else: + cb_m.call(arguments) + break + return GdUnitResult.success(true) diff --git a/addons/gdUnit4/src/cmd/CmdConsole.gd b/addons/gdUnit4/src/cmd/CmdConsole.gd new file mode 100644 index 0000000..8861e78 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdConsole.gd @@ -0,0 +1,147 @@ +# prototype of console with CSI support +# https://notes.burke.libbey.me/ansi-escape-codes/ +class_name CmdConsole +extends RefCounted + +const BOLD = 0x1 +const ITALIC = 0x2 +const UNDERLINE = 0x4 + +const __CSI_BOLD = "" +const __CSI_ITALIC = "" +const __CSI_UNDERLINE = "" + +enum { + COLOR_TABLE, + COLOR_RGB +} + + +# Control Sequence Introducer +#var csi := PackedByteArray([0x1b]).get_string_from_ascii() +var _debug_show_color_codes := false +var _color_mode = COLOR_TABLE + + +func color(p_color :Color) -> CmdConsole: + # using color table 16 - 231 a 6 x 6 x 6 RGB color cube (16 + R * 36 + G * 6 + B) + if _color_mode == COLOR_TABLE: + @warning_ignore("integer_division") + var c2 = 16 + (int(p_color.r8/42) * 36) + (int(p_color.g8/42) * 6) + int(p_color.b8/42) + if _debug_show_color_codes: + printraw("%6d" % [c2]) + printraw("[38;5;%dm" % c2 ) + else: + printraw("[38;2;%d;%d;%dm" % [p_color.r8, p_color.g8, p_color.b8] ) + return self + + +func save_cursor() -> CmdConsole: + printraw("") + return self + + +func restore_cursor() -> CmdConsole: + printraw("") + return self + + +func end_color() -> CmdConsole: + printraw("") + return self + + +func row_pos(row :int) -> CmdConsole: + printraw("[%d;0H" % row ) + return self + + +func scrollArea(from :int, to :int ) -> CmdConsole: + printraw("[%d;%dr" % [from ,to]) + return self + + +func progressBar(p_progress :int, p_color :Color = Color.POWDER_BLUE) -> CmdConsole: + if p_progress < 0: + p_progress = 0 + if p_progress > 100: + p_progress = 100 + color(p_color) + printraw("[%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "■").rpad(50, "-"), p_progress]) + end_color() + return self + + +func printl(value :String) -> CmdConsole: + printraw(value) + return self + + +func new_line() -> CmdConsole: + prints() + return self + + +func reset() -> CmdConsole: + return self + + +func bold(enable :bool) -> CmdConsole: + if enable: + printraw(__CSI_BOLD) + return self + + +func italic(enable :bool) -> CmdConsole: + if enable: + printraw(__CSI_ITALIC) + return self + + +func underline(enable :bool) -> CmdConsole: + if enable: + printraw(__CSI_UNDERLINE) + return self + + +func prints_error(message :String) -> CmdConsole: + return color(Color.CRIMSON).printl(message).end_color().new_line() + + +func prints_warning(message :String) -> CmdConsole: + return color(Color.GOLDENROD).printl(message).end_color().new_line() + + +func prints_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole: + return print_color(p_message, p_color, p_flags).new_line() + + +func print_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole: + return color(p_color)\ + .bold(p_flags&BOLD == BOLD)\ + .italic(p_flags&ITALIC == ITALIC)\ + .underline(p_flags&UNDERLINE == UNDERLINE)\ + .printl(p_message)\ + .end_color() + + +func print_color_table(): + prints_color("Color Table 6x6x6", Color.ANTIQUE_WHITE) + _debug_show_color_codes = true + for green in range(0, 6): + for red in range(0, 6): + for blue in range(0, 6): + print_color("████████ ", Color8(red*42, green*42, blue*42)) + new_line() + new_line() + + prints_color("Color Table RGB", Color.ANTIQUE_WHITE) + _color_mode = COLOR_RGB + for green in range(0, 6): + for red in range(0, 6): + for blue in range(0, 6): + print_color("████████ ", Color8(red*42, green*42, blue*42)) + new_line() + new_line() + _color_mode = COLOR_TABLE + _debug_show_color_codes = false diff --git a/addons/gdUnit4/src/cmd/CmdOption.gd b/addons/gdUnit4/src/cmd/CmdOption.gd new file mode 100644 index 0000000..a562a03 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdOption.gd @@ -0,0 +1,61 @@ +class_name CmdOption +extends RefCounted + + +var _commands :PackedStringArray +var _help :String +var _description :String +var _type :int +var _arg_optional :bool = false + + +# constructs a command option by given arguments +# commands : a string with comma separated list of available commands begining with the short form +# help: a help text show howto use +# description: a full description of the command +# type: the argument type +# arg_optional: defines of the argument optional +func _init(p_commands :String, p_help :String, p_description :String, p_type :int = TYPE_NIL, p_arg_optional :bool = false): + _commands = p_commands.replace(" ", "").replace("\t", "").split(",") + _help = p_help + _description = p_description + _type = p_type + _arg_optional = p_arg_optional + + +func commands() -> PackedStringArray: + return _commands + + +func short_command() -> String: + return _commands[0] + + +func help() -> String: + return _help + + +func description() -> String: + return _description + + +func type() -> int: + return _type + + +func is_argument_optional() -> bool: + return _arg_optional + + +func has_argument() -> bool: + return _type != TYPE_NIL + + +func describe() -> String: + if help().is_empty(): + return " %-32s %s \n" % [commands(), description()] + return " %-32s %s \n %-32s %s\n" % [commands(), description(), "", help()] + + +func _to_string(): + return describe() diff --git a/addons/gdUnit4/src/cmd/CmdOptions.gd b/addons/gdUnit4/src/cmd/CmdOptions.gd new file mode 100644 index 0000000..2884614 --- /dev/null +++ b/addons/gdUnit4/src/cmd/CmdOptions.gd @@ -0,0 +1,31 @@ +class_name CmdOptions +extends RefCounted + + +var _default_options :Array +var _advanced_options :Array + + +func _init(p_options :Array = Array(), p_advanced_options :Array = Array()): + # default help options + _default_options = p_options + _advanced_options = p_advanced_options + + +func default_options() -> Array: + return _default_options + + +func advanced_options() -> Array: + return _advanced_options + + +func options() -> Array: + return default_options() + advanced_options() + + +func get_option(cmd :String) -> CmdOption: + for option in options(): + if Array(option.commands()).has(cmd): + return option + return null diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd b/addons/gdUnit4/src/core/GdArrayTools.gd new file mode 100644 index 0000000..1d1b35b --- /dev/null +++ b/addons/gdUnit4/src/core/GdArrayTools.gd @@ -0,0 +1,101 @@ +## Small helper tool to work with Godot Arrays +class_name GdArrayTools +extends RefCounted + + +const max_elements := 32 +const ARRAY_TYPES := [ + TYPE_ARRAY, + TYPE_PACKED_BYTE_ARRAY, + TYPE_PACKED_INT32_ARRAY, + TYPE_PACKED_INT64_ARRAY, + TYPE_PACKED_FLOAT32_ARRAY, + TYPE_PACKED_FLOAT64_ARRAY, + TYPE_PACKED_STRING_ARRAY, + TYPE_PACKED_VECTOR2_ARRAY, + TYPE_PACKED_VECTOR3_ARRAY, + TYPE_PACKED_COLOR_ARRAY +] + + +static func is_array_type(value) -> bool: + return is_type_array(typeof(value)) + + +static func is_type_array(type :int) -> bool: + return type in ARRAY_TYPES + + +## Filters an array by given value[br] +## If the given value not an array it returns null, will remove all occurence of given value. +static func filter_value(array, value :Variant) -> Variant: + if not is_array_type(array): + return null + var filtered_array = array.duplicate() + var index :int = filtered_array.find(value) + while index != -1: + filtered_array.remove_at(index) + index = filtered_array.find(value) + return filtered_array + + +## Erases a value from given array by using equals(l,r) to find the element to erase +static func erase_value(array :Array, value) -> void: + for element in array: + if GdObjects.equals(element, value): + array.erase(element) + + +## Scans for the array build in type on a untyped array[br] +## Returns the buildin type by scan all values and returns the type if all values has the same type. +## If the values has different types TYPE_VARIANT is returend +static func scan_typed(array :Array) -> int: + if array.is_empty(): + return TYPE_NIL + var actual_type := GdObjects.TYPE_VARIANT + for value in array: + var current_type := typeof(value) + if not actual_type in [GdObjects.TYPE_VARIANT, current_type]: + return GdObjects.TYPE_VARIANT + actual_type = current_type + return actual_type + + +## Converts given array into a string presentation.[br] +## This function is different to the original Godot str() implementation. +## The string presentaion contains fullquallified typed informations. +##[br] +## Examples: +## [codeblock] +## # will result in PackedString(["a", "b"]) +## GdArrayTools.as_string(PackedStringArray("a", "b")) +## # will result in PackedString(["a", "b"]) +## GdArrayTools.as_string(PackedColorArray(Color.RED, COLOR.GREEN)) +## [/codeblock] +static func as_string(elements :Variant, encode_value := true) -> String: + if not is_array_type(elements): + return "ERROR: Not an Array Type!" + var delemiter = ", " + if elements == null: + return "" + if elements.is_empty(): + return "" + var prefix := _typeof_as_string(elements) if encode_value else "" + var formatted := "" + var index := 0 + for element in elements: + if max_elements != -1 and index > max_elements: + return prefix + "[" + formatted + delemiter + "...]" + if formatted.length() > 0 : + formatted += delemiter + formatted += GdDefaultValueDecoder.decode(element) if encode_value else str(element) + index += 1 + return prefix + "[" + formatted + "]" + + +static func _typeof_as_string(value :Variant) -> String: + var type := typeof(value) + # for untyped array we retun empty string + if type == TYPE_ARRAY: + return "" + return GdObjects.typeof_as_string(value) diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd b/addons/gdUnit4/src/core/GdDiffTool.gd new file mode 100644 index 0000000..fe996f2 --- /dev/null +++ b/addons/gdUnit4/src/core/GdDiffTool.gd @@ -0,0 +1,155 @@ +# A tool to find differences between two objects +class_name GdDiffTool +extends RefCounted + + +const DIV_ADD :int = 214 +const DIV_SUB :int = 215 + + +static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array, ldiff: Array, rdiff: Array): + var loffset = lb.size() + var roffset = rb.size() + + while true: + #if last character of X and Y matches + if loffset > 0 && roffset > 0 && lb[loffset - 1] == rb[roffset - 1]: + loffset -= 1 + roffset -= 1 + ldiff.push_front(lb[loffset]) + rdiff.push_front(rb[roffset]) + continue + #current character of Y is not present in X + else: if (roffset > 0 && (loffset == 0 || lookup[loffset][roffset - 1] >= lookup[loffset - 1][roffset])): + roffset -= 1 + ldiff.push_front(rb[roffset]) + ldiff.push_front(DIV_ADD) + rdiff.push_front(rb[roffset]) + rdiff.push_front(DIV_SUB) + continue + #current character of X is not present in Y + else: if (loffset > 0 && (roffset == 0 || lookup[loffset][roffset - 1] < lookup[loffset - 1][roffset])): + loffset -= 1 + ldiff.push_front(lb[loffset]) + ldiff.push_front(DIV_SUB) + rdiff.push_front(lb[loffset]) + rdiff.push_front(DIV_ADD) + continue + break + + +# lookup[i][j] stores the length of LCS of substring X[0..i-1], Y[0..j-1] +static func _createLookUp(lb: PackedByteArray, rb: PackedByteArray) -> Array: + var lookup := Array() + lookup.resize(lb.size() + 1) + for i in lookup.size(): + var x = [] + x.resize(rb.size() + 1) + lookup[i] = x + return lookup + + +static func _buildLookup(lb: PackedByteArray, rb: PackedByteArray) -> Array: + var lookup := _createLookUp(lb, rb) + # first column of the lookup table will be all 0 + for i in lookup.size(): + lookup[i][0] = 0 + # first row of the lookup table will be all 0 + for j in lookup[0].size(): + lookup[0][j] = 0 + + # fill the lookup table in bottom-up manner + for i in range(1, lookup.size()): + for j in range(1, lookup[0].size()): + # if current character of left and right matches + if lb[i - 1] == rb[j - 1]: + lookup[i][j] = lookup[i - 1][j - 1] + 1; + # else if current character of left and right don't match + else: + lookup[i][j] = max(lookup[i - 1][j], lookup[i][j - 1]); + return lookup + + +static func string_diff(left, right) -> Array[PackedByteArray]: + var lb := PackedByteArray() if left == null else str(left).to_ascii_buffer() + var rb := PackedByteArray() if right == null else str(right).to_ascii_buffer() + var ldiff := Array() + var rdiff := Array() + var lookup := _buildLookup(lb, rb); + _diff(lb, rb, lookup, ldiff, rdiff) + return [PackedByteArray(ldiff), PackedByteArray(rdiff)] + + +# prototype +static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStringArray: + var text1Words := text1.split(" ") + var text2Words := text2.split(" ") + var text1WordCount := text1Words.size() + var text2WordCount := text2Words.size() + var solutionMatrix := Array() + for i in text1WordCount+1: + var ar := Array() + for n in text2WordCount+1: + ar.append(0) + solutionMatrix.append(ar) + + for i in range(text1WordCount-1, 0, -1): + for j in range(text2WordCount-1, 0, -1): + if text1Words[i] == text2Words[j]: + solutionMatrix[i][j] = solutionMatrix[i + 1][j + 1] + 1; + else: + solutionMatrix[i][j] = max(solutionMatrix[i + 1][j], solutionMatrix[i][j + 1]); + + var i = 0 + var j = 0 + var lcsResultList := PackedStringArray(); + while (i < text1WordCount && j < text2WordCount): + if text1Words[i] == text2Words[j]: + lcsResultList.append(text2Words[j]) + i += 1 + j += 1 + else: if (solutionMatrix[i + 1][j] >= solutionMatrix[i][j + 1]): + i += 1 + else: + j += 1 + return lcsResultList + + +static func markTextDifferences(text1 :String, text2 :String, lcsList :PackedStringArray, insertColor :Color, deleteColor:Color) -> String: + var stringBuffer = "" + if text1 == null and lcsList == null: + return stringBuffer + + var text1Words := text1.split(" ") + var text2Words := text2.split(" ") + var i = 0 + var j = 0 + var word1LastIndex = 0 + var word2LastIndex = 0 + for k in lcsList.size(): + while i < text1Words.size() and j < text2Words.size(): + if text1Words[i] == lcsList[k] and text2Words[j] == lcsList[k]: + stringBuffer += "" + lcsList[k] + " " + word1LastIndex = i + 1 + word2LastIndex = j + 1 + i = text1Words.size() + j = text2Words.size() + + else: if text1Words[i] != lcsList[k]: + while i < text1Words.size() and text1Words[i] != lcsList[k]: + stringBuffer += "" + text1Words[i] + " " + i += 1 + else: if text2Words[j] != lcsList[k]: + while j < text2Words.size() and text2Words[j] != lcsList[k]: + stringBuffer += "" + text2Words[j] + " " + j += 1 + i = word1LastIndex + j = word2LastIndex + + while word1LastIndex < text1Words.size(): + stringBuffer += "" + text1Words[word1LastIndex] + " " + word1LastIndex += 1 + while word2LastIndex < text2Words.size(): + stringBuffer += "" + text2Words[word2LastIndex] + " " + word2LastIndex += 1 + return stringBuffer diff --git a/addons/gdUnit4/src/core/GdFunctionDoubler.gd b/addons/gdUnit4/src/core/GdFunctionDoubler.gd new file mode 100644 index 0000000..3d3e2ea --- /dev/null +++ b/addons/gdUnit4/src/core/GdFunctionDoubler.gd @@ -0,0 +1,191 @@ +class_name GdFunctionDoubler +extends RefCounted + +const DEFAULT_TYPED_RETURN_VALUES := { + TYPE_NIL: "null", + TYPE_BOOL: "false", + TYPE_INT: "0", + TYPE_FLOAT: "0.0", + TYPE_STRING: "\"\"", + TYPE_STRING_NAME: "&\"\"", + TYPE_VECTOR2: "Vector2.ZERO", + TYPE_VECTOR2I: "Vector2i.ZERO", + TYPE_RECT2: "Rect2()", + TYPE_RECT2I: "Rect2i()", + TYPE_VECTOR3: "Vector3.ZERO", + TYPE_VECTOR3I: "Vector3i.ZERO", + TYPE_VECTOR4: "Vector4.ZERO", + TYPE_VECTOR4I: "Vector4i.ZERO", + TYPE_TRANSFORM2D: "Transform2D()", + TYPE_PLANE: "Plane()", + TYPE_QUATERNION: "Quaternion()", + TYPE_AABB: "AABB()", + TYPE_BASIS: "Basis()", + TYPE_TRANSFORM3D: "Transform3D()", + TYPE_PROJECTION: "Projection()", + TYPE_COLOR: "Color()", + TYPE_NODE_PATH: "NodePath()", + TYPE_RID: "RID()", + TYPE_OBJECT: "null", + TYPE_CALLABLE: "Callable()", + TYPE_SIGNAL: "Signal()", + TYPE_DICTIONARY: "Dictionary()", + TYPE_ARRAY: "Array()", + TYPE_PACKED_BYTE_ARRAY: "PackedByteArray()", + TYPE_PACKED_INT32_ARRAY: "PackedInt32Array()", + TYPE_PACKED_INT64_ARRAY: "PackedInt64Array()", + TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array()", + TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array()", + TYPE_PACKED_STRING_ARRAY: "PackedStringArray()", + TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()", + TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()", + TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()", + GdObjects.TYPE_VARIANT: "null", + GdObjects.TYPE_ENUM: "0" +} + +# @GlobalScript enums +# needs to manually map because of https://github.com/godotengine/godot/issues/73835 +const DEFAULT_ENUM_RETURN_VALUES = { + "Side" : "SIDE_LEFT", + "Corner" : "CORNER_TOP_LEFT", + "Orientation" : "HORIZONTAL", + "ClockDirection" : "CLOCKWISE", + "HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT", + "VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP", + "InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO", + "EulerOrder" : "EULER_ORDER_XYZ", + "Key" : "KEY_NONE", + "KeyModifierMask" : "KEY_CODE_MASK", + "MouseButton" : "MOUSE_BUTTON_NONE", + "MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT", + "JoyButton" : "JOY_BUTTON_INVALID", + "JoyAxis" : "JOY_AXIS_INVALID", + "MIDIMessage" : "MIDI_MESSAGE_NONE", + "Error" : "OK", + "PropertyHint" : "PROPERTY_HINT_NONE", + "Variant.Type" : "TYPE_NIL", +} + +var _push_errors :String + + +# Determine the enum default by reflection +static func get_enum_default(value :String) -> Variant: + var script := GDScript.new() + script.source_code = """ + extends Resource + + static func get_enum_default() -> Variant: + return %s.values()[0] + + """.dedent() % value + script.reload() + script.source_code + return script.new().call("get_enum_default") + + +static func default_return_value(func_descriptor :GdFunctionDescriptor) -> String: + var return_type :Variant = func_descriptor.return_type() + if return_type == GdObjects.TYPE_ENUM: + var enum_class := func_descriptor._return_class + var enum_path := enum_class.split(".") + if enum_path.size() >= 2: + var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1]) + if not keys.is_empty(): + return "%s.%s" % [enum_path[0], keys[0]] + var enum_value := get_enum_default(enum_class) + if enum_value != null: + return str(enum_value) + # we need fallback for @GlobalScript enums, + return DEFAULT_ENUM_RETURN_VALUES.get(func_descriptor._return_class, "0") + return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid") + + +func _init(push_errors :bool = false): + _push_errors = "true" if push_errors else "false" + for type_key in TYPE_MAX: + if not DEFAULT_TYPED_RETURN_VALUES.has(type_key): + push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX]) + prints("missing default definition for type", type_key) + assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!") + + +@warning_ignore("unused_parameter") +func get_template(return_type :Variant, is_vararg :bool) -> String: + push_error("Must be implemented!") + return "" + +func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray: + var func_signature := func_descriptor.typeless() + var is_static := func_descriptor.is_static() + var is_vararg := func_descriptor.is_vararg() + var is_coroutine := func_descriptor.is_coroutine() + var func_name := func_descriptor.name() + var args := func_descriptor.args() + var varargs := func_descriptor.varargs() + var return_value := GdFunctionDoubler.default_return_value(func_descriptor) + var arg_names := extract_arg_names(args) + var vararg_names := extract_arg_names(varargs) + + # save original constructor arguments + if func_name == "_init": + var constructor_args := ",".join(GdFunctionDoubler.extract_constructor_args(args)) + var constructor := "func _init(%s) -> void:\n super(%s)\n pass\n" % [constructor_args, ", ".join(arg_names)] + return constructor.split("\n") + + var double_src := "" + double_src += '@warning_ignore("untyped_declaration")\n' if Engine.get_version_info().hex >= 0x40200 else '\n' + if func_descriptor.is_engine(): + double_src += '@warning_ignore("native_method_override")\n' + if func_descriptor.return_type() == GdObjects.TYPE_ENUM: + double_src += '@warning_ignore("int_as_enum_without_match")\n' + double_src += '@warning_ignore("int_as_enum_without_cast")\n' + double_src += '@warning_ignore("shadowed_variable")\n' + double_src += func_signature + # fix to unix format, this is need when the template is edited under windows than the template is stored with \r\n + var func_template := get_template(func_descriptor.return_type(), is_vararg).replace("\r\n", "\n") + double_src += func_template\ + .replace("$(arguments)", ", ".join(arg_names))\ + .replace("$(varargs)", ", ".join(vararg_names))\ + .replace("$(await)", GdFunctionDoubler.await_is_coroutine(is_coroutine)) \ + .replace("$(func_name)", func_name )\ + .replace("${default_return_value}", return_value)\ + .replace("$(push_errors)", _push_errors) + + if is_static: + double_src = double_src.replace("$(instance)", "__instance().") + else: + double_src = double_src.replace("$(instance)", "") + return double_src.split("\n") + + +func extract_arg_names(argument_signatures :Array[GdFunctionArgument]) -> PackedStringArray: + var arg_names := PackedStringArray() + for arg in argument_signatures: + arg_names.append(arg._name) + return arg_names + + +static func extract_constructor_args(args :Array) -> PackedStringArray: + var constructor_args := PackedStringArray() + for arg in args: + var a := arg as GdFunctionArgument + var arg_name := a._name + var default_value = get_default(a) + if default_value == "null": + constructor_args.append(arg_name + ":Variant=" + default_value) + else: + constructor_args.append(arg_name + ":=" + default_value) + return constructor_args + + +static func get_default(arg :GdFunctionArgument) -> String: + if arg.has_default(): + return arg.value_as_string() + else: + return DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null") + + +static func await_is_coroutine(is_coroutine :bool) -> String: + return "await " if is_coroutine else "" diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd new file mode 100644 index 0000000..66e94b7 --- /dev/null +++ b/addons/gdUnit4/src/core/GdObjects.gd @@ -0,0 +1,686 @@ +# This is a helper class to compare two objects by equals +class_name GdObjects +extends Resource + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const TYPE_VOID = TYPE_MAX + 1000 +const TYPE_VARARG = TYPE_MAX + 1001 +const TYPE_VARIANT = TYPE_MAX + 1002 +const TYPE_FUNC = TYPE_MAX + 1003 +const TYPE_FUZZER = TYPE_MAX + 1004 + +const TYPE_NODE = TYPE_MAX + 2001 +# missing Godot types +const TYPE_CONTROL = TYPE_MAX + 2002 +const TYPE_CANVAS = TYPE_MAX + 2003 +const TYPE_ENUM = TYPE_MAX + 2004 + + +# used as default value for varargs +const TYPE_VARARG_PLACEHOLDER_VALUE = "__null__" + + +const TYPE_AS_STRING_MAPPINGS := { + TYPE_NIL: "null", + TYPE_BOOL: "bool", + TYPE_INT: "int", + TYPE_FLOAT: "float", + TYPE_STRING: "String", + TYPE_VECTOR2: "Vector2", + TYPE_VECTOR2I: "Vector2i", + TYPE_RECT2: "Rect2", + TYPE_RECT2I: "Rect2i", + TYPE_VECTOR3: "Vector3", + TYPE_VECTOR3I: "Vector3i", + TYPE_TRANSFORM2D: "Transform2D", + TYPE_VECTOR4: "Vector4", + TYPE_VECTOR4I: "Vector4i", + TYPE_PLANE: "Plane", + TYPE_QUATERNION: "Quaternion", + TYPE_AABB: "AABB", + TYPE_BASIS: "Basis", + TYPE_TRANSFORM3D: "Transform3D", + TYPE_PROJECTION: "Projection", + TYPE_COLOR: "Color", + TYPE_STRING_NAME: "StringName", + TYPE_NODE_PATH: "NodePath", + TYPE_RID: "RID", + TYPE_OBJECT: "Object", + TYPE_CALLABLE: "Callable", + TYPE_SIGNAL: "Signal", + TYPE_DICTIONARY: "Dictionary", + TYPE_ARRAY: "Array", + TYPE_PACKED_BYTE_ARRAY: "PackedByteArray", + TYPE_PACKED_INT32_ARRAY: "PackedInt32Array", + TYPE_PACKED_INT64_ARRAY: "PackedInt64Array", + TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array", + TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array", + TYPE_PACKED_STRING_ARRAY: "PackedStringArray", + TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array", + TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array", + TYPE_PACKED_COLOR_ARRAY: "PackedColorArray", + TYPE_VOID: "void", + TYPE_VARARG: "VarArg", + TYPE_FUNC: "Func", + TYPE_FUZZER: "Fuzzer", + TYPE_VARIANT: "Variant" +} + + +const NOTIFICATION_AS_STRING_MAPPINGS := { + TYPE_OBJECT: { + Object.NOTIFICATION_POSTINITIALIZE : "POSTINITIALIZE", + Object.NOTIFICATION_PREDELETE: "PREDELETE", + EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED", + }, + TYPE_NODE: { + Node.NOTIFICATION_ENTER_TREE : "ENTER_TREE", + Node.NOTIFICATION_EXIT_TREE: "EXIT_TREE", + Node.NOTIFICATION_MOVED_IN_PARENT: "MOVED_IN_PARENT", + Node.NOTIFICATION_READY: "READY", + Node.NOTIFICATION_PAUSED: "PAUSED", + Node.NOTIFICATION_UNPAUSED: "UNPAUSED", + Node.NOTIFICATION_PHYSICS_PROCESS: "PHYSICS_PROCESS", + Node.NOTIFICATION_PROCESS: "PROCESS", + Node.NOTIFICATION_PARENTED: "PARENTED", + Node.NOTIFICATION_UNPARENTED: "UNPARENTED", + Node.NOTIFICATION_SCENE_INSTANTIATED: "INSTANCED", + Node.NOTIFICATION_DRAG_BEGIN: "DRAG_BEGIN", + Node.NOTIFICATION_DRAG_END: "DRAG_END", + Node.NOTIFICATION_PATH_RENAMED: "PATH_CHANGED", + Node.NOTIFICATION_INTERNAL_PROCESS: "INTERNAL_PROCESS", + Node.NOTIFICATION_INTERNAL_PHYSICS_PROCESS: "INTERNAL_PHYSICS_PROCESS", + Node.NOTIFICATION_POST_ENTER_TREE: "POST_ENTER_TREE", + Node.NOTIFICATION_WM_MOUSE_ENTER: "WM_MOUSE_ENTER", + Node.NOTIFICATION_WM_MOUSE_EXIT: "WM_MOUSE_EXIT", + Node.NOTIFICATION_APPLICATION_FOCUS_IN: "WM_FOCUS_IN", + Node.NOTIFICATION_APPLICATION_FOCUS_OUT: "WM_FOCUS_OUT", + #Node.NOTIFICATION_WM_QUIT_REQUEST: "WM_QUIT_REQUEST", + Node.NOTIFICATION_WM_GO_BACK_REQUEST: "WM_GO_BACK_REQUEST", + Node.NOTIFICATION_WM_WINDOW_FOCUS_OUT: "WM_UNFOCUS_REQUEST", + Node.NOTIFICATION_OS_MEMORY_WARNING: "OS_MEMORY_WARNING", + Node.NOTIFICATION_TRANSLATION_CHANGED: "TRANSLATION_CHANGED", + Node.NOTIFICATION_WM_ABOUT: "WM_ABOUT", + Node.NOTIFICATION_CRASH: "CRASH", + Node.NOTIFICATION_OS_IME_UPDATE: "OS_IME_UPDATE", + Node.NOTIFICATION_APPLICATION_RESUMED: "APP_RESUMED", + Node.NOTIFICATION_APPLICATION_PAUSED: "APP_PAUSED", + Node3D.NOTIFICATION_TRANSFORM_CHANGED: "TRANSFORM_CHANGED", + Node3D.NOTIFICATION_ENTER_WORLD: "ENTER_WORLD", + Node3D.NOTIFICATION_EXIT_WORLD: "EXIT_WORLD", + Node3D.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED", + Skeleton3D.NOTIFICATION_UPDATE_SKELETON: "UPDATE_SKELETON", + CanvasItem.NOTIFICATION_DRAW: "DRAW", + CanvasItem.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED", + CanvasItem.NOTIFICATION_ENTER_CANVAS: "ENTER_CANVAS", + CanvasItem.NOTIFICATION_EXIT_CANVAS: "EXIT_CANVAS", + #Popup.NOTIFICATION_POST_POPUP: "POST_POPUP", + #Popup.NOTIFICATION_POPUP_HIDE: "POPUP_HIDE", + }, + TYPE_CONTROL : { + Container.NOTIFICATION_SORT_CHILDREN: "SORT_CHILDREN", + Control.NOTIFICATION_RESIZED: "RESIZED", + Control.NOTIFICATION_MOUSE_ENTER: "MOUSE_ENTER", + Control.NOTIFICATION_MOUSE_EXIT: "MOUSE_EXIT", + Control.NOTIFICATION_FOCUS_ENTER: "FOCUS_ENTER", + Control.NOTIFICATION_FOCUS_EXIT: "FOCUS_EXIT", + Control.NOTIFICATION_THEME_CHANGED: "THEME_CHANGED", + #Control.NOTIFICATION_MODAL_CLOSE: "MODAL_CLOSE", + Control.NOTIFICATION_SCROLL_BEGIN: "SCROLL_BEGIN", + Control.NOTIFICATION_SCROLL_END: "SCROLL_END", + } +} + + +enum COMPARE_MODE { + OBJECT_REFERENCE, + PARAMETER_DEEP_TEST +} + + +# prototype of better object to dictionary +static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary: + if obj == null: + return {} + var clazz_name := obj.get_class() + var dict := Dictionary() + var clazz_path := "" + + if is_instance_valid(obj) and obj.get_script() != null: + var d := inst_to_dict(obj) + clazz_path = d["@path"] + if d["@subpath"] != NodePath(""): + clazz_name = d["@subpath"] + dict["@inner_class"] = true + else: + clazz_name = clazz_path.get_file().replace(".gd", "") + dict["@path"] = clazz_path + + for property in obj.get_property_list(): + var property_name = property["name"] + var property_type = property["type"] + var property_value = obj.get(property_name) + if property_value is GDScript or property_value is Callable: + continue + if (property["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE|PROPERTY_USAGE_DEFAULT + and not property["usage"] & PROPERTY_USAGE_CATEGORY + and not property["usage"] == 0): + if property_type == TYPE_OBJECT: + # prevent recursion + if hashed_objects.has(obj): + dict[property_name] = str(property_value) + continue + hashed_objects[obj] = true + dict[property_name] = obj2dict(property_value, hashed_objects) + else: + dict[property_name] = property_value + return {"%s" % clazz_name : dict} + + +static func equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: + return _equals(obj_a, obj_b, case_sensitive, compare_mode, [], 0) + + +static func equals_sorted(obj_a :Array, obj_b :Array, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: + var a := obj_a.duplicate() + var b := obj_b.duplicate() + a.sort() + b.sort() + return equals(a, b, case_sensitive, compare_mode) + + +static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compare_mode :COMPARE_MODE, deep_stack :Array, stack_depth :int ) -> bool: + var type_a := typeof(obj_a) + var type_b := typeof(obj_b) + if stack_depth > 32: + prints("stack_depth", stack_depth, deep_stack) + push_error("GdUnit equals has max stack deep reached!") + return false + + # use argument matcher if requested + if is_instance_valid(obj_a) and obj_a is GdUnitArgumentMatcher: + return (obj_a as GdUnitArgumentMatcher).is_match(obj_b) + if is_instance_valid(obj_b) and obj_b is GdUnitArgumentMatcher: + return (obj_b as GdUnitArgumentMatcher).is_match(obj_a) + + stack_depth += 1 + # fast fail is different types + if not _is_type_equivalent(type_a, type_b): + return false + # is same instance + if obj_a == obj_b: + return true + # handle null values + if obj_a == null and obj_b != null: + return false + if obj_b == null and obj_a != null: + return false + + match type_a: + TYPE_OBJECT: + if deep_stack.has(obj_a) or deep_stack.has(obj_b): + return true + deep_stack.append(obj_a) + deep_stack.append(obj_b) + if compare_mode == COMPARE_MODE.PARAMETER_DEEP_TEST: + # fail fast + if not is_instance_valid(obj_a) or not is_instance_valid(obj_b): + return false + if obj_a.get_class() != obj_b.get_class(): + return false + var a = obj2dict(obj_a) + var b = obj2dict(obj_b) + return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth) + return obj_a == obj_b + + TYPE_ARRAY: + if obj_a.size() != obj_b.size(): + return false + for index in obj_a.size(): + if not _equals(obj_a[index], obj_b[index], case_sensitive, compare_mode, deep_stack, stack_depth): + return false + return true + + TYPE_DICTIONARY: + if obj_a.size() != obj_b.size(): + return false + for key in obj_a.keys(): + var value_a = obj_a[key] if obj_a.has(key) else null + var value_b = obj_b[key] if obj_b.has(key) else null + if not _equals(value_a, value_b, case_sensitive, compare_mode, deep_stack, stack_depth): + return false + return true + + TYPE_STRING: + if case_sensitive: + return obj_a.to_lower() == obj_b.to_lower() + else: + return obj_a == obj_b + return obj_a == obj_b + + +@warning_ignore("shadowed_variable_base_class") +static func notification_as_string(instance :Variant, notification :int) -> String: + var error := "Unknown notification: '%s' at instance: %s" % [notification, instance] + if instance is Node: + return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].get(notification, error) + if instance is Control: + return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].get(notification, error) + return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_OBJECT].get(notification, error) + + +static func string_to_type(value :String) -> int: + for type in TYPE_AS_STRING_MAPPINGS.keys(): + if TYPE_AS_STRING_MAPPINGS.get(type) == value: + return type + return TYPE_NIL + + +static func to_camel_case(value :String) -> String: + var p := to_pascal_case(value) + if not p.is_empty(): + p[0] = p[0].to_lower() + return p + + +static func to_pascal_case(value :String) -> String: + return value.capitalize().replace(" ", "") + + +static func to_snake_case(value :String) -> String: + var result = PackedStringArray() + for ch in value: + var lower_ch = ch.to_lower() + if ch != lower_ch and result.size() > 1: + result.append('_') + result.append(lower_ch) + return ''.join(result) + + +static func is_snake_case(value :String) -> bool: + for ch in value: + if ch == '_': + continue + if ch == ch.to_upper(): + return false + return true + + +static func type_as_string(type :int) -> String: + return TYPE_AS_STRING_MAPPINGS.get(type, "Variant") + + +static func typeof_as_string(value :Variant) -> String: + return TYPE_AS_STRING_MAPPINGS.get(typeof(value), "Unknown type") + + +static func all_types() -> PackedInt32Array: + return PackedInt32Array(TYPE_AS_STRING_MAPPINGS.keys()) + + +static func string_as_typeof(type_name :String) -> int: + var type :Variant = TYPE_AS_STRING_MAPPINGS.find_key(type_name) + return type if type != null else TYPE_VARIANT + + +static func is_primitive_type(value :Variant) -> bool: + return typeof(value) in [TYPE_BOOL, TYPE_STRING, TYPE_STRING_NAME, TYPE_INT, TYPE_FLOAT] + + +static func _is_type_equivalent(type_a :int, type_b :int) -> bool: + # don't test for TYPE_STRING_NAME equivalenz + if type_a == TYPE_STRING_NAME or type_b == TYPE_STRING_NAME: + return true + if GdUnitSettings.is_strict_number_type_compare(): + return type_a == type_b + return ( + (type_a == TYPE_FLOAT and type_b == TYPE_INT) + or (type_a == TYPE_INT and type_b == TYPE_FLOAT) + or type_a == type_b) + + +static func is_engine_type(value :Object) -> bool: + if value is GDScript or value is ScriptExtension: + return false + return value.is_class("GDScriptNativeClass") + + +static func is_type(value :Variant) -> bool: + # is an build-in type + if typeof(value) != TYPE_OBJECT: + return false + # is a engine class type + if is_engine_type(value): + return true + # is a custom class type + if value is GDScript and value.can_instantiate(): + return true + return false + + +static func _is_same(left :Variant, right :Variant) -> bool: + var left_type := -1 if left == null else typeof(left) + var right_type := -1 if right == null else typeof(right) + + # if typ different can't be the same + if left_type != right_type: + return false + if left_type == TYPE_OBJECT and right_type == TYPE_OBJECT: + return left.get_instance_id() == right.get_instance_id() + return equals(left, right) + + +static func is_object(value :Variant) -> bool: + return typeof(value) == TYPE_OBJECT + + +static func is_script(value :Variant) -> bool: + return is_object(value) and value is Script + + +static func is_test_suite(script :Script) -> bool: + return is_gd_testsuite(script) or GdUnit4CSharpApiLoader.is_test_suite(script.resource_path) + + +static func is_native_class(value :Variant) -> bool: + return is_object(value) and is_engine_type(value) + + +static func is_scene(value :Variant) -> bool: + return is_object(value) and value is PackedScene + + +static func is_scene_resource_path(value :Variant) -> bool: + return value is String and value.ends_with(".tscn") + + +static func is_gd_script(script :Script) -> bool: + return script is GDScript + + +static func is_cs_script(script :Script) -> bool: + # we need to check by stringify name because checked non mono Godot the class CSharpScript is not available + return str(script).find("CSharpScript") != -1 + + +static func is_gd_testsuite(script :Script) -> bool: + if is_gd_script(script): + var stack := [script] + while not stack.is_empty(): + var current := stack.pop_front() as Script + var base := current.get_base_script() as Script + if base != null: + if base.resource_path.find("GdUnitTestSuite") != -1: + return true + stack.push_back(base) + return false + + +static func is_singleton(value :Variant) -> bool: + if not is_instance_valid(value) or is_native_class(value): + return false + for name in Engine.get_singleton_list(): + if value.is_class(name): + return true + return false + + +static func is_instance(value :Variant) -> bool: + if not is_instance_valid(value) or is_native_class(value): + return false + if is_script(value) and value.get_instance_base_type() == "": + return true + if is_scene(value): + return true + return not value.has_method('new') and not value.has_method('instance') + + +# only object form type Node and attached filename +static func is_instance_scene(instance :Variant) -> bool: + if instance is Node: + var node := instance as Node + return node.get_scene_file_path() != null and not node.get_scene_file_path().is_empty() + return false + + +static func can_be_instantiate(obj :Variant) -> bool: + if not obj or is_engine_type(obj): + return false + return obj.has_method("new") + + +static func create_instance(clazz :Variant) -> GdUnitResult: + match typeof(clazz): + TYPE_OBJECT: + # test is given clazz already an instance + if is_instance(clazz): + return GdUnitResult.success(clazz) + return GdUnitResult.success(clazz.new()) + TYPE_STRING: + if ClassDB.class_exists(clazz): + if Engine.has_singleton(clazz): + return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz) + if not ClassDB.can_instantiate(clazz): + return GdUnitResult.error("Can't instance Engine class '%s'." % clazz) + return GdUnitResult.success(ClassDB.instantiate(clazz)) + else: + var clazz_path :String = extract_class_path(clazz)[0] + if not FileAccess.file_exists(clazz_path): + return GdUnitResult.error("Class '%s' not found." % clazz) + var script := load(clazz_path) + if script != null: + return GdUnitResult.success(script.new()) + else: + return GdUnitResult.error("Can't create instance for '%s'." % clazz) + return GdUnitResult.error("Can't create instance for class '%s'." % clazz) + + +static func extract_class_path(clazz :Variant) -> PackedStringArray: + var clazz_path := PackedStringArray() + if clazz is String: + clazz_path.append(clazz) + return clazz_path + if is_instance(clazz): + # is instance a script instance? + var script := clazz.script as GDScript + if script != null: + return extract_class_path(script) + return clazz_path + + if clazz is GDScript: + if not clazz.resource_path.is_empty(): + clazz_path.append(clazz.resource_path) + return clazz_path + # if not found we go the expensive way and extract the path form the script by creating an instance + var arg_list := build_function_default_arguments(clazz, "_init") + var instance = clazz.callv("new", arg_list) + var clazz_info := inst_to_dict(instance) + GdUnitTools.free_instance(instance) + clazz_path.append(clazz_info["@path"]) + if clazz_info.has("@subpath"): + var sub_path :String = clazz_info["@subpath"] + if not sub_path.is_empty(): + var sub_paths := sub_path.split("/") + clazz_path += sub_paths + return clazz_path + return clazz_path + + +static func extract_class_name_from_class_path(clazz_path :PackedStringArray) -> String: + var base_clazz := clazz_path[0] + # return original class name if engine class + if ClassDB.class_exists(base_clazz): + return base_clazz + var clazz_name := to_pascal_case(base_clazz.get_basename().get_file()) + for path_index in range(1, clazz_path.size()): + clazz_name += "." + clazz_path[path_index] + return clazz_name + + +static func extract_class_name(clazz :Variant) -> GdUnitResult: + if clazz == null: + return GdUnitResult.error("Can't extract class name form a null value.") + + if is_instance(clazz): + # is instance a script instance? + var script := clazz.script as GDScript + if script != null: + return extract_class_name(script) + return GdUnitResult.success(clazz.get_class()) + + # extract name form full qualified class path + if clazz is String: + if ClassDB.class_exists(clazz): + return GdUnitResult.success(clazz) + var source_sript :Script = load(clazz) + var clazz_name :String = load("res://addons/gdUnit4/src/core/parse/GdScriptParser.gd").new().get_class_name(source_sript) + return GdUnitResult.success(to_pascal_case(clazz_name)) + + if is_primitive_type(clazz): + return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz))) + + if is_script(clazz): + if clazz.resource_path.is_empty(): + var class_path := extract_class_name_from_class_path(extract_class_path(clazz)) + return GdUnitResult.success(class_path); + return extract_class_name(clazz.resource_path) + + # need to create an instance for a class typ the extract the class name + var instance :Variant = clazz.new() + if instance == null: + return GdUnitResult.error("Can't create a instance for class '%s'" % clazz) + var result := extract_class_name(instance) + GdUnitTools.free_instance(instance) + return result + + +static func extract_inner_clazz_names(clazz_name :String, script_path :PackedStringArray) -> PackedStringArray: + var inner_classes := PackedStringArray() + + if ClassDB.class_exists(clazz_name): + return inner_classes + var script :GDScript = load(script_path[0]) + var map := script.get_script_constant_map() + for key in map.keys(): + var value = map.get(key) + if value is GDScript: + var class_path := extract_class_path(value) + inner_classes.append(class_path[1]) + return inner_classes + + +static func extract_class_functions(clazz_name :String, script_path :PackedStringArray) -> Array: + if ClassDB.class_get_method_list(clazz_name): + return ClassDB.class_get_method_list(clazz_name) + + if not FileAccess.file_exists(script_path[0]): + return Array() + var script :GDScript = load(script_path[0]) + if script is GDScript: + # if inner class on class path we have to load the script from the script_constant_map + if script_path.size() == 2 and script_path[1] != "": + var inner_classes := script_path[1] + var map := script.get_script_constant_map() + script = map[inner_classes] + var clazz_functions :Array = script.get_method_list() + var base_clazz :String = script.get_instance_base_type() + if base_clazz: + return extract_class_functions(base_clazz, script_path) + return clazz_functions + return Array() + + +# scans all registert script classes for given +# if the class is public in the global space than return true otherwise false +# public class means the script class is defined by 'class_name ' +static func is_public_script_class(clazz_name :String) -> bool: + var script_classes:Array[Dictionary] = ProjectSettings.get_global_class_list() + for class_info in script_classes: + if class_info.has("class"): + if class_info["class"] == clazz_name: + return true + return false + + +static func build_function_default_arguments(script :GDScript, func_name :String) -> Array: + var arg_list := Array() + for func_sig in script.get_script_method_list(): + if func_sig["name"] == func_name: + var args :Array = func_sig["args"] + for arg in args: + var value_type := arg["type"] as int + var default_value = default_value_by_type(value_type) + arg_list.append(default_value) + return arg_list + return arg_list + + +static func default_value_by_type(type :int): + assert(type < TYPE_MAX) + assert(type >= 0) + + match type: + TYPE_NIL: return null + TYPE_BOOL: return false + TYPE_INT: return 0 + TYPE_FLOAT: return 0.0 + TYPE_STRING: return "" + TYPE_VECTOR2: return Vector2.ZERO + TYPE_VECTOR2I: return Vector2i.ZERO + TYPE_VECTOR3: return Vector3.ZERO + TYPE_VECTOR3I: return Vector3i.ZERO + TYPE_VECTOR4: return Vector4.ZERO + TYPE_VECTOR4I: return Vector4i.ZERO + TYPE_RECT2: return Rect2() + TYPE_RECT2I: return Rect2i() + TYPE_TRANSFORM2D: return Transform2D() + TYPE_PLANE: return Plane() + TYPE_QUATERNION: return Quaternion() + TYPE_AABB: return AABB() + TYPE_BASIS: return Basis() + TYPE_TRANSFORM3D: return Transform3D() + TYPE_COLOR: return Color() + TYPE_NODE_PATH: return NodePath() + TYPE_RID: return RID() + TYPE_OBJECT: return null + TYPE_ARRAY: return [] + TYPE_DICTIONARY: return {} + TYPE_PACKED_BYTE_ARRAY: return PackedByteArray() + TYPE_PACKED_COLOR_ARRAY: return PackedColorArray() + TYPE_PACKED_INT32_ARRAY: return PackedInt32Array() + TYPE_PACKED_INT64_ARRAY: return PackedInt64Array() + TYPE_PACKED_FLOAT32_ARRAY: return PackedFloat32Array() + TYPE_PACKED_FLOAT64_ARRAY: return PackedFloat64Array() + TYPE_PACKED_STRING_ARRAY: return PackedStringArray() + TYPE_PACKED_VECTOR2_ARRAY: return PackedVector2Array() + TYPE_PACKED_VECTOR3_ARRAY: return PackedVector3Array() + + push_error("Can't determine a default value for type: '%s', Please create a Bug issue and attach the stacktrace please." % type) + return null + + +static func find_nodes_by_class(root: Node, cls: String, recursive: bool = false) -> Array[Node]: + if not recursive: + return _find_nodes_by_class_no_rec(root, cls) + return _find_nodes_by_class(root, cls) + + +static func _find_nodes_by_class_no_rec(parent: Node, cls: String) -> Array[Node]: + var result :Array[Node] = [] + for ch in parent.get_children(): + if ch.get_class() == cls: + result.append(ch) + return result + + +static func _find_nodes_by_class(root: Node, cls: String) -> Array[Node]: + var result :Array[Node] = [] + var stack :Array[Node] = [root] + while stack: + var node :Node = stack.pop_back() + if node.get_class() == cls: + result.append(node) + for ch in node.get_children(): + stack.push_back(ch) + return result diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd b/addons/gdUnit4/src/core/GdUnit4Version.gd new file mode 100644 index 0000000..14cef99 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnit4Version.gd @@ -0,0 +1,57 @@ +class_name GdUnit4Version +extends RefCounted + +const VERSION_PATTERN = "[center][color=#9887c4]gd[/color][color=#7a57d6]Unit[/color][color=#9887c4]4[/color] [color=#9887c4]${version}[/color][/center]" + +var _major :int +var _minor :int +var _patch :int + + +func _init(major :int,minor :int,patch :int): + _major = major + _minor = minor + _patch = patch + + +static func parse(value :String) -> GdUnit4Version: + var regex := RegEx.new() + regex.compile("[a-zA-Z:,-]+") + var cleaned := regex.sub(value, "", true) + var parts := cleaned.split(".") + var major := parts[0].to_int() + var minor := parts[1].to_int() + var patch := parts[2].to_int() if parts.size() > 2 else 0 + return GdUnit4Version.new(major, minor, patch) + + +static func current() -> GdUnit4Version: + var config = ConfigFile.new() + config.load('addons/gdUnit4/plugin.cfg') + return parse(config.get_value('plugin', 'version')) + + +func equals(other :) -> bool: + return _major == other._major and _minor == other._minor and _patch == other._patch + + +func is_greater(other :) -> bool: + if _major > other._major: + return true + if _major == other._major and _minor > other._minor: + return true + return _major == other._major and _minor == other._minor and _patch > other._patch + + +static func init_version_label(label :Control) -> void: + var config = ConfigFile.new() + config.load('addons/gdUnit4/plugin.cfg') + var version = config.get_value('plugin', 'version') + if label is RichTextLabel: + label.text = VERSION_PATTERN.replace('${version}', version) + else: + label.text = "gdUnit4 " + version + + +func _to_string() -> String: + return "v%d.%d.%d" % [_major, _minor, _patch] diff --git a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd new file mode 100644 index 0000000..30be420 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd @@ -0,0 +1,122 @@ +# A class doubler used to mock and spy checked implementations +class_name GdUnitClassDoubler +extends RefCounted + + +const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_" +const DOUBLER_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd") +const EXCLUDE_VIRTUAL_FUNCTIONS = [ + # we have to exclude notifications because NOTIFICATION_PREDELETE is try + # to delete already freed spy/mock resources and will result in a conflict + "_notification", + # https://github.com/godotengine/godot/issues/67461 + "get_name", + "get_path", + "duplicate", + ] +# define functions to be exclude when spy or mock checked a scene +const EXLCUDE_SCENE_FUNCTIONS = [ + # needs to exclude get/set script functions otherwise it endsup in recursive endless loop + "set_script", + "get_script", + # needs to exclude otherwise verify fails checked collection arguments checked calling to string + "_to_string", +] +const EXCLUDE_FUNCTIONS = ["new", "free", "get_instance_id", "get_tree"] + + +static func check_leaked_instances() -> void: + ## we check that all registered spy/mock instances are removed from the engine meta data + for key in Engine.get_meta_list(): + if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX): + var instance = Engine.get_meta(key) + push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS]) + + +# loads the doubler template +# class_info = { "class_name": <>, "class_path" : <>} +static func load_template(template :String, class_info :Dictionary, instance :Object) -> PackedStringArray: + # store instance id + var source_code = template\ + .replace("${instance_id}", "%s%d" % [DOUBLER_INSTANCE_ID_PREFIX, abs(instance.get_instance_id())])\ + .replace("${source_class}", class_info.get("class_name")) + var lines := GdScriptParser.to_unix_format(source_code).split("\n") + # replace template class_name with Doubled name and extends form source class + lines.insert(0, "class_name Doubled%s" % class_info.get("class_name").replace(".", "_")) + lines.insert(1, extends_clazz(class_info)) + # append Object interactions stuff + lines.append_array(GdScriptParser.to_unix_format(DOUBLER_TEMPLATE.source_code).split("\n")) + return lines + + +static func extends_clazz(class_info :Dictionary) -> String: + var clazz_name :String = class_info.get("class_name") + var clazz_path :PackedStringArray = class_info.get("class_path", []) + # is inner class? + if clazz_path.size() > 1: + return "extends %s" % clazz_name + if clazz_path.size() == 1 and clazz_path[0].ends_with(".gd"): + return "extends '%s'" % clazz_path[0] + return "extends %s" % clazz_name + + +# double all functions of given instance +static func double_functions(instance :Object, clazz_name :String, clazz_path :PackedStringArray, func_doubler: GdFunctionDoubler, exclude_functions :Array) -> PackedStringArray: + var doubled_source := PackedStringArray() + var parser := GdScriptParser.new() + var exclude_override_functions := EXCLUDE_VIRTUAL_FUNCTIONS + EXCLUDE_FUNCTIONS + exclude_functions + var functions := Array() + + # double script functions + if not ClassDB.class_exists(clazz_name): + var result := parser.parse(clazz_name, clazz_path) + if result.is_error(): + push_error(result.error_message()) + return PackedStringArray() + var class_descriptor :GdClassDescriptor = result.value() + while class_descriptor != null: + for func_descriptor in class_descriptor.functions(): + if instance != null and not instance.has_method(func_descriptor.name()): + #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) + continue + if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): + continue + doubled_source += func_doubler.double(func_descriptor) + functions.append(func_descriptor.name()) + class_descriptor = class_descriptor.parent() + + # double regular class functions + var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path) + for method in clazz_functions: + var func_descriptor := GdFunctionDescriptor.extract_from(method) + # exclude private core functions + if func_descriptor.is_private(): + continue + if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): + continue + # GD-110: Hotfix do not double invalid engine functions + if is_invalid_method_descriptior(method): + #prints("'%s': invalid method descriptor found! %s" % [clazz_name, method]) + continue + # do not double on not implemented virtual functions + if instance != null and not instance.has_method(func_descriptor.name()): + #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) + continue + functions.append(func_descriptor.name()) + doubled_source.append_array(func_doubler.double(func_descriptor)) + return doubled_source + + +# GD-110 +static func is_invalid_method_descriptior(method :Dictionary) -> bool: + var return_info = method["return"] + var type :int = return_info["type"] + var usage :int = return_info["usage"] + var clazz_name :String = return_info["class_name"] + # is method returning a type int with a given 'class_name' we have an enum + # and the PROPERTY_USAGE_CLASS_IS_ENUM must be set + if type == TYPE_INT and not clazz_name.is_empty() and not (usage & PROPERTY_USAGE_CLASS_IS_ENUM): + return true + if clazz_name == "Variant.Type": + return true + return false diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd b/addons/gdUnit4/src/core/GdUnitFileAccess.gd new file mode 100644 index 0000000..01022dd --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd @@ -0,0 +1,211 @@ +class_name GdUnitFileAccess +extends RefCounted + +const GDUNIT_TEMP := "user://tmp" + + +static func current_dir() -> String: + return ProjectSettings.globalize_path("res://") + + +static func clear_tmp() -> void: + delete_directory(GDUNIT_TEMP) + + +# Creates a new file under +static func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: + var file_path := create_temp_dir(relative_path) + "/" + file_name + var file := FileAccess.open(file_path, mode) + if file == null: + push_error("Error creating temporary file at: %s, %s" % [file_path, error_string(FileAccess.get_open_error())]) + return file + + +static func temp_dir() -> String: + if not DirAccess.dir_exists_absolute(GDUNIT_TEMP): + DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) + return GDUNIT_TEMP + + +static func create_temp_dir(folder_name :String) -> String: + var new_folder := temp_dir() + "/" + folder_name + if not DirAccess.dir_exists_absolute(new_folder): + DirAccess.make_dir_recursive_absolute(new_folder) + return new_folder + + +static func copy_file(from_file :String, to_dir :String) -> GdUnitResult: + var dir := DirAccess.open(to_dir) + if dir != null: + var to_file := to_dir + "/" + from_file.get_file() + prints("Copy %s to %s" % [from_file, to_file]) + var error := dir.copy(from_file, to_file) + if error != OK: + return GdUnitResult.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_string(error)]) + return GdUnitResult.success(to_file) + return GdUnitResult.error("Directory not found: " + to_dir) + + +static func copy_directory(from_dir :String, to_dir :String, recursive :bool = false) -> bool: + if not DirAccess.dir_exists_absolute(from_dir): + push_error("Source directory not found '%s'" % from_dir) + return false + + # check if destination exists + if not DirAccess.dir_exists_absolute(to_dir): + # create it + var err := DirAccess.make_dir_recursive_absolute(to_dir) + if err != OK: + push_error("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)]) + return false + var source_dir := DirAccess.open(from_dir) + var dest_dir := DirAccess.open(to_dir) + if source_dir != null: + source_dir.list_dir_begin() + var next := "." + + while next != "": + next = source_dir.get_next() + if next == "" or next == "." or next == "..": + continue + var source := source_dir.get_current_dir() + "/" + next + var dest := dest_dir.get_current_dir() + "/" + next + if source_dir.current_is_dir(): + if recursive: + copy_directory(source + "/", dest, recursive) + continue + var err := source_dir.copy(source, dest) + if err != OK: + push_error("Error checked copy file '%s' to '%s'" % [source, dest]) + return false + + return true + else: + push_error("Directory not found: " + from_dir) + return false + + +static func delete_directory(path :String, only_content := false) -> void: + var dir := DirAccess.open(path) + if dir != null: + dir.list_dir_begin() + var file_name := "." + while file_name != "": + file_name = dir.get_next() + if file_name.is_empty() or file_name == "." or file_name == "..": + continue + var next := path + "/" +file_name + if dir.current_is_dir(): + delete_directory(next) + else: + # delete file + var err := dir.remove(next) + if err: + push_error("Delete %s failed: %s" % [next, error_string(err)]) + if not only_content: + var err := dir.remove(path) + if err: + push_error("Delete %s failed: %s" % [path, error_string(err)]) + + +static func delete_path_index_lower_equals_than(path :String, prefix :String, index :int) -> int: + var dir := DirAccess.open(path) + if dir == null: + return 0 + var deleted := 0 + dir.list_dir_begin() + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + if next.begins_with(prefix): + var current_index := next.split("_")[1].to_int() + if current_index <= index: + deleted += 1 + delete_directory(path + "/" + next) + return deleted + + +# scans given path for sub directories by given prefix and returns the highest index numer +# e.g. +static func find_last_path_index(path :String, prefix :String) -> int: + var dir := DirAccess.open(path) + if dir == null: + return 0 + var last_iteration := 0 + dir.list_dir_begin() + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + if next.begins_with(prefix): + var iteration := next.split("_")[1].to_int() + if iteration > last_iteration: + last_iteration = iteration + return last_iteration + + +static func scan_dir(path :String) -> PackedStringArray: + var dir := DirAccess.open(path) + if dir == null or not dir.dir_exists(path): + return PackedStringArray() + var content := PackedStringArray() + dir.list_dir_begin() + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + content.append(next) + return content + + +static func resource_as_array(resource_path :String) -> PackedStringArray: + var file := FileAccess.open(resource_path, FileAccess.READ) + if file == null: + push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())]) + return PackedStringArray() + var file_content := PackedStringArray() + while not file.eof_reached(): + file_content.append(file.get_line()) + return file_content + + +static func resource_as_string(resource_path :String) -> String: + var file := FileAccess.open(resource_path, FileAccess.READ) + if file == null: + push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())]) + return "" + return file.get_as_text(true) + + +static func make_qualified_path(path :String) -> String: + if not path.begins_with("res://"): + if path.begins_with("//"): + return path.replace("//", "res://") + if path.begins_with("/"): + return "res:/" + path + return path + + +static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult: + var zip: ZIPReader = ZIPReader.new() + var err := zip.open(zip_package) + if err != OK: + return GdUnitResult.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) + var zip_entries: PackedStringArray = zip.get_files() + # Get base path and step over archive folder + var archive_path := zip_entries[0] + zip_entries.remove_at(0) + + for zip_entry in zip_entries: + var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "") + if zip_entry.ends_with("/"): + DirAccess.make_dir_recursive_absolute(new_file_path) + continue + var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) + file.store_buffer(zip.read_file(zip_entry)) + zip.close() + return GdUnitResult.success(dest_path) diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd new file mode 100644 index 0000000..7020a32 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd @@ -0,0 +1,42 @@ +class_name GdUnitObjectInteractions +extends RefCounted + + +static func verify(interaction_object :Object, interactions_times): + if not _is_mock_or_spy(interaction_object, "__verify"): + return interaction_object + return interaction_object.__do_verify_interactions(interactions_times) + + +static func verify_no_interactions(interaction_object :Object) -> GdUnitAssert: + var __gd_assert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") + if not _is_mock_or_spy(interaction_object, "__verify"): + return __gd_assert.report_success() + var __summary :Dictionary = interaction_object.__verify_no_interactions() + if __summary.is_empty(): + return __gd_assert.report_success() + return __gd_assert.report_error(GdAssertMessages.error_no_more_interactions(__summary)) + + +static func verify_no_more_interactions(interaction_object :Object) -> GdUnitAssert: + var __gd_assert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") + if not _is_mock_or_spy(interaction_object, "__verify_no_more_interactions"): + return __gd_assert + var __summary :Dictionary = interaction_object.__verify_no_more_interactions() + if __summary.is_empty(): + return __gd_assert + return __gd_assert.report_error(GdAssertMessages.error_no_more_interactions(__summary)) + + +static func reset(interaction_object :Object) -> Object: + if not _is_mock_or_spy(interaction_object, "__reset"): + return interaction_object + interaction_object.__reset_interactions() + return interaction_object + + +static func _is_mock_or_spy(interaction_object :Object, mock_function_signature :String) -> bool: + if interaction_object is GDScript and not interaction_object.get_script().has_script_method(mock_function_signature): + push_error("Error: You try to use a non mock or spy!") + return false + return true diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd new file mode 100644 index 0000000..c06b1d4 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd @@ -0,0 +1,91 @@ +const GdUnitAssertImpl := preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + +var __expected_interactions :int = -1 +var __saved_interactions := Dictionary() +var __verified_interactions := Array() + + +func __save_function_interaction(function_args :Array[Variant]) -> void: + var __matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for __index in __saved_interactions.keys().size(): + var __key :Variant = __saved_interactions.keys()[__index] + if __matcher.is_match(__key): + __saved_interactions[__key] += 1 + return + __saved_interactions[function_args] = 1 + + +func __is_verify_interactions() -> bool: + return __expected_interactions != -1 + + +func __do_verify_interactions(interactions_times :int = 1) -> Object: + __expected_interactions = interactions_times + return self + + +func __verify_interactions(function_args :Array[Variant]) -> void: + var __summary := Dictionary() + var __total_interactions := 0 + var __matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for __index in __saved_interactions.keys().size(): + var __key :Variant = __saved_interactions.keys()[__index] + if __matcher.is_match(__key): + var __interactions :int = __saved_interactions.get(__key, 0) + __total_interactions += __interactions + __summary[__key] = __interactions + # add as verified + __verified_interactions.append(__key) + + var __gd_assert := GdUnitAssertImpl.new("") + if __total_interactions != __expected_interactions: + var __expected_summary := {function_args : __expected_interactions} + var __error_message :String + # if no __interactions macht collect not verified __interactions for failure report + if __summary.is_empty(): + var __current_summary := __verify_no_more_interactions() + __error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary) + else: + __error_message = GdAssertMessages.error_validate_interactions(__summary, __expected_summary) + __gd_assert.report_error(__error_message) + else: + __gd_assert.report_success() + __expected_interactions = -1 + + +func __verify_no_interactions() -> Dictionary: + var __summary := Dictionary() + if not __saved_interactions.is_empty(): + for __index in __saved_interactions.keys().size(): + var func_call :Variant = __saved_interactions.keys()[__index] + __summary[func_call] = __saved_interactions[func_call] + return __summary + + +func __verify_no_more_interactions() -> Dictionary: + var __summary := Dictionary() + var called_functions :Array[Variant] = __saved_interactions.keys() + if called_functions != __verified_interactions: + # collect the not verified functions + var called_but_not_verified := called_functions.duplicate() + for __index in __verified_interactions.size(): + called_but_not_verified.erase(__verified_interactions[__index]) + + for __index in called_but_not_verified.size(): + var not_verified :Variant = called_but_not_verified[__index] + __summary[not_verified] = __saved_interactions[not_verified] + return __summary + + +func __reset_interactions() -> void: + __saved_interactions.clear() + + +func __filter_vargs(arg_values :Array[Variant]) -> Array[Variant]: + var filtered :Array[Variant] = [] + for __index in arg_values.size(): + var arg :Variant = arg_values[__index] + if typeof(arg) == TYPE_STRING and arg == GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE: + continue + filtered.append(arg) + return filtered diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd b/addons/gdUnit4/src/core/GdUnitProperty.gd new file mode 100644 index 0000000..6e338a3 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitProperty.gd @@ -0,0 +1,72 @@ +class_name GdUnitProperty +extends RefCounted + + +var _name :String +var _help :String +var _type :int +var _value :Variant +var _value_set :PackedStringArray +var _default :Variant + + +func _init(p_name :String, p_type :int, p_value :Variant, p_default_value :Variant, p_help :="", p_value_set := PackedStringArray()) -> void: + _name = p_name + _type = p_type + _value = p_value + _value_set = p_value_set + _default = p_default_value + _help = p_help + + +func name() -> String: + return _name + + +func type() -> int: + return _type + + +func value() -> Variant: + return _value + + +func value_set() -> PackedStringArray: + return _value_set + + +func is_selectable_value() -> bool: + return not _value_set.is_empty() + + +func set_value(p_value :Variant) -> void: + match _type: + TYPE_STRING: + _value = str(p_value) + TYPE_BOOL: + _value = bool(p_value) + TYPE_INT: + _value = int(p_value) + TYPE_FLOAT: + _value = float(p_value) + _: + _value = p_value + + +func default() -> Variant: + return _default + + +func category() -> String: + var elements := _name.split("/") + if elements.size() > 3: + return elements[2] + return "" + + +func help() -> String: + return _help + + +func _to_string() -> String: + return "%-64s %-10s %-10s (%s) help:%s set:%s" % [name(), type(), value(), default(), help(), _value_set] diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd b/addons/gdUnit4/src/core/GdUnitResult.gd new file mode 100644 index 0000000..f2d297f --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitResult.gd @@ -0,0 +1,104 @@ +class_name GdUnitResult +extends RefCounted + +enum { + SUCCESS, + WARN, + ERROR, + EMPTY +} + +var _state :Variant +var _warn_message := "" +var _error_message := "" +var _value :Variant = null + + +static func empty() -> GdUnitResult: + var result := GdUnitResult.new() + result._state = EMPTY + return result + + +static func success(p_value :Variant) -> GdUnitResult: + assert(p_value != null, "The value must not be NULL") + var result := GdUnitResult.new() + result._value = p_value + result._state = SUCCESS + return result + + +static func warn(p_warn_message :String, p_value :Variant = null) -> GdUnitResult: + assert(not p_warn_message.is_empty()) #,"The message must not be empty") + var result := GdUnitResult.new() + result._value = p_value + result._warn_message = p_warn_message + result._state = WARN + return result + + +static func error(p_error_message :String) -> GdUnitResult: + assert(not p_error_message.is_empty(), "The message must not be empty") + var result := GdUnitResult.new() + result._value = null + result._error_message = p_error_message + result._state = ERROR + return result + + +func is_success() -> bool: + return _state == SUCCESS + + +func is_warn() -> bool: + return _state == WARN + + +func is_error() -> bool: + return _state == ERROR + + +func is_empty() -> bool: + return _state == EMPTY + + +func value() -> Variant: + return _value + + +func or_else(p_value :Variant) -> Variant: + if not is_success(): + return p_value + return value() + + +func error_message() -> String: + return _error_message + + +func warn_message() -> String: + return _warn_message + + +func _to_string() -> String: + return str(GdUnitResult.serialize(self)) + + +static func serialize(result :GdUnitResult) -> Dictionary: + if result == null: + push_error("Can't serialize a Null object from type GdUnitResult") + return { + "state" : result._state, + "value" : var_to_str(result._value), + "warn_msg" : result._warn_message, + "err_msg" : result._error_message + } + + +static func deserialize(config :Dictionary) -> GdUnitResult: + var result := GdUnitResult.new() + result._value = str_to_var(config.get("value", "")) + result._warn_message = config.get("warn_msg", null) + result._error_message = config.get("err_msg", null) + result._state = config.get("state") + return result diff --git a/addons/gdUnit4/src/core/GdUnitRunner.gd b/addons/gdUnit4/src/core/GdUnitRunner.gd new file mode 100644 index 0000000..686e4f8 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitRunner.gd @@ -0,0 +1,168 @@ +extends Node + +signal sync_rpc_id_result_received + + +@onready var _client :GdUnitTcpClient = $GdUnitTcpClient +@onready var _executor :GdUnitTestSuiteExecutor = GdUnitTestSuiteExecutor.new() + +enum { + INIT, + RUN, + STOP, + EXIT +} + +const GDUNIT_RUNNER = "GdUnitRunner" + +var _config := GdUnitRunnerConfig.new() +var _test_suites_to_process :Array +var _state :int = INIT +var _cs_executor :RefCounted + + +func _init() -> void: + # minimize scene window checked debug mode + if OS.get_cmdline_args().size() == 1: + DisplayServer.window_set_title("GdUnit4 Runner (Debug Mode)") + else: + DisplayServer.window_set_title("GdUnit4 Runner (Release Mode)") + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + # store current runner instance to engine meta data to can be access in as a singleton + Engine.set_meta(GDUNIT_RUNNER, self) + _cs_executor = GdUnit4CSharpApiLoader.create_executor(self) + + +func _ready() -> void: + var config_result := _config.load_config() + if config_result.is_error(): + push_error(config_result.error_message()) + _state = EXIT + return + _client.connect("connection_failed", _on_connection_failed) + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + var result := _client.start("127.0.0.1", _config.server_port()) + if result.is_error(): + push_error(result.error_message()) + return + _state = INIT + + +func _on_connection_failed(message :String) -> void: + prints("_on_connection_failed", message, _test_suites_to_process) + _state = STOP + + +func _notification(what :int) -> void: + #prints("GdUnitRunner", self, GdObjects.notification_as_string(what)) + if what == NOTIFICATION_PREDELETE: + Engine.remove_meta(GDUNIT_RUNNER) + + +func _process(_delta :float) -> void: + match _state: + INIT: + # wait until client is connected to the GdUnitServer + if _client.is_client_connected(): + var time := LocalTime.now() + prints("Scan for test suites.") + _test_suites_to_process = load_test_suits() + prints("Scanning of %d test suites took" % _test_suites_to_process.size(), time.elapsed_since()) + gdUnitInit() + _state = RUN + RUN: + # all test suites executed + if _test_suites_to_process.is_empty(): + _state = STOP + else: + # process next test suite + set_process(false) + var test_suite :Node = _test_suites_to_process.pop_front() + if _cs_executor != null and _cs_executor.IsExecutable(test_suite): + _cs_executor.Execute(test_suite) + await _cs_executor.ExecutionCompleted + else: + await _executor.execute(test_suite) + set_process(true) + STOP: + _state = EXIT + # give the engine small amount time to finish the rpc + _on_gdunit_event(GdUnitStop.new()) + await get_tree().create_timer(0.1).timeout + await get_tree().process_frame + get_tree().quit(0) + + +func load_test_suits() -> Array: + var to_execute := _config.to_execute() + if to_execute.is_empty(): + prints("No tests selected to execute!") + _state = EXIT + return [] + # scan for the requested test suites + var test_suites := Array() + var _scanner := GdUnitTestSuiteScanner.new() + for resource_path in to_execute.keys(): + var selected_tests :PackedStringArray = to_execute.get(resource_path) + var scaned_suites := _scanner.scan(resource_path) + _filter_test_case(scaned_suites, selected_tests) + test_suites += scaned_suites + return test_suites + + +func gdUnitInit() -> void: + #enable_manuall_polling() + send_message("Scaned %d test suites" % _test_suites_to_process.size()) + var total_count := _collect_test_case_count(_test_suites_to_process) + _on_gdunit_event(GdUnitInit.new(_test_suites_to_process.size(), total_count)) + for test_suite in _test_suites_to_process: + send_test_suite(test_suite) + + +func _filter_test_case(test_suites :Array, included_tests :PackedStringArray) -> void: + if included_tests.is_empty(): + return + for test_suite in test_suites: + for test_case in test_suite.get_children(): + _do_filter_test_case(test_suite, test_case, included_tests) + + +func _do_filter_test_case(test_suite :Node, test_case :Node, included_tests :PackedStringArray) -> void: + for included_test in included_tests: + var test_meta :PackedStringArray = included_test.split(":") + var test_name := test_meta[0] + if test_case.get_name() == test_name: + # we have a paremeterized test selection + if test_meta.size() > 1: + var test_param_index := test_meta[1] + test_case.set_test_parameter_index(test_param_index.to_int()) + return + # the test is filtered out + test_suite.remove_child(test_case) + test_case.free() + + +func _collect_test_case_count(testSuites :Array) -> int: + var total :int = 0 + for test_suite in testSuites: + total += test_suite.get_child_count() + return total + + +# RPC send functions +func send_message(message :String) -> void: + _client.rpc_send(RPCMessage.of(message)) + + +func send_test_suite(test_suite :Node) -> void: + _client.rpc_send(RPCGdUnitTestSuite.of(test_suite)) + + +func _on_gdunit_event(event :GdUnitEvent) -> void: + _client.rpc_send(RPCGdUnitEvent.of(event)) + + +# Event bridge from C# GdUnit4.ITestEventListener.cs +func PublishEvent(data :Dictionary) -> void: + var event := GdUnitEvent.new().deserialize(data) + _client.rpc_send(RPCGdUnitEvent.of(event)) diff --git a/addons/gdUnit4/src/core/GdUnitRunner.tscn b/addons/gdUnit4/src/core/GdUnitRunner.tscn new file mode 100644 index 0000000..c1f67b1 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitRunner.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/core/GdUnitRunner.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"] + +[node name="Control" type="Node"] +script = ExtResource("1") + +[node name="GdUnitTcpClient" type="Node" parent="."] +script = ExtResource("2") diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd new file mode 100644 index 0000000..080c18e --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -0,0 +1,153 @@ +class_name GdUnitRunnerConfig +extends Resource + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const CONFIG_VERSION = "1.0" +const VERSION = "version" +const INCLUDED = "included" +const SKIPPED = "skipped" +const SERVER_PORT = "server_port" +const EXIT_FAIL_FAST ="exit_on_first_fail" + +const CONFIG_FILE = "res://addons/gdUnit4/GdUnitRunner.cfg" + +var _config := { + VERSION : CONFIG_VERSION, + # a set of directories or testsuite paths as key and a optional set of testcases as values + INCLUDED : Dictionary(), + # a set of skipped directories or testsuite paths + SKIPPED : Dictionary(), + # the port of running test server for this session + SERVER_PORT : -1 + } + + +func clear() -> GdUnitRunnerConfig: + _config[INCLUDED] = Dictionary() + _config[SKIPPED] = Dictionary() + return self + + +func set_server_port(port :int) -> GdUnitRunnerConfig: + _config[SERVER_PORT] = port + return self + + +func server_port() -> int: + return _config.get(SERVER_PORT, -1) + + +func self_test() -> GdUnitRunnerConfig: + add_test_suite("res://addons/gdUnit4/test/") + add_test_suite("res://addons/gdUnit4/mono/test/") + return self + + +func add_test_suite(p_resource_path :String) -> GdUnitRunnerConfig: + var to_execute_ := to_execute() + to_execute_[p_resource_path] = to_execute_.get(p_resource_path, PackedStringArray()) + return self + + +func add_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig: + for resource_path_ in resource_paths: + add_test_suite(resource_path_) + return self + + +func add_test_case(p_resource_path :String, test_name :StringName, test_param_index :int = -1) -> GdUnitRunnerConfig: + var to_execute_ := to_execute() + var test_cases :PackedStringArray = to_execute_.get(p_resource_path, PackedStringArray()) + if test_param_index != -1: + test_cases.append("%s:%d" % [test_name, test_param_index]) + else: + test_cases.append(test_name) + to_execute_[p_resource_path] = test_cases + return self + + +# supports full path or suite name with optional test case name +# [:] +# '/path/path', res://path/path', 'res://path/path/testsuite.gd' or 'testsuite' +# 'res://path/path/testsuite.gd:test_case' or 'testsuite:test_case' +func skip_test_suite(value :StringName) -> GdUnitRunnerConfig: + var parts :Array = GdUnitFileAccess.make_qualified_path(value).rsplit(":") + if parts[0] == "res": + parts.pop_front() + parts[0] = GdUnitFileAccess.make_qualified_path(parts[0]) + match parts.size(): + 1: skipped()[parts[0]] = PackedStringArray() + 2: skip_test_case(parts[0], parts[1]) + return self + + +func skip_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig: + for resource_path_ in resource_paths: + skip_test_suite(resource_path_) + return self + + +func skip_test_case(p_resource_path :String, test_name :StringName) -> GdUnitRunnerConfig: + var to_ignore := skipped() + var test_cases :PackedStringArray = to_ignore.get(p_resource_path, PackedStringArray()) + test_cases.append(test_name) + to_ignore[p_resource_path] = test_cases + return self + + +func to_execute() -> Dictionary: + return _config.get(INCLUDED, {"res://" : PackedStringArray()}) + + +func skipped() -> Dictionary: + return _config.get(SKIPPED, PackedStringArray()) + + +func save_config(path :String = CONFIG_FILE) -> GdUnitResult: + var file := FileAccess.open(path, FileAccess.WRITE) + if file == null: + var error = FileAccess.get_open_error() + return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, error_string(error)]) + _config[VERSION] = CONFIG_VERSION + file.store_string(JSON.stringify(_config)) + return GdUnitResult.success(path) + + +func load_config(path :String = CONFIG_FILE) -> GdUnitResult: + if not FileAccess.file_exists(path): + return GdUnitResult.error("Can't find test runner configuration '%s'! Please select a test to run." % path) + var file := FileAccess.open(path, FileAccess.READ) + if file == null: + var error = FileAccess.get_open_error() + return GdUnitResult.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, error_string(error)]) + var content := file.get_as_text() + if not content.is_empty() and content[0] == '{': + # Parse as json + var test_json_conv := JSON.new() + var error := test_json_conv.parse(content) + if error != OK: + return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + _config = test_json_conv.get_data() as Dictionary + if not _config.has(VERSION): + return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + fix_value_types() + return GdUnitResult.success(path) + + +func fix_value_types(): + # fix float value to int json stores all numbers as float + var server_port_ :int = _config.get(SERVER_PORT, -1) + _config[SERVER_PORT] = server_port_ + convert_Array_to_PackedStringArray(_config[INCLUDED]) + convert_Array_to_PackedStringArray(_config[SKIPPED]) + + +func convert_Array_to_PackedStringArray(data :Dictionary): + for key in data.keys(): + var values :Array = data[key] + data[key] = PackedStringArray(values) + + +func _to_string() -> String: + return str(_config) diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd new file mode 100644 index 0000000..44498a6 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -0,0 +1,418 @@ +# This class provides a runner for scense to simulate interactions like keyboard or mouse +class_name GdUnitSceneRunnerImpl +extends GdUnitSceneRunner + + +var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) + + +# mapping of mouse buttons and his masks +const MAP_MOUSE_BUTTON_MASKS := { + MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT, + MOUSE_BUTTON_RIGHT : MOUSE_BUTTON_MASK_RIGHT, + MOUSE_BUTTON_MIDDLE : MOUSE_BUTTON_MASK_MIDDLE, + # https://github.com/godotengine/godot/issues/73632 + MOUSE_BUTTON_WHEEL_UP : 1 << (MOUSE_BUTTON_WHEEL_UP - 1), + MOUSE_BUTTON_WHEEL_DOWN : 1 << (MOUSE_BUTTON_WHEEL_DOWN - 1), + MOUSE_BUTTON_XBUTTON1 : MOUSE_BUTTON_MASK_MB_XBUTTON1, + MOUSE_BUTTON_XBUTTON2 : MOUSE_BUTTON_MASK_MB_XBUTTON2, +} + +var _is_disposed := false +var _current_scene :Node = null +var _awaiter :GdUnitAwaiter = GdUnitAwaiter.new() +var _verbose :bool +var _simulate_start_time :LocalTime +var _last_input_event :InputEvent = null +var _mouse_button_on_press := [] +var _key_on_press := [] +var _curent_mouse_position :Vector2 + +# time factor settings +var _time_factor := 1.0 +var _saved_iterations_per_second :float +var _scene_auto_free := false + + +func _init(p_scene, p_verbose :bool, p_hide_push_errors = false): + _verbose = p_verbose + _saved_iterations_per_second = Engine.get_physics_ticks_per_second() + set_time_factor(1) + # handle scene loading by resource path + if typeof(p_scene) == TYPE_STRING: + if !FileAccess.file_exists(p_scene): + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: Can't load scene by given resource path: '%s'. The resource not exists." % p_scene) + return + if !str(p_scene).ends_with("tscn"): + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: The given resource: '%s'. is not a scene." % p_scene) + return + _current_scene = load(p_scene).instantiate() + _scene_auto_free = true + else: + # verify we have a node instance + if not p_scene is Node: + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: The given instance '%s' is not a Node." % p_scene) + return + _current_scene = p_scene + if _current_scene == null: + if not p_hide_push_errors: + push_error("GdUnitSceneRunner: Scene must be not null!") + return + _scene_tree().root.add_child(_current_scene) + # do finally reset all open input events when the scene is removed + _scene_tree().root.child_exiting_tree.connect(func f(child): + if child == _current_scene: + _reset_input_to_default() + ) + _simulate_start_time = LocalTime.now() + # we need to set inital a valid window otherwise the warp_mouse() is not handled + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + # set inital mouse pos to 0,0 + var max_iteration_to_wait = 0 + while get_global_mouse_position() != Vector2.ZERO and max_iteration_to_wait < 100: + Input.warp_mouse(Vector2.ZERO) + max_iteration_to_wait += 1 + + +func _notification(what): + if what == NOTIFICATION_PREDELETE and is_instance_valid(self): + # reset time factor to normal + __deactivate_time_factor() + if is_instance_valid(_current_scene): + _scene_tree().root.remove_child(_current_scene) + # do only free scenes instanciated by this runner + if _scene_auto_free: + _current_scene.free() + _is_disposed = true + _current_scene = null + # we hide the scene/main window after runner is finished + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + + +func _scene_tree() -> SceneTree: + return Engine.get_main_loop() as SceneTree + + +func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + simulate_key_press(key_code, shift_pressed, ctrl_pressed) + simulate_key_release(key_code, shift_pressed, ctrl_pressed) + return self + + +func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + __print_current_focus() + var event = InputEventKey.new() + event.pressed = true + event.keycode = key_code + event.physical_keycode = key_code + event.alt_pressed = key_code == KEY_ALT + event.shift_pressed = shift_pressed or key_code == KEY_SHIFT + event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL + _apply_input_modifiers(event) + _key_on_press.append(key_code) + return _handle_input_event(event) + + +func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + __print_current_focus() + var event = InputEventKey.new() + event.pressed = false + event.keycode = key_code + event.physical_keycode = key_code + event.alt_pressed = key_code == KEY_ALT + event.shift_pressed = shift_pressed or key_code == KEY_SHIFT + event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL + _apply_input_modifiers(event) + _key_on_press.erase(key_code) + return _handle_input_event(event) + + +func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner: + var event := InputEventMouseMotion.new() + event.position = pos + event.global_position = get_global_mouse_position() + _apply_input_modifiers(event) + return _handle_input_event(event) + + +func get_mouse_position() -> Vector2: + if _last_input_event is InputEventMouse: + return _last_input_event.position + var current_scene := scene() + if current_scene != null: + return current_scene.get_viewport().get_mouse_position() + return Vector2.ZERO + + +func get_global_mouse_position() -> Vector2: + return Engine.get_main_loop().root.get_mouse_position() + + +func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner: + var event := InputEventMouseMotion.new() + event.position = pos + event.relative = pos - get_mouse_position() + event.global_position = get_global_mouse_position() + _apply_input_mouse_mask(event) + _apply_input_modifiers(event) + return _handle_input_event(event) + + +func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + var tween := _scene_tree().create_tween() + _curent_mouse_position = get_mouse_position() + var final_position := _curent_mouse_position + relative + tween.tween_property(self, "_curent_mouse_position", final_position, time).set_trans(trans_type) + tween.play() + + while not get_mouse_position().is_equal_approx(final_position): + simulate_mouse_move(_curent_mouse_position) + await _scene_tree().process_frame + return self + + +func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + var tween := _scene_tree().create_tween() + _curent_mouse_position = get_mouse_position() + tween.tween_property(self, "_curent_mouse_position", position, time).set_trans(trans_type) + tween.play() + + while not get_mouse_position().is_equal_approx(position): + simulate_mouse_move(_curent_mouse_position) + await _scene_tree().process_frame + return self + + +func simulate_mouse_button_pressed(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: + simulate_mouse_button_press(buttonIndex, double_click) + simulate_mouse_button_release(buttonIndex) + return self + + +func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: + var event := InputEventMouseButton.new() + event.button_index = buttonIndex + event.pressed = true + event.double_click = double_click + _apply_input_mouse_position(event) + _apply_input_mouse_mask(event) + _apply_input_modifiers(event) + _mouse_button_on_press.append(buttonIndex) + return _handle_input_event(event) + + +func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner: + var event := InputEventMouseButton.new() + event.button_index = buttonIndex + event.pressed = false + _apply_input_mouse_position(event) + _apply_input_mouse_mask(event) + _apply_input_modifiers(event) + _mouse_button_on_press.erase(buttonIndex) + return _handle_input_event(event) + + +func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner: + _time_factor = min(9.0, time_factor) + __activate_time_factor() + __print("set time factor: %f" % _time_factor) + __print("set physics physics_ticks_per_second: %d" % (_saved_iterations_per_second*_time_factor)) + return self + + +func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: + var time_shift_frames :int = max(1, frames / _time_factor) + for frame in time_shift_frames: + if delta_milli == -1: + await _scene_tree().process_frame + else: + await _scene_tree().create_timer(delta_milli * 0.001).timeout + return self + + +func simulate_until_signal(signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner: + var args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000) + return self + + +func simulate_until_object_signal(source :Object, signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner: + var args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000) + return self + + +func await_func(func_name :String, args := []) -> GdUnitFuncAssert: + return GdUnitFuncAssertImpl.new(scene(), func_name, args) + + +func await_func_on(instance :Object, func_name :String, args := []) -> GdUnitFuncAssert: + return GdUnitFuncAssertImpl.new(instance, func_name, args) + + +func await_signal(signal_name :String, args := [], timeout := 2000 ): + await _awaiter.await_signal_on(scene(), signal_name, args, timeout) + + +func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ): + await _awaiter.await_signal_on(source, signal_name, args, timeout) + + +# maximizes the window to bring the scene visible +func maximize_view() -> GdUnitSceneRunner: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_move_to_foreground() + return self + + +func _property_exists(name :String) -> bool: + return scene().get_property_list().any(func(properties :Dictionary) : return properties["name"] == name) + + +func get_property(name :String) -> Variant: + if not _property_exists(name): + return "The property '%s' not exist checked loaded scene." % name + return scene().get(name) + + +func set_property(name :String, value :Variant) -> bool: + if not _property_exists(name): + push_error("The property named '%s' cannot be set, it does not exist!" % name) + return false; + scene().set(name, value) + return true + + +func invoke(name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG): + var args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + if scene().has_method(name): + return scene().callv(name, args) + return "The method '%s' not exist checked loaded scene." % name + + +func find_child(name :String, recursive :bool = true, owned :bool = false) -> Node: + return scene().find_child(name, recursive, owned) + + +func _scene_name() -> String: + var scene_script :GDScript = scene().get_script() + var scene_name :String = scene().get_name() + if not scene_script: + return scene_name + if not scene_name.begins_with("@"): + return scene_name + return scene_script.resource_name.get_basename() + + +func __activate_time_factor() -> void: + Engine.set_time_scale(_time_factor) + Engine.set_physics_ticks_per_second((_saved_iterations_per_second * _time_factor) as int) + + +func __deactivate_time_factor() -> void: + Engine.set_time_scale(1) + Engine.set_physics_ticks_per_second(_saved_iterations_per_second as int) + + +# copy over current active modifiers +func _apply_input_modifiers(event :InputEvent) -> void: + if _last_input_event is InputEventWithModifiers and event is InputEventWithModifiers: + event.meta_pressed = event.meta_pressed or _last_input_event.meta_pressed + event.alt_pressed = event.alt_pressed or _last_input_event.alt_pressed + event.shift_pressed = event.shift_pressed or _last_input_event.shift_pressed + event.ctrl_pressed = event.ctrl_pressed or _last_input_event.ctrl_pressed + # this line results into reset the control_pressed state!!! + #event.command_or_control_autoremap = event.command_or_control_autoremap or _last_input_event.command_or_control_autoremap + + +# copy over current active mouse mask and combine with curren mask +func _apply_input_mouse_mask(event :InputEvent) -> void: + # first apply last mask + if _last_input_event is InputEventMouse and event is InputEventMouse: + event.button_mask |= _last_input_event.button_mask + if event is InputEventMouseButton: + var button_mask = MAP_MOUSE_BUTTON_MASKS.get(event.get_button_index(), 0) + if event.is_pressed(): + event.button_mask |= button_mask + else: + event.button_mask ^= button_mask + + +# copy over last mouse position if need +func _apply_input_mouse_position(event :InputEvent) -> void: + if _last_input_event is InputEventMouse and event is InputEventMouseButton: + event.position = _last_input_event.position + + +## just for testing maunally event to action handling +func _handle_actions(event :InputEvent) -> bool: + var is_action_match := false + for action in InputMap.get_actions(): + if InputMap.event_is_action(event, action, true): + is_action_match = true + prints(action, event, event.is_ctrl_pressed()) + if event.is_pressed(): + Input.action_press(action, InputMap.action_get_deadzone(action)) + else: + Input.action_release(action) + return is_action_match + + +# for handling read https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html?highlight=inputevent#how-does-it-work +func _handle_input_event(event :InputEvent): + if event is InputEventMouse: + Input.warp_mouse(event.position) + Input.parse_input_event(event) + Input.flush_buffered_events() + var current_scene := scene() + if is_instance_valid(current_scene): + __print(" process event %s (%s) <- %s" % [current_scene, _scene_name(), event.as_text()]) + if(current_scene.has_method("_gui_input")): + current_scene._gui_input(event) + if(current_scene.has_method("_unhandled_input")): + current_scene._unhandled_input(event) + current_scene.get_viewport().set_input_as_handled() + # save last input event needs to be merged with next InputEventMouseButton + _last_input_event = event + return self + + +func _reset_input_to_default() -> void: + # reset all mouse button to inital state if need + for m_button in _mouse_button_on_press.duplicate(): + if Input.is_mouse_button_pressed(m_button): + simulate_mouse_button_release(m_button) + _mouse_button_on_press.clear() + + for key_scancode in _key_on_press.duplicate(): + if Input.is_key_pressed(key_scancode): + simulate_key_release(key_scancode) + _key_on_press.clear() + Input.flush_buffered_events() + _last_input_event = null + + +func __print(message :String) -> void: + if _verbose: + prints(message) + + +func __print_current_focus() -> void: + if not _verbose: + return + var focused_node = scene().get_viewport().gui_get_focus_owner() + if focused_node: + prints(" focus checked %s" % focused_node) + else: + prints(" no focus set") + + +func scene() -> Node: + if is_instance_valid(_current_scene): + return _current_scene + if not _is_disposed: + push_error("The current scene instance is not valid anymore! check your test is valid. e.g. check for missing awaits.") + return null diff --git a/addons/gdUnit4/src/core/GdUnitScriptType.gd b/addons/gdUnit4/src/core/GdUnitScriptType.gd new file mode 100644 index 0000000..7e1be51 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitScriptType.gd @@ -0,0 +1,16 @@ +class_name GdUnitScriptType +extends RefCounted + +const UNKNOWN := "" +const CS := "cs" +const GD := "gd" + + +static func type_of(script :Script) -> String: + if script == null: + return UNKNOWN + if GdObjects.is_gd_script(script): + return GD + if GdObjects.is_cs_script(script): + return CS + return UNKNOWN diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd new file mode 100644 index 0000000..f3e91f5 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd @@ -0,0 +1,336 @@ +@tool +class_name GdUnitSettings +extends RefCounted + + +const MAIN_CATEGORY = "gdunit4" +# Common Settings +const COMMON_SETTINGS = MAIN_CATEGORY + "/settings" + +const GROUP_COMMON = COMMON_SETTINGS + "/common" +const UPDATE_NOTIFICATION_ENABLED = GROUP_COMMON + "/update_notification_enabled" +const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes" + +const GROUP_TEST = COMMON_SETTINGS + "/test" +const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds" +const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder" +const TEST_SITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention" + + +# Report Setiings +const REPORT_SETTINGS = MAIN_CATEGORY + "/report" +const GROUP_GODOT = REPORT_SETTINGS + "/godot" +const REPORT_PUSH_ERRORS = GROUP_GODOT + "/push_error" +const REPORT_SCRIPT_ERRORS = GROUP_GODOT + "/script_error" +const REPORT_ORPHANS = REPORT_SETTINGS + "/verbose_orphans" +const GROUP_ASSERT = REPORT_SETTINGS + "/assert" +const REPORT_ASSERT_WARNINGS = GROUP_ASSERT + "/verbose_warnings" +const REPORT_ASSERT_ERRORS = GROUP_ASSERT + "/verbose_errors" +const REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE = GROUP_ASSERT + "/strict_number_type_compare" + +# Godot debug stdout/logging settings +const CATEGORY_LOGGING := "debug/file_logging/" +const STDOUT_ENABLE_TO_FILE = CATEGORY_LOGGING + "enable_file_logging" +const STDOUT_WITE_TO_FILE = CATEGORY_LOGGING + "log_path" + + +# GdUnit Templates +const TEMPLATES = MAIN_CATEGORY + "/templates" +const TEMPLATES_TS = TEMPLATES + "/testsuite" +const TEMPLATE_TS_GD = TEMPLATES_TS + "/GDScript" +const TEMPLATE_TS_CS = TEMPLATES_TS + "/CSharpScript" + + +# UI Setiings +const UI_SETTINGS = MAIN_CATEGORY + "/ui" +const GROUP_UI_INSPECTOR = UI_SETTINGS + "/inspector" +const INSPECTOR_NODE_COLLAPSE = GROUP_UI_INSPECTOR + "/node_collapse" + + +# Shortcut Setiings +const SHORTCUT_SETTINGS = MAIN_CATEGORY + "/Shortcuts" +const GROUP_SHORTCUT_INSPECTOR = SHORTCUT_SETTINGS + "/inspector" +const SHORTCUT_INSPECTOR_RERUN_TEST = GROUP_SHORTCUT_INSPECTOR + "/rerun_test" +const SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG = GROUP_SHORTCUT_INSPECTOR + "/rerun_test_debug" +const SHORTCUT_INSPECTOR_RUN_TEST_OVERALL = GROUP_SHORTCUT_INSPECTOR + "/run_test_overall" +const SHORTCUT_INSPECTOR_RUN_TEST_STOP = GROUP_SHORTCUT_INSPECTOR + "/run_test_stop" + +const GROUP_SHORTCUT_EDITOR = SHORTCUT_SETTINGS + "/editor" +const SHORTCUT_EDITOR_RUN_TEST = GROUP_SHORTCUT_EDITOR + "/run_test" +const SHORTCUT_EDITOR_RUN_TEST_DEBUG = GROUP_SHORTCUT_EDITOR + "/run_test_debug" +const SHORTCUT_EDITOR_CREATE_TEST = GROUP_SHORTCUT_EDITOR + "/create_test" + +const GROUP_SHORTCUT_FILESYSTEM = SHORTCUT_SETTINGS + "/filesystem" +const SHORTCUT_FILESYSTEM_RUN_TEST = GROUP_SHORTCUT_FILESYSTEM + "/run_test" +const SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG = GROUP_SHORTCUT_FILESYSTEM + "/run_test_debug" + + +# Toolbar Setiings +const GROUP_UI_TOOLBAR = UI_SETTINGS + "/toolbar" +const INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL = GROUP_UI_TOOLBAR + "/run_overall" + +# defaults +# server connection timeout in minutes +const DEFAULT_SERVER_TIMEOUT :int = 30 +# test case runtime timeout in seconds +const DEFAULT_TEST_TIMEOUT :int = 60*5 +# the folder to create new test-suites +const DEFAULT_TEST_LOOKUP_FOLDER := "test" + +# help texts +const HELP_TEST_LOOKUP_FOLDER := "Sets the subfolder for the search/creation of test suites. (leave empty to use source folder)" + +enum NAMING_CONVENTIONS { + AUTO_DETECT, + SNAKE_CASE, + PASCAL_CASE, +} + + +static func setup() -> void: + create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Enables/Disables the update notification checked startup.") + create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Sets the server connection timeout in minutes.") + create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Sets the test case runtime timeout in seconds.") + create_property_if_need(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER, HELP_TEST_LOOKUP_FOLDER) + create_property_if_need(TEST_SITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Sets test-suite genrate script name convention.", NAMING_CONVENTIONS.keys()) + create_property_if_need(REPORT_PUSH_ERRORS, false, "Enables/Disables report of push_error() as failure!") + create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Enables/Disables report of script errors as failure!") + create_property_if_need(REPORT_ORPHANS, true, "Enables/Disables orphan reporting.") + create_property_if_need(REPORT_ASSERT_ERRORS, true, "Enables/Disables error reporting checked asserts.") + create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Enables/Disables warning reporting checked asserts") + create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Enabled/disabled number values will be compared strictly by type. (real vs int)") + create_property_if_need(INSPECTOR_NODE_COLLAPSE, true, "Enables/Disables that the testsuite node is closed after a successful test run.") + create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false, "Shows/Hides the 'Run overall Tests' button in the inspector toolbar.") + create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Defines the test suite template") + create_shortcut_properties_if_need() + migrate_properties() + + + +static func migrate_properties() -> void: + var TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder" + if get_property(TEST_ROOT_FOLDER) != null: + migrate_property(TEST_ROOT_FOLDER,\ + TEST_LOOKUP_FOLDER,\ + DEFAULT_TEST_LOOKUP_FOLDER,\ + HELP_TEST_LOOKUP_FOLDER,\ + func(value): return DEFAULT_TEST_LOOKUP_FOLDER if value == null else value) + + +static func create_shortcut_properties_if_need() -> void: + # inspector + create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun of the last tests performed.") + create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun of the last tests performed (Debug).") + create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug).") + create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stops the current test execution.") + # script editor + create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Runs the currently selected test.") + create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Runs the currently selected test (Debug).") + create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Creates a new test case for the currently selected function.") + # filesystem + create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Runs all test suites on the selected folder or file.") + create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Runs all test suites on the selected folder or file (Debug).") + + +static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void: + if not ProjectSettings.has_setting(name): + #prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)]) + ProjectSettings.set_setting(name, default) + + ProjectSettings.set_initial_value(name, default) + help += "" if value_set.is_empty() else " %s" % value_set + set_help(name, default, help) + + +static func set_help(property_name :String, value :Variant, help :String) -> void: + ProjectSettings.add_property_info({ + "name": property_name, + "type": typeof(value), + "hint": PROPERTY_HINT_TYPE_STRING, + "hint_string": help + }) + + +static func get_setting(name :String, default :Variant) -> Variant: + if ProjectSettings.has_setting(name): + return ProjectSettings.get_setting(name) + return default + + +static func is_update_notification_enabled() -> bool: + if ProjectSettings.has_setting(UPDATE_NOTIFICATION_ENABLED): + return ProjectSettings.get_setting(UPDATE_NOTIFICATION_ENABLED) + return false + + +static func set_update_notification(enable :bool) -> void: + ProjectSettings.set_setting(UPDATE_NOTIFICATION_ENABLED, enable) + ProjectSettings.save() + + +static func get_log_path() -> String: + return ProjectSettings.get_setting(STDOUT_WITE_TO_FILE) + + +static func set_log_path(path :String) -> void: + ProjectSettings.set_setting(STDOUT_ENABLE_TO_FILE, true) + ProjectSettings.set_setting(STDOUT_WITE_TO_FILE, path) + ProjectSettings.save() + + +# the configured server connection timeout in ms +static func server_timeout() -> int: + return get_setting(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT) * 60 * 1000 + + +# the configured test case timeout in ms +static func test_timeout() -> int: + return get_setting(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT) * 1000 + + +# the root folder to store/generate test-suites +static func test_root_folder() -> String: + return get_setting(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER) + + +static func is_verbose_assert_warnings() -> bool: + return get_setting(REPORT_ASSERT_WARNINGS, true) + + +static func is_verbose_assert_errors() -> bool: + return get_setting(REPORT_ASSERT_ERRORS, true) + + +static func is_verbose_orphans() -> bool: + return get_setting(REPORT_ORPHANS, true) + + +static func is_strict_number_type_compare() -> bool: + return get_setting(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true) + + +static func is_report_push_errors() -> bool: + return get_setting(REPORT_PUSH_ERRORS, false) + + +static func is_report_script_errors() -> bool: + return get_setting(REPORT_SCRIPT_ERRORS, true) + + +static func is_inspector_node_collapse() -> bool: + return get_setting(INSPECTOR_NODE_COLLAPSE, true) + + +static func is_inspector_toolbar_button_show() -> bool: + return get_setting(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, true) + + +static func is_log_enabled() -> bool: + return ProjectSettings.get_setting(STDOUT_ENABLE_TO_FILE) + + +static func list_settings(category :String) -> Array[GdUnitProperty]: + var settings :Array[GdUnitProperty] = [] + for property in ProjectSettings.get_property_list(): + var property_name :String = property["name"] + if property_name.begins_with(category): + var value :Variant = ProjectSettings.get_setting(property_name) + var default :Variant = ProjectSettings.property_get_revert(property_name) + var help :String = property["hint_string"] + var value_set := extract_value_set_from_help(help) + settings.append(GdUnitProperty.new(property_name, property["type"], value, default, help, value_set)) + return settings + + +static func extract_value_set_from_help(value :String) -> PackedStringArray: + var regex := RegEx.new() + regex.compile("\\[(.+)\\]") + var matches := regex.search_all(value) + if matches.is_empty(): + return PackedStringArray() + var values :String = matches[0].get_string(1) + return values.replacen(" ", "").replacen("\"", "").split(",", false) + + +static func update_property(property :GdUnitProperty) -> Variant: + var current_value :Variant = ProjectSettings.get_setting(property.name()) + if current_value != property.value(): + var error :Variant = validate_property_value(property) + if error != null: + return error + ProjectSettings.set_setting(property.name(), property.value()) + GdUnitSignals.instance().gdunit_settings_changed.emit(property) + _save_settings() + return null + + +static func reset_property(property :GdUnitProperty) -> void: + ProjectSettings.set_setting(property.name(), property.default()) + GdUnitSignals.instance().gdunit_settings_changed.emit(property) + _save_settings() + + +static func validate_property_value(property :GdUnitProperty) -> Variant: + match property.name(): + TEST_LOOKUP_FOLDER: + return validate_lookup_folder(property.value()) + _: return null + + +static func validate_lookup_folder(value :String) -> Variant: + if value.is_empty() or value == "/": + return null + if value.contains("res:"): + return "Test Lookup Folder: do not allowed to contains 'res://'" + if not value.is_valid_filename(): + return "Test Lookup Folder: contains invalid characters! e.g (: / \\ ? * \" | % < >)" + return null + + +static func save_property(name :String, value :Variant) -> void: + ProjectSettings.set_setting(name, value) + _save_settings() + + +static func _save_settings() -> void: + var err := ProjectSettings.save() + if err != OK: + push_error("Save GdUnit4 settings failed : %s" % error_string(err)) + return + + +static func has_property(name :String) -> bool: + return ProjectSettings.get_property_list().any(func(property :Dictionary) -> bool: return property["name"] == name) + + +static func get_property(name :String) -> GdUnitProperty: + for property in ProjectSettings.get_property_list(): + var property_name :String = property["name"] + if property_name == name: + var value :Variant = ProjectSettings.get_setting(property_name) + var default :Variant = ProjectSettings.property_get_revert(property_name) + var help :String = property["hint_string"] + var value_set := extract_value_set_from_help(help) + return GdUnitProperty.new(property_name, property["type"], value, default, help, value_set) + return null + + +static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void: + var property := get_property(old_property) + if property == null: + prints("Migration not possible, property '%s' not found" % old_property) + return + var value :Variant = converter.call(property.value()) if converter.is_valid() else property.value() + ProjectSettings.set_setting(new_property, value) + ProjectSettings.set_initial_value(new_property, default_value) + set_help(new_property, value, help) + ProjectSettings.clear(old_property) + prints("Succesfull migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value]) + + +static func dump_to_tmp() -> void: + ProjectSettings.save_custom("user://project_settings.godot") + + +static func restore_dump_from_tmp() -> void: + DirAccess.copy_absolute("user://project_settings.godot", "res://project.godot") diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd new file mode 100644 index 0000000..0172b73 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd @@ -0,0 +1,64 @@ +class_name GdUnitSignalAwaiter +extends RefCounted + +signal signal_emitted(action) + +const NO_ARG :Variant = GdUnitConstants.NO_ARG + +var _wait_on_idle_frame = false +var _interrupted := false +var _time_left := 0 +var _timeout_millis :int + + +func _init(timeout_millis :int, wait_on_idle_frame := false): + _timeout_millis = timeout_millis + _wait_on_idle_frame = wait_on_idle_frame + + +func _on_signal_emmited(arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG): + var signal_args :Variant = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + signal_emitted.emit(signal_args) + + +func is_interrupted() -> bool: + return _interrupted + + +func elapsed_time() -> int: + return _time_left + + +func on_signal(source :Object, signal_name :String, expected_signal_args :Array) -> Variant: + # register checked signal to wait for + source.connect(signal_name, _on_signal_emmited) + # install timeout timer + var timer = Timer.new() + Engine.get_main_loop().root.add_child(timer) + timer.add_to_group("GdUnitTimers") + timer.set_one_shot(true) + timer.timeout.connect(func do_interrupt(): + _interrupted = true + signal_emitted.emit(null) + , CONNECT_DEFERRED) + timer.start(_timeout_millis * 0.001 * Engine.get_time_scale()) + + # holds the emited value + var value :Variant + # wait for signal is emitted or a timeout is happen + while true: + value = await signal_emitted + if _interrupted: + break + if not (value is Array): + value = [value] + if expected_signal_args.size() == 0 or GdObjects.equals(value, expected_signal_args): + break + await Engine.get_main_loop().process_frame + + source.disconnect(signal_name, _on_signal_emmited) + _time_left = timer.time_left + await Engine.get_main_loop().process_frame + if value is Array and value.size() == 1: + return value[0] + return value diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd new file mode 100644 index 0000000..f797e15 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd @@ -0,0 +1,103 @@ +# It connects to all signals of given emitter and collects received signals and arguments +# The collected signals are cleand finally when the emitter is freed. +class_name GdUnitSignalCollector +extends RefCounted + +const NO_ARG :Variant = GdUnitConstants.NO_ARG +const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"] + +# { +# emitter : { +# signal_name : [signal_args], +# ... +# } +# } +var _collected_signals :Dictionary = {} + + +func clear() -> void: + for emitter in _collected_signals.keys(): + if is_instance_valid(emitter): + unregister_emitter(emitter) + + +# connect to all possible signals defined by the emitter +# prepares the signal collection to store received signals and arguments +func register_emitter(emitter :Object): + if is_instance_valid(emitter): + # check emitter is already registerd + if _collected_signals.has(emitter): + return + _collected_signals[emitter] = Dictionary() + # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. + if emitter is Node and !emitter.tree_exiting.is_connected(unregister_emitter): + emitter.tree_exiting.connect(unregister_emitter.bind(emitter)) + # connect to all signals of the emitter we want to collect + for signal_def in emitter.get_signal_list(): + var signal_name = signal_def["name"] + # set inital collected to empty + if not is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name] = Array() + if SIGNAL_BLACK_LIST.find(signal_name) != -1: + continue + if !emitter.is_connected(signal_name, _on_signal_emmited): + var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + if err != OK: + push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)]) + + +# unregister all acquired resources/connections, otherwise it ends up in orphans +# is called when the emitter is removed from the parent +func unregister_emitter(emitter :Object): + if is_instance_valid(emitter): + for signal_def in emitter.get_signal_list(): + var signal_name = signal_def["name"] + if emitter.is_connected(signal_name, _on_signal_emmited): + emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + _collected_signals.erase(emitter) + + +# receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements +func _on_signal_emmited( arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG, arg10=NO_ARG, arg11=NO_ARG): + var signal_args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG) + # extract the emitter and signal_name from the last two arguments (see line 61 where is added) + var signal_name :String = signal_args.pop_back() + var emitter :Object = signal_args.pop_back() + #prints("_on_signal_emmited:", emitter, signal_name, signal_args) + if is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name].append(signal_args) + + +func reset_received_signals(emitter :Object, signal_name: String, signal_args :Array): + #_debug_signal_list("before claer"); + if _collected_signals.has(emitter): + var signals_by_emitter = _collected_signals[emitter] + if signals_by_emitter.has(signal_name): + _collected_signals[emitter][signal_name].erase(signal_args) + #_debug_signal_list("after claer"); + + +func is_signal_collecting(emitter :Object, signal_name :String) -> bool: + return _collected_signals.has(emitter) and _collected_signals[emitter].has(signal_name) + + +func match(emitter :Object, signal_name :String, args :Array) -> bool: + #prints("match", signal_name, _collected_signals[emitter][signal_name]); + if _collected_signals.is_empty() or not _collected_signals.has(emitter): + return false + for received_args in _collected_signals[emitter][signal_name]: + #prints("testing", signal_name, received_args, "vs", args) + if GdObjects.equals(received_args, args): + return true + return false + + +func _debug_signal_list(message :String): + prints("-----", message, "-------") + prints("senders {") + for emitter in _collected_signals: + prints("\t", emitter) + for signal_name in _collected_signals[emitter]: + var args = _collected_signals[emitter][signal_name] + prints("\t\t", signal_name, args) + prints("}") diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd b/addons/gdUnit4/src/core/GdUnitSignals.gd new file mode 100644 index 0000000..faf089f --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd @@ -0,0 +1,34 @@ +class_name GdUnitSignals +extends RefCounted + +signal gdunit_client_connected(client_id :int) +signal gdunit_client_disconnected(client_id :int) +signal gdunit_client_terminated() + +signal gdunit_event(event :GdUnitEvent) +signal gdunit_event_debug(event :GdUnitEvent) +signal gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto) +signal gdunit_message(message :String) +signal gdunit_report(execution_context_id :int, report :GdUnitReport) +signal gdunit_set_test_failed(is_failed :bool) + +signal gdunit_settings_changed(property :GdUnitProperty) + +const META_KEY := "GdUnitSignals" + + +static func instance() -> GdUnitSignals: + if Engine.has_meta(META_KEY): + return Engine.get_meta(META_KEY) + var instance_ := GdUnitSignals.new() + Engine.set_meta(META_KEY, instance_) + return instance_ + + +static func dispose() -> void: + var signals := instance() + # cleanup connected signals + for signal_ in signals.get_signal_list(): + for connection in signals.get_signal_connection_list(signal_["name"]): + connection["signal"].disconnect(connection["callable"]) + Engine.remove_meta(META_KEY) diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd b/addons/gdUnit4/src/core/GdUnitSingleton.gd new file mode 100644 index 0000000..879c9d4 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -0,0 +1,49 @@ +################################################################################ +# Provides access to a global accessible singleton +# +# This is a workarount to the existing auto load singleton because of some bugs +# around plugin handling +################################################################################ +class_name GdUnitSingleton +extends RefCounted + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const MEATA_KEY := "GdUnitSingletons" + + +static func instance(name :String, clazz :Callable) -> Variant: + if Engine.has_meta(name): + return Engine.get_meta(name) + var singleton :Variant = clazz.call() + Engine.set_meta(name, singleton) + GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton]) + var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + singletons.append(name) + Engine.set_meta(MEATA_KEY, singletons) + return singleton + + +static func unregister(p_singleton :String) -> void: + var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + if singletons.has(p_singleton): + GdUnitTools.prints_verbose("\n Unregister singleton '%s'" % p_singleton); + var index := singletons.find(p_singleton) + singletons.remove_at(index) + var instance_ :Variant = Engine.get_meta(p_singleton) + GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_]) + GdUnitTools.free_instance(instance_) + Engine.remove_meta(p_singleton) + GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton) + Engine.set_meta(MEATA_KEY, singletons) + + +static func dispose() -> void: + # use a copy because unregister is modify the singletons array + var singletons := PackedStringArray(Engine.get_meta(MEATA_KEY, PackedStringArray())) + GdUnitTools.prints_verbose("----------------------------------------------------------------") + GdUnitTools.prints_verbose("Cleanup singletons %s" % singletons) + for singleton in singletons: + unregister(singleton) + Engine.remove_meta(MEATA_KEY) + GdUnitTools.prints_verbose("----------------------------------------------------------------") diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd new file mode 100644 index 0000000..6cd2ee4 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -0,0 +1,18 @@ +class_name GdUnitTestSuiteBuilder +extends RefCounted + + +static func create(source :Script, line_number :int) -> GdUnitResult: + var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder()) + # we need to save and close the testsuite and source if is current opened before modify + ScriptEditorControls.save_an_open_script(source.resource_path) + ScriptEditorControls.save_an_open_script(test_suite_path, true) + if GdObjects.is_cs_script(source): + return GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path) + var parser := GdScriptParser.new() + var lines := source.source_code.split("\n") + var current_line := lines[line_number] + var func_name := parser.parse_func_name(current_line) + if func_name.is_empty(): + return GdUnitResult.error("No function found at line: %d." % line_number) + return GdUnitTestSuiteScanner.create_test_case(test_suite_path, func_name, source.resource_path) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd new file mode 100644 index 0000000..af933b2 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd @@ -0,0 +1,339 @@ +class_name GdUnitTestSuiteScanner +extends RefCounted + +const TEST_FUNC_TEMPLATE =""" + +func test_${func_name}() -> void: + # remove this line and complete your test + assert_not_yet_implemented() +""" + + +# we exclude the gdunit source directorys by default +const exclude_scan_directories = [ + "res://addons/gdUnit4/bin", + "res://addons/gdUnit4/src", + "res://reports"] + + +var _script_parser := GdScriptParser.new() +var _extends_test_suite_classes := Array() +var _expression_runner := GdUnitExpressionRunner.new() + + +func scan_testsuite_classes() -> void: + # scan and cache extends GdUnitTestSuite by class name an resource paths + _extends_test_suite_classes.append("GdUnitTestSuite") + if ProjectSettings.has_setting("_global_script_classes"): + var script_classes:Array = ProjectSettings.get_setting("_global_script_classes") as Array + for element in script_classes: + var script_meta = element as Dictionary + if script_meta["base"] == "GdUnitTestSuite": + _extends_test_suite_classes.append(script_meta["class"]) + + +func scan(resource_path :String) -> Array[Node]: + scan_testsuite_classes() + # if single testsuite requested + if FileAccess.file_exists(resource_path): + var test_suite := _parse_is_test_suite(resource_path) + if test_suite != null: + return [test_suite] + return [] as Array[Node] + var base_dir := DirAccess.open(resource_path) + if base_dir == null: + prints("Given directory or file does not exists:", resource_path) + return [] + return _scan_test_suites(base_dir, []) + + +func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[Node]: + if exclude_scan_directories.has(dir.get_current_dir()): + return collected_suites + prints("Scanning for test suites in:", dir.get_current_dir()) + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name := dir.get_next() + while file_name != "": + var resource_path = GdUnitTestSuiteScanner._file(dir, file_name) + if dir.current_is_dir(): + var sub_dir := DirAccess.open(resource_path) + if sub_dir != null: + _scan_test_suites(sub_dir, collected_suites) + else: + var time = LocalTime.now() + var test_suite := _parse_is_test_suite(resource_path) + if test_suite: + collected_suites.append(test_suite) + if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300: + push_warning("Scanning of test-suite '%s' took more than 300ms: " % resource_path, time.elapsed_since()) + file_name = dir.get_next() + return collected_suites + + +static func _file(dir :DirAccess, file_name :String) -> String: + var current_dir := dir.get_current_dir() + if current_dir.ends_with("/"): + return current_dir + file_name + return current_dir + "/" + file_name + + +func _parse_is_test_suite(resource_path :String) -> Node: + if not GdUnitTestSuiteScanner._is_script_format_supported(resource_path): + return null + if GdUnit4CSharpApiLoader.is_test_suite(resource_path): + return GdUnit4CSharpApiLoader.parse_test_suite(resource_path) + var script :Script = ResourceLoader.load(resource_path) + if not GdObjects.is_test_suite(script): + return null + if GdObjects.is_gd_script(script): + return _parse_test_suite(script) + return null + + +static func _is_script_format_supported(resource_path :String) -> bool: + var ext := resource_path.get_extension() + if ext == "gd": + return true + return GdUnit4CSharpApiLoader.is_csharp_file(resource_path) + + +func _parse_test_suite(script :GDScript) -> GdUnitTestSuite: + # find all test cases + var test_case_names := _extract_test_case_names(script) + # test suite do not contains any tests + if test_case_names.is_empty(): + push_warning("The test suite %s do not contain any tests, it excludes from discovery." % script.resource_path) + return null; + + var test_suite = script.new() + test_suite.set_name(GdUnitTestSuiteScanner.parse_test_suite_name(script)) + # add test cases to test suite and parse test case line nummber + _parse_and_add_test_cases(test_suite, script, test_case_names) + # not all test case parsed? + # we have to scan the base class to + if not test_case_names.is_empty(): + var base_script :GDScript = test_suite.get_script().get_base_script() + while base_script is GDScript: + # do not parse testsuite itself + if base_script.resource_path.find("GdUnitTestSuite") == -1: + _parse_and_add_test_cases(test_suite, base_script, test_case_names) + base_script = base_script.get_base_script() + return test_suite + + +func _extract_test_case_names(script :GDScript) -> PackedStringArray: + var names := PackedStringArray() + for method in script.get_script_method_list(): + var funcName :String = method["name"] + if funcName.begins_with("test"): + names.append(funcName) + return names + + +static func parse_test_suite_name(script :Script) -> String: + return script.resource_path.get_file().replace(".gd", "") + + +func _handle_test_suite_arguments(test_suite, script :GDScript, fd :GdFunctionDescriptor): + for arg in fd.args(): + match arg.name(): + _TestCase.ARGUMENT_SKIP: + var result = _expression_runner.execute(script, arg.value_as_string()) + if result is bool: + test_suite.__is_skipped = result + else: + push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.value_as_string()) + _TestCase.ARGUMENT_SKIP_REASON: + test_suite.__skip_reason = arg.value_as_string() + _: + push_error("Unsuported argument `%s` found on before() at '%s'!" % [arg.name(), script.resource_path]) + + +func _handle_test_case_arguments(test_suite, script :GDScript, fd :GdFunctionDescriptor): + var timeout := _TestCase.DEFAULT_TIMEOUT + var iterations := Fuzzer.ITERATION_DEFAULT_COUNT + var seed_value := -1 + var is_skipped := false + var skip_reason := "Unknown." + var fuzzers :Array[GdFunctionArgument] = [] + var test := _TestCase.new() + + for arg in fd.args(): + # verify argument is allowed + # is test using fuzzers? + if arg.type() == GdObjects.TYPE_FUZZER: + fuzzers.append(arg) + elif arg.has_default(): + match arg.name(): + _TestCase.ARGUMENT_TIMEOUT: + timeout = arg.default() + _TestCase.ARGUMENT_SKIP: + var result = _expression_runner.execute(script, arg.value_as_string()) + if result is bool: + is_skipped = result + else: + push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.value_as_string()) + _TestCase.ARGUMENT_SKIP_REASON: + skip_reason = arg.value_as_string() + Fuzzer.ARGUMENT_ITERATIONS: + iterations = arg.default() + Fuzzer.ARGUMENT_SEED: + seed_value = arg.default() + # create new test + test.configure(fd.name(), fd.line_number(), script.resource_path, timeout, fuzzers, iterations, seed_value) + test.set_function_descriptor(fd) + test.skip(is_skipped, skip_reason) + _validate_argument(fd, test) + test_suite.add_child(test) + + +func _parse_and_add_test_cases(test_suite, script :GDScript, test_case_names :PackedStringArray): + var test_cases_to_find = Array(test_case_names) + var functions_to_scan := test_case_names.duplicate() + functions_to_scan.append("before") + var source := _script_parser.load_source_code(script, [script.resource_path]) + var function_descriptors := _script_parser.parse_functions(source, "", [script.resource_path], functions_to_scan) + for fd in function_descriptors: + if fd.name() == "before": + _handle_test_suite_arguments(test_suite, script, fd) + if test_cases_to_find.has(fd.name()): + _handle_test_case_arguments(test_suite, script, fd) + + +const TEST_CASE_ARGUMENTS = [_TestCase.ARGUMENT_TIMEOUT, _TestCase.ARGUMENT_SKIP, _TestCase.ARGUMENT_SKIP_REASON, Fuzzer.ARGUMENT_ITERATIONS, Fuzzer.ARGUMENT_SEED] + +func _validate_argument(fd :GdFunctionDescriptor, test_case :_TestCase) -> void: + if fd.is_parameterized(): + return + for argument in fd.args(): + if argument.type() == GdObjects.TYPE_FUZZER or argument.name() in TEST_CASE_ARGUMENTS: + continue + test_case.skip(true, "Unknown test case argument '%s' found." % argument.name()) + + +# converts given file name by configured naming convention +static func _to_naming_convention(file_name :String) -> String: + var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SITE_NAMING_CONVENTION, 0) + match nc: + GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT: + if GdObjects.is_snake_case(file_name): + return GdObjects.to_snake_case(file_name + "Test") + return GdObjects.to_pascal_case(file_name + "Test") + GdUnitSettings.NAMING_CONVENTIONS.SNAKE_CASE: + return GdObjects.to_snake_case(file_name + "Test") + GdUnitSettings.NAMING_CONVENTIONS.PASCAL_CASE: + return GdObjects.to_pascal_case(file_name + "Test") + push_error("Unexpected case") + return "--" + + +static func resolve_test_suite_path(source_script_path :String, test_root_folder :String = "test") -> String: + var file_name = source_script_path.get_basename().get_file() + var suite_name := _to_naming_convention(file_name) + if test_root_folder.is_empty() or test_root_folder == "/": + return source_script_path.replace(file_name, suite_name) + + # is user tmp + if source_script_path.begins_with("user://tmp"): + return normalize_path(source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder)).replace(file_name, suite_name) + + # at first look up is the script under a "src" folder located + var test_suite_path :String + var src_folder = source_script_path.find("/src/") + if src_folder != -1: + test_suite_path = source_script_path.replace("/src/", "/"+test_root_folder+"/") + else: + var paths = source_script_path.split("/", false) + # is a plugin script? + if paths[1] == "addons": + test_suite_path = "%s//addons/%s/%s" % [paths[0], paths[2], test_root_folder] + # rebuild plugin path + for index in range(3, paths.size()): + test_suite_path += "/" + paths[index] + else: + test_suite_path = paths[0] + "//" + test_root_folder + for index in range(1, paths.size()): + test_suite_path += "/" + paths[index] + return normalize_path(test_suite_path).replace(file_name, suite_name) + + +static func normalize_path(path :String) -> String: + return path.replace("///", "/") + + +static func create_test_suite(test_suite_path :String, source_path :String) -> GdUnitResult: + # create directory if not exists + if not DirAccess.dir_exists_absolute(test_suite_path.get_base_dir()): + var error := DirAccess.make_dir_recursive_absolute(test_suite_path.get_base_dir()) + if error != OK: + return GdUnitResult.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error]) + var script := GDScript.new() + script.source_code = GdUnitTestSuiteTemplate.build_template(source_path) + var error := ResourceSaver.save(script, test_suite_path) + if error != OK: + return GdUnitResult.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error]) + return GdUnitResult.success(test_suite_path) + + +static func get_test_case_line_number(resource_path :String, func_name :String) -> int: + var file := FileAccess.open(resource_path, FileAccess.READ) + if file != null: + var script_parser := GdScriptParser.new() + var line_number := 0 + while not file.eof_reached(): + var row := GdScriptParser.clean_up_row(file.get_line()) + line_number += 1 + # ignore comments and empty lines and not test functions + if row.begins_with("#") || row.length() == 0 || row.find("functest") == -1: + continue + # abort if test case name found + if script_parser.parse_func_name(row) == "test_" + func_name: + return line_number + return -1 + + +static func add_test_case(resource_path :String, func_name :String) -> GdUnitResult: + var script := load(resource_path) as GDScript + # count all exiting lines and add two as space to add new test case + var line_number := count_lines(script) + 2 + var func_body := TEST_FUNC_TEMPLATE.replace("${func_name}", func_name) + if Engine.is_editor_hint(): + var ep :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + var settings := ep.get_editor_interface().get_editor_settings() + var ident_type :int = settings.get_setting("text_editor/behavior/indent/type") + var ident_size :int = settings.get_setting("text_editor/behavior/indent/size") + if ident_type == 1: + func_body = func_body.replace(" ", "".lpad(ident_size, " ")) + script.source_code += func_body + var error := ResourceSaver.save(script, resource_path) + if error != OK: + return GdUnitResult.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error]) + return GdUnitResult.success({ "path" : resource_path, "line" : line_number}) + + +static func count_lines(script : GDScript) -> int: + return script.source_code.split("\n").size() + + +static func test_suite_exists(test_suite_path :String) -> bool: + return FileAccess.file_exists(test_suite_path) + +static func test_case_exists(test_suite_path :String, func_name :String) -> bool: + if not test_suite_exists(test_suite_path): + return false + var script := ResourceLoader.load(test_suite_path) as GDScript + for f in script.get_script_method_list(): + if f["name"] == "test_" + func_name: + return true + return false + +static func create_test_case(test_suite_path :String, func_name :String, source_script_path :String) -> GdUnitResult: + if test_case_exists(test_suite_path, func_name): + var line_number := get_test_case_line_number(test_suite_path, func_name) + return GdUnitResult.success({ "path" : test_suite_path, "line" : line_number}) + + if not test_suite_exists(test_suite_path): + var result := create_test_suite(test_suite_path, source_script_path) + if result.is_error(): + return result + return add_test_case(test_suite_path, func_name) diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd new file mode 100644 index 0000000..d577c2b --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -0,0 +1,111 @@ +extends RefCounted + +static func normalize_text(text :String) -> String: + return text.replace("\r", ""); + + +static func richtext_normalize(input :String) -> String: + return GdUnitSingleton.instance("regex_richtext", func _regex_richtext() -> RegEx: + return to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]") )\ + .sub(input, "", true).replace("\r", "") + + +static func to_regex(pattern :String) -> RegEx: + var regex := RegEx.new() + var err := regex.compile(pattern) + if err != OK: + push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, error_string(err)]) + return regex + + +static func prints_verbose(message :String) -> void: + if OS.is_stdout_verbose(): + prints(message) + + +static func free_instance(instance :Variant, is_stdout_verbose :=false) -> bool: + if instance is Array: + for element in instance: + free_instance(element) + instance.clear() + return true + # do not free an already freed instance + if not is_instance_valid(instance): + return false + # do not free a class refernece + if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"): + return false + if is_stdout_verbose: + print_verbose("GdUnit4:gc():free instance ", instance) + release_double(instance) + if instance is RefCounted: + instance.notification(Object.NOTIFICATION_PREDELETE) + await Engine.get_main_loop().process_frame + await Engine.get_main_loop().physics_frame + return true + else: + # is instance already freed? + #if not is_instance_valid(instance) or ClassDB.class_get_property(instance, "new"): + # return false + #release_connections(instance) + if instance is Timer: + instance.stop() + instance.call_deferred("free") + await Engine.get_main_loop().process_frame + return true + if instance is Node and instance.get_parent() != null: + if is_stdout_verbose: + print_verbose("GdUnit4:gc():remove node from parent ", instance.get_parent(), instance) + instance.get_parent().remove_child(instance) + instance.set_owner(null) + instance.free() + return !is_instance_valid(instance) + + +static func _release_connections(instance :Object) -> void: + if is_instance_valid(instance): + # disconnect from all connected signals to force freeing, otherwise it ends up in orphans + for connection in instance.get_incoming_connections(): + var signal_ :Signal = connection["signal"] + var callable_ :Callable = connection["callable"] + #prints(instance, connection) + #prints("signal", signal_.get_name(), signal_.get_object()) + #prints("callable", callable_.get_object()) + if instance.has_signal(signal_.get_name()) and instance.is_connected(signal_.get_name(), callable_): + #prints("disconnect signal", signal_.get_name(), callable_) + instance.disconnect(signal_.get_name(), callable_) + release_timers() + + +static func release_timers() -> void: + # we go the new way to hold all gdunit timers in group 'GdUnitTimers' + for node in Engine.get_main_loop().root.get_children(): + if is_instance_valid(node) and node.is_in_group("GdUnitTimers"): + if is_instance_valid(node): + Engine.get_main_loop().root.remove_child(node) + node.stop() + node.free() + + +# the finally cleaup unfreed resources and singletons +static func dispose_all() -> void: + release_timers() + GdUnitSignals.dispose() + GdUnitSingleton.dispose() + + +# if instance an mock or spy we need manually freeing the self reference +static func release_double(instance :Object) -> void: + if instance.has_method("__release_double"): + instance.call("__release_double") + + +static func clear_push_errors() -> void: + var runner :Node = Engine.get_meta("GdUnitRunner") + if runner != null: + runner.clear_push_errors() + + +static func register_expect_interupted_by_timeout(test_suite :Node, test_case_name :String) -> void: + var test_case :Node = test_suite.find_child(test_case_name, false, false) + test_case.expect_to_interupt() diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd b/addons/gdUnit4/src/core/GodotVersionFixures.gd new file mode 100644 index 0000000..2c331d5 --- /dev/null +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd @@ -0,0 +1,21 @@ +## This service class contains helpers to wrap Godot functions and handle them carefully depending on the current Godot version +class_name GodotVersionFixures +extends RefCounted + + + +## Returns the icon property defined by name and theme_type, if it exists. +static func get_icon(control :Control, icon_name :String) -> Texture2D: + if Engine.get_version_info().hex >= 040200: + return control.get_theme_icon(icon_name, "EditorIcons") + return control.theme.get_icon(icon_name, "EditorIcons") + + +@warning_ignore("shadowed_global_identifier") +static func type_convert(value: Variant, type: int): + return convert(value, type) + + +@warning_ignore("shadowed_global_identifier") +static func convert(value: Variant, type: int) -> Variant: + return type_convert(value, type) diff --git a/addons/gdUnit4/src/core/LocalTime.gd b/addons/gdUnit4/src/core/LocalTime.gd new file mode 100644 index 0000000..fc8fc25 --- /dev/null +++ b/addons/gdUnit4/src/core/LocalTime.gd @@ -0,0 +1,110 @@ +# This class provides Date/Time functionallity to Godot +class_name LocalTime +extends Resource + +enum TimeUnit { + MILLIS = 1, + SECOND = 2, + MINUTE = 3, + HOUR = 4, + DAY = 5, + MONTH = 6, + YEAR = 7 +} + +const SECONDS_PER_MINUTE:int = 60 +const MINUTES_PER_HOUR:int = 60 +const HOURS_PER_DAY:int = 24 +const MILLIS_PER_SECOND:int = 1000 +const MILLIS_PER_MINUTE:int = MILLIS_PER_SECOND * SECONDS_PER_MINUTE +const MILLIS_PER_HOUR:int = MILLIS_PER_MINUTE * MINUTES_PER_HOUR + +var _time :int +var _hour :int +var _minute :int +var _second :int +var _millisecond :int + + +static func now() -> LocalTime: + return LocalTime.new(_get_system_time_msecs()) + + +static func of_unix_time(time_ms :int) -> LocalTime: + return LocalTime.new(time_ms) + + +static func local_time(hours :int, minutes :int, seconds :int, milliseconds :int) -> LocalTime: + return LocalTime.new(MILLIS_PER_HOUR * hours\ + + MILLIS_PER_MINUTE * minutes\ + + MILLIS_PER_SECOND * seconds\ + + milliseconds) + + +func elapsed_since() -> String: + return LocalTime.elapsed(LocalTime._get_system_time_msecs() - _time) + + +func elapsed_since_ms() -> int: + return LocalTime._get_system_time_msecs() - _time + + +func plus(time_unit :TimeUnit, value :int) -> LocalTime: + var addValue:int = 0 + match time_unit: + TimeUnit.MILLIS: + addValue = value + TimeUnit.SECOND: + addValue = value * MILLIS_PER_SECOND + TimeUnit.MINUTE: + addValue = value * MILLIS_PER_MINUTE + TimeUnit.HOUR: + addValue = value * MILLIS_PER_HOUR + _init(_time + addValue) + return self + + +static func elapsed(p_time_ms :int) -> String: + var local_time_ := LocalTime.new(p_time_ms) + if local_time_._hour > 0: + return "%dh %dmin %ds %dms" % [local_time_._hour, local_time_._minute, local_time_._second, local_time_._millisecond] + if local_time_._minute > 0: + return "%dmin %ds %dms" % [local_time_._minute, local_time_._second, local_time_._millisecond] + if local_time_._second > 0: + return "%ds %dms" % [local_time_._second, local_time_._millisecond] + return "%dms" % local_time_._millisecond + + +@warning_ignore("integer_division") +# create from epoch timestamp in ms +func _init(time :int): + _time = time + _hour = (time / MILLIS_PER_HOUR) % 24 + _minute = (time / MILLIS_PER_MINUTE) % 60 + _second = (time / MILLIS_PER_SECOND) % 60 + _millisecond = time % 1000 + + +func hour() -> int: + return _hour + + +func minute() -> int: + return _minute + + +func second() -> int: + return _second + + +func millis() -> int: + return _millisecond + + +func _to_string() -> String: + return "%02d:%02d:%02d.%03d" % [_hour, _minute, _second, _millisecond] + + +# wraper to old OS.get_system_time_msecs() function +static func _get_system_time_msecs() -> int: + return Time.get_unix_time_from_system() * 1000 as int diff --git a/addons/gdUnit4/src/core/_TestCase.gd b/addons/gdUnit4/src/core/_TestCase.gd new file mode 100644 index 0000000..868de15 --- /dev/null +++ b/addons/gdUnit4/src/core/_TestCase.gd @@ -0,0 +1,238 @@ +class_name _TestCase +extends Node + +signal completed() + +# default timeout 5min +const DEFAULT_TIMEOUT := -1 +const ARGUMENT_TIMEOUT := "timeout" +const ARGUMENT_SKIP := "do_skip" +const ARGUMENT_SKIP_REASON := "skip_reason" + +var _iterations: int = 1 +var _current_iteration: int = -1 +var _seed: int +var _fuzzers: Array[GdFunctionArgument] = [] +var _test_param_index := -1 +var _line_number: int = -1 +var _script_path: String +var _skipped := false +var _skip_reason := "" +var _expect_to_interupt := false +var _timer: Timer +var _interupted: bool = false +var _failed := false +var _report: GdUnitReport = null +var _parameter_set_resolver: GdUnitTestParameterSetResolver +var _is_disposed := false + +var timeout: int = DEFAULT_TIMEOUT: + set(value): + timeout = value + get: + if timeout == DEFAULT_TIMEOUT: + timeout = GdUnitSettings.test_timeout() + return timeout + + +@warning_ignore("shadowed_variable_base_class") +func configure(p_name: String, p_line_number: int, p_script_path: String, p_timeout: int=DEFAULT_TIMEOUT, p_fuzzers: Array[GdFunctionArgument]=[], p_iterations: int=1, p_seed: int=-1) -> _TestCase: + set_name(p_name) + _line_number = p_line_number + _fuzzers = p_fuzzers + _iterations = p_iterations + _seed = p_seed + _script_path = p_script_path + timeout = p_timeout + return self + + +func execute(p_test_parameter:=Array(), p_iteration:=0): + _failure_received(false) + _current_iteration = p_iteration - 1 + if _current_iteration == - 1: + _set_failure_handler() + set_timeout() + if not p_test_parameter.is_empty(): + update_fuzzers(p_test_parameter, p_iteration) + _execute_test_case(name, p_test_parameter) + else: + _execute_test_case(name, []) + await completed + + +func execute_paramaterized(p_test_parameter: Array): + _failure_received(false) + set_timeout() + # We need here to add a empty array to override the `test_parameters` to prevent initial "default" parameters from being used. + # This prevents objects in the argument list from being unnecessarily re-instantiated. + var test_parameters := p_test_parameter.duplicate() # is strictly need to duplicate the paramters before extend + test_parameters.append([]) + _execute_test_case(name, test_parameters) + await completed + + +func dispose(): + if _is_disposed: + return + _is_disposed = true + Engine.remove_meta("GD_TEST_FAILURE") + stop_timer() + _remove_failure_handler() + _fuzzers.clear() + _report = null + + +@warning_ignore("shadowed_variable_base_class", "redundant_await") +func _execute_test_case(name: String, test_parameter: Array): + # needs at least on await otherwise it breaks the awaiting chain + await get_parent().callv(name, test_parameter) + await Engine.get_main_loop().process_frame + completed.emit() + + +func update_fuzzers(input_values: Array, iteration: int): + for fuzzer in input_values: + if fuzzer is Fuzzer: + fuzzer._iteration_index = iteration + 1 + + +func set_timeout(): + if is_instance_valid(_timer): + return + var time: float = timeout / 1000.0 + _timer = Timer.new() + add_child(_timer) + _timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id()) + _timer.timeout.connect(func do_interrupt(): + if is_fuzzed(): + _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout")) + else: + _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout)) + _interupted = true + completed.emit() + , CONNECT_DEFERRED) + _timer.set_one_shot(true) + _timer.set_wait_time(time) + _timer.set_autostart(false) + _timer.start() + + +func _set_failure_handler() -> void: + if not GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received): + GdUnitSignals.instance().gdunit_set_test_failed.connect(_failure_received) + + +func _remove_failure_handler() -> void: + if GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received): + GdUnitSignals.instance().gdunit_set_test_failed.disconnect(_failure_received) + + +func _failure_received(is_failed: bool) -> void: + # is already failed? + if _failed: + return + _failed = is_failed + Engine.set_meta("GD_TEST_FAILURE", is_failed) + + +func stop_timer(): + # finish outstanding timeouts + if is_instance_valid(_timer): + _timer.stop() + _timer.call_deferred("free") + _timer = null + + +func expect_to_interupt() -> void: + _expect_to_interupt = true + + +func is_interupted() -> bool: + return _interupted + + +func is_expect_interupted() -> bool: + return _expect_to_interupt + + +func is_parameterized() -> bool: + return _parameter_set_resolver.is_parameterized() + + +func is_skipped() -> bool: + return _skipped + + +func report() -> GdUnitReport: + return _report + + +func skip_info() -> String: + return _skip_reason + + +func line_number() -> int: + return _line_number + + +func iterations() -> int: + return _iterations + + +func seed_value() -> int: + return _seed + + +func is_fuzzed() -> bool: + return not _fuzzers.is_empty() + + +func fuzzer_arguments() -> Array[GdFunctionArgument]: + return _fuzzers + + +func script_path() -> String: + return _script_path + + +func ResourcePath() -> String: + return _script_path + + +func generate_seed() -> void: + if _seed != -1: + seed(_seed) + + +func skip(skipped: bool, reason: String="") -> void: + _skipped = skipped + _skip_reason = reason + + +func set_function_descriptor(fd: GdFunctionDescriptor) -> void: + _parameter_set_resolver = GdUnitTestParameterSetResolver.new(fd) + + +func set_test_parameter_index(index: int) -> void: + _test_param_index = index + + +func test_parameter_index() -> int: + return _test_param_index + + +func test_case_names() -> PackedStringArray: + return _parameter_set_resolver.build_test_case_names(self) + + +func load_parameter_sets() -> Array: + return _parameter_set_resolver.load_parameter_sets(self, true) + + +func parameter_set_resolver() -> GdUnitTestParameterSetResolver: + return _parameter_set_resolver + + +func _to_string(): + return "%s :%d (%dms)" % [get_name(), _line_number, timeout] diff --git a/addons/gdUnit4/src/core/command/GdUnitCommand.gd b/addons/gdUnit4/src/core/command/GdUnitCommand.gd new file mode 100644 index 0000000..8a3f06f --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommand.gd @@ -0,0 +1,41 @@ +class_name GdUnitCommand +extends RefCounted + + +func _init(p_name :String, p_is_enabled: Callable, p_runnable: Callable, p_shortcut :GdUnitShortcut.ShortCut = GdUnitShortcut.ShortCut.NONE): + assert(p_name != null, "(%s) missing parameter 'name'" % p_name) + assert(p_is_enabled != null, "(%s) missing parameter 'is_enabled'" % p_name) + assert(p_runnable != null, "(%s) missing parameter 'runnable'" % p_name) + assert(p_shortcut != null, "(%s) missing parameter 'shortcut'" % p_name) + self.name = p_name + self.is_enabled = p_is_enabled + self.shortcut = p_shortcut + self.runnable = p_runnable + + +var name: String: + set(value): + name = value + get: + return name + + +var shortcut: GdUnitShortcut.ShortCut: + set(value): + shortcut = value + get: + return shortcut + + +var is_enabled: Callable: + set(value): + is_enabled = value + get: + return is_enabled + + +var runnable: Callable: + set(value): + runnable = value + get: + return runnable diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd new file mode 100644 index 0000000..386fd26 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -0,0 +1,357 @@ +class_name GdUnitCommandHandler +extends RefCounted + +signal gdunit_runner_start() +signal gdunit_runner_stop(client_id :int) + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const CMD_RUN_OVERALL = "Debug Overall TestSuites" +const CMD_RUN_TESTCASE = "Run TestCases" +const CMD_RUN_TESTCASE_DEBUG = "Run TestCases (Debug)" +const CMD_RUN_TESTSUITE = "Run TestSuites" +const CMD_RUN_TESTSUITE_DEBUG = "Run TestSuites (Debug)" +const CMD_RERUN_TESTS = "ReRun Tests" +const CMD_RERUN_TESTS_DEBUG = "ReRun Tests (Debug)" +const CMD_STOP_TEST_RUN = "Stop Test Run" +const CMD_CREATE_TESTCASE = "Create TestCase" + +const SETTINGS_SHORTCUT_MAPPING := { + "N/A" : GdUnitShortcut.ShortCut.NONE, + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST : GdUnitShortcut.ShortCut.RERUN_TESTS, + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_OVERALL : GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_STOP : GdUnitShortcut.ShortCut.STOP_TEST_RUN, + GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE, + GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG, + GdUnitSettings.SHORTCUT_EDITOR_CREATE_TEST : GdUnitShortcut.ShortCut.CREATE_TEST, + GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE, + GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG +} + +var _editor_interface :EditorInterface +# the current test runner config +var _runner_config := GdUnitRunnerConfig.new() + +# holds the current connected gdUnit runner client id +var _client_id :int +# if no debug mode we have an process id +var _current_runner_process_id :int = 0 +# hold is current an test running +var _is_running :bool = false +# holds if the current running tests started in debug mode +var _running_debug_mode :bool + +var _commands := {} +var _shortcuts := {} + + +static func instance() -> GdUnitCommandHandler: + return GdUnitSingleton.instance("GdUnitCommandHandler", func(): return GdUnitCommandHandler.new()) + + +func _init(): + assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING) + + if Engine.is_editor_hint(): + var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + _editor_interface = editor.get_editor_interface() + GdUnitSignals.instance().gdunit_event.connect(_on_event) + GdUnitSignals.instance().gdunit_client_connected.connect(_on_client_connected) + GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) + # preload previous test execution + _runner_config.load_config() + + init_shortcuts() + var is_running = func(_script :Script) : return _is_running + var is_not_running = func(_script :Script) : return !_is_running + register_command(GdUnitCommand.new(CMD_RUN_OVERALL, is_not_running, cmd_run_overall.bind(true), GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL)) + register_command(GdUnitCommand.new(CMD_RUN_TESTCASE, is_not_running, cmd_editor_run_test.bind(false), GdUnitShortcut.ShortCut.RUN_TESTCASE)) + register_command(GdUnitCommand.new(CMD_RUN_TESTCASE_DEBUG, is_not_running, cmd_editor_run_test.bind(true), GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG)) + register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE, is_not_running, cmd_run_test_suites.bind(false))) + register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE_DEBUG, is_not_running, cmd_run_test_suites.bind(true))) + register_command(GdUnitCommand.new(CMD_RERUN_TESTS, is_not_running, cmd_run.bind(false), GdUnitShortcut.ShortCut.RERUN_TESTS)) + register_command(GdUnitCommand.new(CMD_RERUN_TESTS_DEBUG, is_not_running, cmd_run.bind(true), GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG)) + register_command(GdUnitCommand.new(CMD_CREATE_TESTCASE, is_not_running, cmd_create_test, GdUnitShortcut.ShortCut.CREATE_TEST)) + register_command(GdUnitCommand.new(CMD_STOP_TEST_RUN, is_running, cmd_stop.bind(_client_id), GdUnitShortcut.ShortCut.STOP_TEST_RUN)) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + _commands.clear() + _shortcuts.clear() + + +func _do_process() -> void: + check_test_run_stopped_manually() + + +# is checking if the user has press the editor stop scene +func check_test_run_stopped_manually(): + if is_test_running_but_stop_pressed(): + if GdUnitSettings.is_verbose_assert_warnings(): + push_warning("Test Runner scene was stopped manually, force stopping the current test run!") + cmd_stop(_client_id) + + +func is_test_running_but_stop_pressed(): + return _editor_interface and _running_debug_mode and _is_running and not _editor_interface.is_playing_scene() + + +func assert_shortcut_mappings(mappings :Dictionary) -> void: + for shortcut in GdUnitShortcut.ShortCut.values(): + assert(mappings.values().has(shortcut), "missing settings mapping for shortcut '%s'!" % GdUnitShortcut.ShortCut.keys()[shortcut]) + + +func init_shortcuts() -> void: + for shortcut in GdUnitShortcut.ShortCut.values(): + if shortcut == GdUnitShortcut.ShortCut.NONE: + continue + var property_name :String = SETTINGS_SHORTCUT_MAPPING.find_key(shortcut) + var property := GdUnitSettings.get_property(property_name) + var keys := GdUnitShortcut.default_keys(shortcut) + if property != null: + keys = property.value() + var inputEvent := create_shortcut_input_even(keys) + register_shortcut(shortcut, inputEvent) + + +func create_shortcut_input_even(key_codes : PackedInt32Array) -> InputEventKey: + var inputEvent :InputEventKey = InputEventKey.new() + inputEvent.pressed = true + for key_code in key_codes: + match key_code: + KEY_ALT: + inputEvent.alt_pressed = true + KEY_SHIFT: + inputEvent.shift_pressed = true + KEY_CTRL: + inputEvent.ctrl_pressed = true + _: + inputEvent.keycode = key_code as Key + inputEvent.physical_keycode = key_code as Key + return inputEvent + + +func register_shortcut(p_shortcut :GdUnitShortcut.ShortCut, p_input_event :InputEvent) -> void: + GdUnitTools.prints_verbose("register shortcut: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[p_shortcut], p_input_event.as_text()]) + var shortcut := Shortcut.new() + shortcut.set_events([p_input_event]) + var command_name :String = get_shortcut_command(p_shortcut) + _shortcuts[p_shortcut] = GdUnitShortcutAction.new(p_shortcut, shortcut, command_name) + + +func get_shortcut(shortcut_type :GdUnitShortcut.ShortCut) -> Shortcut: + return get_shortcut_action(shortcut_type).shortcut + + +func get_shortcut_action(shortcut_type :GdUnitShortcut.ShortCut) -> GdUnitShortcutAction: + return _shortcuts.get(shortcut_type) + + +func get_shortcut_command(p_shortcut :GdUnitShortcut.ShortCut) -> String: + return GdUnitShortcut.CommandMapping.get(p_shortcut, "unknown command") + + +func register_command(p_command :GdUnitCommand) -> void: + _commands[p_command.name] = p_command + + +func command(cmd_name :String) -> GdUnitCommand: + return _commands.get(cmd_name) + + +func cmd_run_test_suites(test_suite_paths :PackedStringArray, debug :bool, rerun := false) -> void: + # create new runner runner_config for fresh run otherwise use saved one + if not rerun: + var result := _runner_config.clear()\ + .add_test_suites(test_suite_paths)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + +func cmd_run_test_case(test_suite_resource_path :String, test_case :String, test_param_index :int, debug :bool, rerun := false) -> void: + # create new runner config for fresh run otherwise use saved one + if not rerun: + var result := _runner_config.clear()\ + .add_test_case(test_suite_resource_path, test_case, test_param_index)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + +func cmd_run_overall(debug :bool) -> void: + var test_suite_paths :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://" , GdUnitSettings.test_root_folder(), []) + var result := _runner_config.clear()\ + .add_test_suites(test_suite_paths)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + +func cmd_run(debug :bool) -> void: + # don't start is already running + if _is_running: + return + # save current selected excution config + var result := _runner_config.set_server_port(Engine.get_meta("gdunit_server_port")).save_config() + if result.is_error(): + push_error(result.error_message()) + return + # before start we have to save all changes + ScriptEditorControls.save_all_open_script() + gdunit_runner_start.emit() + _current_runner_process_id = -1 + _running_debug_mode = debug + if debug: + run_debug_mode() + else: + run_release_mode() + + +func cmd_stop(client_id :int) -> void: + # don't stop if is already stopped + if not _is_running: + return + _is_running = false + gdunit_runner_stop.emit(client_id) + if _running_debug_mode: + _editor_interface.stop_playing_scene() + else: if _current_runner_process_id > 0: + var result = OS.kill(_current_runner_process_id) + if result != OK: + push_error("ERROR checked stopping GdUnit Test Runner. error code: %s" % result) + _current_runner_process_id = -1 + + +func cmd_editor_run_test(debug :bool): + var cursor_line := active_base_editor().get_caret_line() + #run test case? + var regex := RegEx.new() + regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") + var result := regex.search(active_base_editor().get_line(cursor_line)) + if result: + var func_name := result.get_string(2).strip_edges() + prints("Run test:", func_name, "debug", debug) + if func_name.begins_with("test_"): + cmd_run_test_case(active_script().resource_path, func_name, -1, debug) + return + # otherwise run the full test suite + var selected_test_suites := [active_script().resource_path] + cmd_run_test_suites(selected_test_suites, debug) + + +func cmd_create_test() -> void: + var cursor_line := active_base_editor().get_caret_line() + var result := GdUnitTestSuiteBuilder.create(active_script(), cursor_line) + if result.is_error(): + # show error dialog + push_error("Failed to create test case: %s" % result.error_message()) + return + var info := result.value() as Dictionary + ScriptEditorControls.edit_script(info.get("path"), info.get("line")) + + +static func scan_test_directorys(base_directory :String, test_directory: String, test_suite_paths :PackedStringArray) -> PackedStringArray: + print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory]) + for directory in DirAccess.get_directories_at(base_directory): + if directory.begins_with("."): + continue + var current_directory := normalize_path(base_directory + "/" + directory) + if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): + continue + if match_test_directory(directory, test_directory): + prints("Collect tests at:", current_directory) + test_suite_paths.append(current_directory) + else: + scan_test_directorys(current_directory, test_directory, test_suite_paths) + return test_suite_paths + + +static func normalize_path(path :String) -> String: + return path.replace("///", "//") + + +static func match_test_directory(directory :String, test_directory: String) -> bool: + return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://" + + +func run_debug_mode(): + _editor_interface.play_custom_scene("res://addons/gdUnit4/src/core/GdUnitRunner.tscn") + _is_running = true + + +func run_release_mode(): + var arguments := Array() + if OS.is_stdout_verbose(): + arguments.append("--verbose") + arguments.append("--no-window") + arguments.append("--path") + arguments.append(ProjectSettings.globalize_path("res://")) + arguments.append("res://addons/gdUnit4/src/core/GdUnitRunner.tscn") + _current_runner_process_id = OS.create_process(OS.get_executable_path(), arguments, false); + _is_running = true + + +func script_editor() -> ScriptEditor: + return _editor_interface.get_script_editor() + + +func active_base_editor() -> TextEdit: + return script_editor().get_current_editor().get_base_editor() + + +func active_script() -> Script: + return script_editor().get_current_script() + + + +################################################################################ +# signals handles +################################################################################ +func _on_event(event :GdUnitEvent): + if event.type() == GdUnitEvent.STOP: + cmd_stop(_client_id) + + +func _on_stop_pressed(): + cmd_stop(_client_id) + + +func _on_run_pressed(debug := false): + cmd_run(debug) + + +func _on_run_overall_pressed(_debug := false): + cmd_run_overall(true) + + +func _on_settings_changed(property :GdUnitProperty): + if SETTINGS_SHORTCUT_MAPPING.has(property.name()): + var shortcut :GdUnitShortcut.ShortCut = SETTINGS_SHORTCUT_MAPPING.get(property.name()) + var input_event := create_shortcut_input_even(property.value()) + prints("Shortcut changed: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[shortcut], input_event.as_text()]) + register_shortcut(shortcut, input_event) + + +################################################################################ +# Network stuff +################################################################################ +func _on_client_connected(client_id :int) -> void: + _client_id = client_id + + +func _on_client_disconnected(client_id :int) -> void: + # only stops is not in debug mode running and the current client + if not _running_debug_mode and _client_id == client_id: + cmd_stop(client_id) + _client_id = -1 diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd new file mode 100644 index 0000000..bf8ac83 --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd @@ -0,0 +1,58 @@ +class_name GdUnitShortcut +extends RefCounted + + +enum ShortCut { + NONE, + RUN_TESTS_OVERALL, + RUN_TESTCASE, + RUN_TESTCASE_DEBUG, + RERUN_TESTS, + RERUN_TESTS_DEBUG, + STOP_TEST_RUN, + CREATE_TEST, +} + + +const CommandMapping = { + ShortCut.RUN_TESTS_OVERALL: GdUnitCommandHandler.CMD_RUN_OVERALL, + ShortCut.RUN_TESTCASE: GdUnitCommandHandler.CMD_RUN_TESTCASE, + ShortCut.RUN_TESTCASE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG, + ShortCut.RERUN_TESTS: GdUnitCommandHandler.CMD_RERUN_TESTS, + ShortCut.RERUN_TESTS_DEBUG: GdUnitCommandHandler.CMD_RERUN_TESTS_DEBUG, + ShortCut.STOP_TEST_RUN: GdUnitCommandHandler.CMD_STOP_TEST_RUN, + ShortCut.CREATE_TEST: GdUnitCommandHandler.CMD_CREATE_TESTCASE, +} + + +const DEFAULTS_MACOS := { + ShortCut.NONE : [], + ShortCut.RUN_TESTCASE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTS_OVERALL : [Key.KEY_META, Key.KEY_F7], + ShortCut.STOP_TEST_RUN : [Key.KEY_META, Key.KEY_F8], + ShortCut.RERUN_TESTS : [Key.KEY_META, Key.KEY_F5], + ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_META, Key.KEY_F6], + ShortCut.CREATE_TEST : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F10], +} + +const DEFAULTS_WINDOWS := { + ShortCut.NONE : [], + ShortCut.RUN_TESTCASE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTS_OVERALL : [Key.KEY_CTRL, Key.KEY_F7], + ShortCut.STOP_TEST_RUN : [Key.KEY_CTRL, Key.KEY_F8], + ShortCut.RERUN_TESTS : [Key.KEY_CTRL, Key.KEY_F5], + ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_CTRL, Key.KEY_F6], + ShortCut.CREATE_TEST : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F10], +} + + +static func default_keys(shortcut :ShortCut) -> PackedInt32Array: + match OS.get_name().to_lower(): + 'windows': + return DEFAULTS_WINDOWS[shortcut] + 'macos': + return DEFAULTS_MACOS[shortcut] + _: + return DEFAULTS_WINDOWS[shortcut] diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd new file mode 100644 index 0000000..ffc9a9c --- /dev/null +++ b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd @@ -0,0 +1,36 @@ +class_name GdUnitShortcutAction +extends RefCounted + + +func _init(p_type :GdUnitShortcut.ShortCut, p_shortcut :Shortcut, p_command :String): + assert(p_type != null, "missing parameter 'type'") + assert(p_shortcut != null, "missing parameter 'shortcut'") + assert(p_command != null, "missing parameter 'command'") + self.type = p_type + self.shortcut = p_shortcut + self.command = p_command + + +var type: GdUnitShortcut.ShortCut: + set(value): + type = value + get: + return type + + +var shortcut: Shortcut: + set(value): + shortcut = value + get: + return shortcut + + +var command: String: + set(value): + command = value + get: + return command + + +func _to_string() -> String: + return "GdUnitShortcutAction: %s (%s) -> %s" % [GdUnitShortcut.ShortCut.keys()[type], shortcut.get_as_text(), command] diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd b/addons/gdUnit4/src/core/event/GdUnitEvent.gd new file mode 100644 index 0000000..cd92208 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd @@ -0,0 +1,185 @@ +class_name GdUnitEvent +extends Resource + +const WARNINGS = "warnings" +const FAILED = "failed" +const ERRORS = "errors" +const SKIPPED = "skipped" +const ELAPSED_TIME = "elapsed_time" +const ORPHAN_NODES = "orphan_nodes" +const ERROR_COUNT = "error_count" +const FAILED_COUNT = "failed_count" +const SKIPPED_COUNT = "skipped_count" + +enum { + INIT, + STOP, + TESTSUITE_BEFORE, + TESTSUITE_AFTER, + TESTCASE_BEFORE, + TESTCASE_AFTER, +} + +var _event_type :int +var _resource_path :String +var _suite_name :String +var _test_name :String +var _total_count :int = 0 +var _statistics := Dictionary() +var _reports :Array[GdUnitReport] = [] + + +func suite_before(p_resource_path :String, p_suite_name :String, p_total_count :int) -> GdUnitEvent: + _event_type = TESTSUITE_BEFORE + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = "before" + _total_count = p_total_count + return self + + +func suite_after(p_resource_path :String, p_suite_name :String, p_statistics :Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent: + _event_type = TESTSUITE_AFTER + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = "after" + _statistics = p_statistics + _reports = p_reports + return self + + +func test_before(p_resource_path :String, p_suite_name :String, p_test_name :String) -> GdUnitEvent: + _event_type = TESTCASE_BEFORE + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = p_test_name + return self + + +func test_after(p_resource_path :String, p_suite_name :String, p_test_name :String, p_statistics :Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent: + _event_type = TESTCASE_AFTER + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = p_test_name + _statistics = p_statistics + _reports = p_reports + return self + + +func type() -> int: + return _event_type + + +func suite_name() -> String: + return _suite_name + + +func test_name() -> String: + return _test_name + + +func elapsed_time() -> int: + return _statistics.get(ELAPSED_TIME, 0) + + +func orphan_nodes() -> int: + return _statistics.get(ORPHAN_NODES, 0) + + +func statistic(p_type :String) -> int: + return _statistics.get(p_type, 0) + + +func total_count() -> int: + return _total_count + + +func success_count() -> int: + return total_count() - error_count() - failed_count() - skipped_count() + + +func error_count() -> int: + return _statistics.get(ERROR_COUNT, 0) + + +func failed_count() -> int: + return _statistics.get(FAILED_COUNT, 0) + + +func skipped_count() -> int: + return _statistics.get(SKIPPED_COUNT, 0) + + +func resource_path() -> String: + return _resource_path + + +func is_success() -> bool: + return not is_warning() and not is_failed() and not is_error() and not is_skipped() + + +func is_warning() -> bool: + return _statistics.get(WARNINGS, false) + + +func is_failed() -> bool: + return _statistics.get(FAILED, false) + + +func is_error() -> bool: + return _statistics.get(ERRORS, false) + + +func is_skipped() -> bool: + return _statistics.get(SKIPPED, false) + + +func reports() -> Array: + return _reports + + +func _to_string() -> String: + return "Event: %s %s:%s, %s, %s" % [_event_type, _suite_name, _test_name, _statistics, _reports] + + +func serialize() -> Dictionary: + var serialized := { + "type" : _event_type, + "resource_path": _resource_path, + "suite_name" : _suite_name, + "test_name" : _test_name, + "total_count" : _total_count, + "statistics" : _statistics + } + serialized["reports"] = _serialize_TestReports() + return serialized + + +func deserialize(serialized :Dictionary) -> GdUnitEvent: + _event_type = serialized.get("type", null) + _resource_path = serialized.get("resource_path", null) + _suite_name = serialized.get("suite_name", null) + _test_name = serialized.get("test_name", "unknown") + _total_count = serialized.get("total_count", 0) + _statistics = serialized.get("statistics", Dictionary()) + if serialized.has("reports"): + # needs this workaround to copy typed values in the array + var reports :Array[Dictionary] = [] + reports.append_array(serialized.get("reports")) + _reports = _deserialize_reports(reports) + return self + + +func _serialize_TestReports() -> Array[Dictionary]: + var serialized_reports :Array[Dictionary] = [] + for report in _reports: + serialized_reports.append(report.serialize()) + return serialized_reports + + +func _deserialize_reports(p_reports :Array[Dictionary]) -> Array[GdUnitReport]: + var deserialized_reports :Array[GdUnitReport] = [] + for report in p_reports: + var test_report := GdUnitReport.new().deserialize(report) + deserialized_reports.append(test_report) + return deserialized_reports diff --git a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd new file mode 100644 index 0000000..8bb1d49 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd @@ -0,0 +1,19 @@ +class_name GdUnitInit +extends GdUnitEvent + + +var _total_testsuites :int + + +func _init(p_total_testsuites :int, p_total_count :int) -> void: + _event_type = INIT + _total_testsuites = p_total_testsuites + _total_count = p_total_count + + +func total_test_suites() -> int: + return _total_testsuites + + +func total_tests() -> int: + return _total_count diff --git a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd new file mode 100644 index 0000000..d7a3c11 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd @@ -0,0 +1,6 @@ +class_name GdUnitStop +extends GdUnitEvent + + +func _init() -> void: + _event_type = STOP diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd new file mode 100644 index 0000000..06e8dc3 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd @@ -0,0 +1,192 @@ +## The execution context +## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor +class_name GdUnitExecutionContext + +var _parent_context :GdUnitExecutionContext +var _sub_context :Array[GdUnitExecutionContext] = [] +var _orphan_monitor :GdUnitOrphanNodesMonitor +var _memory_observer :GdUnitMemoryObserver +var _report_collector :GdUnitTestReportCollector +var _timer :LocalTime +var _test_case_name: StringName +var _name :String + + +var error_monitor : GodotGdErrorMonitor = null: + set (value): + error_monitor = value + get: + if _parent_context != null: + return _parent_context.error_monitor + return error_monitor + + +var test_suite : GdUnitTestSuite = null: + set (value): + test_suite = value + get: + if _parent_context != null: + return _parent_context.test_suite + return test_suite + + +var test_case : _TestCase = null: + get: + if _test_case_name.is_empty(): + return null + return test_suite.find_child(_test_case_name, false, false) + + +func _init(name :String, parent_context :GdUnitExecutionContext = null) -> void: + _name = name + _parent_context = parent_context + _timer = LocalTime.now() + _orphan_monitor = GdUnitOrphanNodesMonitor.new(name) + _orphan_monitor.start() + _memory_observer = GdUnitMemoryObserver.new() + error_monitor = GodotGdErrorMonitor.new() + _report_collector = GdUnitTestReportCollector.new(get_instance_id()) + if parent_context != null: + parent_context._sub_context.append(self) + + +func dispose() -> void: + _timer = null + _orphan_monitor = null + _report_collector = null + _memory_observer = null + _parent_context = null + test_suite = null + test_case = null + for context in _sub_context: + context.dispose() + _sub_context.clear() + + +func set_active() -> void: + test_suite.__execution_context = self + GdUnitThreadManager.get_current_context().set_execution_context(self) + + +static func of_test_suite(test_suite_ :GdUnitTestSuite) -> GdUnitExecutionContext: + assert(test_suite_, "test_suite is null") + var context := GdUnitExecutionContext.new(test_suite_.get_name()) + context.test_suite = test_suite_ + context.set_active() + return context + + +static func of_test_case(pe :GdUnitExecutionContext, test_case_name :StringName) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(test_case_name, pe) + context._test_case_name = test_case_name + context.set_active() + return context + + +static func of(pe :GdUnitExecutionContext) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(pe._test_case_name, pe) + context._test_case_name = pe._test_case_name + context.set_active() + return context + + +func test_failed() -> bool: + return has_failures() or has_errors() + + +func error_monitor_start() -> void: + error_monitor.start() + + +func error_monitor_stop() -> void: + await error_monitor.scan() + for error_report in error_monitor.to_reports(): + if error_report.is_error(): + _report_collector._reports.append(error_report) + + +func orphan_monitor_start() -> void: + _orphan_monitor.start() + + +func orphan_monitor_stop() -> void: + _orphan_monitor.stop() + + +func reports() -> Array[GdUnitReport]: + return _report_collector.reports() + + +func build_report_statistics(orphans :int, recursive := true) -> Dictionary: + return { + GdUnitEvent.ORPHAN_NODES: orphans, + GdUnitEvent.ELAPSED_TIME: _timer.elapsed_since_ms(), + GdUnitEvent.FAILED: has_failures(), + GdUnitEvent.ERRORS: has_errors(), + GdUnitEvent.WARNINGS: has_warnings(), + GdUnitEvent.SKIPPED: has_skipped(), + GdUnitEvent.FAILED_COUNT: count_failures(recursive), + GdUnitEvent.ERROR_COUNT: count_errors(recursive), + GdUnitEvent.SKIPPED_COUNT: count_skipped(recursive) + } + + +func has_failures() -> bool: + return _sub_context.any(func(c): return c.has_failures()) or _report_collector.has_failures() + + +func has_errors() -> bool: + return _sub_context.any(func(c): return c.has_errors()) or _report_collector.has_errors() + + +func has_warnings() -> bool: + return _sub_context.any(func(c): return c.has_warnings()) or _report_collector.has_warnings() + + +func has_skipped() -> bool: + return _sub_context.any(func(c): return c.has_skipped()) or _report_collector.has_skipped() + + +func count_failures(recursive :bool) -> int: + if not recursive: + return _report_collector.count_failures() + return _sub_context\ + .map(func(c): return c.count_failures(recursive))\ + .reduce(sum, _report_collector.count_failures()) + + +func count_errors(recursive :bool) -> int: + if not recursive: + return _report_collector.count_errors() + return _sub_context\ + .map(func(c): return c.count_errors(recursive))\ + .reduce(sum, _report_collector.count_errors()) + + +func count_skipped(recursive :bool) -> int: + if not recursive: + return _report_collector.count_skipped() + return _sub_context\ + .map(func(c): return c.count_skipped(recursive))\ + .reduce(sum, _report_collector.count_skipped()) + + +func count_orphans() -> int: + var orphans := 0 + for c in _sub_context: + orphans += c._orphan_monitor.orphan_nodes() + return _orphan_monitor.orphan_nodes() - orphans + + +func sum(accum :int, number :int) -> int: + return accum + number + + +func register_auto_free(obj :Variant) -> Variant: + return _memory_observer.register_auto_free(obj) + + +## Runs the gdunit garbage collector to free registered object +func gc() -> void: + await _memory_observer.gc() + orphan_monitor_stop() diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd new file mode 100644 index 0000000..084d028 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd @@ -0,0 +1,131 @@ +## The memory watcher for objects that have been registered and are released when 'gc' is called. +class_name GdUnitMemoryObserver +extends RefCounted + +const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_" +const TAG_AUTO_FREE = "GdUnit4_marked_auto_free" +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +var _store :Array[Variant] = [] +# enable for debugging purposes +var _is_stdout_verbose := false +const _show_debug := false + + +## Registration of an instance to be released when an execution phase is completed +func register_auto_free(obj) -> Variant: + if not is_instance_valid(obj): + return obj + # do not register on GDScriptNativeClass + if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : + return obj + #if obj is GDScript or obj is ScriptExtension: + # return obj + if obj is MainLoop: + push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj) + return + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():register auto_free(%s)" % obj) + # only register pure objects + if obj is GdUnitSceneRunner: + _store.push_back(obj) + else: + _store.append(obj) + _tag_object(obj) + return obj + + +# to disable instance guard when run into issues. +static func _is_instance_guard_enabled() -> bool: + return false + + +static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: + if not _show_debug: + return + var script :GDScript= obj if obj is GDScript else obj.get_script() + if script: + var base_script :GDScript = script.get_base_script() + prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path) + if base_script: + debug_observe("+", base_script, indent+1) + else: + prints(name, obj, obj.get_class(), obj.get_name()) + + +static func guard_instance(obj :Object) -> Object: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if Engine.has_meta(tag): + return + debug_observe("Gard on instance", obj) + Engine.set_meta(tag, obj) + return obj + + +static func unguard_instance(obj :Object, verbose := true) -> void: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if verbose: + debug_observe("unguard instance", obj) + if Engine.has_meta(tag): + Engine.remove_meta(tag) + + +static func gc_guarded_instance(name :String, instance :Object) -> void: + if not _is_instance_guard_enabled(): + return + await Engine.get_main_loop().process_frame + unguard_instance(instance, false) + if is_instance_valid(instance) and instance is RefCounted: + # finally do this very hacky stuff + # we need to manually unreferece to avoid leaked scripts + # but still leaked GDScriptFunctionState exists + #var script :GDScript = instance.get_script() + #if script: + # var base_script :GDScript = script.get_base_script() + # if base_script: + # base_script.unreference() + debug_observe(name, instance) + instance.unreference() + await Engine.get_main_loop().process_frame + + +static func gc_on_guarded_instances() -> void: + if not _is_instance_guard_enabled(): + return + for tag in Engine.get_meta_list(): + if tag.begins_with(TAG_OBSERVE_INSTANCE): + var instance = Engine.get_meta(tag) + await gc_guarded_instance("Leaked instance detected:", instance) + await GdUnitTools.free_instance(instance, false) + + +# store the object into global store aswell to be verified by 'is_marked_auto_free' +func _tag_object(obj :Variant) -> void: + var tagged_object := Engine.get_meta(TAG_AUTO_FREE, []) as Array + tagged_object.append(obj) + Engine.set_meta(TAG_AUTO_FREE, tagged_object) + + +## Runs over all registered objects and releases them +func gc() -> void: + if _store.is_empty(): + return + # give engine time to free objects to process objects marked by queue_free() + await Engine.get_main_loop().process_frame + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size()) + var tagged_objects := Engine.get_meta(TAG_AUTO_FREE, []) as Array + while not _store.is_empty(): + var value :Variant = _store.pop_front() + tagged_objects.erase(value) + await GdUnitTools.free_instance(value, _is_stdout_verbose) + + +## Checks whether the specified object is registered for automatic release +static func is_marked_auto_free(obj) -> bool: + return Engine.get_meta(TAG_AUTO_FREE, []).has(obj) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd new file mode 100644 index 0000000..cde5cee --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd @@ -0,0 +1,70 @@ +# Collects all reports seperated as warnings, failures and errors +class_name GdUnitTestReportCollector +extends RefCounted + + +var _execution_context_id :int +var _reports :Array[GdUnitReport] = [] + + +static func __filter_is_error(report :GdUnitReport) -> bool: + return report.is_error() + + +static func __filter_is_failure(report :GdUnitReport) -> bool: + return report.is_failure() + + +static func __filter_is_warning(report :GdUnitReport) -> bool: + return report.is_warning() + + +static func __filter_is_skipped(report :GdUnitReport) -> bool: + return report.is_skipped() + + +func _init(execution_context_id :int): + _execution_context_id = execution_context_id + GdUnitSignals.instance().gdunit_report.connect(on_reports) + + +func count_failures() -> int: + return _reports.filter(__filter_is_failure).size() + + +func count_errors() -> int: + return _reports.filter(__filter_is_error).size() + + +func count_warnings() -> int: + return _reports.filter(__filter_is_warning).size() + + +func count_skipped() -> int: + return _reports.filter(__filter_is_skipped).size() + + +func has_failures() -> bool: + return _reports.any(__filter_is_failure) + + +func has_errors() -> bool: + return _reports.any(__filter_is_error) + + +func has_warnings() -> bool: + return _reports.any(__filter_is_warning) + + +func has_skipped() -> bool: + return _reports.any(__filter_is_skipped) + + +func reports() -> Array[GdUnitReport]: + return _reports + + +# Consumes reports emitted by tests +func on_reports(execution_context_id :int, report :GdUnitReport) -> void: + if execution_context_id == _execution_context_id: + _reports.append(report) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd new file mode 100644 index 0000000..c919469 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd @@ -0,0 +1,26 @@ +## The executor to run a test-suite +class_name GdUnitTestSuiteExecutor + + +# preload all asserts here +@warning_ignore("unused_private_class_variable") +var _assertions := GdUnitAssertions.new() +var _executeStage :IGdUnitExecutionStage = GdUnitTestSuiteExecutionStage.new() + + +func _init(debug_mode :bool = false) -> void: + _executeStage.set_debug_mode(debug_mode) + + +func execute(test_suite :GdUnitTestSuite) -> void: + var orphan_detection_enabled := GdUnitSettings.is_verbose_orphans() + if not orphan_detection_enabled: + prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.") + + Engine.get_main_loop().root.call_deferred("add_child", test_suite) + await Engine.get_main_loop().process_frame + await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite)) + + +func fail_fast(enabled :bool) -> void: + _executeStage.fail_fast(enabled) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd new file mode 100644 index 0000000..3460a68 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd @@ -0,0 +1,100 @@ +## The test case shutdown hook implementation.[br] +## It executes the 'test_after()' block from the test-suite. +class_name GdUnitTestCaseAfterStage +extends IGdUnitExecutionStage + + +var _test_name :StringName = "" +var _call_stage :bool + + +func _init(call_stage := true): + _call_stage = call_stage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.after_test() + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().set_assert(null) + await context.gc() + await context.error_monitor_stop() + if context.test_case.is_skipped(): + fire_test_skipped(context) + else: + fire_test_ended(context) + if is_instance_valid(context.test_case): + context.test_case.dispose() + + +func set_test_name(test_name :StringName): + _test_name = test_name + + +func fire_test_ended(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_name := context._test_case_name if _test_name.is_empty() else _test_name + var reports := collect_reports(context) + var orphans := collect_orphans(context, reports) + + fire_event(GdUnitEvent.new()\ + .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_name, context.build_report_statistics(orphans), reports)) + + +func collect_orphans(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := 0 + if not context._sub_context.is_empty(): + orphans += add_orphan_report_test(context._sub_context[0], reports) + orphans += add_orphan_report_teststage(context, reports) + return orphans + + +func collect_reports(context :GdUnitExecutionContext) -> Array[GdUnitReport]: + var reports := context.reports() + var test_case := context.test_case + if test_case.is_interupted() and not test_case.is_expect_interupted() and test_case.report() != null: + reports.push_back(test_case.report()) + # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` + if not context._sub_context.is_empty(): + reports.append_array(context._sub_context[0].reports()) + # needs finally to clean the test reports to avoid counting twice + context._sub_context[0].reports().clear() + return reports + + +func add_orphan_report_test(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) + return orphans + + +func add_orphan_report_teststage(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans))) + return orphans + + +func fire_test_skipped(context :GdUnitExecutionContext): + var test_suite := context.test_suite + var test_case := context.test_case + var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name + var statistics = { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new()\ + .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name, statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd new file mode 100644 index 0000000..2210fb9 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd @@ -0,0 +1,29 @@ +## The test case startup hook implementation.[br] +## It executes the 'test_before()' block from the test-suite. +class_name GdUnitTestCaseBeforeStage +extends IGdUnitExecutionStage + + +var _test_name :StringName = "" +var _call_stage :bool + + +func _init(call_stage := true): + _call_stage = call_stage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name + + fire_event(GdUnitEvent.new()\ + .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name)) + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.before_test() + context.error_monitor_start() + + +func set_test_name(test_name :StringName): + _test_name = test_name diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd new file mode 100644 index 0000000..dc8c53d --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd @@ -0,0 +1,31 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseExecutionStage +extends IGdUnitExecutionStage + + +var _stage_single_test :IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new() +var _stage_fuzzer_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new() +var _stage_parameterized_test :IGdUnitExecutionStage= GdUnitTestCaseParameterizedExecutionStage.new() + + +## Executes the test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_before() [br] +## -> test_case() [br] +## -> test_after() [br] +@warning_ignore("redundant_await") +func _execute(context :GdUnitExecutionContext) -> void: + var test_case := context.test_case + if test_case.is_parameterized(): + await _stage_parameterized_test.execute(context) + elif test_case.is_fuzzed(): + await _stage_fuzzer_test.execute(context) + else: + await _stage_single_test.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_single_test.set_debug_mode(debug_mode) + _stage_fuzzer_test.set_debug_mode(debug_mode) + _stage_parameterized_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd new file mode 100644 index 0000000..a6de318 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd @@ -0,0 +1,28 @@ +## The test suite shutdown hook implementation.[br] +## It executes the 'after()' block from the test-suite. +class_name GdUnitTestSuiteAfterStage +extends IGdUnitExecutionStage + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + @warning_ignore("redundant_await") + await test_suite.after() + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().set_assert(null) + await context.gc() + + var reports := context.reports() + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new() \ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans))) + fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), context.build_report_statistics(orphans, false), reports)) + + GdUnitFileAccess.clear_tmp() + # Guard that checks if all doubled (spy/mock) objects are released + GdUnitClassDoubler.check_leaked_instances() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd new file mode 100644 index 0000000..2260edb --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd @@ -0,0 +1,14 @@ +## The test suite startup hook implementation.[br] +## It executes the 'before()' block from the test-suite. +class_name GdUnitTestSuiteBeforeStage +extends IGdUnitExecutionStage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + fire_event(GdUnitEvent.new()\ + .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), test_suite.get_child_count())) + + @warning_ignore("redundant_await") + await test_suite.before() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd new file mode 100644 index 0000000..9e796c3 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd @@ -0,0 +1,114 @@ +## The test suite main execution stage.[br] +class_name GdUnitTestSuiteExecutionStage +extends IGdUnitExecutionStage + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _stage_before :IGdUnitExecutionStage = GdUnitTestSuiteBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestSuiteAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseExecutionStage.new() +var _fail_fast := false + + +## Executes all tests of an test suite.[br] +## It executes synchronized following stages[br] +## -> before() [br] +## -> run all test cases [br] +## -> after() [br] +func _execute(context :GdUnitExecutionContext) -> void: + if context.test_suite.__is_skipped: + await fire_test_suite_skipped(context) + else: + GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter) + await _stage_before.execute(context) + for test_case_index in context.test_suite.get_child_count(): + # iterate only over test cases + var test_case := context.test_suite.get_child(test_case_index) as _TestCase + if not is_instance_valid(test_case): + continue + context.test_suite.set_active_test_case(test_case.get_name()) + await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case.get_name())) + # stop on first error or if fail fast is enabled + if _fail_fast and context.test_failed(): + break + if test_case.is_interupted(): + # it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out + # we delete the current test suite where is execute the current test case to kill the function state + # and replace it by a clone without function state + context.test_suite = await clone_test_suite(context.test_suite) + await _stage_after.execute(context) + GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter) + await Engine.get_main_loop().process_frame + context.test_suite.free() + context.dispose() + + +# clones a test suite and moves the test cases to new instance +func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: + await Engine.get_main_loop().process_frame + dispose_timers(test_suite) + await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter) + var parent := test_suite.get_parent() + var _test_suite = GdUnitTestSuite.new() + parent.remove_child(test_suite) + copy_properties(test_suite, _test_suite) + for child in test_suite.get_children(): + test_suite.remove_child(child) + _test_suite.add_child(child) + parent.add_child(_test_suite) + GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter) + # finally free current test suite instance + test_suite.free() + await Engine.get_main_loop().process_frame + return _test_suite + + +func dispose_timers(test_suite :GdUnitTestSuite): + GdUnitTools.release_timers() + for child in test_suite.get_children(): + if child is Timer: + child.stop() + test_suite.remove_child(child) + child.free() + + +func copy_properties(source :Object, target :Object): + if not source is _TestCase and not source is GdUnitTestSuite: + return + for property in source.get_property_list(): + var property_name = property["name"] + if property_name == "__awaiter": + continue + target.set(property_name, source.get(property_name)) + + +func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var skip_count := test_suite.get_child_count() + fire_event(GdUnitEvent.new()\ + .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), skip_count)) + var statistics = { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: skip_count, + GdUnitEvent.SKIPPED: true + } + var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count)) + fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), statistics, [report])) + await Engine.get_main_loop().process_frame + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) + + +func fail_fast(enabled :bool) -> void: + _fail_fast = enabled diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd new file mode 100644 index 0000000..0f6ae93 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd @@ -0,0 +1,39 @@ +## The interface of execution stage.[br] +## An execution stage is defined as an encapsulated task that can execute 1-n substages covered by its own execution context.[br] +## Execution stage are always called synchronously. +class_name IGdUnitExecutionStage +extends RefCounted + +var _debug_mode := false + + +## Executes synchronized the implemented stage in its own execution context.[br] +## example:[br] +## [codeblock] +## # waits for 100ms +## await MyExecutionStage.new().execute() +## [/codeblock][br] +func execute(context :GdUnitExecutionContext) -> void: + context.set_active() + @warning_ignore("redundant_await") + await _execute(context) + + +## Sends the event to registered listeners +func fire_event(event :GdUnitEvent) -> void: + if _debug_mode: + GdUnitSignals.instance().gdunit_event_debug.emit(event) + else: + GdUnitSignals.instance().gdunit_event.emit(event) + + +## Internal testing stuff.[br] +## Sets the executor into debug mode to emit `GdUnitEvent` via signal `gdunit_event_debug` +func set_debug_mode(debug_mode :bool) -> void: + _debug_mode = debug_mode + + +## The execution phase to be carried out. +func _execute(_context :GdUnitExecutionContext) -> void: + @warning_ignore("assert_always_false") + assert(false, "The execution stage is not implemented") diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd new file mode 100644 index 0000000..269a9ea --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd @@ -0,0 +1,21 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseFuzzedExecutionStage +extends IGdUnitExecutionStage + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + await _stage_before.execute(context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(context)) + await _stage_after.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd new file mode 100644 index 0000000..e6d9852 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd @@ -0,0 +1,53 @@ +## The fuzzed test case execution stage.[br] +class_name GdUnitTestCaseFuzzedTestStage +extends IGdUnitExecutionStage + +var _expression_runner := GdUnitExpressionRunner.new() + + +## Executes a test case with given fuzzers 'test_()' iterative.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_case := context.test_case + var fuzzers := create_fuzzers(test_suite, test_case) + + # guard on fuzzers + for fuzzer in fuzzers: + GdUnitMemoryObserver.guard_instance(fuzzer) + + for iteration in test_case.iterations(): + @warning_ignore("redundant_await") + await test_suite.before_test() + await test_case.execute(fuzzers, iteration) + @warning_ignore("redundant_await") + await test_suite.after_test() + if test_case.is_interupted(): + break + # interrupt at first failure + var reports := context.reports() + if not reports.is_empty(): + var report :GdUnitReport = reports.pop_front() + reports.append(GdUnitReport.new() \ + .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) + break + await context.gc() + + # unguard on fuzzers + if not test_case.is_interupted(): + for fuzzer in fuzzers: + GdUnitMemoryObserver.unguard_instance(fuzzer) + + +func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]: + if not test_case.is_fuzzed(): + return Array() + test_case.generate_seed() + var fuzzers :Array[Fuzzer] = [] + for fuzzer_arg in test_case.fuzzer_arguments(): + var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script(), fuzzer_arg.value_as_string()) + fuzzer._iteration_index = 0 + fuzzer._iteration_limit = test_case.iterations() + fuzzers.append(fuzzer) + return fuzzers diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd new file mode 100644 index 0000000..52ccdc4 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd @@ -0,0 +1,22 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseParameterizedExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseParamaterizedTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + await _stage_before.execute(context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(context)) + await _stage_after.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd new file mode 100644 index 0000000..5469f40 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd @@ -0,0 +1,76 @@ +## The parameterized test case execution stage.[br] +class_name GdUnitTestCaseParamaterizedTestStage +extends IGdUnitExecutionStage + +var _stage_before: IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after: IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() + + +## Executes a parameterized test case.[br] +## It executes synchronized following stages[br] +## -> test_case( ) [br] +func _execute(context: GdUnitExecutionContext) -> void: + var test_case := context.test_case + var test_parameter_index := test_case.test_parameter_index() + var is_fail := false + var is_error := false + var failing_index := 0 + var parameter_set_resolver := test_case.parameter_set_resolver() + var test_names := parameter_set_resolver.build_test_case_names(test_case) + + # if all parameter sets has static values we can preload and reuse it for better performance + var parameter_sets :Array = [] + if parameter_set_resolver.is_parameter_sets_static(): + parameter_sets = parameter_set_resolver.load_parameter_sets(test_case, true) + + for parameter_set_index in test_names.size(): + # is test_parameter_index is set, we run this parameterized test only + if test_parameter_index != -1 and test_parameter_index != parameter_set_index: + continue + var current_test_case_name = test_names[parameter_set_index] + _stage_before.set_test_name(current_test_case_name) + _stage_after.set_test_name(current_test_case_name) + + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + var current_parameter_set :Array + if parameter_set_resolver.is_parameter_set_static(parameter_set_index): + current_parameter_set = parameter_sets[parameter_set_index] + else: + current_parameter_set = _load_parameter_set(context, parameter_set_index) + if not test_case.is_interupted(): + await test_case.execute_paramaterized(current_parameter_set) + await _stage_after.execute(test_context) + # we need to clean up the reports here so they are not reported twice + is_fail = is_fail or test_context.count_failures(false) > 0 + is_error = is_error or test_context.count_errors(false) > 0 + failing_index = parameter_set_index - 1 + test_context.reports().clear() + if test_case.is_interupted(): + break + # add report to parent execution context if failed or an error is found + if is_fail: + context.reports().append(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % failing_index)) + if is_error: + context.reports().append(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % failing_index)) + await context.gc() + + +func _load_parameter_set(context: GdUnitExecutionContext, parameter_set_index: int) -> Array: + var test_case := context.test_case + var test_suite := context.test_suite + # we need to exchange temporary for parameter resolving the execution context + # this is necessary because of possible usage of `auto_free` and needs to run in the parent execution context + var save_execution_context: GdUnitExecutionContext = test_suite.__execution_context + context.set_active() + var parameters := test_case.load_parameter_sets() + # restore the original execution context and restart the orphan monitor to get new instances into account + save_execution_context.set_active() + save_execution_context.orphan_monitor_start() + return parameters[parameter_set_index] + + +func set_debug_mode(debug_mode: bool=false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd new file mode 100644 index 0000000..fde7eb2 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd @@ -0,0 +1,22 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseSingleExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new() + + +func _execute(context :GdUnitExecutionContext) -> void: + await _stage_before.execute(context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(context)) + await _stage_after.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd new file mode 100644 index 0000000..6882fe2 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd @@ -0,0 +1,11 @@ +## The single test case execution stage.[br] +class_name GdUnitTestCaseSingleTestStage +extends IGdUnitExecutionStage + + +## Executes a single test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + await context.test_case.execute() + await context.gc() diff --git a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd new file mode 100644 index 0000000..d2ac704 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd @@ -0,0 +1,34 @@ +class_name GdClassDescriptor +extends RefCounted + + +var _name :String +var _parent = null +var _is_inner_class :bool +var _functions + + +func _init(p_name :String, p_is_inner_class :bool, p_functions :Array): + _name = p_name + _is_inner_class = p_is_inner_class + _functions = p_functions + + +func set_parent_clazz(p_parent :GdClassDescriptor): + _parent = p_parent + + +func name() -> String: + return _name + + +func parent() -> GdClassDescriptor: + return _parent + + +func is_inner_class() -> bool: + return _is_inner_class + + +func functions() -> Array: + return _functions diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd new file mode 100644 index 0000000..a67c2f4 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd @@ -0,0 +1,255 @@ +# holds all decodings for default values +class_name GdDefaultValueDecoder +extends GdUnitSingleton + + +@warning_ignore("unused_parameter") +var _decoders = { + TYPE_NIL: func(value): return "null", + TYPE_STRING: func(value): return '"%s"' % value, + TYPE_STRING_NAME: _on_type_StringName, + TYPE_BOOL: func(value): return str(value).to_lower(), + TYPE_FLOAT: func(value): return '%f' % value, + TYPE_COLOR: _on_type_Color, + TYPE_ARRAY: _on_type_Array.bind(TYPE_ARRAY), + TYPE_PACKED_BYTE_ARRAY: _on_type_Array.bind(TYPE_PACKED_BYTE_ARRAY), + TYPE_PACKED_STRING_ARRAY: _on_type_Array.bind(TYPE_PACKED_STRING_ARRAY), + TYPE_PACKED_FLOAT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT32_ARRAY), + TYPE_PACKED_FLOAT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT64_ARRAY), + TYPE_PACKED_INT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT32_ARRAY), + TYPE_PACKED_INT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT64_ARRAY), + TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY), + TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY), + TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY), + TYPE_DICTIONARY: _on_type_Dictionary, + TYPE_RID: _on_type_RID, + TYPE_NODE_PATH: _on_type_NodePath, + TYPE_VECTOR2: _on_type_Vector.bind(TYPE_VECTOR2), + TYPE_VECTOR2I: _on_type_Vector.bind(TYPE_VECTOR2I), + TYPE_VECTOR3: _on_type_Vector.bind(TYPE_VECTOR3), + TYPE_VECTOR3I: _on_type_Vector.bind(TYPE_VECTOR3I), + TYPE_VECTOR4: _on_type_Vector.bind(TYPE_VECTOR4), + TYPE_VECTOR4I: _on_type_Vector.bind(TYPE_VECTOR4I), + TYPE_RECT2: _on_type_Rect2, + TYPE_RECT2I: _on_type_Rect2i, + TYPE_PLANE: _on_type_Plane, + TYPE_QUATERNION: _on_type_Quaternion, + TYPE_AABB: _on_type_AABB, + TYPE_BASIS: _on_type_Basis, + TYPE_CALLABLE: _on_type_Callable, + TYPE_SIGNAL: _on_type_Signal, + TYPE_TRANSFORM2D: _on_type_Transform2D, + TYPE_TRANSFORM3D: _on_type_Transform3D, + TYPE_PROJECTION: _on_type_Projection, + TYPE_OBJECT: _on_type_Object +} + +static func _regex(pattern :String) -> RegEx: + var regex := RegEx.new() + var err = regex.compile(pattern) + if err != OK: + push_error("error '%s' checked pattern '%s'" % [err, pattern]) + return null + return regex + + +func get_decoder(type :int) -> Callable: + return _decoders.get(type, func(value): return '%s' % value) + + +func _on_type_StringName(value :StringName) -> String: + if value.is_empty(): + return 'StringName()' + return 'StringName("%s")' % value + + +func _on_type_Object(value :Object, type :int) -> String: + return str(value) + + +func _on_type_Color(color :Color) -> String: + if color == Color.BLACK: + return "Color()" + return "Color%s" % color + + +func _on_type_NodePath(path :NodePath) -> String: + if path.is_empty(): + return 'NodePath()' + return 'NodePath("%s")' % path + + +func _on_type_Callable(cb :Callable) -> String: + return 'Callable()' + + +func _on_type_Signal(s :Signal) -> String: + return 'Signal()' + + +func _on_type_Dictionary(dict :Dictionary) -> String: + if dict.is_empty(): + return '{}' + return str(dict) + + +func _on_type_Array(value, type :int) -> String: + match type: + TYPE_ARRAY: + return str(value) + + TYPE_PACKED_COLOR_ARRAY: + var colors := PackedStringArray() + for color in value as PackedColorArray: + colors.append(_on_type_Color(color)) + if colors.is_empty(): + return "PackedColorArray()" + return "PackedColorArray([%s])" % ", ".join(colors) + + TYPE_PACKED_VECTOR2_ARRAY: + var vectors := PackedStringArray() + for vector in value as PackedVector2Array: + vectors.append(_on_type_Vector(vector, TYPE_VECTOR2)) + if vectors.is_empty(): + return "PackedVector2Array()" + return "PackedVector2Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_VECTOR3_ARRAY: + var vectors := PackedStringArray() + for vector in value as PackedVector3Array: + vectors.append(_on_type_Vector(vector, TYPE_VECTOR3)) + if vectors.is_empty(): + return "PackedVector3Array()" + return "PackedVector3Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_STRING_ARRAY: + var values := PackedStringArray() + for v in value as PackedStringArray: + values.append('"%s"' % v) + if values.is_empty(): + return "PackedStringArray()" + return "PackedStringArray([%s])" % ", ".join(values) + + TYPE_PACKED_BYTE_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY,\ + TYPE_PACKED_FLOAT64_ARRAY,\ + TYPE_PACKED_INT32_ARRAY,\ + TYPE_PACKED_INT64_ARRAY: + var vectors := PackedStringArray() + for vector in value as Array: + vectors.append(str(vector)) + if vectors.is_empty(): + return GdObjects.type_as_string(type) + "()" + return "%s([%s])" % [GdObjects.type_as_string(type), ", ".join(vectors)] + return "unknown array type %d" % type + + +func _on_type_Vector(value :Variant, type :int) -> String: + match type: + TYPE_VECTOR2: + if value == Vector2(): + return "Vector2()" + return "Vector2%s" % value + TYPE_VECTOR2I: + if value == Vector2i(): + return "Vector2i()" + return "Vector2i%s" % value + TYPE_VECTOR3: + if value == Vector3(): + return "Vector3()" + return "Vector3%s" % value + TYPE_VECTOR3I: + if value == Vector3i(): + return "Vector3i()" + return "Vector3i%s" % value + TYPE_VECTOR4: + if value == Vector4(): + return "Vector4()" + return "Vector4%s" % value + TYPE_VECTOR4I: + if value == Vector4i(): + return "Vector4i()" + return "Vector4i%s" % value + return "unknown vector type %d" % type + + +func _on_type_Transform2D(transform :Transform2D) -> String: + if transform == Transform2D(): + return "Transform2D()" + return "Transform2D(Vector2%s, Vector2%s, Vector2%s)" % [transform.x, transform.y, transform.origin] + + +func _on_type_Transform3D(transform :Transform3D) -> String: + if transform == Transform3D(): + return "Transform3D()" + return "Transform3D(Vector3%s, Vector3%s, Vector3%s, Vector3%s)" % [transform.basis.x, transform.basis.y, transform.basis.z, transform.origin] + + +func _on_type_Projection(projection :Projection) -> String: + return "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % [projection.x, projection.y, projection.z, projection.w] + + +@warning_ignore("unused_parameter") +func _on_type_RID(value :RID) -> String: + return "RID()" + + +func _on_type_Rect2(rect :Rect2) -> String: + if rect == Rect2(): + return "Rect2()" + return "Rect2(Vector2%s, Vector2%s)" % [rect.position, rect.size] + + +func _on_type_Rect2i(rect :Variant) -> String: + if rect == Rect2i(): + return "Rect2i()" + return "Rect2i(Vector2i%s, Vector2i%s)" % [rect.position, rect.size] + + +func _on_type_Plane(plane :Plane) -> String: + if plane == Plane(): + return "Plane()" + return "Plane(%d, %d, %d, %d)" % [plane.x, plane.y, plane.z, plane.d] + + +func _on_type_Quaternion(quaternion :Quaternion) -> String: + if quaternion == Quaternion(): + return "Quaternion()" + return "Quaternion(%d, %d, %d, %d)" % [quaternion.x, quaternion.y, quaternion.z, quaternion.w] + + +func _on_type_AABB(aabb :AABB) -> String: + if aabb == AABB(): + return "AABB()" + return "AABB(Vector3%s, Vector3%s)" % [aabb.position, aabb.size] + + +func _on_type_Basis(basis :Basis) -> String: + if basis == Basis(): + return "Basis()" + return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z] + + +static func decode(value :Variant) -> String: + var type := typeof(value) + if GdArrayTools.is_type_array(type) and value.is_empty(): + return "" + var decoder :Callable = instance("GdUnitDefaultValueDecoders", func(): return GdDefaultValueDecoder.new()).get_decoder(type) + if decoder == null: + push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) + return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) + return decoder.call(value) + + +static func decode_typed(type :int, value :Variant) -> String: + if value == null: + return "null" + var decoder :Callable = instance("GdUnitDefaultValueDecoders", func(): return GdDefaultValueDecoder.new()).get_decoder(type) + if decoder == null: + push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) + return "null" + if type == TYPE_OBJECT: + return decoder.call(value, type) + return decoder.call(value) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd new file mode 100644 index 0000000..4ecbe12 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd @@ -0,0 +1,105 @@ +class_name GdFunctionArgument +extends RefCounted + + +var _cleanup_leading_spaces = RegEx.create_from_string("(?m)^[ \t]+") +var _fix_comma_space := RegEx.create_from_string(""", {0,}\t{0,}(?=(?:[^"]*"[^"]*")*[^"]*$)(?!\\s)""") +var _name: String +var _type: int +var _default_value :Variant +var _parameter_sets :PackedStringArray = [] + +const UNDEFINED :Variant = "<-NO_ARG->" +const ARG_PARAMETERIZED_TEST := "test_parameters" + + +func _init(p_name :String, p_type :int = TYPE_MAX, value :Variant = UNDEFINED): + _name = p_name + _type = p_type + if p_name == ARG_PARAMETERIZED_TEST: + _parameter_sets = _parse_parameter_set(value) + _default_value = value + + +func name() -> String: + return _name + + +func default() -> Variant: + return GodotVersionFixures.convert(_default_value, _type) + + +func value_as_string() -> String: + if has_default(): + return str(_default_value) + return "" + + +func type() -> int: + return _type + + +func has_default() -> bool: + return not is_same(_default_value, UNDEFINED) + + +func is_parameter_set() -> bool: + return _name == ARG_PARAMETERIZED_TEST + + +func parameter_sets() -> PackedStringArray: + return _parameter_sets + + +static func get_parameter_set(parameters :Array) -> GdFunctionArgument: + for current in parameters: + if current != null and current.is_parameter_set(): + return current + return null + + +func _to_string() -> String: + var s = _name + if _type != TYPE_MAX: + s += ":" + GdObjects.type_as_string(_type) + if _default_value != UNDEFINED: + s += "=" + str(_default_value) + return s + + +func _parse_parameter_set(input :String) -> PackedStringArray: + if not input.contains("["): + return [] + + input = _cleanup_leading_spaces.sub(input, "", true) + input = input.trim_prefix("[").trim_suffix("]").replace("\n", "").trim_prefix(" ") + var single_quote := false + var double_quote := false + var array_end := 0 + var current_index = 0 + var output :PackedStringArray = [] + var start_index := 0 + var buf = input.to_ascii_buffer() + for c in buf: + current_index += 1 + match c: + # ignore spaces between array elements + 32: if array_end == 0: + start_index += 1 + continue + # step over array element seperator ',' + 44: if array_end == 0: + start_index += 1 + continue + 39: single_quote = !single_quote + 34: if not single_quote: double_quote = !double_quote + 91: if not double_quote and not single_quote: array_end +=1 # counts array open + 93: if not double_quote and not single_quote: array_end -=1 # counts array closed + + # if array closed than collect the element + if array_end == 0 and current_index > start_index: + var parameters := input.substr(start_index, current_index-start_index) + parameters = _fix_comma_space.sub(parameters, ", ", true) + output.append(parameters) + start_index = current_index + return output diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd new file mode 100644 index 0000000..51c18b0 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd @@ -0,0 +1,250 @@ +class_name GdFunctionDescriptor +extends RefCounted + +var _is_virtual :bool +var _is_static :bool +var _is_engine :bool +var _is_coroutine :bool +var _name :String +var _line_number :int +var _return_type :int +var _return_class :String +var _args : Array[GdFunctionArgument] +var _varargs :Array[GdFunctionArgument] + + +func _init(p_name :String, + p_line_number :int, + p_is_virtual :bool, + p_is_static :bool, + p_is_engine :bool, + p_return_type :int, + p_return_class :String, + p_args : Array[GdFunctionArgument], + p_varargs :Array[GdFunctionArgument] = []): + _name = p_name + _line_number = p_line_number + _return_type = p_return_type + _return_class = p_return_class + _is_virtual = p_is_virtual + _is_static = p_is_static + _is_engine = p_is_engine + _is_coroutine = false + _args = p_args + _varargs = p_varargs + + +func name() -> String: + return _name + + +func line_number() -> int: + return _line_number + + +func is_virtual() -> bool: + return _is_virtual + + +func is_static() -> bool: + return _is_static + + +func is_engine() -> bool: + return _is_engine + + +func is_vararg() -> bool: + return not _varargs.is_empty() + + +func is_coroutine() -> bool: + return _is_coroutine + + +func is_parameterized() -> bool: + for current in _args: + var arg :GdFunctionArgument = current + if arg.name() == GdFunctionArgument.ARG_PARAMETERIZED_TEST: + return true + return false + + +func is_private() -> bool: + return name().begins_with("_") and not is_virtual() + + +func return_type() -> Variant: + return _return_type + + +func return_type_as_string() -> String: + if (return_type() == TYPE_OBJECT or return_type() == GdObjects.TYPE_ENUM) and not _return_class.is_empty(): + return _return_class + return GdObjects.type_as_string(return_type()) + + +func args() -> Array[GdFunctionArgument]: + return _args + + +func varargs() -> Array[GdFunctionArgument]: + return _varargs + + +func typeless() -> String: + var func_signature := "" + if _return_type == TYPE_NIL: + func_signature = "func %s(%s) -> void:" % [name(), typeless_args()] + elif _return_type == GdObjects.TYPE_VARIANT: + func_signature = "func %s(%s) -> Variant:" % [name(), typeless_args()] + else: + func_signature = "func %s(%s) -> %s:" % [name(), typeless_args(), return_type_as_string()] + return "static " + func_signature if is_static() else func_signature + + +func typeless_args() -> String: + var collect := PackedStringArray() + for arg in args(): + if arg.has_default(): + collect.push_back( arg.name() + "=" + arg.value_as_string()) + else: + collect.push_back(arg.name()) + for arg in varargs(): + collect.push_back(arg.name() + "=" + arg.value_as_string()) + return ", ".join(collect) + + +func typed_args() -> String: + var collect := PackedStringArray() + for arg in args(): + collect.push_back(arg._to_string()) + for arg in varargs(): + collect.push_back(arg._to_string()) + return ", ".join(collect) + + +func _to_string() -> String: + var fsignature := "virtual " if is_virtual() else "" + if _return_type == TYPE_NIL: + return fsignature + "[Line:%s] func %s(%s):" % [line_number(), name(), typed_args()] + var func_template := fsignature + "[Line:%s] func %s(%s) -> %s:" + if is_static(): + func_template= "[Line:%s] static func %s(%s) -> %s:" + return func_template % [line_number(), name(), typed_args(), return_type_as_string()] + + +# extract function description given by Object.get_method_list() +static func extract_from(descriptor :Dictionary) -> GdFunctionDescriptor: + var function_flags :int = descriptor["flags"] + var is_virtual_ :bool = function_flags & METHOD_FLAG_VIRTUAL + var is_static_ :bool = function_flags & METHOD_FLAG_STATIC + var is_vararg_ :bool = function_flags & METHOD_FLAG_VARARG + #var is_const :bool = function_flags & METHOD_FLAG_CONST + #var is_core :bool = function_flags & METHOD_FLAG_OBJECT_CORE + #var is_default :bool = function_flags & METHOD_FLAGS_DEFAULT + #prints("is_virtual: ", is_virtual) + #prints("is_static: ", is_static) + #prints("is_const: ", is_const) + #prints("is_core: ", is_core) + #prints("is_default: ", is_default) + #prints("is_vararg: ", is_vararg) + return GdFunctionDescriptor.new( + descriptor["name"], + -1, + is_virtual_, + is_static_, + true, + _extract_return_type(descriptor["return"]), + descriptor["return"]["class_name"], + _extract_args(descriptor), + _build_varargs(is_vararg_) + ) + +# temporary exclude GlobalScope enums +const enum_fix := [ + "Side", + "Corner", + "Orientation", + "ClockDirection", + "HorizontalAlignment", + "VerticalAlignment", + "InlineAlignment", + "EulerOrder", + "Error", + "Key", + "MIDIMessage", + "MouseButton", + "MouseButtonMask", + "JoyButton", + "JoyAxis", + "PropertyHint", + "PropertyUsageFlags", + "MethodFlags", + "Variant.Type", + "Control.LayoutMode"] + + +static func _extract_return_type(return_info :Dictionary) -> Variant: + var type :int = return_info["type"] + var usage :int = return_info["usage"] + if type == TYPE_INT and usage & PROPERTY_USAGE_CLASS_IS_ENUM: + return GdObjects.TYPE_ENUM + if type == TYPE_NIL and usage & PROPERTY_USAGE_NIL_IS_VARIANT: + return GdObjects.TYPE_VARIANT + return type + + +static func _extract_args(descriptor :Dictionary) -> Array[GdFunctionArgument]: + var args_ :Array[GdFunctionArgument] = [] + var arguments :Array = descriptor["args"] + var defaults :Array = descriptor["default_args"] + # iterate backwards because the default values are stored from right to left + while not arguments.is_empty(): + var arg :Dictionary = arguments.pop_back() + var arg_name := _argument_name(arg) + var arg_type := _argument_type(arg) + var arg_default :Variant = GdFunctionArgument.UNDEFINED + if not defaults.is_empty(): + var default_value = defaults.pop_back() + arg_default = GdDefaultValueDecoder.decode_typed(arg_type, default_value) + args_.push_front(GdFunctionArgument.new(arg_name, arg_type, arg_default)) + return args_ + + +static func _build_varargs(p_is_vararg :bool) -> Array[GdFunctionArgument]: + var varargs_ :Array[GdFunctionArgument] = [] + if not p_is_vararg: + return varargs_ + # if function has vararg we need to handle this manually by adding 10 default arguments + var type := GdObjects.TYPE_VARARG + for index in 10: + varargs_.push_back(GdFunctionArgument.new("vararg%d_" % index, type, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE)) + return varargs_ + + +static func _argument_name(arg :Dictionary) -> String: + # add suffix to the name to prevent clash with reserved names + return (arg["name"] + "_") as String + + +static func _argument_type(arg :Dictionary) -> int: + var type :int = arg["type"] + if type == TYPE_OBJECT: + if arg["class_name"] == "Node": + return GdObjects.TYPE_NODE + return type + + +static func _argument_type_as_string(arg :Dictionary) -> String: + var type := _argument_type(arg) + match type: + TYPE_NIL: + return "" + TYPE_OBJECT: + var clazz_name :String = arg["class_name"] + if not clazz_name.is_empty(): + return clazz_name + return "" + _: + return GdObjects.type_as_string(type) diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd b/addons/gdUnit4/src/core/parse/GdScriptParser.gd new file mode 100644 index 0000000..37e0067 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -0,0 +1,824 @@ +class_name GdScriptParser +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"" + +var TOKEN_NOT_MATCH := Token.new("") +var TOKEN_SPACE := SkippableToken.new(" ") +var TOKEN_TABULATOR := SkippableToken.new("\t") +var TOKEN_NEW_LINE := SkippableToken.new("\n") +var TOKEN_COMMENT := SkippableToken.new("#") +var TOKEN_CLASS_NAME := Token.new("class_name") +var TOKEN_INNER_CLASS := Token.new("class") +var TOKEN_EXTENDS := Token.new("extends") +var TOKEN_ENUM := Token.new("enum") +var TOKEN_FUNCTION_STATIC_DECLARATION := Token.new("staticfunc") +var TOKEN_FUNCTION_DECLARATION := Token.new("func") +var TOKEN_FUNCTION := Token.new(".") +var TOKEN_FUNCTION_RETURN_TYPE := Token.new("->") +var TOKEN_FUNCTION_END := Token.new("):") +var TOKEN_ARGUMENT_ASIGNMENT := Token.new("=") +var TOKEN_ARGUMENT_TYPE_ASIGNMENT := Token.new(":=") +var TOKEN_ARGUMENT_FUZZER := FuzzerToken.new(GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)")) +var TOKEN_ARGUMENT_TYPE := Token.new(":") +var TOKEN_ARGUMENT_SEPARATOR := Token.new(",") +var TOKEN_BRACKET_OPEN := Token.new("(") +var TOKEN_BRACKET_CLOSE := Token.new(")") +var TOKEN_ARRAY_OPEN := Token.new("[") +var TOKEN_ARRAY_CLOSE := Token.new("]") + +var OPERATOR_ADD := Operator.new("+") +var OPERATOR_SUB := Operator.new("-") +var OPERATOR_MUL := Operator.new("*") +var OPERATOR_DIV := Operator.new("/") +var OPERATOR_REMAINDER := Operator.new("%") + +var TOKENS := [ + TOKEN_SPACE, + TOKEN_TABULATOR, + TOKEN_NEW_LINE, + TOKEN_COMMENT, + TOKEN_BRACKET_OPEN, + TOKEN_BRACKET_CLOSE, + TOKEN_ARRAY_OPEN, + TOKEN_ARRAY_CLOSE, + TOKEN_CLASS_NAME, + TOKEN_INNER_CLASS, + TOKEN_EXTENDS, + TOKEN_ENUM, + TOKEN_FUNCTION_STATIC_DECLARATION, + TOKEN_FUNCTION_DECLARATION, + TOKEN_ARGUMENT_FUZZER, + TOKEN_ARGUMENT_TYPE_ASIGNMENT, + TOKEN_ARGUMENT_ASIGNMENT, + TOKEN_ARGUMENT_TYPE, + TOKEN_FUNCTION, + TOKEN_ARGUMENT_SEPARATOR, + TOKEN_FUNCTION_RETURN_TYPE, + OPERATOR_ADD, + OPERATOR_SUB, + OPERATOR_MUL, + OPERATOR_DIV, + OPERATOR_REMAINDER, +] + +var _regex_clazz_name :RegEx +var _regex_strip_comments := GdUnitTools.to_regex("^([^#\"']|'[^']*'|\"[^\"]*\")*\\K#.*") +var _base_clazz :String +var _scanned_inner_classes := PackedStringArray() +var _script_constants := {} + + +static func clean_up_row(row :String) -> String: + return to_unix_format(row.replace(" ", "").replace("\t", "")) + + +static func to_unix_format(input :String) -> String: + return input.replace("\r\n", "\n") + + +class Token extends RefCounted: + var _token: String + var _consumed: int + var _is_operator: bool + var _regex :RegEx + + + func _init(p_token: String, p_is_operator := false, p_regex :RegEx = null) -> void: + _token = p_token + _is_operator = p_is_operator + _consumed = p_token.length() + _regex = p_regex + + func match(input: String, pos: int) -> bool: + if _regex: + var result := _regex.search(input, pos) + if result == null: + return false + _consumed = result.get_end() - result.get_start() + return pos == result.get_start() + return input.findn(_token, pos) == pos + + func is_operator() -> bool: + return _is_operator + + func is_inner_class() -> bool: + return _token == "class" + + func is_variable() -> bool: + return false + + func is_token(token_name :String) -> bool: + return _token == token_name + + func is_skippable() -> bool: + return false + + func _to_string(): + return "Token{" + _token + "}" + + +class Operator extends Token: + func _init(value: String): + super(value, true) + + func _to_string(): + return "OperatorToken{%s}" % [_token] + + +# A skippable token, is just a placeholder like space or tabs +class SkippableToken extends Token: + + func _init(p_token: String): + super(p_token) + + func is_skippable() -> bool: + return true + + +# Token to parse Fuzzers +class FuzzerToken extends Token: + var _name: String + + + func _init(regex: RegEx): + super("", false, regex) + + + func match(input: String, pos: int) -> bool: + if _regex: + var result := _regex.search(input, pos) + if result == null: + return false + _name = result.strings[1] + _consumed = result.get_end() - result.get_start() + return pos == result.get_start() + return input.findn(_token, pos) == pos + + + func name() -> String: + return _name + + + func type() -> int: + return GdObjects.TYPE_FUZZER + + + func _to_string(): + return "FuzzerToken{%s: '%s'}" % [_name, _token] + + +# Token to parse function arguments +class Variable extends Token: + var _plain_value + var _typed_value + var _type :int = TYPE_NIL + + + func _init(p_value: String): + super(p_value) + _type = _scan_type(p_value) + _plain_value = p_value + _typed_value = _cast_to_type(p_value, _type) + + + func _scan_type(p_value: String) -> int: + if p_value.begins_with("\"") and p_value.ends_with("\""): + return TYPE_STRING + var type_ := GdObjects.string_to_type(p_value) + if type_ != TYPE_NIL: + return type_ + if p_value.is_valid_int(): + return TYPE_INT + if p_value.is_valid_float(): + return TYPE_FLOAT + if p_value.is_valid_hex_number(): + return TYPE_INT + return TYPE_OBJECT + + + func _cast_to_type(p_value :String, p_type: int) -> Variant: + match p_type: + TYPE_STRING: + return p_value#.substr(1, p_value.length() - 2) + TYPE_INT: + return p_value.to_int() + TYPE_FLOAT: + return p_value.to_float() + return p_value + + + func is_variable() -> bool: + return true + + + func type() -> int: + return _type + + + func value(): + return _typed_value + + + func plain_value(): + return _plain_value + + + func _to_string(): + return "Variable{%s: %s : '%s'}" % [_plain_value, GdObjects.type_as_string(_type), _token] + + +class TokenInnerClass extends Token: + var _clazz_name + var _content := PackedStringArray() + + + static func _strip_leading_spaces(input :String) -> String: + var characters := input.to_ascii_buffer() + while not characters.is_empty(): + if characters[0] != 0x20: + break + characters.remove_at(0) + return characters.get_string_from_ascii() + + + static func _consumed_bytes(row :String) -> int: + return row.replace(" ", "").replace(" ", "").length() + + + func _init(clazz_name :String): + super("class") + _clazz_name = clazz_name + + + func is_class_name(clazz_name :String) -> bool: + return _clazz_name == clazz_name + + + func content() -> PackedStringArray: + return _content + + + func parse(source_rows :PackedStringArray, offset :int) -> void: + # add class signature + _content.append(source_rows[offset]) + # parse class content + for row_index in range(offset+1, source_rows.size()): + # scan until next non tab + var source_row := source_rows[row_index] + var row = TokenInnerClass._strip_leading_spaces(source_row) + if row.is_empty() or row.begins_with("\t") or row.begins_with("#"): + # fold all line to left by removing leading tabs and spaces + if source_row.begins_with("\t"): + source_row = source_row.trim_prefix("\t") + # refomat invalid empty lines + if source_row.dedent().is_empty(): + _content.append("") + else: + _content.append(source_row) + continue + break + _consumed += TokenInnerClass._consumed_bytes("".join(_content)) + + + func _to_string(): + return "TokenInnerClass{%s}" % [_clazz_name] + + +func _init(): + _regex_clazz_name = GdUnitTools.to_regex("(class)([a-zA-Z0-9]+)(extends[a-zA-Z]+:)|(class)([a-zA-Z0-9]+)(:)") + + +func get_token(input :String, current_index) -> Token: + for t in TOKENS: + if t.match(input, current_index): + return t + return TOKEN_NOT_MATCH + + +func next_token(input: String, current_index: int, ignore_tokens :Array[Token] = []) -> Token: + var token := TOKEN_NOT_MATCH + for t in TOKENS.filter(func(token): return not ignore_tokens.has(token)): + if t.match(input, current_index): + token = t + break + if token == OPERATOR_SUB: + token = tokenize_value(input, current_index, token) + if token == TOKEN_INNER_CLASS: + token = tokenize_inner_class(input, current_index, token) + if token == TOKEN_NOT_MATCH: + return tokenize_value(input, current_index, token, ignore_tokens.has(TOKEN_FUNCTION)) + return token + + +func tokenize_value(input: String, current: int, token: Token, ignore_dots := false) -> Token: + var next := 0 + var current_token := "" + # test for '--', '+-', '*-', '/-', '%-', or at least '-x' + var test_for_sign := (token == null or token.is_operator()) and input[current] == "-" + while current + next < len(input): + var character := input[current + next] as String + # if first charater a sign + # or allowend charset + # or is a float value + if (test_for_sign and next==0) \ + or character in ALLOWED_CHARACTERS \ + or (character == "." and (ignore_dots or current_token.is_valid_int())): + current_token += character + next += 1 + continue + break + if current_token != "": + return Variable.new(current_token) + return TOKEN_NOT_MATCH + + +func extract_clazz_name(value :String) -> String: + var result := _regex_clazz_name.search(value) + if result == null: + push_error("Can't extract class name from '%s'" % value) + return "" + if result.get_string(2).is_empty(): + return result.get_string(5) + else: + return result.get_string(2) + + +@warning_ignore("unused_parameter") +func tokenize_inner_class(source_code: String, current: int, token: Token) -> Token: + var clazz_name := extract_clazz_name(source_code.substr(current, 64)) + return TokenInnerClass.new(clazz_name) + + +@warning_ignore("assert_always_false") +func _process_values(left: Token, token_stack: Array, operator: Token) -> Token: + # precheck + if left.is_variable() and operator.is_operator(): + var lvalue = left.value() + var value = null + var next_token_ = token_stack.pop_front() as Token + match operator: + OPERATOR_ADD: + value = lvalue + next_token_.value() + OPERATOR_SUB: + value = lvalue - next_token_.value() + OPERATOR_MUL: + value = lvalue * next_token_.value() + OPERATOR_DIV: + value = lvalue / next_token_.value() + OPERATOR_REMAINDER: + value = lvalue & next_token_.value() + _: + assert(false, "Unsupported operator %s" % operator) + return Variable.new( str(value)) + return operator + + +func parse_func_return_type(row: String) -> int: + var token := parse_return_token(row) + if token == TOKEN_NOT_MATCH: + return TYPE_NIL + return token.type() + + +func parse_return_token(input: String) -> Token: + var index := input.rfind(TOKEN_FUNCTION_RETURN_TYPE._token) + if index == -1: + return TOKEN_NOT_MATCH + index += TOKEN_FUNCTION_RETURN_TYPE._consumed + # We scan for the return value exclusive '.' token because it could be referenced to a + # external or internal class e.g. 'func foo() -> InnerClass.Bar:' + var token := next_token(input, index, [TOKEN_FUNCTION]) + while !token.is_variable() and token != TOKEN_NOT_MATCH: + index += token._consumed + token = next_token(input, index, [TOKEN_FUNCTION]) + return token + + +# Parses the argument into a argument signature +# e.g. func foo(arg1 :int, arg2 = 20) -> [arg1, arg2] +func parse_arguments(input: String) -> Array[GdFunctionArgument]: + var args :Array[GdFunctionArgument] = [] + var current_index := 0 + var token :Token = null + var bracket := 0 + var in_function := false + while current_index < len(input): + token = next_token(input, current_index) + # fallback to not end in a endless loop + if token == TOKEN_NOT_MATCH: + var error : = """ + Parsing Error: Invalid token at pos %d found. + Please report this error! + source_code: + -------------------------------------------------------------- + %s + -------------------------------------------------------------- + """.dedent() % [current_index, input] + push_error(error) + current_index += 1 + continue + current_index += token._consumed + if token.is_skippable(): + continue + if token == TOKEN_BRACKET_OPEN: + in_function = true + bracket += 1 + continue + if token == TOKEN_BRACKET_CLOSE: + bracket -= 1 + # if function end? + if in_function and bracket == 0: + return args + # is function + if token == TOKEN_FUNCTION_DECLARATION: + token = next_token(input, current_index) + current_index += token._consumed + continue + # is fuzzer argument + if token is FuzzerToken: + var arg_value := _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + args.append(GdFunctionArgument.new(token.name(), token.type(), arg_value)) + continue + # is value argument + if in_function and token.is_variable(): + var arg_name :String = token.plain_value() + var arg_type :int = TYPE_NIL + var arg_value = GdFunctionArgument.UNDEFINED + # parse type and default value + while current_index < len(input): + token = next_token(input, current_index) + current_index += token._consumed + if token.is_skippable(): + continue + match token: + TOKEN_ARGUMENT_TYPE: + token = next_token(input, current_index) + if token == TOKEN_SPACE: + current_index += token._consumed + token = next_token(input, current_index) + arg_type = GdObjects.string_as_typeof(token._token) + TOKEN_ARGUMENT_TYPE_ASIGNMENT: + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + TOKEN_ARGUMENT_ASIGNMENT: + token = next_token(input, current_index) + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + TOKEN_BRACKET_OPEN: + bracket += 1 + # if value a function? + if bracket > 1: + # complete the argument value + var func_begin = input.substr(current_index-TOKEN_BRACKET_OPEN._consumed) + var func_body = _parse_end_function(func_begin) + arg_value += func_body + # fix parse index to end of value + current_index += func_body.length() - TOKEN_BRACKET_OPEN._consumed - TOKEN_BRACKET_CLOSE._consumed + TOKEN_BRACKET_CLOSE: + bracket -= 1 + # end of function + if bracket == 0: + break + TOKEN_ARGUMENT_SEPARATOR: + if bracket <= 1: + break + arg_value = arg_value.lstrip(" ") + if arg_type == TYPE_NIL and arg_value != GdFunctionArgument.UNDEFINED: + if arg_value.begins_with("Color."): + arg_type = TYPE_COLOR + elif arg_value.begins_with("Vector2."): + arg_type = TYPE_VECTOR2 + elif arg_value.begins_with("Vector3."): + arg_type = TYPE_VECTOR3 + elif arg_value.begins_with("AABB("): + arg_type = TYPE_AABB + elif arg_value.begins_with("["): + arg_type = TYPE_ARRAY + elif arg_value.begins_with("{"): + arg_type = TYPE_DICTIONARY + else: + arg_type = typeof(str_to_var(arg_value)) + if arg_value.rfind(")") == arg_value.length()-1: + arg_type = GdObjects.TYPE_FUNC + elif arg_type == TYPE_NIL: + arg_type = TYPE_STRING + args.append(GdFunctionArgument.new(arg_name, arg_type, arg_value)) + return args + + +# Parse an string for an argument with given name and returns the value +# if the argument not found the is returned +func parse_argument(row: String, argument_name: String, default_value): + var input := GdScriptParser.clean_up_row(row) + var argument_found := false + var current_index := 0 + var token :Token = null + while current_index < len(input): + token = next_token(input, current_index) as Token + current_index += token._consumed + if token == TOKEN_NOT_MATCH: + return default_value + if not argument_found and not token.is_token(argument_name): + continue + argument_found = true + # extract value + if token == TOKEN_ARGUMENT_TYPE_ASIGNMENT: + token = next_token(input, current_index) as Token + return token.value() + elif token == TOKEN_ARGUMENT_ASIGNMENT: + token = next_token(input, current_index) as Token + return token.value() + return default_value + + +func _parse_end_function(input: String, remove_trailing_char := false) -> String: + # find end of function + var current_index := 0 + var bracket_count := 0 + var in_array := 0 + var end_of_func = false + + while current_index < len(input) and not end_of_func: + var character = input[current_index] + # step over strings + if character == "'" : + current_index = input.find("'", current_index+1) + 1 + if current_index == 0: + push_error("Parsing error on '%s', can't evaluate end of string." % input) + return "" + continue + if character == '"' : + # test for string blocks + if input.find('"""', current_index) == current_index: + current_index = input.find('"""', current_index+3) + 3 + else: + current_index = input.find('"', current_index+1) + 1 + if current_index == 0: + push_error("Parsing error on '%s', can't evaluate end of string." % input) + return "" + continue + + match character: + # count if inside an array + "[": in_array += 1 + "]": in_array -= 1 + # count if inside a function + "(": bracket_count += 1 + ")": + bracket_count -= 1 + if bracket_count < 0 and in_array <= 0: + end_of_func = true + ",": + if bracket_count == 0 and in_array == 0: + end_of_func = true + current_index += 1 + if remove_trailing_char: + # check if the parsed value ends with comma or end of doubled breaked + # `,` or `())` + var trailing_char := input[current_index-1] + if trailing_char == ',' or (bracket_count < 0 and trailing_char == ')'): + return input.substr(0, current_index-1) + return input.substr(0, current_index) + + +func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> PackedStringArray: + for row_index in source_rows.size(): + var input := GdScriptParser.clean_up_row(source_rows[row_index]) + var token := next_token(input, 0) + if token.is_inner_class(): + if token.is_class_name(clazz_name): + token.parse(source_rows, row_index) + return token.content() + return PackedStringArray() + + +func extract_source_code(script_path :PackedStringArray) -> PackedStringArray: + if script_path.is_empty(): + push_error("Invalid script path '%s'" % script_path) + return PackedStringArray() + #load the source code + var resource_path := script_path[0] + var script :GDScript = load(resource_path) + var source_code := load_source_code(script, script_path) + var base_script := script.get_base_script() + if base_script: + _base_clazz = GdObjects.extract_class_name_from_class_path([base_script.resource_path]) + source_code += load_source_code(base_script, script_path) + return source_code + + +func extract_func_signature(rows :PackedStringArray, index :int) -> String: + var signature := "" + + for rowIndex in range(index, rows.size()): + var row := rows[rowIndex] + row = _regex_strip_comments.sub(row, "").strip_edges(false) + if row.is_empty(): + continue + signature += row + "\n" + if is_func_end(row): + return signature.strip_edges() + push_error("Can't fully extract function signature of '%s'" % rows[index]) + return "" + + +func load_source_code(script :GDScript, script_path :PackedStringArray) -> PackedStringArray: + var map := script.get_script_constant_map() + for key in map.keys(): + var value = map.get(key) + if value is GDScript: + var class_path := GdObjects.extract_class_path(value) + if class_path.size() > 1: + _scanned_inner_classes.append(class_path[1]) + + var source_code := GdScriptParser.to_unix_format(script.source_code) + var source_rows := source_code.split("\n") + # extract all inner class names + # want to extract an inner class? + if script_path.size() > 1: + var inner_clazz = script_path[1] + source_rows = extract_inner_class(source_rows, inner_clazz) + return PackedStringArray(source_rows) + + +func get_class_name(script :GDScript) -> String: + var source_code := GdScriptParser.to_unix_format(script.source_code) + var source_rows := source_code.split("\n") + + for index in min(10, source_rows.size()): + var input = GdScriptParser.clean_up_row(source_rows[index]) + var token := next_token(input, 0) + if token == TOKEN_CLASS_NAME: + token = tokenize_value(input, token._consumed, token) + return token.value() + # if no class_name found extract from file name + return GdObjects.to_pascal_case(script.resource_path.get_basename().get_file()) + + +func parse_func_name(row :String) -> String: + var input = GdScriptParser.clean_up_row(row) + var current_index = 0 + var token := next_token(input, current_index) + current_index += token._consumed + if token != TOKEN_FUNCTION_STATIC_DECLARATION and token != TOKEN_FUNCTION_DECLARATION: + return "" + while not token is Variable: + token = next_token(input, current_index) + current_index += token._consumed + return token._token + + +func parse_functions(rows :PackedStringArray, clazz_name :String, clazz_path :PackedStringArray, included_functions := PackedStringArray()) -> Array[GdFunctionDescriptor]: + var func_descriptors :Array[GdFunctionDescriptor] = [] + for rowIndex in rows.size(): + var row = rows[rowIndex] + # step over inner class functions + if row.begins_with("\t"): + continue + var input = GdScriptParser.clean_up_row(row) + # skip comments and empty lines + if input.begins_with("#") or input.length() == 0: + continue + var token := next_token(input, 0) + if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: + if _is_func_included(input, included_functions): + var func_signature = extract_func_signature(rows, rowIndex) + var fd := parse_func_description(func_signature, clazz_name, clazz_path, rowIndex+1) + fd._is_coroutine = is_func_coroutine(rows, rowIndex) + func_descriptors.append(fd) + return func_descriptors + + +func is_func_coroutine(rows :PackedStringArray, index :int) -> bool: + var is_coroutine := false + for rowIndex in range( index+1, rows.size()): + var row = rows[rowIndex] + is_coroutine = row.contains("await") + if is_coroutine: + return true + var input = GdScriptParser.clean_up_row(row) + var token := next_token(input, 0) + if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: + break + return is_coroutine + + +func _is_func_included(row :String, included_functions :PackedStringArray) -> bool: + if included_functions.is_empty(): + return true + for name in included_functions: + if row.find(name) != -1: + return true + return false + + +func parse_func_description(func_signature :String, clazz_name :String, clazz_path :PackedStringArray, line_number :int) -> GdFunctionDescriptor: + var name = parse_func_name(func_signature) + var return_type :int + var return_clazz := "" + var token := parse_return_token(func_signature) + if token == TOKEN_NOT_MATCH: + return_type = GdObjects.TYPE_VARIANT + else: + return_type = token.type() + if token.type() == TYPE_OBJECT: + return_clazz = _patch_inner_class_names(token.value(), clazz_name) + # is return type an enum? + if is_class_enum_type(return_clazz): + return_type = GdObjects.TYPE_ENUM + + return GdFunctionDescriptor.new( + name, + line_number, + is_virtual_func(clazz_name, clazz_path, name), + is_static_func(func_signature), + false, + return_type, + return_clazz, + parse_arguments(func_signature) + ) + + +# caches already parsed classes for virtual functions +# key: value: a Array of virtual function names +var _virtual_func_cache := Dictionary() + +func is_virtual_func(clazz_name :String, clazz_path :PackedStringArray, func_name :String) -> bool: + if _virtual_func_cache.has(clazz_name): + return _virtual_func_cache[clazz_name].has(func_name) + var virtual_functions := Array() + var method_list := GdObjects.extract_class_functions(clazz_name, clazz_path) + for method_descriptor in method_list: + var is_virtual_function :bool = method_descriptor["flags"] & METHOD_FLAG_VIRTUAL + if is_virtual_function: + virtual_functions.append(method_descriptor["name"]) + _virtual_func_cache[clazz_name] = virtual_functions + return _virtual_func_cache[clazz_name].has(func_name) + + +func is_static_func(func_signature :String) -> bool: + var input := GdScriptParser.clean_up_row(func_signature) + var token := next_token(input, 0) + return token == TOKEN_FUNCTION_STATIC_DECLARATION + + +func is_inner_class(clazz_path :PackedStringArray) -> bool: + return clazz_path.size() > 1 + + +func is_func_end(row :String) -> bool: + return row.strip_edges(false, true).ends_with(":") + + +func is_class_enum_type(value :String) -> bool: + # first check is given value a enum from the current class + if _script_constants.has(value): + return true + # otherwise we need to determie it by reflection + var script := GDScript.new() + script.source_code = """ + extends Resource + + static func is_class_enum_type() -> bool: + return typeof(%s) == TYPE_DICTIONARY + + """.dedent() % value + script.reload() + return script.call("is_class_enum_type") + + +func _patch_inner_class_names(clazz :String, clazz_name :String) -> String: + var base_clazz := clazz_name.split(".")[0] + var inner_clazz_name := clazz.split(".")[0] + if _scanned_inner_classes.has(inner_clazz_name): + return base_clazz + "." + clazz + if _script_constants.has(clazz): + return clazz_name + "." + clazz + return clazz + + +func extract_functions(script :GDScript, clazz_name :String, clazz_path :PackedStringArray) -> Array[GdFunctionDescriptor]: + var source_code := load_source_code(script, clazz_path) + _script_constants = script.get_script_constant_map() + return parse_functions(source_code, clazz_name, clazz_path) + + +func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult: + if clazz_path.is_empty(): + return GdUnitResult.error("Invalid script path '%s'" % clazz_path) + var is_inner_class_ := is_inner_class(clazz_path) + var script :GDScript = load(clazz_path[0]) + var function_descriptors := extract_functions(script, clazz_name, clazz_path) + var gd_class := GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors) + # iterate over class dependencies + script = script.get_base_script() + while script != null: + clazz_name = GdObjects.extract_class_name_from_class_path([script.resource_path]) + function_descriptors = extract_functions(script, clazz_name, clazz_path) + gd_class.set_parent_clazz(GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors)) + script = script.get_base_script() + return GdUnitResult.success(gd_class) diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd new file mode 100644 index 0000000..f3130a0 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd @@ -0,0 +1,26 @@ +class_name GdUnitExpressionRunner +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ExpressionRunner extends '${clazz_path}' + +func __run_expression() -> Variant: + return $expression + +""" + +func execute(src_script :GDScript, expression :String) -> Variant: + var script := GDScript.new() + var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path + script.source_code = CLASS_TEMPLATE.dedent()\ + .replace("${clazz_path}", resource_path)\ + .replace("$expression", expression) + script.reload(false) + var runner :Variant = script.new() + if runner.has_method("queue_free"): + runner.queue_free() + return runner.__run_expression() + + +func to_fuzzer(src_script :GDScript, expression :String) -> Fuzzer: + return execute(src_script, expression) as Fuzzer diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd new file mode 100644 index 0000000..b0865b2 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd @@ -0,0 +1,194 @@ +class_name GdUnitTestParameterSetResolver +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ParameterExtractor extends '${clazz_path}' + +func __extract_test_parameters() -> Array: + return ${test_params} + +""" + +const EXCLUDE_PROPERTIES_TO_COPY = [ + "script", + "type", + "Node", + "_import_path"] + + +var _fd: GdFunctionDescriptor +var _test_case_names_cache := PackedStringArray() +var _static_sets_by_index := {} +var _is_static := true + +func _init(fd: GdFunctionDescriptor) -> void: + _fd = fd + + +func is_parameterized() -> bool: + return _fd.is_parameterized() + + +func is_parameter_sets_static() -> bool: + return _is_static + + +func is_parameter_set_static(index: int) -> bool: + return _is_static and _static_sets_by_index.get(index, false) + + +# validates the given arguments are complete and matches to required input fields of the test function +func validate(input_value_set: Array) -> String: + var input_arguments := _fd.args() + # check given parameter set with test case arguments + var expected_arg_count = input_arguments.size() - 1 + for input_values in input_value_set: + var parameter_set_index := input_value_set.find(input_values) + if input_values is Array: + var current_arg_count = input_values.size() + if current_arg_count != expected_arg_count: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count] + var error := validate_parameter_types(input_arguments, input_values, parameter_set_index) + if not error.is_empty(): + return error + else: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n Expecting an array of input values." % parameter_set_index + return "" + + +static func validate_parameter_types(input_arguments: Array, input_values: Array, parameter_set_index: int) -> String: + for i in input_arguments.size(): + var input_param: GdFunctionArgument = input_arguments[i] + # only check the test input arguments + if input_param.is_parameter_set(): + continue + var input_param_type := input_param.type() + var input_value = input_values[i] + var input_value_type := typeof(input_value) + # input parameter is not typed we skip the type test + if input_param_type == TYPE_NIL: + continue + # is input type enum allow int values + if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT: + continue + # allow only equal types and object == null + if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL: + continue + if input_param_type != input_value_type: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The value '%s' does not match the required input parameter <%s>." % [parameter_set_index, input_value, input_param] + return "" + + +func build_test_case_names(test_case: _TestCase) -> PackedStringArray: + if not is_parameterized(): + return [] + # if test names already resolved? + if not _test_case_names_cache.is_empty(): + return _test_case_names_cache + + var fa := GdFunctionArgument.get_parameter_set(_fd.args()) + var parameter_sets := fa.parameter_sets() + # if no parameter set detected we need to resolve it by using reflection + if parameter_sets.size() == 0: + _test_case_names_cache = _extract_test_names_by_reflection(test_case) + _is_static = false + else: + var property_names := _extract_property_names(test_case.get_parent()) + for parameter_set_index in parameter_sets.size(): + var parameter_set := parameter_sets[parameter_set_index] + _static_sets_by_index[parameter_set_index] = _is_static_parameter_set(test_case, parameter_set, property_names) + _test_case_names_cache.append(_build_test_case_name(test_case, parameter_set, parameter_set_index)) + parameter_set_index += 1 + return _test_case_names_cache + + +func _extract_property_names(node :Node) -> PackedStringArray: + return node.get_property_list()\ + .map(func(property): return property["name"])\ + .filter(func(property): return !EXCLUDE_PROPERTIES_TO_COPY.has(property)) + + +# tests if the test property set contains an property reference by name, if not the parameter set holds only static values +func _is_static_parameter_set(test_case: _TestCase, parameters :String, property_names :PackedStringArray) -> bool: + for property_name in property_names: + if parameters.contains(property_name): + _is_static = false + return false + return true + + +func _extract_test_names_by_reflection(test_case: _TestCase) -> PackedStringArray: + var parameter_sets := load_parameter_sets(test_case) + var test_case_names: PackedStringArray = [] + for index in parameter_sets.size(): + test_case_names.append(_build_test_case_name(test_case, str(parameter_sets[index]), index)) + return test_case_names + + +static func _build_test_case_name(test_case: _TestCase, test_parameter: String, parameter_set_index: int) -> String: + if not test_parameter.begins_with("["): + test_parameter = "[" + test_parameter + return "%s:%d %s" % [test_case.get_name(), parameter_set_index, test_parameter.replace("\t", "").replace('"', "'").replace("&'", "'")] + + +# extracts the arguments from the given test case, using kind of reflection solution +# to restore the parameters from a string representation to real instance type +func load_parameter_sets(test_case: _TestCase, validate := false) -> Array: + var source_script = test_case.get_parent().get_script() + var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args()) + var source_code = CLASS_TEMPLATE \ + .replace("${clazz_path}", source_script.resource_path) \ + .replace("${test_params}", parameter_arg.value_as_string()) + var script = GDScript.new() + script.source_code = source_code + # enable this lines only for debuging + #script.resource_path = GdUnitTools.create_temp_dir("parameter_extract") + "/%s__.gd" % fd.name() + #DirAccess.remove_absolute(script.resource_path) + #ResourceSaver.save(script, script.resource_path) + var result = script.reload() + if result != OK: + push_error("Extracting test parameters failed! Script loading error: %s" % result) + return [] + var instance = script.new() + copy_properties(test_case.get_parent(), instance) + instance.queue_free() + var parameter_sets = instance.call("__extract_test_parameters") + if not validate: + return parameter_sets + # validate the parameter set + var error := validate(parameter_sets) + if not error.is_empty(): + test_case.skip(true, error) + test_case._interupted = true + if parameter_sets.size() != _test_case_names_cache.size(): + push_error("Internal Error: The resolved test_case names has invalid size!") + error = """ + %s: + The resolved test_case names has invalid size! + %s + """.dedent().trim_prefix("\n") % [ + GdAssertMessages._error("Internal Error"), + GdAssertMessages._error("Please report this issue as a bug!")] + test_case.get_parent().__execution_context\ + .reports()\ + .append(GdUnitReport.new().create(GdUnitReport.INTERUPTED, test_case.line_number(), error)) + test_case.skip(true, error) + test_case._interupted = true + return parameter_sets + + +static func copy_properties(source: Object, dest: Object) -> void: + var context := GdUnitThreadManager.get_current_context().get_execution_context() + for property in source.get_property_list(): + var property_name = property["name"] + var property_value = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) diff --git a/addons/gdUnit4/src/core/report/GdUnitReport.gd b/addons/gdUnit4/src/core/report/GdUnitReport.gd new file mode 100644 index 0000000..eb7ed2e --- /dev/null +++ b/addons/gdUnit4/src/core/report/GdUnitReport.gd @@ -0,0 +1,74 @@ +class_name GdUnitReport +extends Resource + +# report type +enum { + SUCCESS, + WARN, + FAILURE, + ORPHAN, + TERMINATED, + INTERUPTED, + ABORT, + SKIPPED, +} + +var _type :int +var _line_number :int +var _message :String + + +func create(p_type :int, p_line_number :int, p_message :String) -> GdUnitReport: + _type = p_type + _line_number = p_line_number + _message = p_message + return self + + +func type() -> int: + return _type + + +func line_number() -> int: + return _line_number + + +func message() -> String: + return _message + + +func is_skipped() -> bool: + return _type == SKIPPED + + +func is_warning() -> bool: + return _type == WARN + + +func is_failure() -> bool: + return _type == FAILURE + + +func is_error() -> bool: + return _type == TERMINATED or _type == INTERUPTED or _type == ABORT + + +func _to_string() -> String: + if _line_number == -1: + return "[color=green]line [/color][color=aqua]:[/color] %s" % [_message] + return "[color=green]line [/color][color=aqua]%d:[/color] %s" % [_line_number, _message] + + +func serialize() -> Dictionary: + return { + "type" :_type, + "line_number" :_line_number, + "message" :_message + } + + +func deserialize(serialized :Dictionary) -> GdUnitReport: + _type = serialized["type"] + _line_number = serialized["line_number"] + _message = serialized["message"] + return self diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd new file mode 100644 index 0000000..f7802d2 --- /dev/null +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd @@ -0,0 +1,36 @@ +class_name GdUnitTestSuiteDefaultTemplate +extends RefCounted + + +const DEFAULT_TEMP_TS_GD =""" + # GdUnit generated TestSuite + class_name ${suite_class_name} + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + # TestSuite generated from + const __source = '${source_resource_path}' +""" + + +const DEFAULT_TEMP_TS_CS = """ + // GdUnit generated TestSuite + + using Godot; + using GdUnit3; + + namespace ${name_space} + { + using static Assertions; + using static Utils; + + [TestSuite] + public class ${suite_class_name} + { + // TestSuite generated from + private const string sourceClazzPath = "${source_resource_path}"; + + } + } +""" diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd new file mode 100644 index 0000000..3e241f3 --- /dev/null +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd @@ -0,0 +1,142 @@ +class_name GdUnitTestSuiteTemplate +extends RefCounted + +const TEMPLATE_ID_GD = 1000 +const TEMPLATE_ID_CS = 2000 + +const SUPPORTED_TAGS_GD = """ + GdScript Tags are replaced when the test-suite is created. + + # The class name of the test-suite, formed from the source script. + ${suite_class_name} + # is used to build the test suite class name + class_name ${suite_class_name} + extends GdUnitTestSuite + + + # The class name in pascal case, formed from the source script. + ${source_class} + # can be used to create the class e.g. for source 'MyClass' + var my_test_class := ${source_class}.new() + # will be result in + var my_test_class := MyClass.new() + + # The class as variable name in snake case, formed from the source script. + ${source_var} + # Can be used to build the variable name e.g. for source 'MyClass' + var ${source_var} := ${source_class}.new() + # will be result in + var my_class := MyClass.new() + + # The full resource path from which the file was created. + ${source_resource_path} + # Can be used to load the script in your test + var my_script := load(${source_resource_path}) + # will be result in + var my_script := load("res://folder/my_class.gd") +""" + +const SUPPORTED_TAGS_CS = """ + C# Tags are replaced when the test-suite is created. + + // The namespace name of the test-suite + ${name_space} + namespace ${name_space} + + // The class name of the test-suite, formed from the source class. + ${suite_class_name} + // is used to build the test suite class name + [TestSuite] + public class ${suite_class_name} + + // The class name formed from the source class. + ${source_class} + // can be used to create the class e.g. for source 'MyClass' + private string myTestClass = new ${source_class}(); + // will be result in + private string myTestClass = new MyClass(); + + // The class as variable name in camelCase, formed from the source class. + ${source_var} + // Can be used to build the variable name e.g. for source 'MyClass' + private object ${source_var} = new ${source_class}(); + // will be result in + private object myClass = new MyClass(); + + // The full resource path from which the file was created. + ${source_resource_path} + // Can be used to load the script in your test + private object myScript = GD.Load(${source_resource_path}); + // will be result in + private object myScript = GD.Load("res://folder/MyClass.cs"); +""" + +const TAG_TEST_SUITE_CLASS = "${suite_class_name}" +const TAG_SOURCE_CLASS_NAME = "${source_class}" +const TAG_SOURCE_CLASS_VARNAME = "${source_var}" +const TAG_SOURCE_RESOURCE_PATH = "${source_resource_path}" + + +static func default_GD_template() -> String: + return GdUnitTestSuiteDefaultTemplate.DEFAULT_TEMP_TS_GD.dedent().trim_prefix("\n") + + +static func default_CS_template() -> String: + return GdUnitTestSuiteDefaultTemplate.DEFAULT_TEMP_TS_CS.dedent().trim_prefix("\n") + + +static func build_template(source_path: String) -> String: + var clazz_name :String = GdObjects.to_pascal_case(GdObjects.extract_class_name(source_path).value() as String) + return GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template())\ + .replace(TAG_TEST_SUITE_CLASS, clazz_name+"Test")\ + .replace(TAG_SOURCE_RESOURCE_PATH, source_path)\ + .replace(TAG_SOURCE_CLASS_NAME, clazz_name)\ + .replace(TAG_SOURCE_CLASS_VARNAME, GdObjects.to_snake_case(clazz_name)) + + +static func default_template(template_id :int) -> String: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return "" + if template_id == TEMPLATE_ID_GD: + return default_GD_template() + return default_CS_template() + + +static func load_template(template_id :int) -> String: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return "" + if template_id == TEMPLATE_ID_GD: + return GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template()) + return GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_CS, default_CS_template()) + + +static func save_template(template_id :int, template :String) -> void: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return + if template_id == TEMPLATE_ID_GD: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_GD, template.dedent().trim_prefix("\n")) + elif template_id == TEMPLATE_ID_CS: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_CS, template.dedent().trim_prefix("\n")) + + +static func reset_to_default(template_id :int) -> void: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return + if template_id == TEMPLATE_ID_GD: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template()) + else: + GdUnitSettings.save_property(GdUnitSettings.TEMPLATE_TS_CS, default_CS_template()) + + +static func load_tags(template_id :int) -> String: + if template_id != TEMPLATE_ID_GD and template_id != TEMPLATE_ID_CS: + push_error("Invalid template '%d' id! Cant load testsuite template" % template_id) + return "Error checked loading tags" + if template_id == TEMPLATE_ID_GD: + return SUPPORTED_TAGS_GD + else: + return SUPPORTED_TAGS_CS diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd new file mode 100644 index 0000000..d0eaa16 --- /dev/null +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd @@ -0,0 +1,62 @@ +class_name GdUnitThreadContext +extends RefCounted + +var _thread :Thread +var _thread_name :String +var _thread_id :int +var _assert :GdUnitAssert +var _signal_collector :GdUnitSignalCollector +var _execution_context :GdUnitExecutionContext + + +func _init(thread :Thread = null): + if thread != null: + _thread = thread + _thread_name = thread.get_meta("name") + _thread_id = thread.get_id() as int + else: + _thread_name = "main" + _thread_id = OS.get_main_thread_id() + _signal_collector = GdUnitSignalCollector.new() + + +func dispose() -> void: + _assert = null + if is_instance_valid(_signal_collector): + _signal_collector.clear() + _signal_collector = null + _execution_context = null + _thread = null + + +func set_assert(value :GdUnitAssert) -> GdUnitThreadContext: + _assert = value + return self + + +func get_assert() -> GdUnitAssert: + return _assert + + +func set_execution_context(context :GdUnitExecutionContext) -> void: + _execution_context = context + + +func get_execution_context() -> GdUnitExecutionContext: + return _execution_context + + +func get_execution_context_id() -> int: + return _execution_context.get_instance_id() + + +func get_signal_collector() -> GdUnitSignalCollector: + return _signal_collector + + +func thread_id() -> int: + return _thread_id + + +func _to_string() -> String: + return "ThreadContext <%s>: %s " % [_thread_name, _thread_id] diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd new file mode 100644 index 0000000..532946d --- /dev/null +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd @@ -0,0 +1,62 @@ +## A manager to run new thread and crate a ThreadContext shared over the actual test run +class_name GdUnitThreadManager +extends RefCounted + +## { = } +var _thread_context_by_id := {} +## holds the current thread id +var _current_thread_id :int = -1 + +func _init(): + # add initail the main thread + _current_thread_id = OS.get_thread_caller_id() + _thread_context_by_id[OS.get_main_thread_id()] = GdUnitThreadContext.new() + + +static func instance() -> GdUnitThreadManager: + return GdUnitSingleton.instance("GdUnitThreadManager", func(): return GdUnitThreadManager.new()) + + +## Runs a new thread by given name and Callable.[br] +## A new GdUnitThreadContext is created, which is used for the actual test execution.[br] +## We need this custom implementation while this bug is not solved +## Godot issue https://github.com/godotengine/godot/issues/79637 +static func run(name :String, cb :Callable) -> Variant: + return await instance()._run(name, cb) + + +## Returns the current valid thread context +static func get_current_context() -> GdUnitThreadContext: + return instance()._get_current_context() + + +func _run(name :String, cb :Callable): + # we do this hack because of `OS.get_thread_caller_id()` not returns the current id + # when await process_frame is called inside the fread + var save_current_thread_id = _current_thread_id + var thread := Thread.new() + thread.set_meta("name", name) + thread.start(cb) + _current_thread_id = thread.get_id() as int + _register_thread(thread, _current_thread_id) + var result :Variant = await thread.wait_to_finish() + _unregister_thread(_current_thread_id) + # restore original thread id + _current_thread_id = save_current_thread_id + return result + + +func _register_thread(thread :Thread, thread_id :int) -> void: + var context := GdUnitThreadContext.new(thread) + _thread_context_by_id[thread_id] = context + + +func _unregister_thread(thread_id :int) -> void: + var context := _thread_context_by_id.get(thread_id) as GdUnitThreadContext + if context: + _thread_context_by_id.erase(thread_id) + context.dispose() + + +func _get_current_context() -> GdUnitThreadContext: + return _thread_context_by_id.get(_current_thread_id) diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd new file mode 100644 index 0000000..096149c --- /dev/null +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd @@ -0,0 +1,69 @@ +# This class defines a value extractor by given function name and args +extends GdUnitValueExtractor + +var _func_names :Array +var _args :Array + +func _init(func_name :String, p_args :Array): + _func_names = func_name.split(".") + _args = p_args + + +func func_names() -> Array: + return _func_names + + +func args() -> Array: + return _args + + +# Extracts a value by given `func_name` and `args`, +# Allows to use a chained list of functions setarated ba a dot. +# e.g. "func_a.func_b.name" +# do calls instance.func_a().func_b().name() and returns finally the name +# If a function returns an array, all elements will by collected in a array +# e.g. "get_children.get_name" checked a node +# do calls node.get_children() for all childs get_name() and returns all names in an array +# +# if the value not a Object or not accesible be `func_name` the value is converted to `"n.a."` +# expecing null values +func extract_value(value): + if value == null: + return null + for func_name in func_names(): + if GdArrayTools.is_array_type(value): + var values := Array() + for element in Array(value): + values.append(_call_func(element, func_name)) + value = values + else: + value = _call_func(value, func_name) + var type := typeof(value) + if type == TYPE_STRING_NAME: + return str(value) + if type == TYPE_STRING and value == "n.a.": + return value + return value + + +func _call_func(value, func_name :String): + # for array types we need to call explicit by function name, using funcref is only supported for Objects + # TODO extend to all array functions + if GdArrayTools.is_array_type(value) and func_name == "empty": + return value.is_empty() + + if is_instance_valid(value): + # extract from function + if value.has_method(func_name): + var extract := Callable(value, func_name) + if extract.is_valid(): + return value.call(func_name) if args().is_empty() else value.callv(func_name, args()) + else: + # if no function exists than try to extract form parmeters + var parameter = value.get(func_name) + if parameter != null: + return parameter + # nothing found than return 'n.a.' + if GdUnitSettings.is_verbose_assert_warnings(): + push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) + return "n.a." diff --git a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd new file mode 100644 index 0000000..6a87c37 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd @@ -0,0 +1,13 @@ +class_name FloatFuzzer +extends Fuzzer + +var _from: float = 0 +var _to: float = 0 + +func _init(from: float, to: float): + assert(from <= to, "Invalid range!") + _from = from + _to = to + +func next_value() -> Variant: + return randf_range(_from, _to) diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Fuzzer.gd new file mode 100644 index 0000000..7cd6a58 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd @@ -0,0 +1,39 @@ +# Base interface for fuzz testing +# https://en.wikipedia.org/wiki/Fuzzing +class_name Fuzzer +extends RefCounted +# To run a test with a specific fuzzer you have to add defailt argument checked your test case +# all arguments are optional [] +# syntax: +# func test_foo([fuzzer = ], [fuzzer_iterations=], [fuzzer_seed=]) +# example: +# # runs the test 'test_foo' 10 times with a random int value generated by the IntFuzzer +# func test_foo(fuzzer = Fuzzers.randomInt(), fuzzer_iterations=10) +# +# # runs the test 'test_foo2' 1000 times as default with a random seed='101010101' +# func test_foo2(fuzzer = Fuzzers.randomInt(), fuzzer_seed=101010101) + +const ITERATION_DEFAULT_COUNT = 1000 +const ARGUMENT_FUZZER_INSTANCE := "fuzzer" +const ARGUMENT_ITERATIONS := "fuzzer_iterations" +const ARGUMENT_SEED := "fuzzer_seed" + +var _iteration_index :int = 0 +var _iteration_limit :int = ITERATION_DEFAULT_COUNT + + +# generates the next fuzz value +# needs to be implement +func next_value() -> Variant: + push_error("Invalid vall. Fuzzer not implemented 'next_value()'") + return null + + +# returns the current iteration index +func iteration_index() -> int: + return _iteration_index + + +# returns the amount of iterations where the fuzzer will be run +func iteration_limit() -> int: + return _iteration_limit diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd new file mode 100644 index 0000000..0235ee2 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd @@ -0,0 +1,32 @@ +class_name IntFuzzer +extends Fuzzer + +enum { + NORMAL, + EVEN, + ODD +} + +var _from :int = 0 +var _to : int = 0 +var _mode : int = NORMAL + + +func _init(from: int, to: int, mode :int = NORMAL): + assert(from <= to, "Invalid range!") + _from = from + _to = to + _mode = mode + + +func next_value() -> Variant: + var value := randi_range(_from, _to) + match _mode: + NORMAL: + return value + EVEN: + return int((value / 2.0) * 2) + ODD: + return int((value / 2.0) * 2 + 1) + _: + return value diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd new file mode 100644 index 0000000..40537e7 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd @@ -0,0 +1,64 @@ +class_name StringFuzzer +extends Fuzzer + + +const DEFAULT_CHARSET = "a-zA-Z0-9+-_" + +var _min_length :int +var _max_length :int +var _charset :PackedByteArray + + +func _init(min_length :int,max_length :int,pattern :String = DEFAULT_CHARSET): + assert(min_length>0 and min_length < max_length) + assert(not null or not pattern.is_empty()) + _min_length = min_length + _max_length = max_length + _charset = StringFuzzer.extract_charset(pattern) + + +static func extract_charset(pattern :String) -> PackedByteArray: + var reg := RegEx.new() + if reg.compile(pattern) != OK: + push_error("Invalid pattern to generate Strings! Use e.g 'a-zA-Z0-9+-_'") + return PackedByteArray() + + var charset := Array() + var char_before := -1 + var index := 0 + while index < pattern.length(): + var char_current := pattern.unicode_at(index) + # - range token at first or last pos? + if char_current == 45 and (index == 0 or index == pattern.length()-1): + charset.append(char_current) + index += 1 + continue + index += 1 + # range starts + if char_current == 45 and char_before != -1: + var char_next := pattern.unicode_at(index) + var characters := build_chars(char_before, char_next) + for character in characters: + charset.append(character) + char_before = -1 + index += 1 + continue + char_before = char_current + charset.append(char_current) + return PackedByteArray(charset) + + +static func build_chars(from :int, to :int) -> Array: + var characters := Array() + for character in range(from+1, to+1): + characters.append(character) + return characters + + +func next_value() -> Variant: + var value := PackedByteArray() + var max_char := len(_charset) + var length :int = max(_min_length, randi() % _max_length) + for i in length: + value.append(_charset[randi() % max_char]) + return value.get_string_from_ascii() diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd new file mode 100644 index 0000000..7dcbbe7 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd @@ -0,0 +1,18 @@ +class_name Vector2Fuzzer +extends Fuzzer + + +var _from :Vector2 +var _to : Vector2 + + +func _init(from: Vector2,to: Vector2): + assert(from <= to) #,"Invalid range!") + _from = from + _to = to + + +func next_value() -> Variant: + var x = randf_range(_from.x, _to.x) + var y = randf_range(_from.y, _to.y) + return Vector2(x, y) diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd new file mode 100644 index 0000000..ca3e718 --- /dev/null +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd @@ -0,0 +1,19 @@ +class_name Vector3Fuzzer +extends Fuzzer + + +var _from :Vector3 +var _to : Vector3 + + +func _init(from: Vector3,to: Vector3): + assert(from <= to) #,"Invalid range!") + _from = from + _to = to + + +func next_value() -> Variant: + var x = randf_range(_from.x, _to.x) + var y = randf_range(_from.y, _to.y) + var z = randf_range(_from.z, _to.z) + return Vector3(x, y, z) diff --git a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd new file mode 100644 index 0000000..0c0af7f --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd @@ -0,0 +1,11 @@ +class_name AnyArgumentMatcher +extends GdUnitArgumentMatcher + + +@warning_ignore("unused_parameter") +func is_match(value) -> bool: + return true + + +func _to_string() -> String: + return "any()" diff --git a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd new file mode 100644 index 0000000..04faae9 --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd @@ -0,0 +1,50 @@ +class_name AnyBuildInTypeArgumentMatcher +extends GdUnitArgumentMatcher + +var _type : PackedInt32Array = [] + + +func _init(type :PackedInt32Array): + _type = type + + +func is_match(value) -> bool: + return _type.has(typeof(value)) + + +func _to_string() -> String: + match _type[0]: + TYPE_BOOL: return "any_bool()" + TYPE_STRING, TYPE_STRING_NAME: return "any_string()" + TYPE_INT: return "any_int()" + TYPE_FLOAT: return "any_float()" + TYPE_COLOR: return "any_color()" + TYPE_VECTOR2: return "any_vector2()" if _type.size() == 1 else "any_vector()" + TYPE_VECTOR2I: return "any_vector2i()" + TYPE_VECTOR3: return "any_vector3()" + TYPE_VECTOR3I: return "any_vector3i()" + TYPE_VECTOR4: return "any_vector4()" + TYPE_VECTOR4I: return "any_vector4i()" + TYPE_RECT2: return "any_rect2()" + TYPE_RECT2I: return "any_rect2i()" + TYPE_PLANE: return "any_plane()" + TYPE_QUATERNION: return "any_quat()" + TYPE_AABB: return "any_aabb()" + TYPE_BASIS: return "any_basis()" + TYPE_TRANSFORM2D: return "any_transform_2d()" + TYPE_TRANSFORM3D: return "any_transform_3d()" + TYPE_NODE_PATH: return "any_node_path()" + TYPE_RID: return "any_rid()" + TYPE_OBJECT: return "any_object()" + TYPE_DICTIONARY: return "any_dictionary()" + TYPE_ARRAY: return "any_array()" + TYPE_PACKED_BYTE_ARRAY: return "any_packed_byte_array()" + TYPE_PACKED_INT32_ARRAY: return "any_packed_int32_array()" + TYPE_PACKED_INT64_ARRAY: return "any_packed_int64_array()" + TYPE_PACKED_FLOAT32_ARRAY: return "any_packed_float32_array()" + TYPE_PACKED_FLOAT64_ARRAY: return "any_packed_float64_array()" + TYPE_PACKED_STRING_ARRAY: return "any_packed_string_array()" + TYPE_PACKED_VECTOR2_ARRAY: return "any_packed_vector2_array()" + TYPE_PACKED_VECTOR3_ARRAY: return "any_packed_vector3_array()" + TYPE_PACKED_COLOR_ARRAY: return "any_packed_color_array()" + _: return "any()" diff --git a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd new file mode 100644 index 0000000..2cf0790 --- /dev/null +++ b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd @@ -0,0 +1,30 @@ +class_name AnyClazzArgumentMatcher +extends GdUnitArgumentMatcher + +var _clazz :Object + + +func _init(clazz :Object) -> void: + _clazz = clazz + + +func is_match(value :Variant) -> bool: + if typeof(value) != TYPE_OBJECT: + return false + if is_instance_valid(value) and GdObjects.is_script(_clazz): + return value.get_script() == _clazz + return is_instance_of(value, _clazz) + + +func _to_string() -> String: + if (_clazz as Object).is_class("GDScriptNativeClass"): + var instance :Object = _clazz.new() + var clazz_name := instance.get_class() + if not instance is RefCounted: + instance.free() + return "any_class(<"+clazz_name+">)"; + if _clazz is GDScript: + var result := GdObjects.extract_class_name(_clazz) + if result.is_success(): + return "any_class(<"+ result.value() + ">)" + return "any_class()" diff --git a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd new file mode 100644 index 0000000..d2a0cef --- /dev/null +++ b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd @@ -0,0 +1,22 @@ +class_name ChainedArgumentMatcher +extends GdUnitArgumentMatcher + +var _matchers :Array + + +func _init(matchers :Array): + _matchers = matchers + + +func is_match(arguments :Variant) -> bool: + var arg_array := arguments as Array + if arg_array.size() != _matchers.size(): + return false + + for index in arg_array.size(): + var arg :Variant = arg_array[index] + var matcher = _matchers[index] as GdUnitArgumentMatcher + + if not matcher.is_match(arg): + return false + return true diff --git a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd new file mode 100644 index 0000000..2adc75c --- /dev/null +++ b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd @@ -0,0 +1,22 @@ +class_name EqualsArgumentMatcher +extends GdUnitArgumentMatcher + +var _current +var _auto_deep_check_mode + + +func _init(current, auto_deep_check_mode := false): + _current = current + _auto_deep_check_mode = auto_deep_check_mode + + +func is_match(value) -> bool: + var case_sensitive_check := true + return GdObjects.equals(_current, value, case_sensitive_check, compare_mode(value)) + + +func compare_mode(value) -> GdObjects.COMPARE_MODE: + if _auto_deep_check_mode and is_instance_valid(value): + # we do deep check on all InputEvent's + return GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST if value is InputEvent else GdObjects.COMPARE_MODE.OBJECT_REFERENCE + return GdObjects.COMPARE_MODE.OBJECT_REFERENCE diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd new file mode 100644 index 0000000..aa43b80 --- /dev/null +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd @@ -0,0 +1,8 @@ +## The base class of all argument matchers +class_name GdUnitArgumentMatcher +extends RefCounted + + +@warning_ignore("unused_parameter") +func is_match(value :Variant) -> bool: + return true diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd new file mode 100644 index 0000000..ddd58f1 --- /dev/null +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd @@ -0,0 +1,32 @@ +class_name GdUnitArgumentMatchers +extends RefCounted + +const TYPE_ANY = TYPE_MAX + 100 + + +static func to_matcher(arguments :Array[Variant], auto_deep_check_mode := false) -> ChainedArgumentMatcher: + var matchers :Array[Variant] = [] + for arg in arguments: + # argument is already a matcher + if arg is GdUnitArgumentMatcher: + matchers.append(arg) + else: + # pass argument into equals matcher + matchers.append(EqualsArgumentMatcher.new(arg, auto_deep_check_mode)) + return ChainedArgumentMatcher.new(matchers) + + +static func any() -> GdUnitArgumentMatcher: + return AnyArgumentMatcher.new() + + +static func by_type(type :int) -> GdUnitArgumentMatcher: + return AnyBuildInTypeArgumentMatcher.new([type]) + + +static func by_types(types :PackedInt32Array) -> GdUnitArgumentMatcher: + return AnyBuildInTypeArgumentMatcher.new(types) + + +static func any_class(clazz :Object) -> GdUnitArgumentMatcher: + return AnyClazzArgumentMatcher.new(clazz) diff --git a/addons/gdUnit4/src/mocking/GdUnitMock.gd b/addons/gdUnit4/src/mocking/GdUnitMock.gd new file mode 100644 index 0000000..bb50e5e --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMock.gd @@ -0,0 +1,40 @@ +class_name GdUnitMock +extends RefCounted + +## do call the real implementation +const CALL_REAL_FUNC = "CALL_REAL_FUNC" +## do return a default value for primitive types or null +const RETURN_DEFAULTS = "RETURN_DEFAULTS" +## do return a default value for primitive types and a fully mocked value for Object types +## builds full deep mocked object +const RETURN_DEEP_STUB = "RETURN_DEEP_STUB" + +var _value :Variant + + +func _init(value :Variant) -> void: + _value = value + + +## Selects the mock to work on, used in combination with [method GdUnitTestSuite.do_return][br] +## Example: +## [codeblock] +## do_return(false).on(myMock).is_selected() +## [/codeblock] +func on(obj :Object) -> Object: + if not GdUnitMock._is_mock_or_spy( obj, "__do_return"): + return obj + return obj.__do_return(_value) + + +## [color=yellow]`checked` is obsolete, use `on` instead [/color] +func checked(obj :Object) -> Object: + push_warning("Using a deprecated function 'checked' use `on` instead") + return on(obj) + + +static func _is_mock_or_spy(obj :Object, func_sig :String) -> bool: + if obj is GDScript and not obj.get_script().has_script_method(func_sig): + push_error("Error: You try to use a non mock or spy!") + return false + return true diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd new file mode 100644 index 0000000..3270e38 --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd @@ -0,0 +1,167 @@ +class_name GdUnitMockBuilder +extends GdUnitClassDoubler + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const MOCK_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/mocking/GdUnitMockImpl.gd") + + +static func is_push_errors() -> bool: + return GdUnitSettings.is_report_push_errors() + + +static func build(clazz, mock_mode :String, debug_write := false) -> Object: + var push_errors := is_push_errors() + if not is_mockable(clazz, push_errors): + return null + # mocking a scene? + if GdObjects.is_scene(clazz): + return mock_on_scene(clazz as PackedScene, debug_write) + elif typeof(clazz) == TYPE_STRING and clazz.ends_with(".tscn"): + return mock_on_scene(load(clazz), debug_write) + # mocking a script + var instance := create_instance(clazz) + var mock := mock_on_script(instance, clazz, [ "get_script"], debug_write) + if not instance is RefCounted: + instance.free() + if mock == null: + return null + var mock_instance = mock.new() + mock_instance.__set_script(mock) + mock_instance.__set_singleton() + mock_instance.__set_mode(mock_mode) + return register_auto_free(mock_instance) + + +static func create_instance(clazz) -> Object: + if typeof(clazz) == TYPE_OBJECT and (clazz as Object).is_class("GDScriptNativeClass"): + return clazz.new() + elif (clazz is GDScript) || (typeof(clazz) == TYPE_STRING and clazz.ends_with(".gd")): + var script :GDScript = null + if clazz is GDScript: + script = clazz + else: + script = load(clazz) + + var args = GdObjects.build_function_default_arguments(script, "_init") + return script.callv("new", args) + elif typeof(clazz) == TYPE_STRING and ClassDB.can_instantiate(clazz): + return ClassDB.instantiate(clazz) + push_error("Can't create a mock validation instance from class: `%s`" % clazz) + return null + + +static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Object: + var push_errors := is_push_errors() + if not scene.can_instantiate(): + if push_errors: + push_error("Can't instanciate scene '%s'" % scene.resource_path) + return null + var scene_instance = scene.instantiate() + # we can only mock checked a scene with attached script + if scene_instance.get_script() == null: + if push_errors: + push_error("Can't create a mockable instance for a scene without script '%s'" % scene.resource_path) + GdUnitTools.free_instance(scene_instance) + return null + + var script_path = scene_instance.get_script().get_path() + var mock = mock_on_script(scene_instance, script_path, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write) + if mock == null: + return null + scene_instance.set_script(mock) + scene_instance.__set_singleton() + scene_instance.__set_mode(GdUnitMock.CALL_REAL_FUNC) + return register_auto_free(scene_instance) + + +static func get_class_info(clazz :Variant) -> Dictionary: + var clazz_name :String = GdObjects.extract_class_name(clazz).value() + var clazz_path := GdObjects.extract_class_path(clazz) + return { + "class_name" : clazz_name, + "class_path" : clazz_path + } + + +static func mock_on_script(instance :Object, clazz :Variant, function_excludes :PackedStringArray, debug_write :bool) -> GDScript: + var push_errors := is_push_errors() + var function_doubler := GdUnitMockFunctionDoubler.new(push_errors) + var class_info := get_class_info(clazz) + var lines := load_template(MOCK_TEMPLATE.source_code, class_info, instance) + + var clazz_name :String = class_info.get("class_name") + var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) + lines += double_functions(instance, clazz_name, clazz_path, function_doubler, function_excludes) + + var mock := GDScript.new() + mock.source_code = "\n".join(lines) + mock.resource_name = "Mock%s.gd" % clazz_name + mock.resource_path = GdUnitFileAccess.create_temp_dir("mock") + "/Mock%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] + + if debug_write: + DirAccess.remove_absolute(mock.resource_path) + ResourceSaver.save(mock, mock.resource_path) + var error = mock.reload(true) + if error != OK: + push_error("Critical!!!, MockBuilder error, please contact the developer.") + return null + return mock + + +static func is_mockable(clazz :Variant, push_errors :bool=false) -> bool: + var clazz_type := typeof(clazz) + if clazz_type != TYPE_OBJECT and clazz_type != TYPE_STRING: + push_error("Invalid clazz type is used") + return false + # is PackedScene + if GdObjects.is_scene(clazz): + return true + if GdObjects.is_native_class(clazz): + return true + # verify class type + if GdObjects.is_object(clazz): + if GdObjects.is_instance(clazz): + if push_errors: + push_error("It is not allowed to mock an instance '%s', use class name instead, Read 'Mocker' documentation for details" % clazz) + return false + + if not GdObjects.can_be_instantiate(clazz): + if push_errors: + push_error("Can't create a mockable instance for class '%s'" % clazz) + return false + return true + # verify by class name checked registered classes + var clazz_name := clazz as String + if ClassDB.class_exists(clazz_name): + if Engine.has_singleton(clazz_name): + if push_errors: + push_error("Mocking a singelton class '%s' is not allowed! Read 'Mocker' documentation for details" % clazz_name) + return false + if not ClassDB.can_instantiate(clazz_name): + if push_errors: + push_error("Mocking class '%s' is not allowed it cannot be instantiated!" % clazz_name) + return false + # exclude classes where name starts with a underscore + if clazz_name.find("_") == 0: + if push_errors: + push_error("Can't create a mockable instance for protected class '%s'" % clazz_name) + return false + return true + # at least try to load as a script + var clazz_path := clazz_name + if not FileAccess.file_exists(clazz_path): + if push_errors: + push_error("'%s' cannot be mocked for the specified resource path, the resource does not exist" % clazz_name) + return false + # finally verify is a script resource + var resource = load(clazz_path) + if resource == null: + if push_errors: + push_error("'%s' cannot be mocked the script cannot be loaded." % clazz_name) + return false + # finally check is extending from script + return GdObjects.is_script(resource) or GdObjects.is_scene(resource) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd new file mode 100644 index 0000000..60249bf --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd @@ -0,0 +1,85 @@ +class_name GdUnitMockFunctionDoubler +extends GdFunctionDoubler + + +const TEMPLATE_FUNC_WITH_RETURN_VALUE = """ + var args :Array = ["$(func_name)", $(arguments)] + + if $(instance)__is_prepare_return_value(): + $(instance)__save_function_return_value(args) + return ${default_return_value} + if $(instance)__is_verify_interactions(): + $(instance)__verify_interactions(args) + return ${default_return_value} + else: + $(instance)__save_function_interaction(args) + + if $(instance)__do_call_real_func("$(func_name)", args): + return $(await)super($(arguments)) + return $(instance)__get_mocked_return_value_or_default(args, ${default_return_value}) + +""" + + +const TEMPLATE_FUNC_WITH_RETURN_VOID = """ + var args :Array = ["$(func_name)", $(arguments)] + + if $(instance)__is_prepare_return_value(): + if $(push_errors): + push_error(\"Mocking a void function '$(func_name)() -> void:' is not allowed.\") + return + if $(instance)__is_verify_interactions(): + $(instance)__verify_interactions(args) + return + else: + $(instance)__save_function_interaction(args) + + if $(instance)__do_call_real_func("$(func_name)"): + $(await)super($(arguments)) + +""" + + +const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ + var varargs :Array = __filter_vargs([$(varargs)]) + var args :Array = ["$(func_name)", $(arguments)] + varargs + + if $(instance)__is_prepare_return_value(): + if $(push_errors): + push_error(\"Mocking a void function '$(func_name)() -> void:' is not allowed.\") + $(instance)__save_function_return_value(args) + return ${default_return_value} + if $(instance)__is_verify_interactions(): + $(instance)__verify_interactions(args) + return ${default_return_value} + else: + $(instance)__save_function_interaction(args) + + if $(instance)__do_call_real_func("$(func_name)", args): + match varargs.size(): + 0: return $(await)super($(arguments)) + 1: return $(await)super($(arguments), varargs[0]) + 2: return $(await)super($(arguments), varargs[0], varargs[1]) + 3: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2]) + 4: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3]) + 5: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) + 6: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) + 7: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) + 8: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) + 9: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) + 10: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) + return __get_mocked_return_value_or_default(args, ${default_return_value}) + +""" + + +func _init(push_errors :bool = false): + super._init(push_errors) + + +func get_template(return_type :Variant, is_vararg :bool) -> String: + if is_vararg: + return TEMPLATE_FUNC_VARARG_RETURN_VALUE + if return_type is StringName: + return TEMPLATE_FUNC_WITH_RETURN_VALUE + return TEMPLATE_FUNC_WITH_RETURN_VOID if (return_type == TYPE_NIL or return_type == GdObjects.TYPE_VOID) else TEMPLATE_FUNC_WITH_RETURN_VALUE diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd new file mode 100644 index 0000000..cbeb782 --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd @@ -0,0 +1,140 @@ + +################################################################################ +# internal mocking stuff +################################################################################ +const __INSTANCE_ID = "${instance_id}" +const __SOURCE_CLASS = "${source_class}" + +var __mock_working_mode := GdUnitMock.RETURN_DEFAULTS +var __excluded_methods :PackedStringArray = [] +var __do_return_value :Variant = null +var __prepare_return_value := false + +#{ = { +# = +# } +#} +var __mocked_return_values := Dictionary() + + +static func __instance() -> Object: + return Engine.get_meta(__INSTANCE_ID) + + +func _notification(what :int) -> void: + if what == NOTIFICATION_PREDELETE: + if Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) + + +func __instance_id() -> String: + return __INSTANCE_ID + + +func __set_singleton() -> void: + # store self need to mock static functions + Engine.set_meta(__INSTANCE_ID, self) + + +func __release_double() -> void: + # we need to release the self reference manually to prevent orphan nodes + Engine.remove_meta(__INSTANCE_ID) + + +func __is_prepare_return_value() -> bool: + return __prepare_return_value + + +func __sort_by_argument_matcher(__left_args :Array, __right_args :Array) -> bool: + for __index in __left_args.size(): + var __larg :Variant = __left_args[__index] + if __larg is GdUnitArgumentMatcher: + return false + return true + + +# we need to sort by matcher arguments so that they are all at the end of the list +func __sort_dictionary(__unsorted_args :Dictionary) -> Dictionary: + # only need to sort if contains more than one entry + if __unsorted_args.size() <= 1: + return __unsorted_args + var __sorted_args := __unsorted_args.keys() + __sorted_args.sort_custom(__sort_by_argument_matcher) + var __sorted_result := {} + for __index in __sorted_args.size(): + var key :Variant = __sorted_args[__index] + __sorted_result[key] = __unsorted_args[key] + return __sorted_result + + +func __save_function_return_value(__fuction_args :Array) -> void: + var __func_name :String = __fuction_args[0] + var __func_args :Array = __fuction_args.slice(1) + var __mocked_return_value_by_args :Dictionary = __mocked_return_values.get(__func_name, {}) + __mocked_return_value_by_args[__func_args] = __do_return_value + __mocked_return_values[__func_name] = __sort_dictionary(__mocked_return_value_by_args) + __do_return_value = null + __prepare_return_value = false + + +func __is_mocked_args_match(__func_args :Array, __mocked_args :Array) -> bool: + var __is_matching := false + for __index in __mocked_args.size(): + var __fuction_args :Variant = __mocked_args[__index] + if __func_args.size() != __fuction_args.size(): + continue + __is_matching = true + for __arg_index in __func_args.size(): + var __func_arg :Variant = __func_args[__arg_index] + var __mock_arg :Variant = __fuction_args[__arg_index] + if __mock_arg is GdUnitArgumentMatcher: + __is_matching = __is_matching and __mock_arg.is_match(__func_arg) + else: + __is_matching = __is_matching and typeof(__func_arg) == typeof(__mock_arg) and __func_arg == __mock_arg + if not __is_matching: + break + if __is_matching: + break + return __is_matching + + +func __get_mocked_return_value_or_default(__fuction_args :Array, __default_return_value :Variant) -> Variant: + var __func_name :String = __fuction_args[0] + if not __mocked_return_values.has(__func_name): + return __default_return_value + var __func_args :Array = __fuction_args.slice(1) + var __mocked_args :Array = __mocked_return_values.get(__func_name).keys() + for __index in __mocked_args.size(): + var __margs :Variant = __mocked_args[__index] + if __is_mocked_args_match(__func_args, [__margs]): + return __mocked_return_values[__func_name][__margs] + return __default_return_value + + +func __set_script(__script :GDScript) -> void: + super.set_script(__script) + + +func __set_mode(mock_working_mode :String) -> Object: + __mock_working_mode = mock_working_mode + return self + + +func __do_call_real_func(__func_name :String, __func_args := []) -> bool: + var __is_call_real_func := __mock_working_mode == GdUnitMock.CALL_REAL_FUNC and not __excluded_methods.has(__func_name) + # do not call real funcions for mocked functions + if __is_call_real_func and __mocked_return_values.has(__func_name): + var __fuction_args :Array = __func_args.slice(1) + var __mocked_args :Array = __mocked_return_values.get(__func_name).keys() + return not __is_mocked_args_match(__fuction_args, __mocked_args) + return __is_call_real_func + + +func __exclude_method_call(exluded_methods :PackedStringArray) -> void: + __excluded_methods.append_array(exluded_methods) + + +func __do_return(mock_do_return_value :Variant) -> Object: + __do_return_value = mock_do_return_value + __prepare_return_value = true + return self diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd new file mode 100644 index 0000000..f574147 --- /dev/null +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd @@ -0,0 +1,59 @@ +extends RefCounted +class_name ErrorLogEntry + + +enum TYPE { + SCRIPT_ERROR, + PUSH_ERROR, + PUSH_WARNING +} + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const PATTERN_SCRIPT_ERROR := "USER SCRIPT ERROR:" +const PATTERN_PUSH_ERROR := "USER ERROR:" +const PATTERN_PUSH_WARNING := "USER WARNING:" + + +var _type :TYPE +var _line :int +var _message :String +var _details :String + + +func _init(type :TYPE, line :int, message :String, details :String): + _type = type + _line = line + _message = message + _details = details + + +static func extract_push_warning(records :PackedStringArray, index :int) -> ErrorLogEntry: + return _extract(records, index, TYPE.PUSH_WARNING, PATTERN_PUSH_WARNING) + + +static func extract_push_error(records :PackedStringArray, index :int) -> ErrorLogEntry: + return _extract(records, index, TYPE.PUSH_ERROR, PATTERN_PUSH_ERROR) + + +static func extract_error(records :PackedStringArray, index :int) -> ErrorLogEntry: + return _extract(records, index, TYPE.SCRIPT_ERROR, PATTERN_SCRIPT_ERROR) + + +static func _extract(records :PackedStringArray, index :int, type :TYPE, pattern :String) -> ErrorLogEntry: + var message := records[index] + if message.contains(pattern): + var error := message.replace(pattern, "").strip_edges() + var details := records[index+1].strip_edges() + var line := _parse_error_line_number(details) + return ErrorLogEntry.new(type, line, error, details) + return null + + +static func _parse_error_line_number(record :String) -> int: + var regex := GdUnitSingleton.instance("error_line_regex", func() : return GdUnitTools.to_regex("at: .*res://.*:(\\d+)")) as RegEx + var matches := regex.search(record) + if matches != null: + return matches.get_string(1).to_int() + return -1 diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd new file mode 100644 index 0000000..8ec2e63 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd @@ -0,0 +1,24 @@ +# GdUnit Monitoring Base Class +class_name GdUnitMonitor +extends RefCounted + +var _id :String + +# constructs new Monitor with given id +func _init(p_id :String): + _id = p_id + + +# Returns the id of the monitor to uniqe identify +func id() -> String: + return _id + + +# starts monitoring +func start(): + pass + + +# stops monitoring +func stop(): + pass diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd new file mode 100644 index 0000000..9bf76e4 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd @@ -0,0 +1,27 @@ +class_name GdUnitOrphanNodesMonitor +extends GdUnitMonitor + +var _initial_count := 0 +var _orphan_count := 0 +var _orphan_detection_enabled :bool + + +func _init(name :String = ""): + super("OrphanNodesMonitor:" + name) + _orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + + +func start(): + _initial_count = _orphans() + + +func stop(): + _orphan_count = max(0, _orphans() - _initial_count) + + +func _orphans() -> int: + return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) as int + + +func orphan_nodes() -> int: + return _orphan_count if _orphan_detection_enabled else 0 diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd new file mode 100644 index 0000000..a2f8f14 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd @@ -0,0 +1,84 @@ +class_name GodotGdErrorMonitor +extends GdUnitMonitor + +var _godot_log_file :String +var _eof :int +var _report_enabled := false +var _entries: Array[ErrorLogEntry] = [] + + +func _init(): + super("GodotGdErrorMonitor") + _godot_log_file = GdUnitSettings.get_log_path() + _report_enabled = _is_reporting_enabled() + + +func start(): + var file = FileAccess.open(_godot_log_file, FileAccess.READ) + if file: + file.seek_end(0) + _eof = file.get_length() + + +func stop(): + pass + + +func to_reports() -> Array[GdUnitReport]: + var reports_ :Array[GdUnitReport] = [] + if _report_enabled: + reports_.assign(_entries.map(_to_report)) + return reports_ + + +static func _to_report(errorLog :ErrorLogEntry) -> GdUnitReport: + var failure := "%s\n\t%s\n%s %s" % [ + GdAssertMessages._error("Godot Runtime Error !"), + GdAssertMessages._colored_value(errorLog._details), + GdAssertMessages._error("Error:"), + GdAssertMessages._colored_value(errorLog._message)] + return GdUnitReport.new().create(GdUnitReport.ABORT, errorLog._line, failure) + + +func scan(force_collect_reports := false) -> Array[ErrorLogEntry]: + await Engine.get_main_loop().process_frame + await Engine.get_main_loop().physics_frame + _entries.append_array(_collect_log_entries(force_collect_reports)) + return _entries + + +func erase_log_entry(entry :ErrorLogEntry) -> void: + _entries.erase(entry) + + +func _collect_log_entries(force_collect_reports :bool) -> Array[ErrorLogEntry]: + var file = FileAccess.open(_godot_log_file, FileAccess.READ) + file.seek(_eof) + var records := PackedStringArray() + while not file.eof_reached(): + records.append(file.get_line()) + file.seek_end(0) + _eof = file.get_length() + var log_entries :Array[ErrorLogEntry]= [] + var is_report_errors := force_collect_reports or _is_report_push_errors() + var is_report_script_errors := force_collect_reports or _is_report_script_errors() + for index in records.size(): + if force_collect_reports: + log_entries.append(ErrorLogEntry.extract_push_warning(records, index)) + if is_report_errors: + log_entries.append(ErrorLogEntry.extract_push_error(records, index)) + if is_report_script_errors: + log_entries.append(ErrorLogEntry.extract_error(records, index)) + return log_entries.filter(func(value): return value != null ) + + +func _is_reporting_enabled() -> bool: + return _is_report_script_errors() or _is_report_push_errors() + + +func _is_report_push_errors() -> bool: + return GdUnitSettings.is_report_push_errors() + + +func _is_report_script_errors() -> bool: + return GdUnitSettings.is_report_script_errors() diff --git a/addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs b/addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs new file mode 100644 index 0000000..ec07ff4 --- /dev/null +++ b/addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; +using System.Linq; + +using Godot; +using Godot.Collections; +using GdUnit4; + + +// GdUnit4 GDScript - C# API wrapper +public partial class GdUnit4CSharpApi : RefCounted +{ + private static Type? apiType; + + private static Type GetApiType() + { + if (apiType == null) + { + var assembly = Assembly.Load("gdUnit4Api"); + apiType = GdUnit4NetVersion() < new Version(4, 2, 2) ? + assembly.GetType("GdUnit4.GdUnit4MonoAPI") : + assembly.GetType("GdUnit4.GdUnit4NetAPI"); + Godot.GD.PrintS($"GdUnit4CSharpApi type:{apiType} loaded."); + } + return apiType!; + } + + private static Version GdUnit4NetVersion() + { + var assembly = Assembly.Load("gdUnit4Api"); + return assembly.GetName().Version!; + } + + private static T InvokeApiMethod(string methodName, params object[] args) + { + var method = GetApiType().GetMethod(methodName)!; + return (T)method.Invoke(null, args)!; + } + + public static string Version() => GdUnit4NetVersion().ToString(); + + public static bool IsTestSuite(string classPath) => InvokeApiMethod("IsTestSuite", classPath); + + public static RefCounted Executor(Node listener) => InvokeApiMethod("Executor", listener); + + public static CsNode? ParseTestSuite(string classPath) => InvokeApiMethod("ParseTestSuite", classPath); + + public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) => + InvokeApiMethod("CreateTestSuite", sourcePath, lineNumber, testSuitePath); +} diff --git a/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd b/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd new file mode 100644 index 0000000..29c4b41 --- /dev/null +++ b/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd @@ -0,0 +1,64 @@ +extends RefCounted +class_name GdUnit4CSharpApiLoader + + +static func instance() -> Object: + return GdUnitSingleton.instance("GdUnit4CSharpApi", func() -> Object: + if not GdUnit4CSharpApiLoader.is_mono_supported(): + return null + return load("res://addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs") + ) + + +static func is_engine_version_supported(engine_version :int = Engine.get_version_info().hex) -> bool: + return engine_version >= 0x40200 + + +# test is Godot mono running +static func is_mono_supported() -> bool: + return ClassDB.class_exists("CSharpScript") and is_engine_version_supported() + + +static func version() -> String: + if not GdUnit4CSharpApiLoader.is_mono_supported(): + return "unknown" + return instance().Version() + + +static func create_test_suite(source_path :String, line_number :int, test_suite_path :String) -> GdUnitResult: + if not GdUnit4CSharpApiLoader.is_mono_supported(): + return GdUnitResult.error("Can't create test suite. No C# support found.") + var result := instance().CreateTestSuite(source_path, line_number, test_suite_path) as Dictionary + if result.has("error"): + return GdUnitResult.error(result.get("error")) + return GdUnitResult.success(result) + + +static func is_test_suite(resource_path :String) -> bool: + if not is_csharp_file(resource_path) or not GdUnit4CSharpApiLoader.is_mono_supported(): + return false + + if resource_path.is_empty(): + if GdUnitSettings.is_report_push_errors(): + push_error("Can't create test suite. Missing resource path.") + return false + return instance().IsTestSuite(resource_path) + + +static func parse_test_suite(source_path :String) -> Node: + if not GdUnit4CSharpApiLoader.is_mono_supported(): + if GdUnitSettings.is_report_push_errors(): + push_error("Can't create test suite. No c# support found.") + return null + return instance().ParseTestSuite(source_path) + + +static func create_executor(listener :Node) -> RefCounted: + if not GdUnit4CSharpApiLoader.is_mono_supported(): + return null + return instance().Executor(listener) + + +static func is_csharp_file(resource_path :String) -> bool: + var ext := resource_path.get_extension() + return ext == "cs" and GdUnit4CSharpApiLoader.is_mono_supported() diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd b/addons/gdUnit4/src/network/GdUnitServer.gd new file mode 100644 index 0000000..1a0ae99 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServer.gd @@ -0,0 +1,41 @@ +@tool +extends Node + +@onready var _server :GdUnitTcpServer = $TcpServer + + +func _ready(): + var result := _server.start() + if result.is_error(): + push_error(result.error_message()) + return + var server_port :int = result.value() + Engine.set_meta("gdunit_server_port", server_port) + _server.client_connected.connect(_on_client_connected) + _server.client_disconnected.connect(_on_client_disconnected) + _server.rpc_data.connect(_receive_rpc_data) + GdUnitCommandHandler.instance().gdunit_runner_stop.connect(_on_gdunit_runner_stop) + + +func _on_client_connected(client_id :int) -> void: + GdUnitSignals.instance().gdunit_client_connected.emit(client_id) + + +func _on_client_disconnected(client_id :int) -> void: + GdUnitSignals.instance().gdunit_client_disconnected.emit(client_id) + + +func _on_gdunit_runner_stop(client_id :int): + if _server: + _server.disconnect_client(client_id) + + +func _receive_rpc_data(p_rpc :RPC) -> void: + if p_rpc is RPCMessage: + GdUnitSignals.instance().gdunit_message.emit(p_rpc.message()) + return + if p_rpc is RPCGdUnitEvent: + GdUnitSignals.instance().gdunit_event.emit(p_rpc.event()) + return + if p_rpc is RPCGdUnitTestSuite: + GdUnitSignals.instance().gdunit_add_test_suite.emit(p_rpc.dto()) diff --git a/addons/gdUnit4/src/network/GdUnitServer.tscn b/addons/gdUnit4/src/network/GdUnitServer.tscn new file mode 100644 index 0000000..4c7645c --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServer.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/gdUnit4/src/network/GdUnitServer.gd" type="Script" id=1] +[ext_resource path="res://addons/gdUnit4/src/network/GdUnitTcpServer.gd" type="Script" id=2] + +[node name="Control" type="Node"] +script = ExtResource( 1 ) + +[node name="TcpServer" type="Node" parent="."] +script = ExtResource( 2 ) diff --git a/addons/gdUnit4/src/network/GdUnitServerConstants.gd b/addons/gdUnit4/src/network/GdUnitServerConstants.gd new file mode 100644 index 0000000..d31eee7 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitServerConstants.gd @@ -0,0 +1,6 @@ +class_name GdUnitServerConstants +extends RefCounted + +const DEFAULT_SERVER_START_RETRY_TIMES :int = 5 +const GD_TEST_SERVER_PORT :int = 31002 +const JSON_RESPONSE_DELIMITER :String = "<>" diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd b/addons/gdUnit4/src/network/GdUnitTask.gd new file mode 100644 index 0000000..ee48bee --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTask.gd @@ -0,0 +1,25 @@ +class_name GdUnitTask +extends RefCounted + +const TASK_NAME = "task_name" +const TASK_ARGS = "task_args" + +var _task_name :String +var _fref :Callable + + +func _init(task_name :String,instance :Object,func_name :String): + _task_name = task_name + if not instance.has_method(func_name): + push_error("Can't create GdUnitTask, Invalid func name '%s' for instance '%s'" % [instance, func_name]) + _fref = Callable(instance, func_name) + + +func name() -> String: + return _task_name + + +func execute(args :Array) -> GdUnitResult: + if args.is_empty(): + return _fref.call() + return _fref.callv(args) diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd b/addons/gdUnit4/src/network/GdUnitTcpClient.gd new file mode 100644 index 0000000..1a9b66e --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd @@ -0,0 +1,137 @@ +class_name GdUnitTcpClient +extends Node + +signal connection_succeeded(message) +signal connection_failed(message) + +var _timer :Timer + +var _host :String +var _port :int +var _client_id :int +var _connected :bool +var _stream :StreamPeerTCP + + +func _ready(): + _connected = false + _stream = StreamPeerTCP.new() + _stream.set_big_endian(true) + _timer = Timer.new() + add_child(_timer) + _timer.set_one_shot(true) + _timer.connect('timeout', Callable(self, '_connecting_timeout')) + + +func stop() -> void: + console("Client: disconnect from server") + if _stream != null: + rpc_send(RPCClientDisconnect.new().with_id(_client_id)) + if _stream != null: + _stream.disconnect_from_host() + _connected = false + + +func start(host :String, port :int) -> GdUnitResult: + _host = host + _port = port + if _connected: + return GdUnitResult.warn("Client already connected ... %s:%d" % [_host, _port]) + + # Connect client to server + if _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + var err := _stream.connect_to_host(host, port) + #prints("connect_to_host", host, port, err) + if err != OK: + return GdUnitResult.error("GdUnit3: Can't establish client, error code: %s" % err) + return GdUnitResult.success("GdUnit3: Client connected checked port %d" % port) + + +func _process(_delta): + match _stream.get_status(): + StreamPeerTCP.STATUS_NONE: + return + + StreamPeerTCP.STATUS_CONNECTING: + set_process(false) + # wait until client is connected to server + for retry in 10: + _stream.poll() + console("wait to connect ..") + if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTING: + await get_tree().create_timer(0.500).timeout + if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED: + set_process(true) + return + set_process(true) + _stream.disconnect_from_host() + console("connection failed") + emit_signal("connection_failed", "Connect to TCP Server %s:%d faild!" % [_host, _port]) + + StreamPeerTCP.STATUS_CONNECTED: + if not _connected: + var rpc_ = null + set_process(false) + while rpc_ == null: + await get_tree().create_timer(0.500).timeout + rpc_ = rpc_receive() + set_process(true) + _client_id = rpc_.client_id() + console("Connected to Server: %d" % _client_id) + emit_signal("connection_succeeded", "Connect to TCP Server %s:%d success." % [_host, _port]) + _connected = true + process_rpc() + + StreamPeerTCP.STATUS_ERROR: + console("connection failed") + _stream.disconnect_from_host() + emit_signal("connection_failed", "Connect to TCP Server %s:%d faild!" % [_host, _port]) + return + + +func is_client_connected() -> bool: + return _connected + + +func process_rpc() -> void: + if _stream.get_available_bytes() > 0: + var rpc_ = rpc_receive() + if rpc_ is RPCClientDisconnect: + stop() + + +func rpc_send(p_rpc :RPC) -> void: + if _stream != null: + var data := GdUnitServerConstants.JSON_RESPONSE_DELIMITER + p_rpc.serialize() + GdUnitServerConstants.JSON_RESPONSE_DELIMITER + _stream.put_data(data.to_ascii_buffer()) + + +func rpc_receive() -> RPC: + if _stream != null: + while _stream.get_available_bytes() > 0: + var available_bytes := _stream.get_available_bytes() + var data := _stream.get_data(available_bytes) + var received_data := data[1] as PackedByteArray + # data send by Godot has this magic header of 12 bytes + var header := Array(received_data.slice(0, 4)) + if header == [0, 0, 0, 124]: + received_data = received_data.slice(12, available_bytes) + var decoded := received_data.get_string_from_ascii() + if decoded == "": + #prints("decoded is empty", available_bytes, received_data.get_string_from_ascii()) + return null + return RPC.deserialize(decoded) + return null + + +func console(message :String) -> void: + prints("TCP Client:", message) + pass + + +func _on_connection_failed(message :String): + console("connection faild: " + message) + + +func _on_connection_succeeded(message :String): + console("connected: " + message) diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd new file mode 100644 index 0000000..8cf28b5 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -0,0 +1,162 @@ +@tool +class_name GdUnitTcpServer +extends Node + +signal client_connected(client_id) +signal client_disconnected(client_id) +signal rpc_data(rpc_data) + +var _server :TCPServer + + +class TcpConnection extends Node: + var _id :int + var _stream + var _readBuffer :String = "" + + + func _init(p_server): + #assert(p_server is TCPServer) + _stream = p_server.take_connection() + _stream.set_big_endian(true) + _id = _stream.get_instance_id() + rpc_send(RPCClientConnect.new().with_id(_id)) + + + func _ready(): + server().client_connected.emit(_id) + + + func close() -> void: + rpc_send(RPCClientDisconnect.new().with_id(_id)) + server().client_disconnected.emit(_id) + _stream.disconnect_from_host() + _readBuffer = "" + + + func id() -> int: + return _id + + + func server() -> GdUnitTcpServer: + return get_parent() + + + func rpc_send(p_rpc :RPC) -> void: + _stream.put_var(p_rpc.serialize(), true) + + + func _process(_delta): + if _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return + receive_packages() + + + func receive_packages() -> void: + var available_bytes = _stream.get_available_bytes() + if available_bytes > 0: + var partial_data = _stream.get_partial_data(available_bytes) + # Check for read error. + if partial_data[0] != OK: + push_error("Error getting data from stream: %s " % partial_data[0]) + return + else: + var received_data := partial_data[1] as PackedByteArray + for package in _read_next_data_packages(received_data): + var rpc_ = RPC.deserialize(package) + if rpc_ is RPCClientDisconnect: + close() + server().rpc_data.emit(rpc_) + + + func _read_next_data_packages(data_package :PackedByteArray) -> PackedStringArray: + _readBuffer += data_package.get_string_from_ascii() + var json_array := _readBuffer.split(GdUnitServerConstants.JSON_RESPONSE_DELIMITER) + # We need to check if the current data is terminated by the delemiter (data packets can be split unspecifically). + # If not, store the last part in _readBuffer and complete it on the next data packet that is received + if not _readBuffer.ends_with(GdUnitServerConstants.JSON_RESPONSE_DELIMITER): + _readBuffer = json_array[-1] + json_array.remove_at(json_array.size()-1) + else: + # Reset the buffer if a completely terminated packet was received + _readBuffer = "" + # remove empty packages + for index in json_array.size(): + if index < json_array.size() and json_array[index].is_empty(): + json_array.remove_at(index) + return json_array + + + func console(_message :String) -> void: + #print_debug("TCP Connection:", _message) + pass + + +func _ready(): + _server = TCPServer.new() + client_connected.connect(Callable(self, "_on_client_connected")) + client_disconnected.connect(Callable(self, "_on_client_disconnected")) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + stop() + + +func start() -> GdUnitResult: + var server_port := GdUnitServerConstants.GD_TEST_SERVER_PORT + var err := OK + for retry in GdUnitServerConstants.DEFAULT_SERVER_START_RETRY_TIMES: + err = _server.listen(server_port, "127.0.0.1") + if err != OK: + prints("GdUnit4: Can't establish server checked port: %d, Error: %s" % [server_port, error_string(err)]) + server_port += 1 + prints("GdUnit4: Retry (%d) ..." % retry) + else: + break + if err != OK: + if err == ERR_ALREADY_IN_USE: + return GdUnitResult.error("GdUnit3: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) + return GdUnitResult.error("GdUnit3: Can't establish server. Error: %s." % error_string(err)) + prints("GdUnit4: Test server successfully started checked port: %d" % server_port) + return GdUnitResult.success(server_port) + + +func stop() -> void: + if _server: + _server.stop() + for connection in get_children(): + if connection is TcpConnection: + connection.close() + remove_child(connection) + + +func disconnect_client(client_id :int) -> void: + for connection in get_children(): + if connection is TcpConnection and connection.id() == client_id: + connection.close() + + +func _process(_delta): + if not _server.is_listening(): + return + # check if connection is ready to be used + if _server.is_connection_available(): + add_child(TcpConnection.new(_server)) + + +func _on_client_connected(client_id :int): + console("Client connected %d" % client_id) + + +func _on_client_disconnected(client_id :int): + console("Client disconnected %d" % client_id) + for connection in get_children(): + if connection is TcpConnection and connection.id() == client_id: + remove_child(connection) + + + +func console(_message :String) -> void: + #print_debug("TCP Server:", _message) + pass diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd b/addons/gdUnit4/src/network/rpc/RPC.gd new file mode 100644 index 0000000..190b05a --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPC.gd @@ -0,0 +1,24 @@ +class_name RPC +extends RefCounted + + +func serialize() -> String: + return JSON.stringify(inst_to_dict(self)) + + +# using untyped version see comments below +static func deserialize(json_value :String) -> Object: + var json := JSON.new() + var err := json.parse(json_value) + if err != OK: + push_error("Can't deserialize JSON, error at line %d: %s \n json: '%s'" % [json.get_error_line(), json.get_error_message(), json_value]) + return null + var result := json.get_data() as Dictionary + if not typeof(result) == TYPE_DICTIONARY: + push_error("Can't deserialize JSON, error at line %d: %s \n json: '%s'" % [result.error_line, result.error_string, json_value]) + return null + return dict_to_inst(result) + +# this results in orpan node, for more details https://github.com/godotengine/godot/issues/50069 +#func deserialize2(data :Dictionary) -> RPC: +# return dict_to_inst(data) as RPC diff --git a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd new file mode 100644 index 0000000..115fea2 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd @@ -0,0 +1,13 @@ +class_name RPCClientConnect +extends RPC + +var _client_id :int + + +func with_id(p_client_id :int) -> RPCClientConnect: + _client_id = p_client_id + return self + + +func client_id() -> int: + return _client_id diff --git a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd new file mode 100644 index 0000000..52ef259 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd @@ -0,0 +1,13 @@ +class_name RPCClientDisconnect +extends RPC + +var _client_id :int + + +func with_id(p_client_id :int) -> RPCClientDisconnect: + _client_id = p_client_id + return self + + +func client_id() -> int: + return _client_id diff --git a/addons/gdUnit4/src/network/rpc/RPCData.gd b/addons/gdUnit4/src/network/rpc/RPCData.gd new file mode 100644 index 0000000..7b6bac8 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCData.gd @@ -0,0 +1,13 @@ +class_name RPCData +extends RPC + +var _value + + +func with_data(value) -> RPCData: + _value = value + return self + + +func data() : + return _value diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd new file mode 100644 index 0000000..2cc3e10 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd @@ -0,0 +1,18 @@ +class_name RPCGdUnitEvent +extends RPC + +var _event :Dictionary + + +static func of(p_event :GdUnitEvent) -> RPCGdUnitEvent: + var rpc = RPCGdUnitEvent.new() + rpc._event = p_event.serialize() + return rpc + + +func event() -> GdUnitEvent: + return GdUnitEvent.new().deserialize(_event) + + +func _to_string(): + return "RPCGdUnitEvent: " + str(_event) diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd b/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd new file mode 100644 index 0000000..96c4d96 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd @@ -0,0 +1,18 @@ +class_name RPCGdUnitTestSuite +extends RPC + +var _data :Dictionary + + +static func of(test_suite :Node) -> RPCGdUnitTestSuite: + var rpc := RPCGdUnitTestSuite.new() + rpc._data = GdUnitTestSuiteDto.new().serialize(test_suite) + return rpc + + +func dto() -> GdUnitResourceDto: + return GdUnitTestSuiteDto.new().deserialize(_data) + + +func _to_string() -> String: + return "RPCGdUnitTestSuite: " + str(_data) diff --git a/addons/gdUnit4/src/network/rpc/RPCMessage.gd b/addons/gdUnit4/src/network/rpc/RPCMessage.gd new file mode 100644 index 0000000..6b47b5a --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/RPCMessage.gd @@ -0,0 +1,18 @@ +class_name RPCMessage +extends RPC + +var _message :String + + +static func of(p_message :String) -> RPCMessage: + var rpc = RPCMessage.new() + rpc._message = p_message + return rpc + + +func message() -> String: + return _message + + +func _to_string(): + return "RPCMessage: " + _message diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd new file mode 100644 index 0000000..9152c8d --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd @@ -0,0 +1,26 @@ +class_name GdUnitResourceDto +extends Resource + +var _name :String +var _path :String + + +func serialize(resource :Node) -> Dictionary: + var serialized := Dictionary() + serialized["name"] = resource.get_name() + serialized["resource_path"] = resource.ResourcePath() + return serialized + + +func deserialize(data :Dictionary) -> GdUnitResourceDto: + _name = data.get("name", "n.a.") + _path = data.get("resource_path", "") + return self + + +func name() -> String: + return _name + + +func path() -> String: + return _path diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd new file mode 100644 index 0000000..26f5dda --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd @@ -0,0 +1,33 @@ +class_name GdUnitTestCaseDto +extends GdUnitResourceDto + +var _line_number :int = -1 +var _test_case_names :PackedStringArray = [] + + +func serialize(test_case :Node) -> Dictionary: + var serialized := super.serialize(test_case) + if test_case.has_method("line_number"): + serialized["line_number"] = test_case.line_number() + else: + serialized["line_number"] = test_case.get("LineNumber") + if test_case.has_method("test_case_names"): + serialized["test_case_names"] = test_case.test_case_names() + elif test_case.has_method("TestCaseNames"): + serialized["test_case_names"] = test_case.TestCaseNames() + return serialized + + +func deserialize(data :Dictionary) -> GdUnitResourceDto: + super.deserialize(data) + _line_number = data.get("line_number", -1) + _test_case_names = data.get("test_case_names", []) + return self + + +func line_number() -> int: + return _line_number + + +func test_case_names() -> PackedStringArray: + return _test_case_names diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd new file mode 100644 index 0000000..9ecc9f6 --- /dev/null +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd @@ -0,0 +1,33 @@ +class_name GdUnitTestSuiteDto +extends GdUnitResourceDto + +var _test_cases_by_name := Dictionary() + + +func serialize(test_suite :Node) -> Dictionary: + var serialized := super.serialize(test_suite) + var test_cases_ := Array() + serialized["test_cases"] = test_cases_ + for test_case in test_suite.get_children(): + test_cases_.append(GdUnitTestCaseDto.new().serialize(test_case)) + return serialized + + +func deserialize(data :Dictionary) -> GdUnitResourceDto: + super.deserialize(data) + var test_cases_ :Array = data.get("test_cases", []) + for test_case in test_cases_: + add_test_case(GdUnitTestCaseDto.new().deserialize(test_case)) + return self + + +func add_test_case(test_case :GdUnitTestCaseDto) -> void: + _test_cases_by_name[test_case.name()] = test_case + + +func test_case_count() -> int: + return _test_cases_by_name.size() + + +func test_cases() -> Array: + return _test_cases_by_name.values() diff --git a/addons/gdUnit4/src/report/GdUnitByPathReport.gd b/addons/gdUnit4/src/report/GdUnitByPathReport.gd new file mode 100644 index 0000000..81d8281 --- /dev/null +++ b/addons/gdUnit4/src/report/GdUnitByPathReport.gd @@ -0,0 +1,47 @@ +class_name GdUnitByPathReport +extends GdUnitReportSummary + + +func _init(path_ :String, reports_ :Array[GdUnitReportSummary]): + _resource_path = path_ + _reports = reports_ + + +static func sort_reports_by_path(reports_ :Array[GdUnitReportSummary]) -> Dictionary: + var by_path := Dictionary() + for report in reports_: + var suite_path :String = report.path() + var suite_report :Array[GdUnitReportSummary] = by_path.get(suite_path, [] as Array[GdUnitReportSummary]) + suite_report.append(report) + by_path[suite_path] = suite_report + return by_path + + +func path() -> String: + return _resource_path.replace("res://", "") + + +func create_record(report_link :String) -> String: + return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_PATH, self, report_link) + + +func write(report_dir :String) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/folder_report.html") + var path_report := GdUnitHtmlPatterns.build(template, self, "") + path_report = apply_testsuite_reports(report_dir, path_report, _reports) + + var output_path := "%s/path/%s.html" % [report_dir, path().replace("/", ".")] + var dir := output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + DirAccess.make_dir_recursive_absolute(dir) + FileAccess.open(output_path, FileAccess.WRITE).store_string(path_report) + return output_path + + +func apply_testsuite_reports(report_dir :String, template :String, reports_ :Array[GdUnitReportSummary]) -> String: + var table_records := PackedStringArray() + + for report in reports_: + var report_link = report.output_path(report_dir).replace(report_dir, "..") + table_records.append(report.create_record(report_link)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) diff --git a/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd b/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd new file mode 100644 index 0000000..1d9cf86 --- /dev/null +++ b/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd @@ -0,0 +1,94 @@ +class_name GdUnitHtmlPatterns +extends RefCounted + +const TABLE_RECORD_TESTSUITE = """ + + ${testsuite_name} + ${test_count} + ${skipped_count} + ${failure_count} + ${orphan_count} + ${duration} + ${success_percent} + +""" + +const TABLE_RECORD_PATH = """ + + ${path} + ${test_count} + ${skipped_count} + ${failure_count} + ${orphan_count} + ${duration} + ${success_percent} + +""" + + +const TABLE_REPORT_TESTSUITE = """ + + TestSuite hooks + n/a + ${orphan_count} + ${duration} + ${failure-report} + +""" + + +const TABLE_RECORD_TESTCASE = """ + + ${testcase_name} + ${skipped_count} + ${orphan_count} + ${duration} + ${failure-report} + +""" + +const TABLE_BY_PATHS = "${report_table_paths}" +const TABLE_BY_TESTSUITES = "${report_table_testsuites}" +const TABLE_BY_TESTCASES = "${report_table_tests}" + +# the report state success, error, warning +const REPORT_STATE = "${report_state}" +const PATH = "${path}" +const TESTSUITE_COUNT = "${suite_count}" +const TESTCASE_COUNT = "${test_count}" +const FAILURE_COUNT = "${failure_count}" +const SKIPPED_COUNT = "${skipped_count}" +const ORPHAN_COUNT = "${orphan_count}" +const DURATION = "${duration}" +const FAILURE_REPORT = "${failure-report}" +const SUCCESS_PERCENT = "${success_percent}" + +const TESTSUITE_NAME = "${testsuite_name}" +const TESTCASE_NAME = "${testcase_name}" +const REPORT_LINK = "${report_link}" +const BREADCRUMP_PATH_LINK = "${breadcrumb_path_link}" +const BUILD_DATE = "${buid_date}" + + +static func current_date() -> String: + return Time.get_datetime_string_from_system(true, true) + + +static func build(template :String, report :GdUnitReportSummary, report_link :String) -> String: + return template\ + .replace(PATH, report.path())\ + .replace(TESTSUITE_NAME, report.name())\ + .replace(TESTSUITE_COUNT, str(report.suite_count()))\ + .replace(TESTCASE_COUNT, str(report.test_count()))\ + .replace(FAILURE_COUNT, str(report.error_count() + report.failure_count()))\ + .replace(SKIPPED_COUNT, str(report.skipped_count()))\ + .replace(ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(DURATION, LocalTime.elapsed(report.duration()))\ + .replace(SUCCESS_PERCENT, report.calculate_succes_rate(report.test_count(), report.error_count(), report.failure_count()))\ + .replace(REPORT_STATE, report.report_state())\ + .replace(REPORT_LINK, report_link)\ + .replace(BUILD_DATE, current_date()) + + +static func load_template(template_name :String) -> String: + return FileAccess.open(template_name, FileAccess.READ).get_as_text() diff --git a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd new file mode 100644 index 0000000..e5a0c92 --- /dev/null +++ b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd @@ -0,0 +1,91 @@ +class_name GdUnitHtmlReport +extends GdUnitReportSummary + +const REPORT_DIR_PREFIX = "report_" + +var _report_path :String +var _iteration :int + + +func _init(path_ :String): + _iteration = GdUnitFileAccess.find_last_path_index(path_, REPORT_DIR_PREFIX) + 1 + _report_path = "%s/%s%d" % [path_, REPORT_DIR_PREFIX, _iteration] + DirAccess.make_dir_recursive_absolute(_report_path) + + +func add_testsuite_report(suite_report :GdUnitTestSuiteReport): + _reports.append(suite_report) + + +func add_testcase_report(resource_path_ :String, suite_report :GdUnitTestCaseReport) -> void: + for report in _reports: + if report.resource_path() == resource_path_: + report.add_report(suite_report) + + +func update_test_suite_report( + resource_path_ :String, + duration_ :int, + _is_error :bool, + is_failed_: bool, + _is_warning :bool, + is_skipped_ :bool, + skipped_count_ :int, + failed_count_ :int, + orphan_count_ :int, + reports_ :Array = []) -> void: + + for report in _reports: + if report.resource_path() == resource_path_: + report.set_duration(duration_) + report.set_failed(is_failed_, failed_count_) + report.set_orphans(orphan_count_) + report.set_reports(reports_) + if is_skipped_: + _skipped_count = skipped_count_ + + +func update_testcase_report(resource_path_ :String, test_report :GdUnitTestCaseReport): + for report in _reports: + if report.resource_path() == resource_path_: + report.update(test_report) + + +func write() -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/index.html") + var to_write = GdUnitHtmlPatterns.build(template, self, "") + to_write = apply_path_reports(_report_path, to_write, _reports) + to_write = apply_testsuite_reports(_report_path, to_write, _reports) + # write report + var index_file := "%s/index.html" % _report_path + FileAccess.open(index_file, FileAccess.WRITE).store_string(to_write) + GdUnitFileAccess.copy_directory("res://addons/gdUnit4/src/report/template/css/", _report_path + "/css") + return index_file + + +func delete_history(max_reports :int) -> int: + return GdUnitFileAccess.delete_path_index_lower_equals_than(_report_path.get_base_dir(), REPORT_DIR_PREFIX, _iteration-max_reports) + + +func apply_path_reports(report_dir :String, template :String, reports_ :Array) -> String: + var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(reports_) + var table_records := PackedStringArray() + var paths := path_report_mapping.keys() + paths.sort() + for path_ in paths: + var report := GdUnitByPathReport.new(path_, path_report_mapping.get(path_)) + var report_link :String = report.write(report_dir).replace(report_dir, ".") + table_records.append(report.create_record(report_link)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) + + +func apply_testsuite_reports(report_dir :String, template :String, reports_ :Array) -> String: + var table_records := PackedStringArray() + for report in reports_: + var report_link :String = report.write(report_dir).replace(report_dir, ".") + table_records.append(report.create_record(report_link)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) + + +func iteration() -> int: + return _iteration diff --git a/addons/gdUnit4/src/report/GdUnitReportSummary.gd b/addons/gdUnit4/src/report/GdUnitReportSummary.gd new file mode 100644 index 0000000..1aefa1f --- /dev/null +++ b/addons/gdUnit4/src/report/GdUnitReportSummary.gd @@ -0,0 +1,131 @@ +class_name GdUnitReportSummary +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const CHARACTERS_TO_ENCODE := { + '<' : '<', + '>' : '>' +} + +var _resource_path :String +var _name :String +var _test_count := 0 +var _failure_count := 0 +var _error_count := 0 +var _orphan_count := 0 +var _skipped_count := 0 +var _duration := 0 +var _reports :Array[GdUnitReportSummary] = [] + + +func name() -> String: + return html_encode(_name) + + +func path() -> String: + return _resource_path.get_base_dir().replace("res://", "") + + +func resource_path() -> String: + return _resource_path + + +func suite_count() -> int: + return _reports.size() + + +func test_count() -> int: + var count := _test_count + for report in _reports: + count += report.test_count() + return count + + +func error_count() -> int: + var count := _error_count + for report in _reports: + count += report.error_count() + return count + + +func failure_count() -> int: + var count := _failure_count + for report in _reports: + count += report.failure_count() + return count + + +func skipped_count() -> int: + var count := _skipped_count + for report in _reports: + count += report.skipped_count() + return count + + +func orphan_count() -> int: + var count := _orphan_count + for report in _reports: + count += report.orphan_count() + return count + + +func duration() -> int: + var count := _duration + for report in _reports: + count += report.duration() + return count + + +func reports() -> Array: + return _reports + + +func add_report(report :GdUnitReportSummary) -> void: + _reports.append(report) + + +func report_state() -> String: + return calculate_state(error_count(), failure_count(), orphan_count()) + + +func succes_rate() -> String: + return calculate_succes_rate(test_count(), error_count(), failure_count()) + + +func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int) -> String: + if p_error_count > 0: + return "error" + if p_failure_count > 0: + return "failure" + if p_orphan_count > 0: + return "warning" + return "success" + + +func calculate_succes_rate(p_test_count :int, p_error_count :int, p_failure_count :int) -> String: + if p_failure_count == 0: + return "100%" + var count = p_test_count-p_failure_count-p_error_count + if count < 0: + return "0%" + return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" + + +func create_summary(_report_dir :String) -> String: + return "" + + +func html_encode(value :String) -> String: + for key in CHARACTERS_TO_ENCODE.keys(): + value =value.replace(key, CHARACTERS_TO_ENCODE[key]) + return value + + +func convert_rtf_to_html(bbcode :String) -> String: + var as_text: = GdUnitTools.richtext_normalize(bbcode) + var converted := PackedStringArray() + var lines := as_text.split("\n") + for line in lines: + converted.append("

%s

" % line) + return "\n".join(converted) diff --git a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd new file mode 100644 index 0000000..d153ec7 --- /dev/null +++ b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd @@ -0,0 +1,59 @@ +class_name GdUnitTestCaseReport +extends GdUnitReportSummary + +var _suite_name :String +var _failure_reports :Array + + +func _init( + p_resource_path :String, + p_suite_name :String, + test_name :String, + is_error := false, + _is_failed := false, + failed_count :int = 0, + orphan_count_ :int = 0, + is_skipped := false, + failure_reports :Array = [], + p_duration :int = 0): + _resource_path = p_resource_path + _suite_name = p_suite_name + _name = test_name + _test_count = 1 + _error_count = is_error + _failure_count = failed_count + _orphan_count = orphan_count_ + _skipped_count = is_skipped + _failure_reports = failure_reports + _duration = p_duration + + +func suite_name() -> String: + return _suite_name + + +func failure_report() -> String: + var html_report := "" + for r in _failure_reports: + var report: GdUnitReport = r + html_report += convert_rtf_to_html(report._to_string()) + return html_report + + +func create_record(_report_dir :String) -> String: + return GdUnitHtmlPatterns.TABLE_RECORD_TESTCASE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state())\ + .replace(GdUnitHtmlPatterns.TESTCASE_NAME, name())\ + .replace(GdUnitHtmlPatterns.SKIPPED_COUNT, str(skipped_count()))\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(_duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, failure_report()) + + +func update(report :GdUnitTestCaseReport) -> void: + _error_count += report.error_count() + _failure_count += report.failure_count() + _orphan_count += report.orphan_count() + _skipped_count += report.skipped_count() + _failure_reports += report._failure_reports + _duration += report.duration() diff --git a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd new file mode 100644 index 0000000..2ffe75c --- /dev/null +++ b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd @@ -0,0 +1,95 @@ +class_name GdUnitTestSuiteReport +extends GdUnitReportSummary + +var _time_stamp :int +var _failure_reports :Array = [] + + +func _init(p_resource_path :String, p_name :String): + _resource_path = p_resource_path + _name = p_name + _time_stamp = Time.get_unix_time_from_system() as int + + +func create_record(report_link :String) -> String: + return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_TESTSUITE, self, report_link) + + +func output_path(report_dir :String) -> String: + return "%s/test_suites/%s.%s.html" % [report_dir, path().replace("/", "."), name()] + + +func path_as_link() -> String: + return "../path/%s.html" % path().replace("/", ".") + + +func failure_report() -> String: + var html_report := "" + for r in _failure_reports: + var report: GdUnitReport = r + html_report += convert_rtf_to_html(report._to_string()) + return html_report + + +func test_suite_failure_report() -> String: + return GdUnitHtmlPatterns.TABLE_REPORT_TESTSUITE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state())\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(_duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, failure_report()) + + +func write(report_dir :String) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/suite_report.html") + template = GdUnitHtmlPatterns.build(template, self, "")\ + .replace(GdUnitHtmlPatterns.BREADCRUMP_PATH_LINK, path_as_link()) + + var report_output_path := output_path(report_dir) + var test_report_table := PackedStringArray() + if not _failure_reports.is_empty(): + test_report_table.append(test_suite_failure_report()) + for test_report in _reports: + test_report_table.append(test_report.create_record(report_output_path)) + + template = template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTCASES, "\n".join(test_report_table)) + + var dir := report_output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + DirAccess.make_dir_recursive_absolute(dir) + FileAccess.open(report_output_path, FileAccess.WRITE).store_string(template) + return report_output_path + + +func set_duration(p_duration :int) -> void: + _duration = p_duration + + +func time_stamp() -> int: + return _time_stamp + + +func duration() -> int: + return _duration + + +func set_skipped(skipped :int) -> void: + _skipped_count = skipped + + +func set_orphans(orphans :int) -> void: + _orphan_count = orphans + + +func set_failed(failed :bool, count :int) -> void: + if failed: + _failure_count += count + + +func set_reports(reports_ :Array) -> void: + _failure_reports = reports_ + + +func update(test_report :GdUnitTestCaseReport) -> void: + for report in _reports: + if report.name() == test_report.name(): + report.update(test_report) diff --git a/addons/gdUnit4/src/report/JUnitXmlReport.gd b/addons/gdUnit4/src/report/JUnitXmlReport.gd new file mode 100644 index 0000000..65a708b --- /dev/null +++ b/addons/gdUnit4/src/report/JUnitXmlReport.gd @@ -0,0 +1,143 @@ +# This class implements the JUnit XML file format +# based checked https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd +class_name JUnitXmlReport +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +const ATTR_CLASSNAME := "classname" +const ATTR_ERRORS := "errors" +const ATTR_FAILURES := "failures" +const ATTR_HOST := "hostname" +const ATTR_ID := "id" +const ATTR_MESSAGE := "message" +const ATTR_NAME := "name" +const ATTR_PACKAGE := "package" +const ATTR_SKIPPED := "skipped" +const ATTR_TESTS := "tests" +const ATTR_TIME := "time" +const ATTR_TIMESTAMP := "timestamp" +const ATTR_TYPE := "type" + +const HEADER := '\n' + +var _report_path :String +var _iteration :int + + +func _init(path :String,iteration :int): + _iteration = iteration + _report_path = path + + +func write(report :GdUnitReportSummary) -> String: + var result_file: String = "%s/results.xml" % _report_path + var file = FileAccess.open(result_file, FileAccess.WRITE) + if file == null: + push_warning("Can't saving the result to '%s'\n Error: %s" % [result_file, error_string(FileAccess.get_open_error())]) + file.store_string(build_junit_report(report)) + return result_file + + +func build_junit_report(report :GdUnitReportSummary) -> String: + var ISO8601_datetime := Time.get_date_string_from_system() + var test_suites := XmlElement.new("testsuites")\ + .attribute(ATTR_ID, ISO8601_datetime)\ + .attribute(ATTR_NAME, "report_%s" % _iteration)\ + .attribute(ATTR_TESTS, report.test_count())\ + .attribute(ATTR_FAILURES, report.failure_count())\ + .attribute(ATTR_TIME, JUnitXmlReport.to_time(report.duration()))\ + .add_childs(build_test_suites(report)) + var as_string = test_suites.to_xml() + test_suites.dispose() + return HEADER + as_string + + +func build_test_suites(summary :GdUnitReportSummary) -> Array: + var test_suites :Array = Array() + for index in summary.reports().size(): + var suite_report :GdUnitTestSuiteReport = summary.reports()[index] + var ISO8601_datetime = Time.get_datetime_string_from_unix_time(suite_report.time_stamp()) + test_suites.append(XmlElement.new("testsuite")\ + .attribute(ATTR_ID, index)\ + .attribute(ATTR_NAME, suite_report.name())\ + .attribute(ATTR_PACKAGE, suite_report.path())\ + .attribute(ATTR_TIMESTAMP, ISO8601_datetime)\ + .attribute(ATTR_HOST, "localhost")\ + .attribute(ATTR_TESTS, suite_report.test_count())\ + .attribute(ATTR_FAILURES, suite_report.failure_count())\ + .attribute(ATTR_ERRORS, suite_report.error_count())\ + .attribute(ATTR_SKIPPED, suite_report.skipped_count())\ + .attribute(ATTR_TIME, JUnitXmlReport.to_time(suite_report.duration()))\ + .add_childs(build_test_cases(suite_report))) + return test_suites + + +func build_test_cases(suite_report :GdUnitTestSuiteReport) -> Array: + var test_cases :Array = Array() + for index in suite_report.reports().size(): + var report :GdUnitTestCaseReport = suite_report.reports()[index] + test_cases.append( XmlElement.new("testcase")\ + .attribute(ATTR_NAME, encode_xml(report.name()))\ + .attribute(ATTR_CLASSNAME, report.suite_name())\ + .attribute(ATTR_TIME, JUnitXmlReport.to_time(report.duration()))\ + .add_childs(build_reports(report))) + return test_cases + + +func build_reports(testReport :GdUnitTestCaseReport) -> Array: + var failure_reports :Array = Array() + if testReport.failure_count() or testReport.error_count(): + for failure in testReport._failure_reports: + var report := failure as GdUnitReport + if report.is_failure(): + failure_reports.append( XmlElement.new("failure")\ + .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [testReport._resource_path, report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ + .text(convert_rtf_to_text(report.message()))) + elif report.is_error(): + failure_reports.append( XmlElement.new("error")\ + .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [testReport._resource_path, report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ + .text(convert_rtf_to_text(report.message()))) + if testReport.skipped_count(): + for failure in testReport._failure_reports: + var report := failure as GdUnitReport + failure_reports.append( XmlElement.new("skipped")\ + .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [testReport._resource_path, report.line_number()])) + return failure_reports + + +func convert_rtf_to_text(bbcode :String) -> String: + return GdUnitTools.richtext_normalize(bbcode) + + +static func to_type(type :int) -> String: + match type: + GdUnitReport.SUCCESS: + return "SUCCESS" + GdUnitReport.WARN: + return "WARN" + GdUnitReport.FAILURE: + return "FAILURE" + GdUnitReport.ORPHAN: + return "ORPHAN" + GdUnitReport.TERMINATED: + return "TERMINATED" + GdUnitReport.INTERUPTED: + return "INTERUPTED" + GdUnitReport.ABORT: + return "ABORT" + return "UNKNOWN" + + +static func to_time(duration :int) -> String: + return "%4.03f" % (duration / 1000.0) + + +static func encode_xml(value :String) -> String: + return value.xml_escape(true) + + +#static func to_ISO8601_datetime() -> String: + #return "%04d-%02d-%02dT%02d:%02d:%02d" % [date["year"], date["month"], date["day"], date["hour"], date["minute"], date["second"]] diff --git a/addons/gdUnit4/src/report/XmlElement.gd b/addons/gdUnit4/src/report/XmlElement.gd new file mode 100644 index 0000000..815f169 --- /dev/null +++ b/addons/gdUnit4/src/report/XmlElement.gd @@ -0,0 +1,67 @@ +class_name XmlElement +extends RefCounted + +var _name :String +var _attributes :Dictionary = {} +var _childs :Array = [] +var _parent = null +var _text :String = "" + + +func _init(name :String): + _name = name + + +func dispose(): + for child in _childs: + child.dispose() + _childs.clear() + _attributes.clear() + _parent = null + + +func attribute(name :String, value) -> XmlElement: + _attributes[name] = str(value) + return self + + +func text(p_text :String) -> XmlElement: + _text = p_text if p_text.ends_with("\n") else p_text + "\n" + return self + + +func add_child(child :XmlElement) -> XmlElement: + _childs.append(child) + child._parent = self + return self + + +func add_childs(childs :Array) -> XmlElement: + for child in childs: + add_child(child) + return self + + +func _indentation() -> String: + return "" if _parent == null else _parent._indentation() + " " + + +func to_xml() -> String: + var attributes := "" + for key in _attributes.keys(): + attributes += ' {attr}="{value}"'.format({"attr": key, "value": _attributes.get(key)}) + + var childs = "" + for child in _childs: + childs += child.to_xml() + + return "{_indentation}<{name}{attributes}>\n{childs}{text}{_indentation}\n"\ + .format({"name": _name, + "attributes": attributes, + "childs": childs, + "_indentation": _indentation(), + "text": cdata(_text)}) + + +func cdata(p_text :String) -> String: + return "" if p_text.is_empty() else "\n".format({"text" : p_text}) diff --git a/addons/gdUnit4/src/report/template/css/breadcrumb.css b/addons/gdUnit4/src/report/template/css/breadcrumb.css new file mode 100644 index 0000000..2dd65fe --- /dev/null +++ b/addons/gdUnit4/src/report/template/css/breadcrumb.css @@ -0,0 +1,67 @@ + +.breadcrumb { + display: flex; + border-radius: 6px; + overflow: hidden; + height: 45px; + z-index: 1; + background-color: #055d9c; + font-weight: bold; + font-size: 16px; + margin-top: 0px; + margin-bottom: 20px; + box-shadow: 0 0 3px black; +} + +.breadcrumb a { + position: relative; + display: flex; + -ms-flex-positive: 1; + flex-grow: 1; + text-decoration: none; + margin: auto; + height: 100%; + color: white; +} + +.breadcrumb a:first-child { + padding-left: 5.2px; +} + +.breadcrumb a:last-child { + padding-right: 5.2px; +} + +.breadcrumb a:after { + content: ""; + position: absolute; + display: inline-block; + width: 45px; + height: 45px; + top: 0; + right: -20px; + background-color: #055d9c; + border-top-right-radius: 5px; + transform: scale(0.707) rotate(45deg); + box-shadow: 2px -2px rgba(0,0,0,0.25); + z-index: 1; +} + +.breadcrumb a:last-child:after { + content: none; +} + +.breadcrumb a.active, .breadcrumb a:hover { + background: #347bad; + color: white; + text-decoration: underline; +} + +.breadcrumb a.active:after, .breadcrumb a:hover:after { + background: #347bad; +} + +.breadcrumb span { + margin:inherit; + z-index: 2; +} diff --git a/addons/gdUnit4/src/report/template/css/icon.png b/addons/gdUnit4/src/report/template/css/icon.png new file mode 100644 index 0000000..eeac292 Binary files /dev/null and b/addons/gdUnit4/src/report/template/css/icon.png differ diff --git a/addons/gdUnit4/src/report/template/css/icon.png.import b/addons/gdUnit4/src/report/template/css/icon.png.import new file mode 100644 index 0000000..c1a2062 --- /dev/null +++ b/addons/gdUnit4/src/report/template/css/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c8duoevjrvx65" +path="res://.godot/imported/icon.png-62697e17645466394a12e1821970bc28.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/report/template/css/icon.png" +dest_files=["res://.godot/imported/icon.png-62697e17645466394a12e1821970bc28.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/report/template/css/style.css b/addons/gdUnit4/src/report/template/css/style.css new file mode 100644 index 0000000..747d57c --- /dev/null +++ b/addons/gdUnit4/src/report/template/css/style.css @@ -0,0 +1,312 @@ + +body { + margin: 0; + padding: 0; + font-family: sans-serif; + font-size: 12pt; + background: #dbf1ff; +} + +footer { + position: fixed; + left: 0; + bottom: 0; + width: 100%; + height: 30px; + font-size: 14px; + white-space: nowrap; + background-image: linear-gradient(to bottom right, #347bad, #055d9c); +} + +footer a { + padding-left: 5px; +} + +footer p { + padding-left: 10px; +} + +.header { + padding-top: 5px; + padding-bottom: 5px; + width: 100%; + height: 100px; + text-align: left; + background-image: linear-gradient(to bottom right, #347bad, #055d9c); +} + +.header h1 { + font-size: 200%; + display: flex; + line-height: 64px; + text-align: center; + text-shadow: 2px 2px 5px black; +} + +.header h1 .color1 { + color: #9887c4; +} + +.header h1 .color2 { + color: #7a57d6; +} + +.header h1 .color3 { + color: #2fa5f5; + padding-left: 10px; +} + +.header img { + padding-left: 50px; + width: 64px; + height: 64px; + padding-right: 10px; +} + +.content { + padding-left: 50px; + padding-right: 50px; + padding-top: 30px; + padding-bottom: 30px; +} + +.content h1 { + font-size: 130%; + text-align: left; + margin-bottom: 10px; + margin: 0; + padding: 0; +} + +.grid-container { + display: grid; + grid-template-columns: 420px 400px auto; + margin-bottom: 20px; +} + +.grid-item { + border-radius: 15px; + border: 2px solid #9887c4; + padding: 10px; + margin: 5px; + box-shadow: 0 0 10px #9887c4; +} + +.summary table { + border-collapse: collapse; + border-collapse: inherit; + border-spacing: 10px 10px; +} + +.summary cell { + width: 110px; + font-size: 110%; + text-align: left; +} + +.summary .counter, .percent { + width: 300px; + font-size: 120%; + font-weight: bold; + text-align: right; + box-shadow: 0 0 3px black; +} + +.report { + margin-top: 10px; + margin-bottom: 10px; +} + +/* Style tab links */ +input, section { + clear: both; + padding-top: 0px; + padding-bottom: 0px; + display: none; + background-color: transparent; + box-shadow: 0 0 5px black; +} + +label { + width: auto !important; + min-width: 200px; + height: 20px; + + font-weight: bold; + font-size: 16px; + color: white; + + margin: auto; + display: flex; + float: left; + justify-content: space-between; + + padding-top: 18px; + padding-left: 10px; + padding-right: 10px; + padding-bottom: 9px; + + border-top: 1px solid #DDD; + border-right: 1px solid #DDD; + border-left: 1px solid #DDD; + + -moz-border-radius: 7px; + border-radius: 8px 4px 0px 0px; + text-decoration: none; + background-color: #055d9c +} + +label:hover { + cursor: pointer; + text-decoration: underline; +} + +#tab1:checked ~ #content1, #tab2:checked ~ #content2, #tab3:checked ~ #content3 { + display: block; +} + +input:checked + label { + height: 14px; + border-top: 8px solid #9887c4; + border-right: 1px solid #9887c4; + border-left: 1px solid #9887c4; + border-bottom-color: transparent; + background-color: #347bad; + box-shadow: -2px -2px 2px #9887c4; +} + +div.selected { + display: block; +} + +div.deselected { + display: none; +} + +.tab-report-grid { + display: grid; + grid-template-columns: 60% 40%; + height: 400px; + margin-bottom: 20px; +} + +.grid-item { + overflow: auto; +} + +div.tab table { + border-collapse: collapse; +} + +div.tab th, div.tab table { + border-bottom: solid 1px #055d9c; +} + +div.tab th { + text-align: left; + white-space: nowrap; + padding-left: 6em; + padding-right: 10px; +} + +div.tab th:first-child { + padding-left: 5px; +} + +div.tab td { + white-space: nowrap; + padding-left: 6em; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; +} + +div.tab td:first-child { + padding-left: 5px; + text-align: left; +} + +div.tab td.numeric, div.tab { + text-align: right; +} + +div.tab td.report-column, th.report-column { + display:none; +} + +div.tab td.success, div.tab a.success { + color: #03a606; +} + +div.tab td.error, div.tab a.error { + color: #bf3600; +} + +div.tab td.failure, div.tab a.failure { + color: #bf3600; +} + +div.tab td.warning, div.tab a.warning { + color: #cca704; +} + +div.tab tr:hover { + background-color: #d9e7fa; + box-shadow: 0 0 5px black; +} + +div.tab tr.selected { + background-color: #347bad; +} + +div.tab th { + width: 100%; + height: 40px; + background-color: #347bad; + color: white; + font-weight: bold; +} + +.success_percent { + font-size: 750%; +} + +.border_success { + border: solid 3px #03a606; + background-image: repeating-linear-gradient(45deg, white, white 20%, #03a606); +} + +.border_error { + border: solid 3px #bf3600; + background-image: repeating-linear-gradient(45deg, #e86363, white 0%, #bf3600); +} + +.border_failure { + border: solid 3px #bf3600; + background-image: repeating-linear-gradient(45deg, #e86363, white 0%, #bf3600); +} + +div.logging { + padding: 10px; + height: 400px; + margin: 0px; + background-image: linear-gradient(to bottom right, #347bad, #055d9c); + color: white; +} + +div.logging p { + padding: 10px; + font: arial; + background-color: #dbf1ff; + height: 85%; +} + +div.report-column { + margin-top: 10px; + width: 100%; + height: 300px; + font: arial; + font-size: 16; + text-align: left; + background-color: #dbf1ff; +} diff --git a/addons/gdUnit4/src/report/template/folder_report.html b/addons/gdUnit4/src/report/template/folder_report.html new file mode 100644 index 0000000..6f21772 --- /dev/null +++ b/addons/gdUnit4/src/report/template/folder_report.html @@ -0,0 +1,99 @@ + + + + + + Testsuite Results + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TestSuites${suite_count}
Tests${test_count}
Skipped${skipped_count}
Failures${failure_count}
Orphans${orphan_count}
Duration${duration}
+
+
+

Success Rate

+ + + + +
${success_percent}
+
+
+

History

+

Coming Next

+
+
+ + + +
+
+ + + +
+
+ + + + + + + + + + + + + + ${report_table_testsuites} + +
TestsSkippedFailuresOrphansDurationSuccess rate
+
+
+
+
+
+ +
+

Generated byGdUnit4 at ${buid_date}

+
+ + + diff --git a/addons/gdUnit4/src/report/template/index.html b/addons/gdUnit4/src/report/template/index.html new file mode 100644 index 0000000..2f6571e --- /dev/null +++ b/addons/gdUnit4/src/report/template/index.html @@ -0,0 +1,123 @@ + + + + + + Report Summary + + + + +
+

GdUnit4 Report

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TestSuites${suite_count}
Tests${test_count}
Skipped${skipped_count}
Failures${failure_count}
Orphans${orphan_count}
Duration${duration}
+
+
+

Success Rate

+ + + + +
${success_percent}
+
+
+

History

+

Coming Next

+
+
+ +
+

Reports

+
+ + + + + + + +
+
+ + + + + + + + + + + + + + ${report_table_testsuites} + +
TestsSkippedFailuresOrphansDurationSuccess rate
+
+
+
+
+ + + + + + + + + + + + + + ${report_table_paths} + +
TestsSkippedFailuresOrphansDurationSuccess rate
+
+
+
+
+

${log_file}

+

+
+
+
+
+
+ +
+

Generated byGdUnit4 at ${buid_date}

+
+ + diff --git a/addons/gdUnit4/src/report/template/suite_report.html b/addons/gdUnit4/src/report/template/suite_report.html new file mode 100644 index 0000000..94fa47d --- /dev/null +++ b/addons/gdUnit4/src/report/template/suite_report.html @@ -0,0 +1,109 @@ + + + + + + + Testsuite results + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Tests${test_count}
Skipped${skipped_count}
Failures${failure_count}
Orphans${orphan_count}
Duration${duration}
+
+
+

Success Rate

+ + + + +
${success_percent}
+
+
+

History

+

Coming Next

+
+
+ + + +
+
+
+ + + + + + + + + + + + ${report_table_tests} + +
TestcaseSkippedOrphansDurationReport
+
+
+

Failure Report

+
+
+
+
+
+ +
+

Generated byGdUnit4 at ${buid_date}

+
+ + + \ No newline at end of file diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd new file mode 100644 index 0000000..1254868 --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -0,0 +1,112 @@ +class_name GdUnitSpyBuilder +extends GdUnitClassDoubler + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd") + + +static func build(to_spy, debug_write := false) -> Object: + if GdObjects.is_singleton(to_spy): + push_error("Spy on a Singleton is not allowed! '%s'" % to_spy.get_class()) + return null + # if resource path load it before + if GdObjects.is_scene_resource_path(to_spy): + if not FileAccess.file_exists(to_spy): + push_error("Can't build spy on scene '%s'! The given resource not exists!" % to_spy) + return null + to_spy = load(to_spy) + # spy checked PackedScene + if GdObjects.is_scene(to_spy): + return spy_on_scene(to_spy.instantiate(), debug_write) + # spy checked a scene instance + if GdObjects.is_instance_scene(to_spy): + return spy_on_scene(to_spy, debug_write) + + var spy := spy_on_script(to_spy, [], debug_write) + if spy == null: + return null + var spy_instance = spy.new() + copy_properties(to_spy, spy_instance) + GdUnitObjectInteractions.reset(spy_instance) + spy_instance.__set_singleton(to_spy) + # we do not call the original implementation for _ready and all input function, this is actualy done by the engine + spy_instance.__exclude_method_call([ "_input", "_gui_input", "_input_event", "_unhandled_input"]) + return register_auto_free(spy_instance) + + +static func get_class_info(clazz :Variant) -> Dictionary: + var clazz_path := GdObjects.extract_class_path(clazz) + var clazz_name :String = GdObjects.extract_class_name(clazz).value() + return { + "class_name" : clazz_name, + "class_path" : clazz_path + } + + +static func spy_on_script(instance, function_excludes :PackedStringArray, debug_write) -> GDScript: + if GdArrayTools.is_array_type(instance): + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't build spy checked type '%s'! Spy checked Container Built-In Type not supported!" % instance.get_class()) + return null + var class_info := get_class_info(instance) + var clazz_name :String = class_info.get("class_name") + var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) + if not GdObjects.is_instance(instance): + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't build spy for class type '%s'! Using an instance instead e.g. 'spy()'" % [clazz_name]) + return null + var lines := load_template(SPY_TEMPLATE.source_code, class_info, instance) + lines += double_functions(instance, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) + + var spy := GDScript.new() + spy.source_code = "\n".join(lines) + spy.resource_name = "Spy%s.gd" % clazz_name + spy.resource_path = GdUnitFileAccess.create_temp_dir("spy") + "/Spy%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] + + if debug_write: + DirAccess.remove_absolute(spy.resource_path) + ResourceSaver.save(spy, spy.resource_path) + var error := spy.reload(true) + if error != OK: + push_error("Unexpected Error!, SpyBuilder error, please contact the developer.") + return null + return spy + + +static func spy_on_scene(scene :Node, debug_write) -> Object: + if scene.get_script() == null: + if GdUnitSettings.is_verbose_assert_errors(): + push_error("Can't create a spy checked a scene without script '%s'" % scene.get_scene_file_path()) + return null + # buils spy checked original script + var scene_script = scene.get_script().new() + var spy := spy_on_script(scene_script, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write) + scene_script.free() + if spy == null: + return null + # replace original script whit spy + scene.set_script(spy) + return register_auto_free(scene) + + +const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"] + + +static func copy_properties(source :Object, dest :Object) -> void: + for property in source.get_property_list(): + var property_name = property["name"] + var property_value = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd new file mode 100644 index 0000000..a8c80ea --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd @@ -0,0 +1,75 @@ +class_name GdUnitSpyFunctionDoubler +extends GdFunctionDoubler + + +const TEMPLATE_RETURN_VARIANT = """ + var args :Array = ["$(func_name)", $(arguments)] + + if $(instance)__is_verify_interactions(): + $(instance)__verify_interactions(args) + return ${default_return_value} + else: + $(instance)__save_function_interaction(args) + + if $(instance)__do_call_real_func("$(func_name)"): + return $(await)super($(arguments)) + return ${default_return_value} + +""" + + +const TEMPLATE_RETURN_VOID = """ + var args :Array = ["$(func_name)", $(arguments)] + + if $(instance)__is_verify_interactions(): + $(instance)__verify_interactions(args) + return + else: + $(instance)__save_function_interaction(args) + + if $(instance)__do_call_real_func("$(func_name)"): + $(await)super($(arguments)) + +""" + + +const TEMPLATE_RETURN_VOID_VARARG = """ + var varargs :Array = __filter_vargs([$(varargs)]) + var args :Array = ["$(func_name)", $(arguments)] + varargs + + if $(instance)__is_verify_interactions(): + $(instance)__verify_interactions(args) + return + else: + $(instance)__save_function_interaction(args) + + $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs) + +""" + + +const TEMPLATE_RETURN_VARIANT_VARARG = """ + var varargs :Array = __filter_vargs([$(varargs)]) + var args :Array = ["$(func_name)", $(arguments)] + varargs + + if $(instance)__is_verify_interactions(): + $(instance)__verify_interactions(args) + return ${default_return_value} + else: + $(instance)__save_function_interaction(args) + + return $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs) + +""" + + +func _init(push_errors :bool = false): + super._init(push_errors) + + +func get_template(return_type :Variant, is_vararg :bool) -> String: + if is_vararg: + return TEMPLATE_RETURN_VOID_VARARG if return_type == TYPE_NIL else TEMPLATE_RETURN_VARIANT_VARARG + if return_type is StringName: + return TEMPLATE_RETURN_VARIANT + return TEMPLATE_RETURN_VOID if (return_type == TYPE_NIL or return_type == GdObjects.TYPE_VOID) else TEMPLATE_RETURN_VARIANT diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd new file mode 100644 index 0000000..3b61e86 --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd @@ -0,0 +1,44 @@ + +const __INSTANCE_ID = "${instance_id}" +const __SOURCE_CLASS = "${source_class}" + +var __instance_delegator :Object +var __excluded_methods :PackedStringArray = [] + + +static func __instance() -> Variant: + return Engine.get_meta(__INSTANCE_ID) + + +func _notification(what :int) -> void: + if what == NOTIFICATION_PREDELETE: + if Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) + + +func __instance_id() -> String: + return __INSTANCE_ID + + +func __set_singleton(delegator :Object) -> void: + # store self need to mock static functions + Engine.set_meta(__INSTANCE_ID, self) + __instance_delegator = delegator + + +func __release_double() -> void: + # we need to release the self reference manually to prevent orphan nodes + Engine.remove_meta(__INSTANCE_ID) + __instance_delegator = null + + +func __do_call_real_func(func_name :String) -> bool: + return not __excluded_methods.has(func_name) + + +func __exclude_method_call(exluded_methods :PackedStringArray) -> void: + __excluded_methods.append_array(exluded_methods) + + +func __call_func(func_name :String, arguments :Array) -> Variant: + return __instance_delegator.callv(func_name, arguments) diff --git a/addons/gdUnit4/src/ui/EditorFileSystemControls.gd b/addons/gdUnit4/src/ui/EditorFileSystemControls.gd new file mode 100644 index 0000000..eef80e1 --- /dev/null +++ b/addons/gdUnit4/src/ui/EditorFileSystemControls.gd @@ -0,0 +1,33 @@ +# A tool to provide extended filesystem editor functionallity +class_name EditorFileSystemControls +extends RefCounted + + +# Returns the EditorInterface instance +static func editor_interface() -> EditorInterface: + if not Engine.has_meta("GdUnitEditorPlugin"): + return null + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + return plugin.get_editor_interface() + + +# Register the given context menu to the filesystem dock +# Is called when the plugin is activated +# The filesystem popup is connected to the EditorFileSystemContextMenuHandler +static func register_context_menu(menu :Array[GdUnitContextMenuItem]) -> void: + Engine.get_main_loop().root.call_deferred("add_child", EditorFileSystemContextMenuHandler.new(menu)) + + +# Unregisteres all registerend context menus and gives the EditorFileSystemContextMenuHandler> free +# Is called when the plugin is deactivated +static func unregister_context_menu() -> void: + EditorFileSystemContextMenuHandler.dispose() + + +static func _print_menu(popup :PopupMenu): + for itemIndex in popup.item_count: + prints( "get_item_id", popup.get_item_id(itemIndex)) + prints( "get_item_accelerator", popup.get_item_accelerator(itemIndex)) + prints( "get_item_shortcut", popup.get_item_shortcut(itemIndex)) + prints( "get_item_text", popup.get_item_text(itemIndex)) + prints() diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd new file mode 100644 index 0000000..cd823fc --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd @@ -0,0 +1,161 @@ +@tool +extends Control + +const TITLE = "gdUnit4 ${version} Console" + +@onready var header := $VBoxContainer/Header +@onready var title :RichTextLabel = $VBoxContainer/Header/header_title +@onready var output :RichTextLabel = $VBoxContainer/Console/TextEdit + +var _text_color :Color +var _function_color :Color +var _engine_type_color :Color +var _statistics = {} +var _summary = { + "total_count": 0, + "error_count": 0, + "failed_count": 0, + "skipped_count": 0, + "orphan_nodes": 0 +} + + +func _ready(): + init_colors() + GdUnitFonts.init_fonts(output) + GdUnit4Version.init_version_label(title) + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_message.connect(_on_gdunit_message) + GdUnitSignals.instance().gdunit_client_connected.connect(_on_gdunit_client_connected) + GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_gdunit_client_disconnected) + output.clear() + + +func _notification(what): + if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: + init_colors() + if what == NOTIFICATION_PREDELETE: + GdUnitSignals.instance().gdunit_event.disconnect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_message.disconnect(_on_gdunit_message) + GdUnitSignals.instance().gdunit_client_connected.disconnect(_on_gdunit_client_connected) + GdUnitSignals.instance().gdunit_client_disconnected.disconnect(_on_gdunit_client_disconnected) + + +func init_colors() -> void: + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + var settings := plugin.get_editor_interface().get_editor_settings() + _text_color = settings.get_setting("text_editor/theme/highlighting/text_color") + _function_color = settings.get_setting("text_editor/theme/highlighting/function_color") + _engine_type_color = settings.get_setting("text_editor/theme/highlighting/engine_type_color") + + +func init_statistics(event :GdUnitEvent) : + _statistics["total_count"] = event.total_count() + _statistics["error_count"] = 0 + _statistics["failed_count"] = 0 + _statistics["skipped_count"] = 0 + _statistics["orphan_nodes"] = 0 + _summary["total_count"] += event.total_count() + + +func reset_statistics() -> void: + for k in _statistics.keys(): + _statistics[k] = 0 + for k in _summary.keys(): + _summary[k] = 0 + + +func update_statistics(event :GdUnitEvent) : + _statistics["error_count"] += event.error_count() + _statistics["failed_count"] += event.failed_count() + _statistics["skipped_count"] += event.skipped_count() + _statistics["orphan_nodes"] += event.orphan_nodes() + _summary["error_count"] += event.error_count() + _summary["failed_count"] += event.failed_count() + _summary["skipped_count"] += event.skipped_count() + _summary["orphan_nodes"] += event.orphan_nodes() + + +func print_message(message :String, color :Color = _text_color, indent :int = 0) -> void: + for i in indent: + output.push_indent(1) + output.push_color(color) + output.append_text(message) + output.pop() + for i in indent: + output.pop() + + +func println_message(message :String, color :Color = _text_color, indent :int = -1) -> void: + print_message(message, color, indent) + output.newline() + + +func _on_gdunit_event(event :GdUnitEvent): + match event.type(): + GdUnitEvent.INIT: + reset_statistics() + + GdUnitEvent.STOP: + print_message("Summary:", Color.DODGER_BLUE) + println_message("| %d total | %d error | %d failed | %d skipped | %d orphans |" % [_summary["total_count"], _summary["error_count"], _summary["failed_count"], _summary["skipped_count"], _summary["orphan_nodes"]], _text_color, 1) + print_message("[wave][/wave]") + + GdUnitEvent.TESTSUITE_BEFORE: + init_statistics(event) + print_message("Execute: ", Color.DODGER_BLUE) + println_message(event._suite_name, _engine_type_color) + + GdUnitEvent.TESTSUITE_AFTER: + update_statistics(event) + if not event.reports().is_empty(): + var report :GdUnitReport = event.reports().front() + println_message("\t" +event._suite_name, _engine_type_color) + println_message("line %d %s" % [report._line_number, report._message], _text_color, 2) + if event.is_success(): + print_message("[wave]PASSED[/wave]", Color.LIGHT_GREEN) + else: + print_message("[shake rate=5 level=10][b]FAILED[/b][/shake]", Color.FIREBRICK) + print_message(" | %d total | %d error | %d failed | %d skipped | %d orphans |" % [_statistics["total_count"], _statistics["error_count"], _statistics["failed_count"], _statistics["skipped_count"], _statistics["orphan_nodes"]]) + println_message("%+12s" % LocalTime.elapsed(event.elapsed_time())) + println_message(" ") + + GdUnitEvent.TESTCASE_BEFORE: + var spaces = "-%d" % (80 - event._suite_name.length()) + print_message(event._suite_name, _engine_type_color, 1) + print_message(":") + print_message(("%"+spaces+"s") % event._test_name, _function_color) + + GdUnitEvent.TESTCASE_AFTER: + var reports := event.reports() + update_statistics(event) + if event.is_success(): + print_message("PASSED", Color.LIGHT_GREEN) + elif event.is_skipped(): + print_message("SKIPPED", Color.GOLDENROD) + elif event.is_error() or event.is_failed(): + print_message("[wave]FAILED[/wave]", Color.FIREBRICK) + elif event.is_warning(): + print_message("WARNING", Color.YELLOW) + println_message(" %+12s" % LocalTime.elapsed(event.elapsed_time())) + + var report :GdUnitReport = null if reports.is_empty() else reports[0] + if report: + println_message("line %d %s" % [report._line_number, report._message], _text_color, 2) + + +func _on_gdunit_client_connected(client_id :int) -> void: + output.clear() + output.append_text("[color=#9887c4]GdUnit Test Client connected with id %d[/color]\n" % client_id) + output.newline() + + +func _on_gdunit_client_disconnected(client_id :int) -> void: + output.append_text("[color=#9887c4]GdUnit Test Client disconnected with id %d[/color]\n" % client_id) + output.newline() + + +func _on_gdunit_message(message :String): + output.newline() + output.append_text(message) + output.newline() diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.tscn b/addons/gdUnit4/src/ui/GdUnitConsole.tscn new file mode 100644 index 0000000..6aa9c78 --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitConsole.tscn @@ -0,0 +1,61 @@ +[gd_scene load_steps=2 format=3 uid="uid://dm0wvfyeew7vd"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitConsole.gd" id="1"] + +[node name="Control" type="Control"] +use_parent_material = true +clip_contents = true +custom_minimum_size = Vector2(0, 200) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +use_parent_material = true +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Header" type="PanelContainer" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +auto_translate = false +localize_numeral_system = false +mouse_filter = 2 + +[node name="header_title" type="RichTextLabel" parent="VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +auto_translate = false +localize_numeral_system = false +mouse_filter = 2 +bbcode_enabled = true +scroll_active = false +autowrap_mode = 0 +shortcut_keys_enabled = false + +[node name="Console" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="TextEdit" type="RichTextLabel" parent="VBoxContainer/Console"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +bbcode_enabled = true +scroll_following = true diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd b/addons/gdUnit4/src/ui/GdUnitFonts.gd new file mode 100644 index 0000000..46ee80c --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd @@ -0,0 +1,44 @@ +class_name GdUnitFonts +extends RefCounted + +const FONT_MONO = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf" +const FONT_MONO_BOLT = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf" +const FONT_MONO_BOLT_ITALIC = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf" +const FONT_MONO_ITALIC = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf" + + +static func init_fonts(item: CanvasItem) -> float: + # add a default fallback font + item.set("theme_override_fonts/font", load_and_resize_font(FONT_MONO, 16)) + item.set("theme_override_fonts/normal_font", load_and_resize_font(FONT_MONO, 16)) + item.set("theme_override_font_sizes/font_size", 16) + if Engine.is_editor_hint(): + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + var settings := plugin.get_editor_interface().get_editor_settings() + var scale_factor := plugin.get_editor_interface().get_editor_scale() + var font_size :float = settings.get_setting("interface/editor/main_font_size") + font_size *= scale_factor + var font_mono := load_and_resize_font(FONT_MONO, font_size) + item.set("theme_override_fonts/normal_font", font_mono) + item.set("theme_override_fonts/bold_font", load_and_resize_font(FONT_MONO_BOLT, font_size)) + item.set("theme_override_fonts/italics_font", load_and_resize_font(FONT_MONO_ITALIC, font_size)) + item.set("theme_override_fonts/bold_italics_font", load_and_resize_font(FONT_MONO_BOLT_ITALIC, font_size)) + item.set("theme_override_fonts/mono_font", font_mono) + item.set("theme_override_font_sizes/font_size", font_size) + item.set("theme_override_font_sizes/normal_font_size", font_size) + item.set("theme_override_font_sizes/bold_font_size", font_size) + item.set("theme_override_font_sizes/italics_font_size", font_size) + item.set("theme_override_font_sizes/bold_italics_font_size", font_size) + item.set("theme_override_font_sizes/mono_font_size", font_size) + return font_size + return 16.0 + + +static func load_and_resize_font(font_resource: String, size: float) -> Font: + var font :FontFile = ResourceLoader.load(font_resource, "FontFile") + if font == null: + push_error("Can't load font '%s'" % font_resource) + return null + var resized_font := font.duplicate() + resized_font.fixed_size = int(size) + return resized_font diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd new file mode 100644 index 0000000..1eb0eba --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -0,0 +1,85 @@ +@tool +class_name GdUnitInspecor +extends Panel + + +var _command_handler := GdUnitCommandHandler.instance() + + +func _ready(): + if Engine.is_editor_hint(): + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + _getEditorThemes(plugin.get_editor_interface()) + GdUnitCommandHandler.instance().gdunit_runner_start.connect(func(): + var tab_container :TabContainer = get_parent_control() + for tab_index in tab_container.get_tab_count(): + if tab_container.get_tab_title(tab_index) == "GdUnit": + tab_container.set_current_tab(tab_index) + ) + + +func _enter_tree(): + if Engine.is_editor_hint(): + add_script_editor_context_menu() + add_file_system_dock_context_menu() + + +func _exit_tree(): + if Engine.is_editor_hint(): + ScriptEditorControls.unregister_context_menu() + EditorFileSystemControls.unregister_context_menu() + + +func _process(_delta): + _command_handler._do_process() + + +func _getEditorThemes(interface :EditorInterface) -> void: + if interface == null: + return + # example to access current theme + #var editiorTheme := interface.get_base_control().theme + # setup inspector button icons + #var stylebox_types :PackedStringArray = editiorTheme.get_stylebox_type_list() + #for stylebox_type in stylebox_types: + #prints("stylebox_type", stylebox_type) + # if "Tree" == stylebox_type: + # prints(editiorTheme.get_stylebox_list(stylebox_type)) + #var style:StyleBoxFlat = editiorTheme.get_stylebox("panel", "Tree") + #style.bg_color = Color.RED + #var locale = interface.get_editor_settings().get_setting("interface/editor/editor_language") + #sessions_label.add_theme_color_override("font_color", get_color("contrast_color_2", "Editor")) + #status_label.add_theme_color_override("font_color", get_color("contrast_color_2", "Editor")) + #no_sessions_label.add_theme_color_override("font_color", get_color("contrast_color_2", "Editor")) + + +# Context menu registrations ---------------------------------------------------------------------- +func add_file_system_dock_context_menu() -> void: + var is_test_suite := func is_visible(script :Script, is_test_suite :bool): + if script == null: + return true + return GdObjects.is_test_suite(script) == is_test_suite + var menu :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Testsuites", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Testsuites", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG)), + ] + EditorFileSystemControls.register_context_menu(menu) + + +func add_script_editor_context_menu(): + var is_test_suite := func is_visible(script :Script, is_test_suite :bool): + return GdObjects.is_test_suite(script) == is_test_suite + var menu :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Tests", is_test_suite.bind(true),_command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.CREATE_TEST, "Create Test", is_test_suite.bind(false), _command_handler.command(GdUnitCommandHandler.CMD_CREATE_TESTCASE)) + ] + ScriptEditorControls.register_context_menu(menu) + + +func _on_MainPanel_run_testsuite(test_suite_paths :Array, debug :bool): + _command_handler.cmd_run_test_suites(test_suite_paths, debug) + + +func _on_MainPanel_run_testcase(resource_path :String, test_case :String, test_param_index :int, debug :bool): + _command_handler.cmd_run_test_case(resource_path, test_case, test_param_index, debug) diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.tscn b/addons/gdUnit4/src/ui/GdUnitInspector.tscn new file mode 100644 index 0000000..7bfc79c --- /dev/null +++ b/addons/gdUnit4/src/ui/GdUnitInspector.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=7 format=3 uid="uid://mpo5o6d4uybu"] + +[ext_resource type="PackedScene" uid="uid://dx7xy4dgi3wwb" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn" id="1"] +[ext_resource type="PackedScene" uid="uid://dva3tonxsxrlk" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn" id="2"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn" id="3"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn" id="4"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"] +[ext_resource type="PackedScene" uid="uid://bqfpidewtpeg0" path="res://addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn" id="7"] + +[node name="GdUnit" type="Panel"] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 11 +size_flags_vertical = 3 +focus_mode = 2 +script = ExtResource("5") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +use_parent_material = true +clip_contents = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_vertical = 11 + +[node name="Header" type="VBoxContainer" parent="VBoxContainer"] +use_parent_material = true +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 9 + +[node name="ToolBar" parent="VBoxContainer/Header" instance=ExtResource("1")] +layout_mode = 2 +size_flags_vertical = 1 + +[node name="ProgressBar" parent="VBoxContainer/Header" instance=ExtResource("2")] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 +size_flags_horizontal = 5 + +[node name="StatusBar" parent="VBoxContainer/Header" instance=ExtResource("3")] +layout_mode = 2 +size_flags_horizontal = 11 + +[node name="MainPanel" parent="VBoxContainer" instance=ExtResource("7")] +layout_mode = 2 + +[node name="Monitor" parent="VBoxContainer" instance=ExtResource("4")] +layout_mode = 2 + +[connection signal="failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_StatusBar_failure_next"] +[connection signal="failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_StatusBar_failure_prevous"] +[connection signal="run_testcase" from="VBoxContainer/MainPanel" to="." method="_on_MainPanel_run_testcase"] +[connection signal="run_testsuite" from="VBoxContainer/MainPanel" to="." method="_on_MainPanel_run_testsuite"] +[connection signal="jump_to_orphan_nodes" from="VBoxContainer/Monitor" to="VBoxContainer/MainPanel" method="_on_Monitor_jump_to_orphan_nodes"] diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd b/addons/gdUnit4/src/ui/ScriptEditorControls.gd new file mode 100644 index 0000000..29d2992 --- /dev/null +++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd @@ -0,0 +1,128 @@ +# A tool to provide extended script editor functionallity +class_name ScriptEditorControls +extends RefCounted + + +# https://github.com/godotengine/godot/blob/master/editor/plugins/script_editor_plugin.h +# the Editor menu popup items +enum { + FILE_NEW, + FILE_NEW_TEXTFILE, + FILE_OPEN, + FILE_REOPEN_CLOSED, + FILE_OPEN_RECENT, + FILE_SAVE, + FILE_SAVE_AS, + FILE_SAVE_ALL, + FILE_THEME, + FILE_RUN, + FILE_CLOSE, + CLOSE_DOCS, + CLOSE_ALL, + CLOSE_OTHER_TABS, + TOGGLE_SCRIPTS_PANEL, + SHOW_IN_FILE_SYSTEM, + FILE_COPY_PATH, + FILE_TOOL_RELOAD_SOFT, + SEARCH_IN_FILES, + REPLACE_IN_FILES, + SEARCH_HELP, + SEARCH_WEBSITE, + HELP_SEARCH_FIND, + HELP_SEARCH_FIND_NEXT, + HELP_SEARCH_FIND_PREVIOUS, + WINDOW_MOVE_UP, + WINDOW_MOVE_DOWN, + WINDOW_NEXT, + WINDOW_PREV, + WINDOW_SORT, + WINDOW_SELECT_BASE = 100 +} + + +# Returns the EditorInterface instance +static func editor_interface() -> EditorInterface: + if not Engine.has_meta("GdUnitEditorPlugin"): + return null + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + return plugin.get_editor_interface() + + +# Returns the ScriptEditor instance +static func script_editor() -> ScriptEditor: + return editor_interface().get_script_editor() + + +# Saves the given script and closes if requested by +# The script is saved when is opened in the editor. +# The script is closed when is set to true. +static func save_an_open_script(script_path :String, close := false) -> bool: + #prints("save_an_open_script", script_path, close) + if !Engine.is_editor_hint(): + return false + var interface := editor_interface() + var editor := script_editor() + var editor_popup := _menu_popup() + # search for the script in all opened editor scrips + for open_script in editor.get_open_scripts(): + if open_script.resource_path == script_path: + # select the script in the editor + interface.edit_script(open_script, 0); + # save and close + editor_popup.id_pressed.emit(FILE_SAVE) + if close: + editor_popup.id_pressed.emit(FILE_CLOSE) + return true + return false + + +# Saves all opened script +static func save_all_open_script() -> void: + if Engine.is_editor_hint(): + _menu_popup().id_pressed.emit(FILE_SAVE_ALL) + + +static func close_open_editor_scripts() -> void: + if Engine.is_editor_hint(): + _menu_popup().id_pressed.emit(CLOSE_ALL) + + +# Edits the given script. +# The script is openend in the current editor and selected in the file system dock. +# The line and column on which to open the script can also be specified. +# The script will be open with the user-configured editor for the script's language which may be an external editor. +static func edit_script(script_path :String, line_number :int = -1): + var interface := editor_interface() + var file_system := interface.get_resource_filesystem() + file_system.update_file(script_path) + var file_system_dock := interface.get_file_system_dock() + file_system_dock.navigate_to_path(script_path) + interface.select_file(script_path) + var script = load(script_path) + interface.edit_script(script, line_number) + + +# Register the given context menu to the current script editor +# Is called when the plugin is activated +# The active script is connected to the ScriptEditorContextMenuHandler +static func register_context_menu(menu :Array[GdUnitContextMenuItem]) -> void: + Engine.get_main_loop().root.call_deferred("add_child", ScriptEditorContextMenuHandler.new(menu, script_editor())) + + +# Unregisteres all registerend context menus and gives the ScriptEditorContextMenuHandler> free +# Is called when the plugin is deactivated +static func unregister_context_menu() -> void: + ScriptEditorContextMenuHandler.dispose(script_editor()) + + +static func _menu_popup() -> PopupMenu: + return script_editor().get_child(0).get_child(0).get_child(0).get_popup() + + +static func _print_menu(popup :PopupMenu): + for itemIndex in popup.item_count: + prints( "get_item_id", popup.get_item_id(itemIndex)) + prints( "get_item_accelerator", popup.get_item_accelerator(itemIndex)) + prints( "get_item_shortcut", popup.get_item_shortcut(itemIndex)) + prints( "get_item_text", popup.get_item_text(itemIndex)) + prints() diff --git a/addons/gdUnit4/src/ui/assets/PlayDebug.svg b/addons/gdUnit4/src/ui/assets/PlayDebug.svg new file mode 100644 index 0000000..a0be440 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/PlayDebug.svg @@ -0,0 +1,121 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/PlayDebug.svg.import b/addons/gdUnit4/src/ui/assets/PlayDebug.svg.import new file mode 100644 index 0000000..d6a3fcc --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/PlayDebug.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tkrsqx2oxw6o" +path="res://.godot/imported/PlayDebug.svg-d3618ec14e2e4cb6b467c3249916f8dd.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/PlayDebug.svg" +dest_files=["res://.godot/imported/PlayDebug.svg-d3618ec14e2e4cb6b467c3249916f8dd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/PlayOverall.svg b/addons/gdUnit4/src/ui/assets/PlayOverall.svg new file mode 100644 index 0000000..5efccab --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/PlayOverall.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/PlayOverall.svg.import b/addons/gdUnit4/src/ui/assets/PlayOverall.svg.import new file mode 100644 index 0000000..4e73567 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/PlayOverall.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://de1q5raia84bn" +path="res://.godot/imported/PlayOverall.svg-d07157735d6bab5d74465733e8213328.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/PlayOverall.svg" +dest_files=["res://.godot/imported/PlayOverall.svg-d07157735d6bab5d74465733e8213328.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/TestCase.svg b/addons/gdUnit4/src/ui/assets/TestCase.svg new file mode 100644 index 0000000..e6a40f0 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCase.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/TestCase.svg.import b/addons/gdUnit4/src/ui/assets/TestCase.svg.import new file mode 100644 index 0000000..0e29cda --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCase.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://chwatiivlcovb" +path="res://.godot/imported/TestCase.svg-f6ee172ad0e725d3612bec1b6f3c8078.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/TestCase.svg" +dest_files=["res://.godot/imported/TestCase.svg-f6ee172ad0e725d3612bec1b6f3c8078.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/TestCaseError.svg b/addons/gdUnit4/src/ui/assets/TestCaseError.svg new file mode 100644 index 0000000..0858086 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCaseError.svg @@ -0,0 +1,65 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/TestCaseError.svg.import b/addons/gdUnit4/src/ui/assets/TestCaseError.svg.import new file mode 100644 index 0000000..3b8ea16 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCaseError.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bafys3jjkjhqw" +path="res://.godot/imported/TestCaseError.svg-373307086979f3f0e012eb3660cc91ec.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/TestCaseError.svg" +dest_files=["res://.godot/imported/TestCaseError.svg-373307086979f3f0e012eb3660cc91ec.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/TestCaseFailed.svg b/addons/gdUnit4/src/ui/assets/TestCaseFailed.svg new file mode 100644 index 0000000..10531b8 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCaseFailed.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/TestCaseFailed.svg.import b/addons/gdUnit4/src/ui/assets/TestCaseFailed.svg.import new file mode 100644 index 0000000..636cc7c --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCaseFailed.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4ej20o0cedro" +path="res://.godot/imported/TestCaseFailed.svg-df47525fd14d5e4149690cacd8eb08db.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/TestCaseFailed.svg" +dest_files=["res://.godot/imported/TestCaseFailed.svg-df47525fd14d5e4149690cacd8eb08db.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg b/addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg new file mode 100644 index 0000000..b080b69 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg.import b/addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg.import new file mode 100644 index 0000000..cee02fc --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://4yciagtse0nx" +path="res://.godot/imported/TestCaseSuccess.svg-aaf852c6aeda68c93a410c7480502895.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg" +dest_files=["res://.godot/imported/TestCaseSuccess.svg-aaf852c6aeda68c93a410c7480502895.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/TestCase_error_orphan.tres b/addons/gdUnit4/src/ui/assets/TestCase_error_orphan.tres new file mode 100644 index 0000000..49a70cd --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCase_error_orphan.tres @@ -0,0 +1,12 @@ +[gd_resource type="AnimatedTexture" load_steps=3 format=2] + +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg" type="Texture2D" id=1] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg" type="Texture2D" id=2] + +[resource] +flags = 4 +frames = 2 +fps = 1.1 +frame_0/texture = ExtResource( 1 ) +frame_1/texture = ExtResource( 2 ) +frame_1/delay_sec = 0.0 diff --git a/addons/gdUnit4/src/ui/assets/TestCase_failed_orphan.tres b/addons/gdUnit4/src/ui/assets/TestCase_failed_orphan.tres new file mode 100644 index 0000000..532b843 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCase_failed_orphan.tres @@ -0,0 +1,12 @@ +[gd_resource type="AnimatedTexture" load_steps=3 format=2] + +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg" type="Texture2D" id=1] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg" type="Texture2D" id=2] + +[resource] +flags = 4 +frames = 2 +fps = 1.1 +frame_0/texture = ExtResource( 2 ) +frame_1/texture = ExtResource( 1 ) +frame_1/delay_sec = 0.0 diff --git a/addons/gdUnit4/src/ui/assets/TestCase_success_orphan.tres b/addons/gdUnit4/src/ui/assets/TestCase_success_orphan.tres new file mode 100644 index 0000000..c1b8a33 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestCase_success_orphan.tres @@ -0,0 +1,12 @@ +[gd_resource type="AnimatedTexture" load_steps=3 format=2] + +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg" type="Texture2D" id=1] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg" type="Texture2D" id=2] + +[resource] +flags = 4 +frames = 2 +fps = 1.1 +frame_0/texture = ExtResource( 1 ) +frame_1/texture = ExtResource( 2 ) +frame_1/delay_sec = 0.0 diff --git a/addons/gdUnit4/src/ui/assets/TestSuite.svg b/addons/gdUnit4/src/ui/assets/TestSuite.svg new file mode 100644 index 0000000..3d9b84c --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestSuite.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/TestSuite.svg.import b/addons/gdUnit4/src/ui/assets/TestSuite.svg.import new file mode 100644 index 0000000..d23a52f --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/TestSuite.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bo6xbdqouosbs" +path="res://.godot/imported/TestSuite.svg-f3ba31540dedae19e6c1b7b050a1b5d7.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/TestSuite.svg" +dest_files=["res://.godot/imported/TestSuite.svg-f3ba31540dedae19e6c1b7b050a1b5d7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/clock.svg b/addons/gdUnit4/src/ui/assets/clock.svg new file mode 100644 index 0000000..88d9d07 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/clock.svg @@ -0,0 +1,151 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/clock.svg.import b/addons/gdUnit4/src/ui/assets/clock.svg.import new file mode 100644 index 0000000..9422c2c --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/clock.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dmu35vmwstrwg" +path="res://.godot/imported/clock.svg-b16f5d68e1dedc017f1ce1df1e590248.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/clock.svg" +dest_files=["res://.godot/imported/clock.svg-b16f5d68e1dedc017f1ce1df1e590248.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/errors.svg b/addons/gdUnit4/src/ui/assets/errors.svg new file mode 100644 index 0000000..c8af23c --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/errors.svg @@ -0,0 +1,57 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/errors.svg.import b/addons/gdUnit4/src/ui/assets/errors.svg.import new file mode 100644 index 0000000..05cf51c --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/errors.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bftg23n8uymlk" +path="res://.godot/imported/errors.svg-53aa38a5c5d3309095cd34f36952f16b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/errors.svg" +dest_files=["res://.godot/imported/errors.svg-53aa38a5c5d3309095cd34f36952f16b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/failures.svg b/addons/gdUnit4/src/ui/assets/failures.svg new file mode 100644 index 0000000..1a7a96e --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/failures.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/failures.svg.import b/addons/gdUnit4/src/ui/assets/failures.svg.import new file mode 100644 index 0000000..7270f85 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/failures.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://davt5rxc7ao4s" +path="res://.godot/imported/failures.svg-8659a946adf9b0616e867a8bf0855d3d.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/failures.svg" +dest_files=["res://.godot/imported/failures.svg-8659a946adf9b0616e867a8bf0855d3d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/icon.png b/addons/gdUnit4/src/ui/assets/icon.png new file mode 100644 index 0000000..eeac292 Binary files /dev/null and b/addons/gdUnit4/src/ui/assets/icon.png differ diff --git a/addons/gdUnit4/src/ui/assets/icon.png.import b/addons/gdUnit4/src/ui/assets/icon.png.import new file mode 100644 index 0000000..4bd6b84 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c7sk0yhd52lg3" +path="res://.godot/imported/icon.png-3b59f326b0d0310df661a9bddfa24566.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/icon.png" +dest_files=["res://.godot/imported/icon.png-3b59f326b0d0310df661a9bddfa24566.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg new file mode 100644 index 0000000..3993ec4 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg.import b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg.import new file mode 100644 index 0000000..f2c1fe8 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdl7p3rf6wpfw" +path="res://.godot/imported/TestCaseError1.svg-43d13ea25f9f8c66fecf5a0ab4a752ad.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseError1.svg" +dest_files=["res://.godot/imported/TestCaseError1.svg-43d13ea25f9f8c66fecf5a0ab4a752ad.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg new file mode 100644 index 0000000..364a44e --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg @@ -0,0 +1,166 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg.import b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg.import new file mode 100644 index 0000000..f5da025 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://fqf674gwq375" +path="res://.godot/imported/TestCaseError2.svg-27dc52b88f226d741b1f9c1294295841.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseError2.svg" +dest_files=["res://.godot/imported/TestCaseError2.svg-27dc52b88f226d741b1f9c1294295841.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed.svg b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed.svg new file mode 100644 index 0000000..10531b8 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed.svg.import b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed.svg.import new file mode 100644 index 0000000..23d9eb3 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqvlmgi6qpre0" +path="res://.godot/imported/TestCaseFailed.svg-151182f42f6b32bd8c6dc168e9469b54.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed.svg" +dest_files=["res://.godot/imported/TestCaseFailed.svg-151182f42f6b32bd8c6dc168e9469b54.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg new file mode 100644 index 0000000..1e36768 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg @@ -0,0 +1,69 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg.import b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg.import new file mode 100644 index 0000000..6d870d6 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://deo4r8koimsfd" +path="res://.godot/imported/TestCaseFailed1.svg-32464e8f6fa7f74ad74a7534dfaba019.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed1.svg" +dest_files=["res://.godot/imported/TestCaseFailed1.svg-32464e8f6fa7f74ad74a7534dfaba019.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg new file mode 100644 index 0000000..7112f3f --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg @@ -0,0 +1,166 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg.import b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg.import new file mode 100644 index 0000000..4934f26 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ctl52ddcptdxb" +path="res://.godot/imported/TestCaseFailed2.svg-aa38e10f09edf43b31b1c0a4caf549c5.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseFailed2.svg" +dest_files=["res://.godot/imported/TestCaseFailed2.svg-aa38e10f09edf43b31b1c0a4caf549c5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg new file mode 100644 index 0000000..b8e15d8 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg @@ -0,0 +1,129 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg.import b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg.import new file mode 100644 index 0000000..fb750b7 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bufumcx0iq38d" +path="res://.godot/imported/TestCaseSuccess1.svg-3eadebb620a3275b67f53d505bc0f96b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess1.svg" +dest_files=["res://.godot/imported/TestCaseSuccess1.svg-3eadebb620a3275b67f53d505bc0f96b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg new file mode 100644 index 0000000..f8448b0 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg @@ -0,0 +1,166 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg.import b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg.import new file mode 100644 index 0000000..567d828 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://clj5nj84vnocn" +path="res://.godot/imported/TestCaseSuccess2.svg-7767c6ebe7bd14d031b8c87b24e08595.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/TestCaseSuccess2.svg" +dest_files=["res://.godot/imported/TestCaseSuccess2.svg-7767c6ebe7bd14d031b8c87b24e08595.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_animated_icon.tres b/addons/gdUnit4/src/ui/assets/orphan/orphan_animated_icon.tres new file mode 100644 index 0000000..832928c --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_animated_icon.tres @@ -0,0 +1,32 @@ +[gd_resource type="AnimatedTexture" load_steps=7 format=2] + +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg" type="Texture2D" id=1] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg" type="Texture2D" id=2] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg" type="Texture2D" id=3] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg" type="Texture2D" id=4] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg" type="Texture2D" id=5] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg" type="Texture2D" id=6] + +[resource] +flags = 4 +frames = 10 +fps = 9.0 +frame_0/texture = ExtResource( 5 ) +frame_1/texture = ExtResource( 4 ) +frame_1/delay_sec = 0.0 +frame_2/texture = ExtResource( 1 ) +frame_2/delay_sec = 0.0 +frame_3/texture = ExtResource( 2 ) +frame_3/delay_sec = 0.0 +frame_4/texture = ExtResource( 3 ) +frame_4/delay_sec = 0.0 +frame_5/texture = ExtResource( 6 ) +frame_5/delay_sec = 0.5 +frame_6/texture = ExtResource( 3 ) +frame_6/delay_sec = 0.0 +frame_7/texture = ExtResource( 2 ) +frame_7/delay_sec = 0.0 +frame_8/texture = ExtResource( 1 ) +frame_8/delay_sec = 0.0 +frame_9/texture = ExtResource( 4 ) +frame_9/delay_sec = 0.0 diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg new file mode 100644 index 0000000..64612eb --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg.import new file mode 100644 index 0000000..5b3d517 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cqrikobu314r3" +path="res://.godot/imported/orphan_green.svg-9ce01031b489dea619e91196cb66dce9.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg" +dest_files=["res://.godot/imported/orphan_green.svg-9ce01031b489dea619e91196cb66dce9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red1.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_red1.svg new file mode 100644 index 0000000..16883e1 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red1.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red1.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_red1.svg.import new file mode 100644 index 0000000..e6af64e --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red1.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://da6s7yd3mpcbp" +path="res://.godot/imported/orphan_red1.svg-8ddad0e8a9c8884f19621c8f733ce341.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red1.svg" +dest_files=["res://.godot/imported/orphan_red1.svg-8ddad0e8a9c8884f19621c8f733ce341.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg new file mode 100644 index 0000000..7a124c0 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg.import new file mode 100644 index 0000000..639d3ca --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cnvenq5hici48" +path="res://.godot/imported/orphan_red2.svg-3ab7610a37b37b2e46ca7faaba5a2c40.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red2.svg" +dest_files=["res://.godot/imported/orphan_red2.svg-3ab7610a37b37b2e46ca7faaba5a2c40.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg new file mode 100644 index 0000000..bb73371 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg.import new file mode 100644 index 0000000..14993e0 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbnqfoh4n6bj5" +path="res://.godot/imported/orphan_red3.svg-2f1ae0a474446c730d13e401878da7f2.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red3.svg" +dest_files=["res://.godot/imported/orphan_red3.svg-2f1ae0a474446c730d13e401878da7f2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg new file mode 100644 index 0000000..67b172d --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg.import new file mode 100644 index 0000000..f80ecd4 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjm3fxk3ieapk" +path="res://.godot/imported/orphan_red4.svg-b6aa919f74c7c5f9e6a5a87210e2194e.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red4.svg" +dest_files=["res://.godot/imported/orphan_red4.svg-b6aa919f74c7c5f9e6a5a87210e2194e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg new file mode 100644 index 0000000..38f86b5 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg.import new file mode 100644 index 0000000..e62f742 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://nj6xpoxpf6i5" +path="res://.godot/imported/orphan_red5.svg-94eac4c39a2d4020f502eab72982b3f2.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red5.svg" +dest_files=["res://.godot/imported/orphan_red5.svg-94eac4c39a2d4020f502eab72982b3f2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg new file mode 100644 index 0000000..b143d54 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg.import new file mode 100644 index 0000000..61256b8 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bylpj2dbacbaw" +path="res://.godot/imported/orphan_red6.svg-fb927f6dc4442199133d2e25e9d9d21a.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red6.svg" +dest_files=["res://.godot/imported/orphan_red6.svg-fb927f6dc4442199133d2e25e9d9d21a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg b/addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg new file mode 100644 index 0000000..bd9ae54 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg @@ -0,0 +1,156 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg.import b/addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg.import new file mode 100644 index 0000000..f78f85d --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bpqfgn22gmpjm" +path="res://.godot/imported/orphan_red7.svg-4b4ab8aec1bc8343d5df6b64a24f7c22.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/orphan/orphan_red7.svg" +dest_files=["res://.godot/imported/orphan_red7.svg-4b4ab8aec1bc8343d5df6b64a24f7c22.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/running.png b/addons/gdUnit4/src/ui/assets/running.png new file mode 100644 index 0000000..0150b58 Binary files /dev/null and b/addons/gdUnit4/src/ui/assets/running.png differ diff --git a/addons/gdUnit4/src/ui/assets/running.png.import b/addons/gdUnit4/src/ui/assets/running.png.import new file mode 100644 index 0000000..6e2868f --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/running.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://6x6ew21g57u" +path="res://.godot/imported/running.png-0263ac82d8ccdea59d5efbcae7336155.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/running.png" +dest_files=["res://.godot/imported/running.png-0263ac82d8ccdea59d5efbcae7336155.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/ui/assets/spinner.tres b/addons/gdUnit4/src/ui/assets/spinner.tres new file mode 100644 index 0000000..c200d88 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner.tres @@ -0,0 +1,30 @@ +[gd_resource type="AnimatedTexture" load_steps=9 format=3 uid="uid://ck3p6wmq42arh"] + +[ext_resource type="Texture2D" uid="uid://cct6crbhix7u8" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress7.svg" id="1"] +[ext_resource type="Texture2D" uid="uid://bkj6kjyjyi7cd" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress5.svg" id="2"] +[ext_resource type="Texture2D" uid="uid://ddxpytkht0m5p" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress1.svg" id="3"] +[ext_resource type="Texture2D" uid="uid://dqc521iq12a7l" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress8.svg" id="4"] +[ext_resource type="Texture2D" uid="uid://dowca7ike2thl" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress2.svg" id="5"] +[ext_resource type="Texture2D" uid="uid://bsljbs1aiyels" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress6.svg" id="6"] +[ext_resource type="Texture2D" uid="uid://cwh8md6qipmdw" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress3.svg" id="7"] +[ext_resource type="Texture2D" uid="uid://dm0jpqdjetv2c" path="res://addons/gdUnit4/src/ui/assets/spinner/Progress4.svg" id="8"] + +[resource] +frames = 8 +speed_scale = 2.5 +frame_0/texture = ExtResource("3") +frame_0/duration = 0.2 +frame_1/texture = ExtResource("5") +frame_1/duration = 0.2 +frame_2/texture = ExtResource("7") +frame_2/duration = 0.2 +frame_3/texture = ExtResource("8") +frame_3/duration = 0.2 +frame_4/texture = ExtResource("2") +frame_4/duration = 0.2 +frame_5/texture = ExtResource("6") +frame_5/duration = 0.2 +frame_6/texture = ExtResource("1") +frame_6/duration = 0.2 +frame_7/texture = ExtResource("4") +frame_7/duration = 0.2 diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress1.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress1.svg new file mode 100644 index 0000000..07505dd --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress1.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress1.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress1.svg.import new file mode 100644 index 0000000..17fe27b --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress1.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddxpytkht0m5p" +path="res://.godot/imported/Progress1.svg-baca226eb5c6ca50a0b5f3af77fe615c.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress1.svg" +dest_files=["res://.godot/imported/Progress1.svg-baca226eb5c6ca50a0b5f3af77fe615c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress2.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress2.svg new file mode 100644 index 0000000..0a48f7d --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress2.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress2.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress2.svg.import new file mode 100644 index 0000000..250618b --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress2.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dowca7ike2thl" +path="res://.godot/imported/Progress2.svg-6a0cbcb42a8df535c533cf79599952d6.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress2.svg" +dest_files=["res://.godot/imported/Progress2.svg-6a0cbcb42a8df535c533cf79599952d6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress3.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress3.svg new file mode 100644 index 0000000..a7f0f9c --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress3.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress3.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress3.svg.import new file mode 100644 index 0000000..662380f --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress3.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cwh8md6qipmdw" +path="res://.godot/imported/Progress3.svg-0b465f11e95f98f7b157a0bf0ded40c1.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress3.svg" +dest_files=["res://.godot/imported/Progress3.svg-0b465f11e95f98f7b157a0bf0ded40c1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress4.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress4.svg new file mode 100644 index 0000000..1719209 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress4.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress4.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress4.svg.import new file mode 100644 index 0000000..023ebb8 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress4.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dm0jpqdjetv2c" +path="res://.godot/imported/Progress4.svg-09def7d3fee66ec2764c9bbd72c3a961.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress4.svg" +dest_files=["res://.godot/imported/Progress4.svg-09def7d3fee66ec2764c9bbd72c3a961.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress5.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress5.svg new file mode 100644 index 0000000..7289b7b --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress5.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress5.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress5.svg.import new file mode 100644 index 0000000..289c8a9 --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress5.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkj6kjyjyi7cd" +path="res://.godot/imported/Progress5.svg-9874b4bd1c734fd859a28d95960c17c5.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress5.svg" +dest_files=["res://.godot/imported/Progress5.svg-9874b4bd1c734fd859a28d95960c17c5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress6.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress6.svg new file mode 100644 index 0000000..3deba6d --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress6.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress6.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress6.svg.import new file mode 100644 index 0000000..7259c6a --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress6.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bsljbs1aiyels" +path="res://.godot/imported/Progress6.svg-0a3b2b954e3a285cee1f29222aac701b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress6.svg" +dest_files=["res://.godot/imported/Progress6.svg-0a3b2b954e3a285cee1f29222aac701b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress7.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress7.svg new file mode 100644 index 0000000..546155d --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress7.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress7.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress7.svg.import new file mode 100644 index 0000000..85b3ebc --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress7.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cct6crbhix7u8" +path="res://.godot/imported/Progress7.svg-e824844cb9cfdf076f9196cf47098a7d.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress7.svg" +dest_files=["res://.godot/imported/Progress7.svg-e824844cb9cfdf076f9196cf47098a7d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress8.svg b/addons/gdUnit4/src/ui/assets/spinner/Progress8.svg new file mode 100644 index 0000000..b56ffcb --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress8.svg @@ -0,0 +1 @@ + diff --git a/addons/gdUnit4/src/ui/assets/spinner/Progress8.svg.import b/addons/gdUnit4/src/ui/assets/spinner/Progress8.svg.import new file mode 100644 index 0000000..6d3c23d --- /dev/null +++ b/addons/gdUnit4/src/ui/assets/spinner/Progress8.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dqc521iq12a7l" +path="res://.godot/imported/Progress8.svg-b37d84176a257f378f0f5dff81bfc322.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/ui/assets/spinner/Progress8.svg" +dest_files=["res://.godot/imported/Progress8.svg-b37d84176a257f378f0f5dff81bfc322.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd new file mode 100644 index 0000000..3d8cf9b --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd @@ -0,0 +1,90 @@ +class_name EditorFileSystemContextMenuHandler +extends Control + + +var _context_menus := Dictionary() + + +func _init(context_menus :Array[GdUnitContextMenuItem]): + set_name("EditorFileSystemContextMenuHandler") + for menu in context_menus: + _context_menus[menu.id] = menu + var popup := _menu_popup() + var file_tree := _file_tree() + popup.about_to_popup.connect(on_context_menu_show.bind(popup, file_tree)) + popup.id_pressed.connect(on_context_menu_pressed.bind(file_tree)) + + +static func dispose(): + var handler :EditorFileSystemContextMenuHandler = Engine.get_main_loop().root.find_child("EditorFileSystemContextMenuHandler*", false, false) + if handler: + var popup := _menu_popup() + if popup.about_to_popup.is_connected(Callable(handler, "on_context_menu_show")): + popup.about_to_popup.disconnect(Callable(handler, "on_context_menu_show")) + if popup.id_pressed.is_connected(Callable(handler, "on_context_menu_pressed")): + popup.id_pressed.disconnect(Callable(handler, "on_context_menu_pressed")) + Engine.get_main_loop().root.call_deferred("remove_child", handler) + handler.queue_free() + + +func on_context_menu_show(context_menu :PopupMenu, file_tree :Tree) -> void: + context_menu.add_separator() + var current_index := context_menu.get_item_count() + var selected_test_suites := collect_testsuites(_context_menus.values()[0], file_tree) + + for menu_id in _context_menus.keys(): + var menu_item :GdUnitContextMenuItem = _context_menus[menu_id] + if selected_test_suites.size() != 0: + context_menu.add_item(menu_item.name, menu_id) + context_menu.set_item_disabled(current_index, !menu_item.is_enabled(null)) + current_index += 1 + + +func on_context_menu_pressed(id :int, file_tree :Tree) -> void: + #prints("on_context_menu_pressed", id) + if !_context_menus.has(id): + return + var menu_item :GdUnitContextMenuItem = _context_menus[id] + var selected_test_suites := collect_testsuites(menu_item, file_tree) + menu_item.execute([selected_test_suites]) + + +func collect_testsuites(_menu_item :GdUnitContextMenuItem, file_tree :Tree) -> PackedStringArray: + var file_system := editor_interface().get_resource_filesystem() + var selected_item := file_tree.get_selected() + var selected_test_suites := PackedStringArray() + while selected_item: + var resource_path :String = selected_item.get_metadata(0) + var file_type := file_system.get_file_type(resource_path) + var is_dir := DirAccess.dir_exists_absolute(resource_path) + if is_dir: + selected_test_suites.append(resource_path) + elif is_dir or file_type == "GDScript" or file_type == "CSharpScript": + # find a performant way to check if the selected item a testsuite + #var resource := ResourceLoader.load(resource_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) + #prints("loaded", resource) + #if resource is GDScript and menu_item.is_visible(resource): + selected_test_suites.append(resource_path) + selected_item = file_tree.get_next_selected(selected_item) + return selected_test_suites + + +# Returns the EditorInterface instance +static func editor_interface() -> EditorInterface: + if not Engine.has_meta("GdUnitEditorPlugin"): + return null + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + return plugin.get_editor_interface() + + +# Returns the FileSystemDock instance +static func filesystem_dock() -> FileSystemDock: + return editor_interface().get_file_system_dock() + + +static func _file_tree() -> Tree: + return GdObjects.find_nodes_by_class(filesystem_dock(), "Tree", true)[-1] + + +static func _menu_popup() -> PopupMenu: + return GdObjects.find_nodes_by_class(filesystem_dock(), "PopupMenu")[-1] diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd new file mode 100644 index 0000000..edb5d80 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd @@ -0,0 +1,65 @@ +class_name GdUnitContextMenuItem + +enum MENU_ID { + TEST_RUN = 1000, + TEST_DEBUG = 1001, + TEST_RERUN = 1002, + CREATE_TEST = 1010, +} + + +func _init(p_id :MENU_ID, p_name :StringName, p_is_visible :Callable, p_command :GdUnitCommand): + assert(p_id != null, "(%s) missing parameter 'MENU_ID'" % p_name) + assert(p_is_visible != null, "(%s) missing parameter 'GdUnitCommand'" % p_name) + assert(p_command != null, "(%s) missing parameter 'GdUnitCommand'" % p_name) + self.id = p_id + self.name = p_name + self.command = p_command + self.visible = p_is_visible + + +var id: MENU_ID: + set(value): + id = value + get: + return id + + +var name: StringName: + set(value): + name = value + get: + return name + + +var command: GdUnitCommand: + set(value): + command = value + get: + return command + + +var visible: Callable: + set(value): + visible = value + get: + return visible + + +func shortcut() -> Shortcut: + return GdUnitCommandHandler.instance().get_shortcut(command.shortcut) + + +func is_enabled(script :Script) -> bool: + return command.is_enabled.call(script) + + +func is_visible(script :Script) -> bool: + return visible.call(script) + + +func execute(arguments := []) -> void: + if arguments.is_empty(): + command.runnable.call() + else: + command.runnable.callv(arguments) diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd new file mode 100644 index 0000000..cda0ca5 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd @@ -0,0 +1,81 @@ +class_name ScriptEditorContextMenuHandler +extends Control + +var _context_menus := Dictionary() +var _editor :ScriptEditor + + +func _init(context_menus :Array[GdUnitContextMenuItem], p_editor :ScriptEditor): + set_name("ScriptEditorContextMenuHandler") + for menu in context_menus: + _context_menus[menu.id] = menu + _editor = p_editor + p_editor.editor_script_changed.connect(on_script_changed) + on_script_changed(active_script()) + + +static func dispose(p_editor :ScriptEditor) -> void: + var handler :ScriptEditorContextMenuHandler = Engine.get_main_loop().root.find_child("ScriptEditorContextMenuHandler*", false, false) + if handler: + if p_editor.editor_script_changed.is_connected(handler.on_script_changed): + p_editor.editor_script_changed.disconnect(handler.on_script_changed) + Engine.get_main_loop().root.call_deferred("remove_child", handler) + handler.queue_free() + + +func _input(event): + if event is InputEventKey and event.is_pressed(): + for shortcut_action in _context_menus.values(): + var action :GdUnitContextMenuItem = shortcut_action + if action.shortcut().matches_event(event) and action.is_visible(active_script()): + #if not has_editor_focus(): + # return + action.execute() + accept_event() + return + + +func has_editor_focus() -> bool: + return Engine.get_main_loop().root.gui_get_focus_owner() == active_base_editor() + + +func on_script_changed(script :Script): + if script is Script: + var popups :Array[Node] = GdObjects.find_nodes_by_class(active_editor(), "PopupMenu", true) + for popup in popups: + if not popup.about_to_popup.is_connected(on_context_menu_show): + popup.about_to_popup.connect(on_context_menu_show.bind(script, popup)) + if not popup.id_pressed.is_connected(on_context_menu_pressed): + popup.id_pressed.connect(on_context_menu_pressed) + + +func on_context_menu_show(script :Script, context_menu :PopupMenu): + #prints("on_context_menu_show", _context_menus.keys(), context_menu, self) + context_menu.add_separator() + var current_index := context_menu.get_item_count() + for menu_id in _context_menus.keys(): + var menu_item :GdUnitContextMenuItem = _context_menus[menu_id] + if menu_item.is_visible(script): + context_menu.add_item(menu_item.name, menu_id) + context_menu.set_item_disabled(current_index, !menu_item.is_enabled(script)) + context_menu.set_item_shortcut(current_index, menu_item.shortcut(), true) + current_index += 1 + + +func on_context_menu_pressed(id :int): + if !_context_menus.has(id): + return + var menu_item :GdUnitContextMenuItem = _context_menus[id] + menu_item.execute() + + +func active_editor() -> ScriptEditorBase: + return _editor.get_current_editor() + + +func active_base_editor() -> TextEdit: + return active_editor().get_base_editor() + + +func active_script() -> Script: + return _editor.get_current_script() diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd new file mode 100644 index 0000000..ae6cbf9 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd @@ -0,0 +1,50 @@ +@tool +extends PanelContainer + +signal jump_to_orphan_nodes + +@onready var ICON_GREEN = load("res://addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg") +@onready var ICON_RED = load("res://addons/gdUnit4/src/ui/assets/orphan/orphan_animated_icon.tres") + +@onready var _time = $GridContainer/Time/value +@onready var _orphans = $GridContainer/Orphan/value +@onready var _orphan_button := $GridContainer/Orphan/Button + +var total_elapsed_time := 0 +var total_orphans := 0 + + +func _ready(): + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + _time.text = "" + _orphans.text = "0" + + +func status_changed(elapsed_time :int, orphan_nodes :int): + total_elapsed_time += elapsed_time + total_orphans += orphan_nodes + _time.text = LocalTime.elapsed(total_elapsed_time) + _orphans.text = str(total_orphans) + if total_orphans > 0: + _orphan_button.icon = ICON_RED + + +func _on_gdunit_event(event :GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + _orphan_button.icon = ICON_GREEN + total_elapsed_time = 0 + total_orphans = 0 + status_changed(0, 0) + GdUnitEvent.TESTCASE_BEFORE: + pass + GdUnitEvent.TESTCASE_AFTER: + status_changed(0, event.orphan_nodes()) + GdUnitEvent.TESTSUITE_BEFORE: + pass + GdUnitEvent.TESTSUITE_AFTER: + status_changed(event.elapsed_time(), event.orphan_nodes()) + + +func _on_ToolButton_pressed(): + emit_signal("jump_to_orphan_nodes") diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn new file mode 100644 index 0000000..1bbe911 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn @@ -0,0 +1,91 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://addons/gdUnit4/src/ui/assets/orphan/orphan_green.svg" type="Texture2D" id=1] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/clock.svg" type="Texture2D" id=2] +[ext_resource path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.gd" type="Script" id=3] + +[node name="Monitor" type="PanelContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -793.0 +offset_bottom = -564.0 +clip_contents = true +size_flags_horizontal = 9 +size_flags_vertical = 9 +script = ExtResource( 3 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="GridContainer" type="GridContainer" parent="."] +offset_left = 7.0 +offset_top = 7.0 +offset_right = 224.0 +offset_bottom = 29.0 +clip_contents = true +size_flags_horizontal = 9 +columns = 2 + +[node name="Time" type="GridContainer" parent="GridContainer"] +offset_right = 63.0 +offset_bottom = 22.0 +clip_contents = true +columns = 2 + +[node name="Button" type="Button" parent="GridContainer/Time"] +offset_right = 59.0 +offset_bottom = 22.0 +tooltip_text = "Shows the total elapsed time of test execution." +size_flags_horizontal = 3 +text = "Time" +icon = ExtResource( 2 ) +align = 0 + +[node name="value" type="Label" parent="GridContainer/Time"] +use_parent_material = true +offset_left = 63.0 +offset_right = 63.0 +offset_bottom = 22.0 +size_flags_horizontal = 3 +size_flags_vertical = 1 +align = 2 +max_lines_visible = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Orphan" type="GridContainer" parent="GridContainer"] +offset_left = 67.0 +offset_right = 160.0 +offset_bottom = 22.0 +clip_contents = true +size_flags_horizontal = 9 +columns = 2 + +[node name="Button" type="Button" parent="GridContainer/Orphan"] +offset_right = 81.0 +offset_bottom = 22.0 +clip_contents = true +tooltip_text = "Shows the total detected orphan nodes. + +(Click) to jump to test." +size_flags_horizontal = 9 +size_flags_vertical = 3 +text = "Orphans" +icon = ExtResource( 1 ) +align = 0 + +[node name="value" type="Label" parent="GridContainer/Orphan"] +use_parent_material = true +offset_left = 85.0 +offset_right = 93.0 +offset_bottom = 22.0 +size_flags_horizontal = 3 +size_flags_vertical = 1 +text = "0" +align = 2 +max_lines_visible = 1 +__meta__ = { +"_edit_use_anchors_": false +} +[connection signal="pressed" from="GridContainer/Orphan/Button" to="." method="_on_ToolButton_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd new file mode 100644 index 0000000..60ad483 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd @@ -0,0 +1,45 @@ +@tool +extends ProgressBar + +@onready var bar = $"." +@onready var status = $Label +@onready var style :StyleBoxFlat = bar.get("theme_override_styles/fill") + + +func _ready(): + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + style.bg_color = Color.DARK_GREEN + update_text() + + +func progress_init(p_max_value :int) -> void: + bar.value = 0 + bar.max_value = p_max_value + style.bg_color = Color.DARK_GREEN + update_text() + + +func progress_update(p_value :int, is_failed :bool) -> void: + bar.value += p_value + update_text() + if is_failed: + style.bg_color = Color.DARK_RED + + +func update_text() -> void: + status.text = "%d:%d" % [bar.value, bar.max_value] + + +func _on_gdunit_event(event :GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + progress_init(event.total_count()) + + GdUnitEvent.TESTCASE_AFTER: + # we only count when the test is finished (excluding parameterized test iterrations) + # test_name: indicates a parameterized test run + if event.test_name().find(":") == -1: + progress_update(1, event.is_failed() or event.is_error()) + + GdUnitEvent.TESTSUITE_AFTER: + progress_update(0, event.is_failed() or event.is_error()) diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn new file mode 100644 index 0000000..582d483 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=3 format=3 uid="uid://dva3tonxsxrlk"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd" id="1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ayfir"] +bg_color = Color(0, 0.392157, 0, 1) + +[node name="ProgressBar" type="ProgressBar"] +custom_minimum_size = Vector2(0, 20) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 9 +theme_override_styles/fill = SubResource("StyleBoxFlat_ayfir") +max_value = 0.0 +rounded = true +allow_greater = true +show_percentage = false +script = ExtResource("1") + +[node name="Label" type="Label" parent="."] +use_parent_material = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +horizontal_alignment = 1 +vertical_alignment = 1 +max_lines_visible = 1 diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd new file mode 100644 index 0000000..9622058 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd @@ -0,0 +1,60 @@ +@tool +extends PanelContainer + +signal failure_next +signal failure_prevous + +@onready var _errors = $GridContainer/Errors/value +@onready var _failures = $GridContainer/Failures/value +@onready var _button_failure_up := $GridContainer/Failures/buttons/failure_up +@onready var _button_failure_down := $GridContainer/Failures/buttons/failure_down + +var total_failed := 0 +var total_errors := 0 + + +func _ready(): + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + _failures.text = "0" + _errors.text = "0" + var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + var editior_control := editor.get_editor_interface().get_base_control() + _button_failure_up.icon = GodotVersionFixures.get_icon(editior_control, "ArrowUp") + _button_failure_down.icon = GodotVersionFixures.get_icon(editior_control, "ArrowDown") + + +func status_changed(errors :int, failed :int): + total_failed += failed + total_errors += errors + _failures.text = str(total_failed) + _errors.text = str(total_errors) + + +func _on_gdunit_event(event :GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + total_failed = 0 + total_errors = 0 + status_changed(0, 0) + GdUnitEvent.TESTCASE_BEFORE: + pass + GdUnitEvent.TESTCASE_AFTER: + if event.is_error(): + status_changed(event.error_count(), 0) + else: + status_changed(0, event.failed_count()) + GdUnitEvent.TESTSUITE_BEFORE: + pass + GdUnitEvent.TESTSUITE_AFTER: + if event.is_error(): + status_changed(event.error_count(), 0) + else: + status_changed(0, event.failed_count()) + + +func _on_failure_up_pressed(): + emit_signal("failure_prevous") + + +func _on_failure_down_pressed(): + emit_signal("failure_next") diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn new file mode 100644 index 0000000..86982f3 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn @@ -0,0 +1,147 @@ +[gd_scene load_steps=8 format=2] + +[ext_resource path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd" type="Script" id=3] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/failures.svg" type="Texture2D" id=4] +[ext_resource path="res://addons/gdUnit4/src/ui/assets/errors.svg" type="Texture2D" id=5] + +[sub_resource type="Image" id=1] +data = { +"data": PackedByteArray( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 218, 218, 0, 222, 222, 222, 0, 222, 222, 222, 0, 218, 218, 218, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 218, 218, 0, 218, 218, 218, 21, 222, 222, 222, 199, 222, 222, 222, 198, 218, 218, 218, 21, 218, 218, 218, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 218, 218, 0, 218, 218, 218, 21, 223, 223, 223, 211, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 209, 218, 218, 218, 21, 217, 217, 217, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 217, 217, 217, 0, 218, 218, 218, 21, 223, 223, 223, 210, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 209, 216, 216, 216, 20, 215, 215, 215, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 216, 216, 216, 0, 216, 216, 216, 20, 223, 223, 223, 209, 223, 223, 223, 254, 222, 222, 222, 206, 223, 223, 223, 254, 223, 223, 223, 251, 222, 222, 222, 207, 223, 223, 223, 254, 223, 223, 223, 209, 214, 214, 214, 19, 214, 214, 214, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 193, 223, 223, 223, 254, 222, 222, 222, 206, 214, 214, 214, 19, 223, 223, 223, 254, 223, 223, 223, 249, 214, 214, 214, 19, 223, 223, 223, 208, 223, 223, 223, 254, 223, 223, 223, 193, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 176, 223, 223, 223, 192, 214, 214, 214, 19, 217, 217, 217, 0, 223, 223, 223, 254, 223, 223, 223, 249, 217, 217, 217, 0, 214, 214, 214, 19, 223, 223, 223, 192, 223, 223, 223, 176, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 0, 214, 214, 214, 0, 223, 223, 223, 0, 223, 223, 223, 254, 223, 223, 223, 249, 223, 223, 223, 0, 214, 214, 214, 0, 223, 223, 223, 0, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 254, 223, 223, 223, 249, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 176, 223, 223, 223, 176, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id=2] +flags = 0 +flags = 0 +image = SubResource( 1 ) +size = Vector2( 16, 16 ) + +[sub_resource type="Image" id=3] +data = { +"data": PackedByteArray( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 176, 223, 223, 223, 176, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 249, 223, 223, 223, 254, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 0, 214, 214, 214, 0, 223, 223, 223, 0, 223, 223, 223, 249, 223, 223, 223, 254, 223, 223, 223, 0, 212, 212, 212, 0, 222, 222, 222, 0, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 176, 223, 223, 223, 192, 214, 214, 214, 19, 216, 216, 216, 0, 223, 223, 223, 249, 223, 223, 223, 254, 215, 215, 215, 0, 212, 212, 212, 18, 222, 222, 222, 191, 223, 223, 223, 176, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 223, 223, 0, 223, 223, 223, 194, 223, 223, 223, 254, 222, 222, 222, 206, 212, 212, 212, 18, 223, 223, 223, 249, 223, 223, 223, 254, 210, 210, 210, 17, 222, 222, 222, 206, 223, 223, 223, 254, 223, 223, 223, 193, 223, 223, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 218, 218, 0, 218, 218, 218, 21, 223, 223, 223, 212, 223, 223, 223, 254, 222, 222, 222, 206, 223, 223, 223, 251, 223, 223, 223, 254, 222, 222, 222, 206, 223, 223, 223, 254, 223, 223, 223, 212, 218, 218, 218, 21, 218, 218, 218, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 218, 218, 0, 218, 218, 218, 21, 223, 223, 223, 212, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 212, 220, 220, 220, 22, 219, 219, 219, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 219, 219, 219, 0, 220, 220, 220, 22, 223, 223, 223, 212, 223, 223, 223, 254, 223, 223, 223, 254, 223, 223, 223, 212, 221, 221, 221, 23, 220, 220, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 220, 220, 0, 221, 221, 221, 23, 222, 222, 222, 198, 223, 223, 223, 200, 221, 221, 221, 23, 221, 221, 221, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 221, 221, 221, 0, 222, 222, 222, 0, 223, 223, 223, 0, 221, 221, 221, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id=4] +flags = 0 +flags = 0 +image = SubResource( 3 ) +size = Vector2( 16, 16 ) + +[node name="StatusBar" type="PanelContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -793.0 +offset_bottom = -564.0 +clip_contents = true +size_flags_horizontal = 9 +size_flags_vertical = 9 +script = ExtResource( 3 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="GridContainer" type="GridContainer" parent="."] +offset_left = 7.0 +offset_top = 7.0 +offset_right = 240.0 +offset_bottom = 31.0 +clip_contents = true +size_flags_horizontal = 9 +columns = 3 + +[node name="Errors" type="GridContainer" parent="GridContainer"] +offset_right = 76.0 +offset_bottom = 24.0 +clip_contents = true +columns = 2 + +[node name="Button" type="Button" parent="GridContainer/Errors"] +offset_right = 64.0 +offset_bottom = 24.0 +tooltip_text = "Shows the total test errors." +size_flags_horizontal = 3 +size_flags_vertical = 3 +text = "Errors" +icon = ExtResource( 5 ) +align = 0 + +[node name="value" type="Label" parent="GridContainer/Errors"] +use_parent_material = true +offset_left = 68.0 +offset_right = 76.0 +offset_bottom = 24.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +text = "0" +align = 2 +max_lines_visible = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Failures" type="GridContainer" parent="GridContainer"] +offset_left = 80.0 +offset_right = 233.0 +offset_bottom = 24.0 +clip_contents = true +size_flags_horizontal = 9 +columns = 3 + +[node name="Button" type="Button" parent="GridContainer/Failures"] +offset_right = 77.0 +offset_bottom = 24.0 +clip_contents = true +tooltip_text = "Shows the total test failures." +size_flags_horizontal = 9 +size_flags_vertical = 3 +text = "Failures" +icon = ExtResource( 4 ) +align = 0 + +[node name="value" type="Label" parent="GridContainer/Failures"] +use_parent_material = true +offset_left = 81.0 +offset_right = 89.0 +offset_bottom = 24.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +text = "0" +align = 2 +max_lines_visible = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="buttons" type="GridContainer" parent="GridContainer/Failures"] +offset_left = 93.0 +offset_right = 153.0 +offset_bottom = 24.0 +size_flags_vertical = 3 +columns = 2 + +[node name="failure_up" type="Button" parent="GridContainer/Failures/buttons"] +offset_right = 28.0 +offset_bottom = 24.0 +tooltip_text = "Shows the total test errors." +size_flags_horizontal = 3 +size_flags_vertical = 3 +icon = SubResource( 2 ) + +[node name="failure_down" type="Button" parent="GridContainer/Failures/buttons"] +offset_left = 32.0 +offset_right = 60.0 +offset_bottom = 24.0 +tooltip_text = "Shows the total test errors." +size_flags_horizontal = 3 +size_flags_vertical = 3 +icon = SubResource( 4 ) + +[connection signal="pressed" from="GridContainer/Failures/buttons/failure_up" to="." method="_on_failure_up_pressed"] +[connection signal="pressed" from="GridContainer/Failures/buttons/failure_down" to="." method="_on_failure_down_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd new file mode 100644 index 0000000..0072249 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd @@ -0,0 +1,113 @@ +@tool +extends HBoxContainer + +signal run_overall_pressed(debug :bool) +signal run_pressed(debug :bool) +signal stop_pressed() + +@onready var debug_icon_image :Texture2D = load("res://addons/gdUnit4/src/ui/assets/PlayDebug.svg") +@onready var overall_icon_image :Texture2D = load("res://addons/gdUnit4/src/ui/assets/PlayOverall.svg") +@onready var _version_label := %version +@onready var _button_wiki := %help +@onready var _tool_button := %tool +@onready var _button_run_overall :Button = %"run-overall" +@onready var _button_run := %run +@onready var _button_run_debug := %debug +@onready var _button_stop := %stop + + +const SETTINGS_SHORTCUT_MAPPING := { + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST : GdUnitShortcut.ShortCut.RERUN_TESTS, + GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_OVERALL : GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL, + GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_STOP : GdUnitShortcut.ShortCut.STOP_TEST_RUN, +} + + +func _ready(): + GdUnit4Version.init_version_label(_version_label) + var command_handler := GdUnitCommandHandler.instance() + run_pressed.connect(command_handler._on_run_pressed) + run_overall_pressed.connect(command_handler._on_run_overall_pressed) + stop_pressed.connect(command_handler._on_stop_pressed) + command_handler.gdunit_runner_start.connect(_on_gdunit_runner_start) + command_handler.gdunit_runner_stop.connect(_on_gdunit_runner_stop) + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_gdunit_settings_changed) + init_buttons() + init_shortcuts(command_handler) + + +func init_buttons() -> void: + var editor :EditorPlugin = EditorPlugin.new() + var editior_control := editor.get_editor_interface().get_base_control() + _button_run_overall.icon = overall_icon_image + _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() + _button_run.icon = GodotVersionFixures.get_icon(editior_control, "Play") + _button_run_debug.icon = debug_icon_image + _button_stop.icon = GodotVersionFixures.get_icon(editior_control, "Stop") + _tool_button.icon = GodotVersionFixures.get_icon(editior_control, "Tools") + _button_wiki.icon = GodotVersionFixures.get_icon(editior_control, "HelpSearch") + + +func init_shortcuts(command_handler :GdUnitCommandHandler) -> void: + _button_run.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.RERUN_TESTS) + _button_run_overall.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL) + _button_run_debug.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG) + _button_stop.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.STOP_TEST_RUN) + # register for shortcut changes + GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed.bind(command_handler)) + + +func _on_runoverall_pressed(debug := false): + run_overall_pressed.emit(debug) + + +func _on_run_pressed(debug := false): + run_pressed.emit(debug) + + +func _on_stop_pressed(): + stop_pressed.emit() + + +func _on_gdunit_runner_start(): + _button_run_overall.disabled = true + _button_run.disabled = true + _button_run_debug.disabled = true + _button_stop.disabled = false + + +func _on_gdunit_runner_stop(_client_id :int): + _button_run_overall.disabled = false + _button_run.disabled = false + _button_run_debug.disabled = false + _button_stop.disabled = true + + +func _on_gdunit_settings_changed(_property :GdUnitProperty): + _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() + + +func _on_wiki_pressed(): + OS.shell_open("https://mikeschulze.github.io/gdUnit4/") + + +func _on_btn_tool_pressed(): + var tool_popup = load("res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn").instantiate() + get_parent_control().add_child(tool_popup) + + +func _on_settings_changed(property :GdUnitProperty, command_handler :GdUnitCommandHandler): + # needs to wait a frame to be command handler notified first for settings changes + await get_tree().process_frame + if SETTINGS_SHORTCUT_MAPPING.has(property.name()): + var shortcut :GdUnitShortcut.ShortCut = SETTINGS_SHORTCUT_MAPPING.get(property.name(), GdUnitShortcut.ShortCut.NONE) + match shortcut: + GdUnitShortcut.ShortCut.RERUN_TESTS: + _button_run.shortcut = command_handler.get_shortcut(shortcut) + GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL: + _button_run_overall.shortcut = command_handler.get_shortcut(shortcut) + GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG: + _button_run_debug.shortcut = command_handler.get_shortcut(shortcut) + GdUnitShortcut.ShortCut.STOP_TEST_RUN: + _button_stop.shortcut = command_handler.get_shortcut(shortcut) diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn new file mode 100644 index 0000000..c44fea8 --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn @@ -0,0 +1,83 @@ +[gd_scene load_steps=4 format=3 uid="uid://dx7xy4dgi3wwb"] + +[ext_resource type="Texture2D" uid="uid://tkrsqx2oxw6o" path="res://addons/gdUnit4/src/ui/assets/PlayDebug.svg" id="2_4h4dw"] +[ext_resource type="Texture2D" uid="uid://de1q5raia84bn" path="res://addons/gdUnit4/src/ui/assets/PlayOverall.svg" id="2_s3tbo"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.gd" id="3"] + +[node name="ToolBar" type="HBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 0 +grow_vertical = 2 +size_flags_vertical = 3 +script = ExtResource("3") + +[node name="Tools" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="VSeparator2" type="VSeparator" parent="Tools"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 9 + +[node name="help" type="Button" parent="Tools"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="tool" type="Button" parent="Tools"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "GdUnit Settings" + +[node name="VSeparator" type="VSeparator" parent="Tools"] +layout_mode = 2 + +[node name="run-overall" type="Button" parent="Tools"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Run overall tests" +icon = ExtResource("2_s3tbo") + +[node name="run" type="Button" parent="Tools"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Rerun unit tests" + +[node name="debug" type="Button" parent="Tools"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Rerun unit tests (Debug)" +icon = ExtResource("2_4h4dw") + +[node name="stop" type="Button" parent="Tools"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +tooltip_text = "Stops runing unit tests" +disabled = true + +[node name="VSeparator3" type="VSeparator" parent="Tools"] +layout_mode = 2 + +[node name="CenterContainer" type="MarginContainer" parent="Tools"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="version" type="Label" parent="Tools/CenterContainer"] +unique_name_in_owner = true +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 13 + +[connection signal="pressed" from="Tools/help" to="." method="_on_wiki_pressed"] +[connection signal="pressed" from="Tools/tool" to="." method="_on_btn_tool_pressed"] +[connection signal="pressed" from="Tools/run-overall" to="." method="_on_runoverall_pressed" binds= [false]] +[connection signal="pressed" from="Tools/run" to="." method="_on_run_pressed" binds= [false]] +[connection signal="pressed" from="Tools/debug" to="." method="_on_run_pressed" binds= [true]] +[connection signal="pressed" from="Tools/stop" to="." method="_on_stop_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd new file mode 100644 index 0000000..fdd6f2f --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd @@ -0,0 +1,589 @@ +@tool +extends VSplitContainer + +signal run_testcase(test_suite_resource_path, test_case, test_param_index, run_debug) +signal run_testsuite + +const CONTEXT_MENU_RUN_ID = 0 +const CONTEXT_MENU_DEBUG_ID = 1 + +@onready var _tree :Tree = $Panel/Tree +@onready var _report_list :Node = $report/ScrollContainer/list +@onready var _report_template :RichTextLabel = $report/report_template +@onready var _context_menu :PopupMenu = $contextMenu + + +# tree icons +@onready var ICON_SPINNER = load("res://addons/gdUnit4/src/ui/assets/spinner.tres") +@onready var ICON_TEST_DEFAULT = load("res://addons/gdUnit4/src/ui/assets/TestCase.svg") +@onready var ICON_TEST_SUCCESS = load("res://addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg") +@onready var ICON_TEST_FAILED = load("res://addons/gdUnit4/src/ui/assets/TestCaseFailed.svg") +@onready var ICON_TEST_ERROR = load("res://addons/gdUnit4/src/ui/assets/TestCaseError.svg") +@onready var ICON_TEST_SUCCESS_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_success_orphan.tres") +@onready var ICON_TEST_FAILED_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_failed_orphan.tres") +@onready var ICON_TEST_ERRORS_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_error_orphan.tres") +@onready var debug_icon_image :Texture2D = load("res://addons/gdUnit4/src/ui/assets/PlayDebug.svg") + +enum GdUnitType { + TEST_SUITE, + TEST_CASE +} + +enum STATE { + INITIAL, + RUNNING, + SUCCESS, + WARNING, + FAILED, + ERROR, + ABORDED, + SKIPPED +} + +const META_GDUNIT_NAME := "gdUnit_name" +const META_GDUNIT_STATE := "gdUnit_state" +const META_GDUNIT_TYPE := "gdUnit_type" +const META_GDUNIT_TOTAL_TESTS := "gdUnit_suite_total_tests" +const META_GDUNIT_SUCCESS_TESTS := "gdUnit_suite_success_tests" +const META_GDUNIT_REPORT := "gdUnit_report" +const META_GDUNIT_ORPHAN := "gdUnit_orphan" +const META_RESOURCE_PATH := "resource_path" +const META_LINE_NUMBER := "line_number" +const META_TEST_PARAM_INDEX := "test_param_index" + +var _editor :EditorPlugin +var _tree_root :TreeItem +var _current_failures := Array() +var _item_hash := Dictionary() + + +func _build_cache_key(resource_path :String, test_name :String) -> Array: + return [resource_path, test_name] + + +func get_tree_item(event :GdUnitEvent) -> TreeItem: + var key := _build_cache_key(event.resource_path(), event.test_name()) + return _item_hash.get(key, null) + + +func add_tree_item_to_cache(resource_path :String, test_name :String, item :TreeItem) -> void: + var key := _build_cache_key(resource_path, test_name) + _item_hash[key] = item + + +func clear_tree_item_cache() -> void: + _item_hash.clear() + + +func _find_item(parent :TreeItem, resource_path :String, test_case :String = "") -> TreeItem: + var item = _find_by_resource_path(parent, resource_path) + if not item: + return null + if test_case.is_empty(): + return item + return _find_by_name(item, test_case) + + +func _find_by_resource_path(parent :TreeItem, resource_path :String) -> TreeItem: + for item in parent.get_children(): + if item.get_meta(META_RESOURCE_PATH) == resource_path: + return item + return null + + +func _find_by_name(parent :TreeItem, item_name :String) -> TreeItem: + for item in parent.get_children(): + if item.get_meta(META_GDUNIT_NAME) == item_name: + return item + return null + + +func is_state_running(item :TreeItem) -> bool: + return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.RUNNING + + +func is_state_success(item :TreeItem) -> bool: + return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.SUCCESS + + +func is_state_warning(item :TreeItem) -> bool: + return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.WARNING + + +func is_state_failed(item :TreeItem) -> bool: + return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.FAILED + + +func is_state_error(item :TreeItem) -> bool: + return item.has_meta(META_GDUNIT_STATE) and (item.get_meta(META_GDUNIT_STATE) == STATE.ERROR or item.get_meta(META_GDUNIT_STATE) == STATE.ABORDED) + + +func is_item_state_orphan(item :TreeItem) -> bool: + return item.has_meta(META_GDUNIT_ORPHAN) + + +func is_test_suite(item :TreeItem) -> bool: + return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE + + +func _ready(): + if Engine.is_editor_hint(): + _editor = Engine.get_meta("GdUnitEditorPlugin") + var editior_control := _editor.get_editor_interface().get_base_control() + _context_menu.set_item_icon(CONTEXT_MENU_RUN_ID, GodotVersionFixures.get_icon(editior_control, "Play")) + _context_menu.set_item_icon(CONTEXT_MENU_DEBUG_ID, debug_icon_image) + init_tree() + GdUnitSignals.instance().gdunit_add_test_suite.connect(_on_gdunit_add_test_suite) + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + var command_handler := GdUnitCommandHandler.instance() + command_handler.gdunit_runner_start.connect(_on_gdunit_runner_start) + command_handler.gdunit_runner_stop.connect(_on_gdunit_runner_stop) + + +# we need current to manually redraw bacause of the animation bug +# https://github.com/godotengine/godot/issues/69330 +func _process(_delta): + if is_visible_in_tree(): + queue_redraw() + + +func init_tree() -> void: + cleanup_tree() + _tree.set_hide_root(true) + _tree.ensure_cursor_is_visible() + _tree.allow_rmb_select = true + _tree_root = _tree.create_item() + # fix tree icon scaling + var scale_factor := _editor.get_editor_interface().get_editor_scale() if Engine.is_editor_hint() else 1.0 + _tree.set("theme_override_constants/icon_max_width", 16*scale_factor) + + +func cleanup_tree() -> void: + clear_failures() + clear_tree_item_cache() + if not _tree_root: + return + _free_recursive() + _tree.clear() + # clear old reports + for child in _report_list.get_children(): + _report_list.remove_child(child) + + +func _free_recursive(items := _tree_root.get_children()) -> void: + for item in items: + _free_recursive(item.get_children()) + item.call_deferred("free") + + +func select_item(item :TreeItem) -> void: + if not item.is_selected(0): + item.select(0) + # _tree.ensure_cursor_is_visible() + _tree.scroll_to_item(item) + + +func set_state_running(item :TreeItem) -> void: + item.set_custom_color(0, Color.DARK_GREEN) + item.set_icon(0, ICON_SPINNER) + item.set_tooltip_text(0, "") + item.set_meta(META_GDUNIT_STATE, STATE.RUNNING) + item.remove_meta(META_GDUNIT_REPORT) + item.remove_meta(META_GDUNIT_ORPHAN) + item.collapsed = false + # force scrolling to current test case + select_item(item) + + +func set_state_succeded(item :TreeItem) -> void: + item.set_custom_color(0, Color.GREEN) + item.set_icon(0, ICON_TEST_SUCCESS) + item.set_meta(META_GDUNIT_STATE, STATE.SUCCESS) + item.collapsed = GdUnitSettings.is_inspector_node_collapse() + + +func set_state_skipped(item :TreeItem) -> void: + item.set_meta(META_GDUNIT_STATE, STATE.SKIPPED) + item.set_suffix(0, "(skipped)") + item.set_custom_color(0, Color.DARK_GRAY) + item.set_icon(0, ICON_TEST_DEFAULT) + item.collapsed = false + + +func set_state_warnings(item :TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item) or is_state_failed(item): + return + item.set_meta(META_GDUNIT_STATE, STATE.WARNING) + item.set_custom_color(0, Color.YELLOW) + item.set_icon(0, ICON_TEST_SUCCESS) + item.collapsed = false + + +func set_state_failed(item :TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item): + return + item.set_meta(META_GDUNIT_STATE, STATE.FAILED) + item.set_custom_color(0, Color.LIGHT_BLUE) + item.set_icon(0, ICON_TEST_FAILED) + item.collapsed = false + + +func set_state_error(item :TreeItem) -> void: + item.set_meta(META_GDUNIT_STATE, STATE.ERROR) + item.set_custom_color(0, Color.DARK_RED) + item.set_suffix(0, item.get_suffix(0)) + item.set_icon(0, ICON_TEST_ERROR) + item.collapsed = false + + +func set_state_aborted(item :TreeItem) -> void: + item.set_meta(META_GDUNIT_STATE, STATE.ABORDED) + item.set_icon(0, ICON_TEST_ERROR) + item.set_custom_color(0, Color.DARK_RED) + item.set_suffix(0, "(aborted)") + item.clear_custom_bg_color(0) + item.collapsed = false + + +func set_elapsed_time(item :TreeItem, time :int) -> void: + item.set_suffix(0, "(%s)" % LocalTime.elapsed(time)) + + +func set_state_orphan(item :TreeItem, event: GdUnitEvent) -> void: + var orphan_count = event.statistic(GdUnitEvent.ORPHAN_NODES) + if orphan_count == 0: + return + if item.has_meta(META_GDUNIT_ORPHAN): + orphan_count += item.get_meta(META_GDUNIT_ORPHAN) + item.set_meta(META_GDUNIT_ORPHAN, orphan_count) + item.set_custom_color(0, Color.YELLOW) + item.set_tooltip_text(0, "Total <%d> orphan nodes detected." % orphan_count) + if is_state_error(item): + item.set_icon(0, ICON_TEST_ERRORS_ORPHAN) + elif is_state_failed(item): + item.set_icon(0, ICON_TEST_FAILED_ORPHAN) + elif is_state_warning(item): + item.set_icon(0, ICON_TEST_SUCCESS_ORPHAN) + + +func update_state(item: TreeItem, event :GdUnitEvent) -> void: + if is_state_running(item) and event.is_success(): + set_state_succeded(item) + else: + if event.is_skipped(): + set_state_skipped(item) + elif event.is_error(): + set_state_error(item) + elif event.is_failed(): + set_state_failed(item) + elif event.is_warning(): + set_state_warnings(item) + for report in event.reports(): + add_report(item, report) + set_state_orphan(item, event) + + +func add_report(item :TreeItem, report: GdUnitReport) -> void: + var reports = [] + if item.has_meta(META_GDUNIT_REPORT): + reports = item.get_meta(META_GDUNIT_REPORT) + reports.append(report) + item.set_meta(META_GDUNIT_REPORT, reports) + + +func abort_running(items := _tree_root.get_children()) -> void: + for item in items: + if is_state_running(item): + set_state_aborted(item) + abort_running(item.get_children()) + + +func select_first_failure() -> void: + if not _current_failures.is_empty(): + select_item(_current_failures[0]) + + +func select_last_failure() -> void: + if not _current_failures.is_empty(): + select_item(_current_failures[-1]) + + +func clear_failures() -> void: + _current_failures.clear() + + +func collect_failures_and_errors(items := _tree_root.get_children()) -> Array: + for item in items: + if not is_test_suite(item) and (is_state_failed(item) or is_state_error(item)): + _current_failures.append(item) + collect_failures_and_errors(item.get_children()) + return _current_failures + + +func select_next_failure() -> void: + var current_selected := _tree.get_selected() + if current_selected == null: + select_first_failure() + return + if _current_failures.is_empty(): + return + var index := _current_failures.find(current_selected) + if index == -1 or index == _current_failures.size()-1: + select_item(_current_failures[0]) + else: + select_item(_current_failures[index+1]) + + +func select_previous_failure() -> void: + var current_selected := _tree.get_selected() + if current_selected == null: + select_last_failure() + return + if _current_failures.is_empty(): + return + var index := _current_failures.find(current_selected) + if index == -1 or index == 0: + select_item(_current_failures[_current_failures.size()-1]) + else: + select_item(_current_failures[index-1]) + + +func select_first_orphan() -> void: + for parent in _tree_root.get_children(): + if not is_state_success(parent): + for item in parent.get_children(): + if is_item_state_orphan(item): + parent.set_collapsed(false) + select_item(item) + return + + +func show_failed_report(selected_item) -> void: + # clear old reports + for child in _report_list.get_children(): + _report_list.remove_child(child) + child.queue_free() + + if selected_item == null or not selected_item.has_meta(META_GDUNIT_REPORT): + return + # add new reports + for r in selected_item.get_meta(META_GDUNIT_REPORT): + var report := r as GdUnitReport + var reportNode :RichTextLabel = _report_template.duplicate() + _report_list.add_child(reportNode) + reportNode.append_text(report.to_string()) + reportNode.visible = true + + +func update_test_suite(event :GdUnitEvent) -> void: + var item := _find_by_resource_path(_tree_root, event.resource_path()) + if not item: + push_error("Internal Error: Can't find test suite %s" % event.suite_name()) + return + if event.type() == GdUnitEvent.TESTSUITE_BEFORE: + set_state_running(item) + return + if event.type() == GdUnitEvent.TESTSUITE_AFTER: + set_elapsed_time(item, event.elapsed_time()) + update_state(item, event) + + +func update_test_case(event :GdUnitEvent) -> void: + var item := get_tree_item(event) + if not item: + push_error("Internal Error: Can't find test case %s:%s" % [event.suite_name(), event.test_name()]) + return + if event.type() == GdUnitEvent.TESTCASE_BEFORE: + set_state_running(item) + return + if event.type() == GdUnitEvent.TESTCASE_AFTER: + set_elapsed_time(item, event.elapsed_time()) + _update_parent_item_state(item, event.is_success()) + update_state(item, event) + + +func add_test_suite(test_suite :GdUnitTestSuiteDto) -> void: + var item := _tree.create_item(_tree_root) + var suite_name := test_suite.name() + var test_count := test_suite.test_case_count() + + item.set_icon(0, ICON_TEST_DEFAULT) + item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) + item.set_meta(META_GDUNIT_NAME, suite_name) + item.set_meta(META_GDUNIT_TYPE, GdUnitType.TEST_SUITE) + item.set_meta(META_GDUNIT_TOTAL_TESTS, test_count) + item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) + item.set_meta(META_RESOURCE_PATH, test_suite.path()) + item.set_meta(META_LINE_NUMBER, 1) + item.collapsed = true + _update_item_counter(item) + for test_case in test_suite.test_cases(): + add_test(item, test_case) + + +func _update_item_counter(item: TreeItem): + if item.has_meta(META_GDUNIT_TOTAL_TESTS): + item.set_text(0, "(%s/%s) %s" % [ + item.get_meta(META_GDUNIT_SUCCESS_TESTS), + item.get_meta(META_GDUNIT_TOTAL_TESTS), + item.get_meta(META_GDUNIT_NAME)]) + + +func _update_parent_item_state(item: TreeItem, success : bool): + if success: + var parent_item := item.get_parent() + var successes: int = parent_item.get_meta(META_GDUNIT_SUCCESS_TESTS) + parent_item.set_meta(META_GDUNIT_SUCCESS_TESTS, successes + 1) + _update_item_counter(parent_item) + + +func add_test(parent :TreeItem, test_case :GdUnitTestCaseDto) -> void: + var item := _tree.create_item(parent) + var test_name := test_case.name() + item.set_text(0, test_name) + item.set_icon(0, ICON_TEST_DEFAULT) + item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) + item.set_meta(META_GDUNIT_NAME, test_name) + item.set_meta(META_GDUNIT_TYPE, GdUnitType.TEST_CASE) + item.set_meta(META_RESOURCE_PATH, parent.get_meta(META_RESOURCE_PATH)) + item.set_meta(META_LINE_NUMBER, test_case.line_number()) + item.set_meta(META_TEST_PARAM_INDEX, -1) + add_tree_item_to_cache(parent.get_meta(META_RESOURCE_PATH), test_name, item) + + var test_case_names := test_case.test_case_names() + if not test_case_names.is_empty(): + item.set_meta(META_GDUNIT_TOTAL_TESTS, test_case_names.size()) + item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) + _update_item_counter(item) + add_test_cases(item, test_case_names) + + +func add_test_cases(parent :TreeItem, test_case_names :Array) -> void: + for index in test_case_names.size(): + var test_case_name = test_case_names[index] + var item := _tree.create_item(parent) + item.set_text(0, test_case_name) + item.set_icon(0, ICON_TEST_DEFAULT) + item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) + item.set_meta(META_GDUNIT_NAME, test_case_name) + item.set_meta(META_GDUNIT_TYPE, GdUnitType.TEST_CASE) + item.set_meta(META_RESOURCE_PATH, parent.get_meta(META_RESOURCE_PATH)) + item.set_meta(META_LINE_NUMBER, parent.get_meta(META_LINE_NUMBER)) + item.set_meta(META_TEST_PARAM_INDEX, index) + add_tree_item_to_cache(parent.get_meta(META_RESOURCE_PATH), test_case_name, item) + + +################################################################################ +# Tree signal receiver +################################################################################ +func _on_tree_item_mouse_selected(mouse_position :Vector2, mouse_button_index :int): + if mouse_button_index == MOUSE_BUTTON_RIGHT: + _context_menu.position = get_screen_position() + mouse_position + _context_menu.popup() + + +func _on_run_pressed(run_debug :bool) -> void: + _context_menu.hide() + var item := _tree.get_selected() + if item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE: + var resource_path = item.get_meta(META_RESOURCE_PATH) + run_testsuite.emit([resource_path], run_debug) + return + var parent = item.get_parent() + var test_suite_resource_path = parent.get_meta(META_RESOURCE_PATH) + var test_case = item.get_meta(META_GDUNIT_NAME) + # handle parameterized test selection + var test_param_index = item.get_meta(META_TEST_PARAM_INDEX) + if test_param_index != -1: + test_case = parent.get_meta(META_GDUNIT_NAME) + run_testcase.emit(test_suite_resource_path, test_case, test_param_index, run_debug) + + +func _on_Tree_item_selected() -> void: + # only show report checked manual item selection + # we need to check the run mode here otherwise it will be called every selection + if not _context_menu.is_item_disabled(CONTEXT_MENU_RUN_ID): + var selected_item :TreeItem = _tree.get_selected() + show_failed_report(selected_item) + + +# Opens the test suite +func _on_Tree_item_activated() -> void: + var selected_item := _tree.get_selected() + var resource_path = selected_item.get_meta(META_RESOURCE_PATH) + var line_number = selected_item.get_meta(META_LINE_NUMBER) + var resource = load(resource_path) + + if selected_item.has_meta(META_GDUNIT_REPORT): + var reports :Array = selected_item.get_meta(META_GDUNIT_REPORT) + var report_line_number = reports[0].line_number() + # if number -1 we use original stored line number of the test case + # in non debug mode the line number is not available + if report_line_number != -1: + line_number = report_line_number + + var editor_interface := _editor.get_editor_interface() + editor_interface.get_file_system_dock().navigate_to_path(resource_path) + editor_interface.edit_resource(resource) + editor_interface.get_script_editor().goto_line(line_number-1) + + +################################################################################ +# external signal receiver +################################################################################ +func _on_gdunit_runner_start(): + _context_menu.set_item_disabled(CONTEXT_MENU_RUN_ID, true) + _context_menu.set_item_disabled(CONTEXT_MENU_DEBUG_ID, true) + clear_failures() + + +func _on_gdunit_runner_stop(_client_id :int): + _context_menu.set_item_disabled(CONTEXT_MENU_RUN_ID, false) + _context_menu.set_item_disabled(CONTEXT_MENU_DEBUG_ID, false) + abort_running() + clear_failures() + collect_failures_and_errors() + select_first_failure() + + +func _on_gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto) -> void: + add_test_suite(test_suite) + + +func _on_gdunit_event(event :GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + init_tree() + GdUnitEvent.STOP: + select_first_failure() + show_failed_report(_tree.get_selected()) + GdUnitEvent.TESTCASE_BEFORE: + update_test_case(event) + GdUnitEvent.TESTCASE_AFTER: + update_test_case(event) + GdUnitEvent.TESTSUITE_BEFORE: + update_test_suite(event) + GdUnitEvent.TESTSUITE_AFTER: + update_test_suite(event) + + +func _on_Monitor_jump_to_orphan_nodes(): + select_first_orphan() + + +func _on_StatusBar_failure_next(): + select_next_failure() + + +func _on_StatusBar_failure_prevous(): + select_previous_failure() + + +func _on_context_m_index_pressed(index): + match index: + CONTEXT_MENU_DEBUG_ID: + _on_run_pressed(true) + CONTEXT_MENU_RUN_ID: + _on_run_pressed(false) diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn new file mode 100644 index 0000000..0f00baf --- /dev/null +++ b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn @@ -0,0 +1,74 @@ +[gd_scene load_steps=2 format=3 uid="uid://bqfpidewtpeg0"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd" id="1"] + +[node name="MainPanel" type="VSplitContainer"] +use_parent_material = true +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -924.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +split_offset = 200 +script = ExtResource("1") + +[node name="Panel" type="PanelContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Tree" type="Tree" parent="Panel"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/icon_max_width = 16 +allow_rmb_select = true +hide_root = true +select_mode = 1 + +[node name="report" type="PanelContainer" parent="."] +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 11 +size_flags_vertical = 11 + +[node name="report_template" type="RichTextLabel" parent="report"] +use_parent_material = true +clip_contents = false +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 2 +bbcode_enabled = true +fit_content = true +selection_enabled = true + +[node name="ScrollContainer" type="ScrollContainer" parent="report"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 11 + +[node name="list" type="VBoxContainer" parent="report/ScrollContainer"] +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="contextMenu" type="PopupMenu" parent="."] +size = Vector2i(120, 60) +auto_translate = false +item_count = 2 +item_0/text = "Run" +item_0/id = 0 +item_1/text = "Debug" +item_1/id = 1 + +[connection signal="item_activated" from="Panel/Tree" to="." method="_on_Tree_item_activated"] +[connection signal="item_mouse_selected" from="Panel/Tree" to="." method="_on_tree_item_mouse_selected"] +[connection signal="item_selected" from="Panel/Tree" to="." method="_on_Tree_item_selected"] +[connection signal="index_pressed" from="contextMenu" to="." method="_on_context_m_index_pressed"] diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd new file mode 100644 index 0000000..839d82e --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd @@ -0,0 +1,54 @@ +@tool +class_name GdUnitInputCapture +extends Control + + +signal input_completed(input_event :InputEventKey) + +@onready var _label = %Label + + +var _tween :Tween +var _input_event :InputEventKey + + +func _ready(): + reset() + _tween = create_tween() + _tween.set_loops(-1) + _tween.tween_property(self, "modulate", Color(0, 0, 0, .1), 1.0).from_current().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + + +func reset() -> void: + _input_event = InputEventKey.new() + + +func _input(event :InputEvent): + if not is_visible_in_tree(): + return + if event is InputEventKey and event.is_pressed() and not event.is_echo(): + match event.keycode: + KEY_CTRL: + _input_event.ctrl_pressed = true + KEY_SHIFT: + _input_event.shift_pressed = true + KEY_ALT: + _input_event.alt_pressed = true + KEY_META: + _input_event.meta_pressed = true + _: + _input_event.keycode = event.keycode + _apply_input_modifiers(event) + accept_event() + + if event is InputEventKey and not event.is_pressed(): + input_completed.emit(_input_event) + hide() + + +func _apply_input_modifiers(event :InputEvent) -> void: + if event is InputEventWithModifiers: + _input_event.meta_pressed = event.meta_pressed or _input_event.meta_pressed + _input_event.alt_pressed = event.alt_pressed or _input_event.alt_pressed + _input_event.shift_pressed = event.shift_pressed or _input_event.shift_pressed + _input_event.ctrl_pressed = event.ctrl_pressed or _input_event.ctrl_pressed diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn new file mode 100644 index 0000000..8081a66 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd" id="1_gki1u"] + +[node name="GdUnitInputMapper" type="Control"] +top_level = true +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_gki1u") + +[node name="Label" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -60.5 +offset_top = -19.5 +offset_right = 60.5 +offset_bottom = 19.5 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_font_sizes/font_size = 26 +text = "Press keys for shortcut" diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd new file mode 100644 index 0000000..8ea55fd --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd @@ -0,0 +1,292 @@ +@tool +extends Window + +const EAXAMPLE_URL := "https://github.com/MikeSchulze/gdUnit4-examples/archive/refs/heads/master.zip" + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +@onready var _update_client :GdUnitUpdateClient = $GdUnitUpdateClient +@onready var _version_label :RichTextLabel = %version +@onready var _btn_install :Button = %btn_install_examples +@onready var _progress_bar :ProgressBar = %ProgressBar +@onready var _progress_text :Label = %progress_lbl +@onready var _properties_template :Node = $property_template +@onready var _properties_common :Node = %"common-content" +@onready var _properties_ui :Node = %"ui-content" +@onready var _properties_shortcuts :Node = %"shortcut-content" +@onready var _properties_report :Node = %"report-content" +@onready var _input_capture :GdUnitInputCapture = %GdUnitInputCapture +@onready var _property_error :Window = %"propertyError" +var _font_size :float + + +func _ready(): + # initialize for testing + if not Engine.is_editor_hint(): + GdUnitSettings.setup() + GdUnit4Version.init_version_label(_version_label) + _font_size = GdUnitFonts.init_fonts(_version_label) + setup_properties(_properties_common, GdUnitSettings.COMMON_SETTINGS) + setup_properties(_properties_ui, GdUnitSettings.UI_SETTINGS) + setup_properties(_properties_report, GdUnitSettings.REPORT_SETTINGS) + setup_properties(_properties_shortcuts, GdUnitSettings.SHORTCUT_SETTINGS) + await get_tree().process_frame + if not Engine.is_editor_hint(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_set_size(Vector2i(1600, 800)) + popup_centered_ratio(1) + else: + popup_centered_ratio(.75) + + +func _sort_by_key(left :GdUnitProperty, right :GdUnitProperty) -> bool: + return left.name() < right.name() + + +func setup_properties(properties_parent :Node, property_category) -> void: + var category_properties := GdUnitSettings.list_settings(property_category) + # sort by key + category_properties.sort_custom(_sort_by_key) + var theme_ := Theme.new() + theme_.set_constant("h_separation", "GridContainer", 12) + var last_category := "!" + var min_size_overall := 0.0 + for p in category_properties: + var min_size_ := 0.0 + var grid := GridContainer.new() + grid.columns = 4 + grid.theme = theme_ + var property : GdUnitProperty = p + var current_category = property.category() + if current_category != last_category: + var sub_category :Node = _properties_template.get_child(3).duplicate() + sub_category.get_child(0).text = current_category.capitalize() + sub_category.custom_minimum_size.y = _font_size + 16 + properties_parent.add_child(sub_category) + last_category = current_category + # property name + var label :Label = _properties_template.get_child(0).duplicate() + label.text = _to_human_readable(property.name()) + label.custom_minimum_size = Vector2(_font_size * 20, 0) + grid.add_child(label) + min_size_ += label.size.x + + # property reset btn + var reset_btn :Button = _properties_template.get_child(1).duplicate() + reset_btn.icon = _get_btn_icon("Reload") + reset_btn.disabled = property.value() == property.default() + grid.add_child(reset_btn) + min_size_ += reset_btn.size.x + + # property type specific input element + var input :Node = _create_input_element(property, reset_btn) + input.custom_minimum_size = Vector2(_font_size * 15, 0) + grid.add_child(input) + min_size_ += input.size.x + reset_btn.pressed.connect(_on_btn_property_reset_pressed.bind(property, input, reset_btn)) + # property help text + var info :Node = _properties_template.get_child(2).duplicate() + info.text = property.help() + grid.add_child(info) + min_size_ += info.text.length() * _font_size + if min_size_overall < min_size_: + min_size_overall = min_size_ + properties_parent.add_child(grid) + properties_parent.custom_minimum_size.x = min_size_overall + + +func _create_input_element(property: GdUnitProperty, reset_btn :Button) -> Node: + if property.is_selectable_value(): + var options := OptionButton.new() + options.alignment = HORIZONTAL_ALIGNMENT_CENTER + var values_set := Array(property.value_set()) + for value in values_set: + options.add_item(value) + options.item_selected.connect(_on_option_selected.bind(property, reset_btn)) + options.select(property.value()) + return options + if property.type() == TYPE_BOOL: + var check_btn := CheckButton.new() + check_btn.toggled.connect(_on_property_text_changed.bind(property, reset_btn)) + check_btn.button_pressed = property.value() + return check_btn + if property.type() in [TYPE_INT, TYPE_STRING]: + var input := LineEdit.new() + input.text_changed.connect(_on_property_text_changed.bind(property, reset_btn)) + input.set_context_menu_enabled(false) + input.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER) + input.set_expand_to_text_length_enabled(true) + input.text = str(property.value()) + return input + if property.type() == TYPE_PACKED_INT32_ARRAY: + var key_input_button := Button.new() + key_input_button.text = to_shortcut(property.value()) + key_input_button.pressed.connect(_on_shortcut_change.bind(key_input_button, property, reset_btn)) + return key_input_button + return Control.new() + + +func to_shortcut(keys :PackedInt32Array) -> String: + var input_event := InputEventKey.new() + for key in keys: + match key: + KEY_CTRL: input_event.ctrl_pressed = true + KEY_SHIFT: input_event.shift_pressed = true + KEY_ALT: input_event.alt_pressed = true + KEY_META: input_event.meta_pressed = true + _: + input_event.keycode = key as Key + return input_event.as_text() + + +func to_keys(input_event :InputEventKey) -> PackedInt32Array: + var keys := PackedInt32Array() + if input_event.ctrl_pressed: + keys.append(KEY_CTRL) + if input_event.shift_pressed: + keys.append(KEY_SHIFT) + if input_event.alt_pressed: + keys.append(KEY_ALT) + if input_event.meta_pressed: + keys.append(KEY_META) + keys.append(input_event.keycode) + return keys + + +func _to_human_readable(value :String) -> String: + return value.split("/")[-1].capitalize() + + +func _get_btn_icon(p_name :String) -> Texture2D: + if not Engine.is_editor_hint(): + var placeholder := PlaceholderTexture2D.new() + placeholder.size = Vector2(8,8) + return placeholder + var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + if editor: + var editior_control := editor.get_editor_interface().get_base_control() + return GodotVersionFixures.get_icon(editior_control, p_name) + return null + + +func _install_examples() -> void: + _init_progress(5) + update_progress("Downloading examples") + await get_tree().process_frame + var tmp_path := GdUnitFileAccess.create_temp_dir("download") + var zip_file := tmp_path + "/examples.zip" + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_zip_package(EAXAMPLE_URL, zip_file) + if response.code() != 200: + push_warning("Examples cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.code(), response.response()]) + update_progress("Install examples failed! Try it later again.") + await get_tree().create_timer(3).timeout + stop_progress() + return + # extract zip to tmp + update_progress("Install examples into project") + var result := GdUnitFileAccess.extract_zip(zip_file, "res://gdUnit4-examples/") + if result.is_error(): + update_progress("Install examples failed! %s" % result.error_message()) + await get_tree().create_timer(3).timeout + stop_progress() + return + update_progress("Refresh project") + await rescan(true) + update_progress("Examples successfully installed") + await get_tree().create_timer(3).timeout + stop_progress() + + +func rescan(update_scripts :bool = false) -> void: + await get_tree().idle_frame + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + var fs := plugin.get_editor_interface().get_resource_filesystem() + fs.scan_sources() + while fs.is_scanning(): + await get_tree().create_timer(1).timeout + if update_scripts: + plugin.get_editor_interface().get_resource_filesystem().update_script_classes() + + +func _on_btn_report_bug_pressed(): + OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=bug&template=bug_report.md&title=") + + +func _on_btn_request_feature_pressed(): + OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=enhancement&template=feature_request.md&title=") + + +func _on_btn_install_examples_pressed(): + _btn_install.disabled = true + await _install_examples() + _btn_install.disabled = false + + +func _on_btn_close_pressed(): + hide() + + +func _on_btn_property_reset_pressed(property: GdUnitProperty, input :Node, reset_btn :Button): + if input is CheckButton: + input.button_pressed = property.default() + elif input is LineEdit: + input.text = str(property.default()) + # we have to update manually for text input fields because of no change event is emited + _on_property_text_changed(property.default(), property, reset_btn) + elif input is OptionButton: + input.select(0) + _on_option_selected(0, property, reset_btn) + elif input is Button: + input.text = to_shortcut(property.default()) + _on_property_text_changed(property.default(), property, reset_btn) + + +func _on_property_text_changed(new_value :Variant, property: GdUnitProperty, reset_btn :Button): + property.set_value(new_value) + reset_btn.disabled = property.value() == property.default() + var error :Variant = GdUnitSettings.update_property(property) + if error: + var label :Label = _property_error.get_child(0) as Label + label.set_text(error) + var control := gui_get_focus_owner() + _property_error.show() + if control != null: + _property_error.position = control.global_position + Vector2(self.position) + Vector2(40, 40) + + +func _on_option_selected(index :int, property: GdUnitProperty, reset_btn :Button): + property.set_value(index) + reset_btn.disabled = property.value() == property.default() + GdUnitSettings.update_property(property) + + +func _on_shortcut_change(input_button :Button, property: GdUnitProperty, reset_btn :Button) -> void: + _input_capture.set_custom_minimum_size(_properties_shortcuts.get_size()) + _input_capture.visible = true + _input_capture.show() + set_process_input(false) + _input_capture.reset() + var input_event :InputEventKey = await _input_capture.input_completed + input_button.text = input_event.as_text() + _on_property_text_changed(to_keys(input_event), property, reset_btn) + set_process_input(true) + + +func _init_progress(max_value : int) -> void: + _progress_bar.visible = true + _progress_bar.max_value = max_value + _progress_bar.value = 0 + + +func _progress() -> void: + _progress_bar.value += 1 + + +func stop_progress() -> void: + _progress_bar.visible = false + + +func update_progress(message :String) -> void: + _progress_text.text = message + _progress_bar.value += 1 diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn new file mode 100644 index 0000000..0650fba --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn @@ -0,0 +1,315 @@ +[gd_scene load_steps=7 format=3 uid="uid://dwgat6j2u77g4"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"] +[ext_resource type="Texture2D" uid="uid://c7sk0yhd52lg3" path="res://addons/gdUnit4/src/ui/assets/icon.png" id="2_w63lb"] +[ext_resource type="PackedScene" uid="uid://dte0m2endcgtu" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn" id="4"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbbq5"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.172549, 0.113725, 0.141176, 1) +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.87451, 0.0705882, 0.160784, 1) +border_blend = true +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +shadow_color = Color(0, 0, 0, 0.756863) +shadow_size = 10 +shadow_offset = Vector2(10, 10) + +[node name="Control" type="Window"] +disable_3d = true +title = "GdUnitSettings" +initial_position = 1 +visible = false +wrap_controls = true +transient = true +exclusive = true +script = ExtResource("2") + +[node name="property_template" type="Control" parent="."] +visible = false +layout_mode = 3 +anchors_preset = 0 +offset_left = 4.0 +offset_top = 4.0 +offset_right = 956.0 +offset_bottom = 656.0 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="property_template"] +layout_mode = 0 +offset_top = 13.0 +offset_right = 131.0 +offset_bottom = 27.0 + +[node name="btn_reset" type="Button" parent="property_template"] +layout_mode = 0 +offset_right = 12.0 +offset_bottom = 40.0 +tooltip_text = "Reset to default value" +clip_text = true + +[node name="info" type="Label" parent="property_template"] +layout_mode = 0 +offset_left = 390.0 +offset_top = 11.0 +offset_right = 590.0 +offset_bottom = 25.0 +size_flags_horizontal = 3 +text = "Enables/disables the update notification " +clip_text = true +max_lines_visible = 1 + +[node name="sub_category" type="Panel" parent="property_template"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 30.0 +grow_horizontal = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="property_template/sub_category"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(0.196078, 0.631373, 0.639216, 1) + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("8_2ggr0") + +[node name="Panel" type="Panel" parent="."] +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="v" type="VBoxContainer" parent="Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="Panel/v"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 + +[node name="GridContainer" type="HBoxContainer" parent="Panel/v/MarginContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="PanelContainer" type="MarginContainer" parent="Panel/v/MarginContainer/GridContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Panel" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/PanelContainer"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="CenterContainer" type="CenterContainer" parent="Panel/v/MarginContainer/GridContainer/PanelContainer/Panel"] +use_parent_material = true +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="logo" type="TextureRect" parent="Panel/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer"] +custom_minimum_size = Vector2(120, 120) +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 4 +texture = ExtResource("2_w63lb") +expand_mode = 1 +stretch_mode = 5 + +[node name="CenterContainer2" type="MarginContainer" parent="Panel/v/MarginContainer/GridContainer/PanelContainer/Panel"] +use_parent_material = true +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="version" type="RichTextLabel" parent="Panel/v/MarginContainer/GridContainer/PanelContainer/Panel/CenterContainer2"] +unique_name_in_owner = true +use_parent_material = true +clip_contents = false +layout_mode = 2 +size_flags_horizontal = 3 +auto_translate = false +localize_numeral_system = false +bbcode_enabled = true +scroll_active = false +meta_underlined = false + +[node name="VBoxContainer" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/PanelContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 2 + +[node name="btn_report_bug" type="Button" parent="Panel/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to create a bug report" +text = "Report Bug" + +[node name="btn_request_feature" type="Button" parent="Panel/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to create a feature request" +text = "Request Feature" + +[node name="btn_install_examples" type="Button" parent="Panel/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Press to install the advanced test examples" +disabled = true +text = "Install Examples" + +[node name="Properties" type="TabContainer" parent="Panel/v/MarginContainer/GridContainer"] +layout_mode = 2 +size_flags_horizontal = 11 + +[node name="Common" type="ScrollContainer" parent="Panel/v/MarginContainer/GridContainer/Properties"] +layout_mode = 2 + +[node name="common-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/Common"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(1445, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="UI" type="ScrollContainer" parent="Panel/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 + +[node name="ui-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/UI"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(1249, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Shortcuts" type="ScrollContainer" parent="Panel/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 + +[node name="shortcut-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/Shortcuts"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(983, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="GdUnitInputCapture" parent="Panel/v/MarginContainer/GridContainer/Properties/Shortcuts/shortcut-content" instance=ExtResource("5_xu3j8")] +unique_name_in_owner = true +visible = false +modulate = Color(0.000201742, 0.000201742, 0.000201742, 0.100182) +z_index = 1 +z_as_relative = false +layout_mode = 2 +size_flags_horizontal = 1 +size_flags_vertical = 1 + +[node name="Report" type="ScrollContainer" parent="Panel/v/MarginContainer/GridContainer/Properties"] +visible = false +layout_mode = 2 + +[node name="report-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/Report"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(1249, 0) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Templates" parent="Panel/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4")] +visible = false +layout_mode = 2 + +[node name="propertyError" type="PopupPanel" parent="Panel/v/MarginContainer/GridContainer/Properties"] +unique_name_in_owner = true +initial_position = 1 +size = Vector2i(400, 100) +theme_override_styles/panel = SubResource("StyleBoxFlat_hbbq5") + +[node name="Label" type="Label" parent="Panel/v/MarginContainer/GridContainer/Properties/propertyError"] +offset_left = 10.0 +offset_top = 4.0 +offset_right = 390.0 +offset_bottom = 96.0 +theme_override_colors/font_color = Color(0.858824, 0, 0.109804, 1) +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="MarginContainer2" type="MarginContainer" parent="Panel/v"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/v/MarginContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 +alignment = 2 + +[node name="ProgressBar" type="ProgressBar" parent="Panel/v/MarginContainer2/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="progress_lbl" type="Label" parent="Panel/v/MarginContainer2/HBoxContainer/ProgressBar"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +clip_text = true + +[node name="btn_close" type="Button" parent="Panel/v/MarginContainer2/HBoxContainer"] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +text = "Close" + +[connection signal="pressed" from="Panel/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_report_bug" to="." method="_on_btn_report_bug_pressed"] +[connection signal="pressed" from="Panel/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_request_feature" to="." method="_on_btn_request_feature_pressed"] +[connection signal="pressed" from="Panel/v/MarginContainer/GridContainer/PanelContainer/VBoxContainer/btn_install_examples" to="." method="_on_btn_install_examples_pressed"] +[connection signal="pressed" from="Panel/v/MarginContainer2/HBoxContainer/btn_close" to="." method="_on_btn_close_pressed"] diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd new file mode 100644 index 0000000..323ed46 --- /dev/null +++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd @@ -0,0 +1,122 @@ +@tool +extends MarginContainer + +@onready var _template_editor :CodeEdit = $VBoxContainer/EdiorLayout/Editor +@onready var _tags_editor :CodeEdit = $Tags/MarginContainer/TextEdit +@onready var _title_bar :Panel = $VBoxContainer/sub_category +@onready var _save_button :Button = $VBoxContainer/Panel/HBoxContainer/Save +@onready var _selected_type :OptionButton = $VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/SelectType +@onready var _show_tags :PopupPanel = $Tags + + +var gd_key_words :PackedStringArray = ["extends", "class_name", "const", "var", "onready", "func", "void", "pass"] +var gdunit_key_words :PackedStringArray = ["GdUnitTestSuite", "before", "after", "before_test", "after_test"] +var _selected_template :int + + +func _ready() -> void: + setup_editor_colors() + setup_fonts() + setup_supported_types() + load_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + setup_tags_help() + + +func _notification(what :int) -> void: + if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: + setup_fonts() + + +func setup_editor_colors() -> void: + if not Engine.is_editor_hint(): + return + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + var settings := plugin.get_editor_interface().get_editor_settings() + var background_color :Color = settings.get_setting("text_editor/theme/highlighting/background_color") + var text_color :Color = settings.get_setting("text_editor/theme/highlighting/text_color") + var selection_color :Color = settings.get_setting("text_editor/theme/highlighting/selection_color") + + for e in [_template_editor, _tags_editor]: + var editor :CodeEdit = e + editor.add_theme_color_override("background_color", background_color) + editor.add_theme_color_override("font_color", text_color) + editor.add_theme_color_override("font_readonly_color", text_color) + editor.add_theme_color_override("font_selected_color", selection_color) + setup_highlighter(editor, settings) + + +func setup_highlighter(editor :CodeEdit, settings :EditorSettings) -> void: + var highlighter := CodeHighlighter.new() + editor.set_syntax_highlighter(highlighter) + var number_color :Color = settings.get_setting("text_editor/theme/highlighting/number_color") + var symbol_color :Color = settings.get_setting("text_editor/theme/highlighting/symbol_color") + var function_color :Color = settings.get_setting("text_editor/theme/highlighting/function_color") + var member_variable_color :Color = settings.get_setting("text_editor/theme/highlighting/member_variable_color") + var comment_color :Color = settings.get_setting("text_editor/theme/highlighting/comment_color") + var keyword_color :Color = settings.get_setting("text_editor/theme/highlighting/keyword_color") + var base_type_color :Color = settings.get_setting("text_editor/theme/highlighting/base_type_color") + var annotation_color :Color = settings.get_setting("text_editor/theme/highlighting/gdscript/annotation_color") + + highlighter.clear_color_regions() + highlighter.clear_keyword_colors() + highlighter.add_color_region("#", "", comment_color, true) + highlighter.add_color_region("${", "}", Color.YELLOW) + highlighter.add_color_region("'", "'", Color.YELLOW) + highlighter.add_color_region("\"", "\"", Color.YELLOW) + highlighter.number_color = number_color + highlighter.symbol_color = symbol_color + highlighter.function_color = function_color + highlighter.member_variable_color = member_variable_color + highlighter.add_keyword_color("@", annotation_color) + highlighter.add_keyword_color("warning_ignore", annotation_color) + for word in gd_key_words: + highlighter.add_keyword_color(word, keyword_color) + for word in gdunit_key_words: + highlighter.add_keyword_color(word, base_type_color) + + +func setup_fonts() -> void: + if _template_editor: + GdUnitFonts.init_fonts(_template_editor) + var font_size := GdUnitFonts.init_fonts(_tags_editor) + _title_bar.size.y = font_size + 16 + _title_bar.custom_minimum_size.y = font_size + 16 + + +func setup_supported_types() -> void: + _selected_type.clear() + _selected_type.add_item("GD - GDScript", GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + _selected_type.add_item("C# - CSharpScript", GdUnitTestSuiteTemplate.TEMPLATE_ID_CS) + + +func setup_tags_help() -> void: + _tags_editor.set_text(GdUnitTestSuiteTemplate.load_tags(_selected_template)) + + +func load_template(template_id :int) -> void: + _selected_template = template_id + _template_editor.set_text(GdUnitTestSuiteTemplate.load_template(template_id)) + + +func _on_Restore_pressed() -> void: + _template_editor.set_text(GdUnitTestSuiteTemplate.default_template(_selected_template)) + GdUnitTestSuiteTemplate.reset_to_default(_selected_template) + _save_button.disabled = true + + +func _on_Save_pressed() -> void: + GdUnitTestSuiteTemplate.save_template(_selected_template, _template_editor.get_text()) + _save_button.disabled = true + + +func _on_Tags_pressed() -> void: + _show_tags.popup_centered_ratio(.5) + + +func _on_Editor_text_changed() -> void: + _save_button.disabled = false + + +func _on_SelectType_item_selected(index :int) -> void: + load_template(_selected_type.get_item_id(index)) + setup_tags_help() diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn new file mode 100644 index 0000000..5ccc443 --- /dev/null +++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn @@ -0,0 +1,127 @@ +[gd_scene load_steps=2 format=3 uid="uid://dte0m2endcgtu"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd" id="1"] + +[node name="TestSuiteTemplate" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="sub_category" type="Panel" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VBoxContainer/sub_category"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 4.0 +offset_right = 4.0 +offset_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +text = "Test Suite Template +" + +[node name="EdiorLayout" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Editor" type="CodeEdit" parent="VBoxContainer/EdiorLayout"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/EdiorLayout/Editor"] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -31.0 +grow_horizontal = 2 +grow_vertical = 0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer"] +layout_mode = 2 +size_flags_vertical = 8 +alignment = 2 + +[node name="Tags" type="Button" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"] +layout_mode = 2 +tooltip_text = "Shows supported tags." +text = "Supported Tags" + +[node name="SelectType" type="OptionButton" parent="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer"] +layout_mode = 2 +tooltip_text = "Select the script type specific template." +item_count = 2 +selected = 0 +popup/item_0/text = "GD - GDScript" +popup/item_0/id = 1000 +popup/item_1/text = "C# - CSharpScript" +popup/item_1/id = 2000 + +[node name="Panel" type="MarginContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Panel"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 2 + +[node name="Restore" type="Button" parent="VBoxContainer/Panel/HBoxContainer"] +layout_mode = 2 +text = "Restore" + +[node name="Save" type="Button" parent="VBoxContainer/Panel/HBoxContainer"] +layout_mode = 2 +disabled = true +text = "Save" + +[node name="Tags" type="PopupPanel" parent="."] +size = Vector2i(300, 100) +unresizable = false +content_scale_aspect = 4 + +[node name="MarginContainer" type="MarginContainer" parent="Tags"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 4.0 +offset_top = 4.0 +offset_right = -856.0 +offset_bottom = -552.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TextEdit" type="CodeEdit" parent="Tags/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +editable = false +context_menu_enabled = false +shortcut_keys_enabled = false +virtual_keyboard_enabled = false + +[connection signal="text_changed" from="VBoxContainer/EdiorLayout/Editor" to="." method="_on_Editor_text_changed"] +[connection signal="pressed" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/Tags" to="." method="_on_Tags_pressed"] +[connection signal="item_selected" from="VBoxContainer/EdiorLayout/Editor/MarginContainer/HBoxContainer/SelectType" to="." method="_on_SelectType_item_selected"] +[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Restore" to="." method="_on_Restore_pressed"] +[connection signal="pressed" from="VBoxContainer/Panel/HBoxContainer/Save" to="." method="_on_Save_pressed"] diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd b/addons/gdUnit4/src/update/GdMarkDownReader.gd new file mode 100644 index 0000000..cc5bfb3 --- /dev/null +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd @@ -0,0 +1,337 @@ +extends RefCounted + +const FONT_H1 := 32 +const FONT_H2 := 28 +const FONT_H3 := 24 +const FONT_H4 := 20 +const FONT_H5 := 16 +const FONT_H6 := 12 + +const HORIZONTAL_RULE := "[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img]\n" +const HEADER_RULE := "[font_size=%d]$1[/font_size]\n" +const HEADER_CENTERED_RULE := "[font_size=%d][center]$1[/center][/font_size]\n" + +const image_download_folder := "res://addons/gdUnit4/tmp-update/" + +const exclude_font_size := "\b(?!(?:(font_size))\b)" + +var md_replace_patterns := [ + # horizontal rules + [regex("(?m)^[ ]{0,3}---$"), HORIZONTAL_RULE], + [regex("(?m)^[ ]{0,3}___$"), HORIZONTAL_RULE], + [regex("(?m)^[ ]{0,3}\\*\\*\\*$"), HORIZONTAL_RULE], + + # headers + [regex("(?m)^###### (.*)"), HEADER_RULE % FONT_H6], + [regex("(?m)^##### (.*)"), HEADER_RULE % FONT_H5], + [regex("(?m)^#### (.*)"), HEADER_RULE % FONT_H4], + [regex("(?m)^### (.*)"), HEADER_RULE % FONT_H3], + [regex("(?m)^## (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], + [regex("(?m)^# (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("(?m)^(.+)=={2,}$"), HEADER_RULE % FONT_H1], + [regex("(?m)^(.+)--{2,}$"), HEADER_RULE % FONT_H2], + # html headers + [regex("

((.*?\\R?)+)<\\/h1>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("((.*?\\R?)+)<\\/h1>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("

((.*?\\R?)+)<\\/h2>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2], + [regex("((.*?\\R?)+)<\\/h2>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1], + [regex("

((.*?\\R?)+)<\\/h3>"), HEADER_RULE % FONT_H3], + [regex("((.*?\\R?)+)<\\/h3>"), HEADER_CENTERED_RULE % FONT_H3], + [regex("

((.*?\\R?)+)<\\/h4>"), HEADER_RULE % FONT_H4], + [regex("((.*?\\R?)+)<\\/h4>"), HEADER_CENTERED_RULE % FONT_H4], + [regex("
((.*?\\R?)+)<\\/h5>"), HEADER_RULE % FONT_H5], + [regex("((.*?\\R?)+)<\\/h5>"), HEADER_CENTERED_RULE % FONT_H5], + [regex("
((.*?\\R?)+)<\\/h6>"), HEADER_RULE % FONT_H6], + [regex("((.*?\\R?)+)<\\/h6>"), HEADER_CENTERED_RULE % FONT_H6], + + # asterics + #[regex("(\\*)"), "xxx$1xxx"], + + # extract/compile image references + [regex("!\\[(.*?)\\]\\[(.*?)\\]"), Callable(self, "process_image_references")], + # extract images with path and optional tool tip + [regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)"), Callable(self, "process_image")], + + # links + [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)\\)"), "[url={\"url\":\"$3\"}]$2[/url]"], + # links with tool tip + [regex("([!]|)\\[(.+)\\]\\(([^ ]+?)( \"(.+)\")?\\)"), "[url={\"url\":\"$3\", \"tool_tip\":\"$5\"}]$2[/url]"], + + # embeded text + [regex("(?m)^[ ]{0,3}>(.*?)$"), "[img=50x14]res://addons/gdUnit4/src/update/assets/embedded.png[/img][i]$1[/i]"], + + # italic + bold font + [regex("[_]{3}(.*?)[_]{3}"), "[i][b]$1[/b][/i]"], + [regex("[\\*]{3}(.*?)[\\*]{3}"), "[i][b]$1[/b][/i]"], + # bold font + [regex("(.*?)<\\/b>"), "[b]$1[/b]"], + [regex("[_]{2}(.*?)[_]{2}"), "[b]$1[/b]"], + [regex("[\\*]{2}(.*?)[\\*]{2}"), "[b]$1[/b]"], + # italic font + [regex("(.*?)<\\/i>"), "[i]$1[/i]"], + [regex(exclude_font_size+"_(.*?)_"), "[i]$1[/i]"], + [regex("\\*(.*?)\\*"), "[i]$1[/i]"], + + # strikethrough font + [regex("(.*?)"), "[s]$1[/s]"], + [regex("~~(.*?)~~"), "[s]$1[/s]"], + [regex("~(.*?)~"), "[s]$1[/s]"], + + # handling lists + # using an image for dots as workaroud because list is not supported checked Godot 3.x + [regex("(?m)^[ ]{0,1}[*\\-+] (.*)$"), list_replace(0)], + [regex("(?m)^[ ]{2,3}[*\\-+] (.*)$"), list_replace(1)], + [regex("(?m)^[ ]{4,5}[*\\-+] (.*)$"), list_replace(2)], + [regex("(?m)^[ ]{6,7}[*\\-+] (.*)$"), list_replace(3)], + [regex("(?m)^[ ]{8,9}[*\\-+] (.*)$"), list_replace(4)], + + # code blocks, code blocks looks not like code blocks in richtext + [regex("```(javascript|python|shell|gdscript)([\\s\\S]*?\n)```"), code_block("$2", true)], + [regex("``([\\s\\S]*?)``"), code_block("$1")], + [regex("`([\\s\\S]*?)`{1,2}"), code_block("$1")], +] + +var _img_replace_regex := RegEx.new() +var _image_urls := PackedStringArray() +var _on_table_tag := false +var _client + + +func regex(pattern :String) -> RegEx: + var regex_ := RegEx.new() + var err = regex_.compile(pattern) + if err != OK: + push_error("error '%s' checked pattern '%s'" % [err, pattern]) + return null + return regex_ + + +func _init(): + _img_replace_regex.compile("\\[img\\]((.*?))\\[/img\\]") + + +func set_http_client(client) -> void: + _client = client + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + # finally remove_at the downloaded images + for image in _image_urls: + DirAccess.remove_absolute(image) + DirAccess.remove_absolute(image + ".import") + + +func list_replace(indent :int) -> String: + var replace_pattern := "[img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img]" if indent %2 else "[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img]" + replace_pattern += " $1" + + for index in indent: + replace_pattern = replace_pattern.insert(0, " ") + return replace_pattern + + +func code_block(replace :String, border :bool = false) -> String: + var cb := "[code][color=aqua][font_size=16]%s[/font_size][/color][/code]" % replace + if border: + return "[img=1400x14]res://addons/gdUnit4/src/update/assets/border_top.png[/img]"\ + + "[indent]" + cb + "[/indent]"\ + + "[img=1400x14]res://addons/gdUnit4/src/update/assets/border_bottom.png[/img]\n" + return cb + + +func to_bbcode(input :String) -> String: + input = process_tables(input) + + for pattern in md_replace_patterns: + var regex_ :RegEx = pattern[0] + var bb_replace = pattern[1] + if bb_replace is Callable: + input = await bb_replace.call(regex_, input) + else: + input = regex_.sub(input, bb_replace, true) + return input + "\n" + + +func process_tables(input :String) -> String: + var bbcode := Array() + var lines := Array(input.split("\n")) + while not lines.is_empty(): + if is_table(lines[0]): + bbcode.append_array(parse_table(lines)) + continue + bbcode.append(lines.pop_front()) + return "\n".join(PackedStringArray(bbcode)) + + +class Table: + var _columns :int + var _rows := Array() + + class Row: + var _cells := PackedStringArray() + + func _init(cells :PackedStringArray,columns :int): + _cells = cells + for i in range(_cells.size(), columns): + _cells.append("") + + func to_bbcode(cell_sizes :PackedInt32Array, bold :bool) -> String: + var cells := PackedStringArray() + for cell_index in _cells.size(): + var cell :String = _cells[cell_index] + if cell.strip_edges() == "--": + cell = create_line(cell_sizes[cell_index]) + if bold: + cell = "[b]%s[/b]" % cell + cells.append("[cell]%s[/cell]" % cell) + return "|".join(cells) + + func create_line(length :int) -> String: + var line := "" + for i in length: + line += "-" + return line + + func _init(columns :int): + _columns = columns + + func parse_row(line :String) -> bool: + # is line containing cells? + if line.find("|") == -1: + return false + _rows.append(Row.new(line.split("|"), _columns)) + return true + + func calculate_max_cell_sizes() -> PackedInt32Array: + var cells_size := PackedInt32Array() + for column in _columns: + cells_size.append(0) + + for row_index in _rows.size(): + var row :Row = _rows[row_index] + for cell_index in row._cells.size(): + var cell_size :int = cells_size[cell_index] + var size := row._cells[cell_index].length() + if size > cell_size: + cells_size[cell_index] = size + return cells_size + + func to_bbcode() -> PackedStringArray: + var cell_sizes := calculate_max_cell_sizes() + var bb_code := PackedStringArray() + + bb_code.append("[table=%d]" % _columns) + for row_index in _rows.size(): + bb_code.append(_rows[row_index].to_bbcode(cell_sizes, row_index==0)) + bb_code.append("[/table]\n") + return bb_code + + +func parse_table(lines :Array) -> PackedStringArray: + var line :String = lines[0] + var table := Table.new(line.count("|") + 1) + while not lines.is_empty(): + line = lines.pop_front() + if not table.parse_row(line): + break + return table.to_bbcode() + + +func is_table(line :String) -> bool: + return line.find("|") != -1 + + +func open_table(line :String) -> String: + _on_table_tag = true + return "[table=%d]" % (line.count("|") + 1) + + +func close_table() -> String: + _on_table_tag = false + return "[/table]" + + +func extract_cells(line :String, bold := false) -> String: + var cells := "" + for cell in line.split("|"): + if bold: + cell = "[b]%s[/b]" % cell + cells += "[cell]%s[/cell]" % cell + return cells + + +func process_image_references(p_regex :RegEx, p_input :String) -> String: + # exists references? + var matches := p_regex.search_all(p_input) + if matches.is_empty(): + return p_input + # collect image references and remove_at it + var references := Dictionary() + var link_regex := regex("\\[(\\S+)\\]:(\\S+)([ ]\"(.*)\")?") + # create copy of original source to replace checked it + var input := p_input.replace("\r", "") + var extracted_references := p_input.replace("\r", "") + for reg_match in link_regex.search_all(input): + var line = reg_match.get_string(0) + "\n" + var ref = reg_match.get_string(1) + #var topl_tip = reg_match.get_string(4) + # collect reference and url + references[ref] = reg_match.get_string(2) + extracted_references = extracted_references.replace(line, "") + + # replace image references by collected url's + for reference_key in references.keys(): + var regex_key := regex("\\](\\[%s\\])" % reference_key) + for reg_match in regex_key.search_all(extracted_references): + var ref :String = reg_match.get_string(0) + var image_url :String = "](%s)" % references.get(reference_key) + extracted_references = extracted_references.replace(ref, image_url) + return extracted_references + + +func process_image(p_regex :RegEx, p_input :String) -> String: + var to_replace := PackedStringArray() + var tool_tips := PackedStringArray() + # find all matches + var matches := p_regex.search_all(p_input) + if matches.is_empty(): + return p_input + for reg_match in matches: + # grap the parts to replace and store temporay because a direct replace will distort the offsets + to_replace.append(p_input.substr(reg_match.get_start(0), reg_match.get_end(0))) + # grap optional tool tips + tool_tips.append(reg_match.get_string(5)) + # finally replace all findings + for replace in to_replace: + var re := p_regex.sub(replace, "[img]$2[/img]") + p_input = p_input.replace(replace, re) + return await _process_external_image_resources(p_input) + + +func _process_external_image_resources(input :String) -> String: + DirAccess.make_dir_recursive_absolute(image_download_folder) + # scan all img for external resources and download it + for value in _img_replace_regex.search_all(input): + if value.get_group_count() >= 1: + var image_url :String = value.get_string(1) + # if not a local resource we need to download it + if image_url.begins_with("http"): + if OS.is_stdout_verbose(): + prints("download image:", image_url) + var response = await _client.request_image(image_url) + if response.code() == 200: + var image = Image.new() + var error = image.load_png_from_buffer(response.body()) + if error != OK: + prints("Error creating image from response", error) + # replace characters where format characters + var new_url := image_download_folder + image_url.get_file().replace("_", "-") + if new_url.get_extension() != 'png': + new_url = new_url + '.png' + var err := image.save_png(new_url) + if err: + push_error("Can't save image to '%s'. Error: %s" % [new_url, error_string(err)]) + _image_urls.append(new_url) + input = input.replace(image_url, new_url) + return input diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd b/addons/gdUnit4/src/update/GdUnitPatch.gd new file mode 100644 index 0000000..3745687 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd @@ -0,0 +1,20 @@ +class_name GdUnitPatch +extends RefCounted + +const PATCH_VERSION = "patch_version" + +var _version :GdUnit4Version + + +func _init(version_ :GdUnit4Version): + _version = version_ + + +func version() -> GdUnit4Version: + return _version + + +# this function needs to be implement +func execute() -> bool: + push_error("The function 'execute()' is not implemented at %s" % self) + return false diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd b/addons/gdUnit4/src/update/GdUnitPatcher.gd new file mode 100644 index 0000000..d83cb82 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd @@ -0,0 +1,72 @@ +class_name GdUnitPatcher +extends RefCounted + + +const _base_dir := "res://addons/gdUnit4/src/update/patches/" + +var _patches := Dictionary() + + +func scan(current :) -> void: + _scan(_base_dir, current) + + +func _scan(scan_path :String, current) -> void: + _patches = Dictionary() + var patch_paths := _collect_patch_versions(scan_path, current) + for path in patch_paths: + prints("scan for patches checked '%s'" % path) + _patches[path] = _scan_patches(path) + + +func patch_count() -> int: + var count := 0 + for key in _patches.keys(): + count += _patches[key].size() + return count + + +func execute() -> void: + for key in _patches.keys(): + var patch_root :String = key + for path in _patches[key]: + var patch :GdUnitPatch = load(patch_root + "/" + path).new() + if patch: + prints("execute patch", patch.version(), patch.get_script().resource_path) + if not patch.execute(): + prints("error checked execution patch %s" % patch_root + "/" + path) + + +func _collect_patch_versions(scan_path :String, current :) -> PackedStringArray: + if not DirAccess.dir_exists_absolute(scan_path): + return PackedStringArray() + var patches := Array() + var dir := DirAccess.open(scan_path) + if dir != null: + dir.list_dir_begin() # TODO GODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + var version := GdUnit4Version.parse(next) + if version.is_greater(current): + patches.append(scan_path + next) + patches.sort() + return PackedStringArray(patches) + + +func _scan_patches(path :String) -> PackedStringArray: + var patches := Array() + var dir := DirAccess.open(path) + if dir != null: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var next := "." + while next != "": + next = dir.get_next() + if next.is_empty() or next == "." or next == "..": + continue + patches.append(next) + # make sorted from lowest to high version + patches.sort() + return PackedStringArray(patches) diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd new file mode 100644 index 0000000..815ee42 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -0,0 +1,230 @@ +@tool +extends ConfirmationDialog + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +const spinner_icon := "res://addons/gdUnit4/src/ui/assets/spinner.tres" + + +@onready var _progress_content :RichTextLabel = %message +@onready var _progress_bar :TextureProgressBar = %progress + + +var _debug_mode := false +var _editor_interface :EditorInterface +var _update_client :GdUnitUpdateClient +var _download_url :String + + +func _ready(): + message_h4("Press 'Update' to start!", Color.GREEN) + init_progress(5) + + +func _process(_delta): + if _progress_content != null and _progress_content.is_visible_in_tree(): + _progress_content.queue_redraw() + + +func init_progress(max_value : int) -> void: + _progress_bar.max_value = max_value + _progress_bar.value = 1 + + +func setup(editor_interface :EditorInterface, update_client :GdUnitUpdateClient, download_url :String) -> void: + _editor_interface = editor_interface + _update_client = update_client + _download_url = download_url + + +func update_progress(message :String) -> void: + message_h4(message, Color.GREEN) + _progress_bar.value += 1 + if _debug_mode: + await get_tree().create_timer(3).timeout + await get_tree().create_timer(.2).timeout + + +func _colored(message :String, color :Color) -> String: + return "[color=#%s]%s[/color]" % [color.to_html(), message] + + +func message_h4(message :String, color :Color) -> void: + _progress_content.clear() + _progress_content.append_text("[font_size=16]%s[/font_size]" % _colored(message, color)) + + +func run_update() -> void: + get_cancel_button().disabled = true + get_ok_button().disabled = true + + await update_progress("Download Release ... [img=24x24]%s[/img]" % spinner_icon) + await download_release() + await update_progress("Extract update ... [img=24x24]%s[/img]" % spinner_icon) + var zip_file := temp_dir() + "/update.zip" + var tmp_path := create_temp_dir("update") + var result :Variant = extract_zip(zip_file, tmp_path) + if result == null: + await update_progress("Update failed!") + await get_tree().create_timer(3).timeout + queue_free() + return + + await update_progress("Uninstall GdUnit4 ... [img=24x24]%s[/img]" % spinner_icon) + disable_gdUnit() + if not _debug_mode: + delete_directory("res://addons/gdUnit4/") + # give editor time to react on deleted files + await get_tree().create_timer(1).timeout + + await update_progress("Install new GdUnit4 version ...") + if _debug_mode: + copy_directory(tmp_path, "res://debug") + else: + copy_directory(tmp_path, "res://") + + await update_progress("New GdUnit version successfully installed, Restarting Godot ...") + await get_tree().create_timer(3).timeout + enable_gdUnit() + hide() + delete_directory("res://addons/.gdunit_update") + restart_godot() + + +func restart_godot() -> void: + prints("Force restart Godot") + if _editor_interface: + _editor_interface.restart_editor(true) + + +func enable_gdUnit() -> void: + var enabled_plugins := PackedStringArray() + if ProjectSettings.has_setting("editor_plugins/enabled"): + enabled_plugins = ProjectSettings.get_setting("editor_plugins/enabled") + if not enabled_plugins.has("res://addons/gdUnit4/plugin.cfg"): + enabled_plugins.append("res://addons/gdUnit4/plugin.cfg") + ProjectSettings.set_setting("editor_plugins/enabled", enabled_plugins) + ProjectSettings.save() + + +func disable_gdUnit() -> void: + if _editor_interface: + _editor_interface.set_plugin_enabled("gdUnit4", false) + + +const GDUNIT_TEMP := "user://tmp" + +func temp_dir() -> String: + if not DirAccess.dir_exists_absolute(GDUNIT_TEMP): + DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) + return GDUNIT_TEMP + + +func create_temp_dir(folder_name :String) -> String: + var new_folder = temp_dir() + "/" + folder_name + delete_directory(new_folder) + if not DirAccess.dir_exists_absolute(new_folder): + DirAccess.make_dir_recursive_absolute(new_folder) + return new_folder + + +func delete_directory(path :String, only_content := false) -> void: + var dir := DirAccess.open(path) + if dir != null: + dir.list_dir_begin() + var file_name := "." + while file_name != "": + file_name = dir.get_next() + if file_name.is_empty() or file_name == "." or file_name == "..": + continue + var next := path + "/" +file_name + if dir.current_is_dir(): + delete_directory(next) + else: + # delete file + var err = dir.remove(next) + if err: + push_error("Delete %s failed: %s" % [next, error_string(err)]) + if not only_content: + var err := dir.remove(path) + if err: + push_error("Delete %s failed: %s" % [path, error_string(err)]) + + +func copy_directory(from_dir :String, to_dir :String) -> bool: + if not DirAccess.dir_exists_absolute(from_dir): + push_error("Source directory not found '%s'" % from_dir) + return false + # check if destination exists + if not DirAccess.dir_exists_absolute(to_dir): + # create it + var err := DirAccess.make_dir_recursive_absolute(to_dir) + if err != OK: + push_error("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)]) + return false + var source_dir := DirAccess.open(from_dir) + var dest_dir := DirAccess.open(to_dir) + if source_dir != null: + source_dir.list_dir_begin() + var next := "." + + while next != "": + next = source_dir.get_next() + if next == "" or next == "." or next == "..": + continue + var source := source_dir.get_current_dir() + "/" + next + var dest := dest_dir.get_current_dir() + "/" + next + if source_dir.current_is_dir(): + copy_directory(source + "/", dest) + continue + var err = source_dir.copy(source, dest) + if err != OK: + push_error("Error checked copy file '%s' to '%s'" % [source, dest]) + return false + return true + else: + push_error("Directory not found: " + from_dir) + return false + + +func extract_zip(zip_package :String, dest_path :String) -> Variant: + var zip: ZIPReader = ZIPReader.new() + var err := zip.open(zip_package) + if err != OK: + push_error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) + return null + var zip_entries: PackedStringArray = zip.get_files() + # Get base path and step over archive folder + var archive_path = zip_entries[0] + zip_entries.remove_at(0) + + for zip_entry in zip_entries: + var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "") + if zip_entry.ends_with("/"): + DirAccess.make_dir_recursive_absolute(new_file_path) + continue + var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) + file.store_buffer(zip.read_file(zip_entry)) + zip.close() + return dest_path + + +func download_release() -> void: + var zip_file := GdUnitFileAccess.temp_dir() + "/update.zip" + var response :GdUnitUpdateClient.HttpResponse + if _debug_mode: + response = GdUnitUpdateClient.HttpResponse.new(200, PackedByteArray()) + zip_file = "res://update.zip" + else: + response = await _update_client.request_zip_package(_download_url, zip_file) + _update_client.queue_free() + if response.code() != 200: + push_warning("Update information cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.code(), response.response()]) + message_h4("Update failed! Try it later again.", Color.RED) + await get_tree().create_timer(3).timeout + return + + +func _on_confirmed(): + await run_update() diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.tscn b/addons/gdUnit4/src/update/GdUnitUpdate.tscn new file mode 100644 index 0000000..81fff38 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdate.tscn @@ -0,0 +1,74 @@ +[gd_scene load_steps=3 format=3 uid="uid://2eahgaw88y6q"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdate.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://cwxuep3lbnu3p" path="res://addons/gdUnit4/src/update/assets/progress-background.png" id="2_q6rkd"] + +[node name="GdUnitUpdate" type="ConfirmationDialog"] +disable_3d = true +title = "Update GdUnit4 to new version" +size = Vector2i(1000, 141) +visible = true +extend_to_title = true +min_size = Vector2i(1000, 140) +ok_button_text = "Update" +dialog_hide_on_ok = false +script = ExtResource("1") + +[node name="UpdateProgress" type="PanelContainer" parent="."] +custom_minimum_size = Vector2(0, 80) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -49.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="UpdateProgress"] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="UpdateProgress/VBoxContainer"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 + +[node name="message" type="RichTextLabel" parent="UpdateProgress/VBoxContainer/Panel"] +unique_name_in_owner = true +custom_minimum_size = Vector2(400, 40) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +bbcode_enabled = true +fit_content = true +scroll_active = false +shortcut_keys_enabled = false + +[node name="Panel2" type="Panel" parent="UpdateProgress/VBoxContainer"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 + +[node name="progress" type="TextureProgressBar" parent="UpdateProgress/VBoxContainer/Panel2"] +unique_name_in_owner = true +clip_contents = true +custom_minimum_size = Vector2(980, 30) +layout_mode = 0 +offset_left = 2.0 +offset_right = 982.0 +offset_bottom = 36.0 +auto_translate = false +localize_numeral_system = false +min_value = 1.0 +max_value = 5.0 +value = 1.0 +rounded = true +nine_patch_stretch = true +texture_progress = ExtResource("2_q6rkd") +texture_progress_offset = Vector2(0, 2) + +[connection signal="confirmed" from="." to="." method="_on_confirmed"] diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd new file mode 100644 index 0000000..3d1e876 --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd @@ -0,0 +1,83 @@ +@tool +extends Node + +signal request_completed(response) + +class HttpResponse: + var _code :int + var _body :PackedByteArray + + + func _init(code_ :int, body_ :PackedByteArray): + _code = code_ + _body = body_ + + func code() -> int: + return _code + + func response() -> Variant: + var test_json_conv := JSON.new() + test_json_conv.parse(_body.get_string_from_utf8()) + return test_json_conv.get_data() + + func body() -> PackedByteArray: + return _body + +var _http_request :HTTPRequest = HTTPRequest.new() + + +func _ready(): + add_child(_http_request) + _http_request.connect("request_completed", Callable(self, "_on_request_completed")) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + if is_instance_valid(_http_request): + _http_request.queue_free() + + +#func list_tags() -> void: +# _http_request.connect("request_completed",Callable(self,"_response_request_tags")) +# var error = _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags") +# if error != OK: +# push_error("An error occurred in the HTTP request.") + + +func request_latest_version() -> HttpResponse: + var error = _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/tags") + if error != OK: + var message = "request_latest_version failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_releases() -> HttpResponse: + var error = _http_request.request("https://api.github.com/repos/MikeSchulze/gdUnit4/releases") + if error != OK: + var message = "request_releases failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_image(url :String) -> HttpResponse: + var error = _http_request.request(url) + if error != OK: + var message = "request_image failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func request_zip_package(url :String, file :String) -> HttpResponse: + _http_request.set_download_file(file) + var error = _http_request.request(url) + if error != OK: + var message = "request_zip_package failed: %d" % error + return HttpResponse.new(error, message.to_utf8_buffer()) + return await self.request_completed + + +func _on_request_completed(_result :int, response_code :int, _headers :PackedStringArray, body :PackedByteArray): + if _http_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: + _http_request.set_download_file("") + request_completed.emit(HttpResponse.new(response_code, body)) diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd new file mode 100644 index 0000000..cc363fc --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd @@ -0,0 +1,183 @@ +@tool +extends Window + +signal request_completed(response) + +const GdMarkDownReader = preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd") +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const spinner_icon := "res://addons/gdUnit4/src/ui/assets/spinner.tres" + +@onready var _md_reader :GdMarkDownReader = GdMarkDownReader.new() +@onready var _update_client :GdUnitUpdateClient = $GdUnitUpdateClient +@onready var _header :Label = $Panel/GridContainer/PanelContainer/header +@onready var _update_button :Button = $Panel/GridContainer/Panel/HBoxContainer/update +@onready var _close_button :Button = $Panel/GridContainer/Panel/HBoxContainer/close +@onready var _content :RichTextLabel = $Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content + +var _debug_mode := false + +var _editor_interface :EditorInterface +var _patcher :GdUnitPatcher = GdUnitPatcher.new() +var _current_version := GdUnit4Version.current() +var _available_versions :Array +var _download_zip_url :String + + +func _ready(): + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + _editor_interface = plugin.get_editor_interface() + _update_button.disabled = true + _md_reader.set_http_client(_update_client) + GdUnitFonts.init_fonts(_content) + await request_releases() + + +func request_releases() -> void: + if _debug_mode: + _header.text = "A new version 'v4.1.0_debug' is available" + await show_update() + return + + # wait 20s to allow the editor to initialize itself + await get_tree().create_timer(20).timeout + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_latest_version() + if response.code() != 200: + push_warning("Update information cannot be retrieved from GitHub! \n %s" % response.response()) + return + var latest_version := extract_latest_version(response) + # if same version exit here no update need + if latest_version.is_greater(_current_version): + _patcher.scan(_current_version) + _header.text = "A new version '%s' is available" % latest_version + _download_zip_url = extract_zip_url(response) + await show_update() + + +func _colored(message :String, color :Color) -> String: + return "[color=#%s]%s[/color]" % [color.to_html(), message] + + +func message_h4(message :String, color :Color, clear := true) -> void: + if clear: + _content.clear() + _content.append_text("[font_size=16]%s[/font_size]" % _colored(message, color)) + + +func message(message :String, color :Color) -> void: + _content.clear() + _content.append_text(_colored(message, color)) + + +func _process(_delta): + if _content != null and _content.is_visible_in_tree(): + _content.queue_redraw() + + +func show_update() -> void: + message_h4("\n\n\nRequest release infos ... [img=24x24]%s[/img]" % spinner_icon, Color.SNOW) + popup_centered_ratio(.5) + prints("Scan for GdUnit4 Update ...") + var content :String + if _debug_mode: + var template = FileAccess.open("res://addons/gdUnit4/test/update/resources/markdown.txt", FileAccess.READ).get_as_text() + content = await _md_reader.to_bbcode(template) + else: + var response :GdUnitUpdateClient.HttpResponse = await _update_client.request_releases() + if response.code() == 200: + content = await extract_releases(response, _current_version) + else: + message_h4("\n\n\nError checked request available releases!", Color.RED) + return + + # finally force rescan to import images as textures + if Engine.is_editor_hint(): + await rescan() + message(content, Color.DODGER_BLUE) + _update_button.set_disabled(false) + + +static func extract_latest_version(response :GdUnitUpdateClient.HttpResponse) -> GdUnit4Version: + var body :Array = response.response() + return GdUnit4Version.parse(body[0]["name"]) + + +static func extract_zip_url(response :GdUnitUpdateClient.HttpResponse) -> String: + var body :Array = response.response() + return body[0]["zipball_url"] + + +func extract_releases(response :GdUnitUpdateClient.HttpResponse, current_version) -> String: + await get_tree().process_frame + var result := "" + for release in response.response(): + if GdUnit4Version.parse(release["tag_name"]).equals(current_version): + break + var release_description :String = release["body"] + result += await _md_reader.to_bbcode(release_description) + return result + + +func rescan() -> void: + if Engine.is_editor_hint(): + if OS.is_stdout_verbose(): + prints(".. reimport release resources") + var fs := _editor_interface.get_resource_filesystem() + fs.scan() + while fs.is_scanning(): + if OS.is_stdout_verbose(): + progressBar(fs.get_scanning_progress() * 100 as int) + await Engine.get_main_loop().process_frame + await Engine.get_main_loop().process_frame + await get_tree().create_timer(1).timeout + + +func progressBar(p_progress :int, p_color :Color = Color.POWDER_BLUE): + if p_progress < 0: + p_progress = 0 + if p_progress > 100: + p_progress = 100 + printraw("scan [%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "#").rpad(50, "-"), p_progress]) + + +func _on_update_pressed(): + _update_button.set_disabled(true) + # close all opend scripts before start the update + if not _debug_mode: + ScriptEditorControls.close_open_editor_scripts() + # copy update source to a temp because the update is deleting the whole gdUnit folder + DirAccess.make_dir_absolute("res://addons/.gdunit_update") + DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", "res://addons/.gdunit_update/GdUnitUpdate.tscn") + DirAccess.copy_absolute("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd") + var source := FileAccess.open("res://addons/gdUnit4/src/update/GdUnitUpdate.tscn", FileAccess.READ) + var content := source.get_as_text().replace("res://addons/gdUnit4/src/update/GdUnitUpdate.gd", "res://addons/.gdunit_update/GdUnitUpdate.gd") + var dest := FileAccess.open("res://addons/.gdunit_update/GdUnitUpdate.tscn", FileAccess.WRITE) + dest.store_string(content) + hide() + var update = load("res://addons/.gdunit_update/GdUnitUpdate.tscn").instantiate() + update.setup(_editor_interface, _update_client, _download_zip_url) + Engine.get_main_loop().root.add_child(update) + update.popup_centered() + + +func _on_show_next_toggled(enabled :bool): + GdUnitSettings.set_update_notification(enabled) + + +func _on_cancel_pressed(): + hide() + + +func _on_content_meta_clicked(meta :String): + var properties = str_to_var(meta) + if properties.has("url"): + OS.shell_open(properties.get("url")) + + +func _on_content_meta_hover_started(meta :String): + var properties = str_to_var(meta) + if properties.has("tool_tip"): + _content.set_tooltip_text(properties.get("tool_tip")) + + +func _on_content_meta_hover_ended(meta): + _content.set_tooltip_text("") diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn new file mode 100644 index 0000000..11993eb --- /dev/null +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn @@ -0,0 +1,114 @@ +[gd_scene load_steps=3 format=3 uid="uid://0xyeci1tqebj"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.gd" id="1_112wo"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="2_18asx"] + +[node name="Control" type="Window"] +disable_3d = true +gui_embed_subwindows = true +initial_position = 1 +title = "GdUnit Update Notification" +size = Vector2i(800, 400) +visible = false +wrap_controls = true +transient = true +exclusive = true +min_size = Vector2i(800, 400) +script = ExtResource("1_112wo") + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("2_18asx") + +[node name="Panel" type="Panel" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="GridContainer" type="VBoxContainer" parent="Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 1 + +[node name="PanelContainer" type="MarginContainer" parent="Panel/GridContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="header" type="Label" parent="Panel/GridContainer/PanelContainer"] +layout_mode = 2 +size_flags_horizontal = 9 + +[node name="PanelContainer2" type="PanelContainer" parent="Panel/GridContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="Panel/GridContainer/PanelContainer2"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="Panel/GridContainer/PanelContainer2/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="content" type="RichTextLabel" parent="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +bbcode_enabled = true + +[node name="Panel" type="MarginContainer" parent="Panel/GridContainer"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/GridContainer/Panel"] +use_parent_material = true +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="show_next" type="CheckBox" parent="Panel/GridContainer/Panel/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +text = "show next time" + +[node name="update" type="Button" parent="Panel/GridContainer/Panel/HBoxContainer"] +custom_minimum_size = Vector2(100, 40) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +disabled = true +text = "Update" + +[node name="close" type="Button" parent="Panel/GridContainer/Panel/HBoxContainer"] +custom_minimum_size = Vector2(100, 40) +layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +text = "Close" + +[connection signal="meta_clicked" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_clicked"] +[connection signal="meta_hover_ended" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_ended"] +[connection signal="meta_hover_started" from="Panel/GridContainer/PanelContainer2/ScrollContainer/MarginContainer/content" to="." method="_on_content_meta_hover_started"] +[connection signal="toggled" from="Panel/GridContainer/Panel/HBoxContainer/show_next" to="." method="_on_show_next_toggled"] +[connection signal="pressed" from="Panel/GridContainer/Panel/HBoxContainer/update" to="." method="_on_update_pressed"] +[connection signal="pressed" from="Panel/GridContainer/Panel/HBoxContainer/close" to="." method="_on_cancel_pressed"] diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png b/addons/gdUnit4/src/update/assets/border_bottom.png new file mode 100644 index 0000000..aa16bb7 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/border_bottom.png differ diff --git a/addons/gdUnit4/src/update/assets/border_bottom.png.import b/addons/gdUnit4/src/update/assets/border_bottom.png.import new file mode 100644 index 0000000..70e9a02 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_bottom.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dmv3ld2otx1e2" +path="res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/border_bottom.png" +dest_files=["res://.godot/imported/border_bottom.png-30d66a4c67e3a03ad191e37cdf16549d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/border_top.png b/addons/gdUnit4/src/update/assets/border_top.png new file mode 100644 index 0000000..b1b1039 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/border_top.png differ diff --git a/addons/gdUnit4/src/update/assets/border_top.png.import b/addons/gdUnit4/src/update/assets/border_top.png.import new file mode 100644 index 0000000..814882a --- /dev/null +++ b/addons/gdUnit4/src/update/assets/border_top.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4sio0j5om50s" +path="res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/border_top.png" +dest_files=["res://.godot/imported/border_top.png-c47cbebdb755144731c6ae309e18bbaa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/dot1.png b/addons/gdUnit4/src/update/assets/dot1.png new file mode 100644 index 0000000..d5ea77b Binary files /dev/null and b/addons/gdUnit4/src/update/assets/dot1.png differ diff --git a/addons/gdUnit4/src/update/assets/dot1.png.import b/addons/gdUnit4/src/update/assets/dot1.png.import new file mode 100644 index 0000000..f01f709 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot1.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ce2eojg0pwpov" +path="res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/dot1.png" +dest_files=["res://.godot/imported/dot1.png-380baf1b5247addda93bce3c799aa4e7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/dot2.png b/addons/gdUnit4/src/update/assets/dot2.png new file mode 100644 index 0000000..4a74498 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/dot2.png differ diff --git a/addons/gdUnit4/src/update/assets/dot2.png.import b/addons/gdUnit4/src/update/assets/dot2.png.import new file mode 100644 index 0000000..f1e1017 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/dot2.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvwa5lg3qj0e2" +path="res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/dot2.png" +dest_files=["res://.godot/imported/dot2.png-86a9db80ef4413e353c4339ad8f68a5f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/embedded.png b/addons/gdUnit4/src/update/assets/embedded.png new file mode 100644 index 0000000..15cb9ab Binary files /dev/null and b/addons/gdUnit4/src/update/assets/embedded.png differ diff --git a/addons/gdUnit4/src/update/assets/embedded.png.import b/addons/gdUnit4/src/update/assets/embedded.png.import new file mode 100644 index 0000000..26f3726 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/embedded.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://63wk5nib3r7q" +path="res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/embedded.png" +dest_files=["res://.godot/imported/embedded.png-29390948772209a603567d24f8766495.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/fonts/LICENSE.txt b/addons/gdUnit4/src/update/assets/fonts/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/addons/gdUnit4/src/update/assets/fonts/README.txt b/addons/gdUnit4/src/update/assets/fonts/README.txt new file mode 100644 index 0000000..b34e21b --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/README.txt @@ -0,0 +1,77 @@ +Roboto Mono Variable Font +========================= + +This download contains Roboto Mono as both variable fonts and static fonts. + +Roboto Mono is a variable font with this axis: + wght + +This means all the styles are contained in these files: + RobotoMono-VariableFont_wght.ttf + RobotoMono-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Roboto Mono: + static/RobotoMono-Thin.ttf + static/RobotoMono-ExtraLight.ttf + static/RobotoMono-Light.ttf + static/RobotoMono-Regular.ttf + static/RobotoMono-Medium.ttf + static/RobotoMono-SemiBold.ttf + static/RobotoMono-Bold.ttf + static/RobotoMono-ThinItalic.ttf + static/RobotoMono-ExtraLightItalic.ttf + static/RobotoMono-LightItalic.ttf + static/RobotoMono-Italic.ttf + static/RobotoMono-MediumItalic.ttf + static/RobotoMono-SemiBoldItalic.ttf + static/RobotoMono-BoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (LICENSE.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them freely in your products & projects - print or digital, +commercial or otherwise. However, you can't sell the fonts on their own. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf new file mode 100644 index 0000000..900fce6 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf.import new file mode 100644 index 0000000..0bc39e4 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://dgpj1y3a73vc" +path="res://.godot/imported/RobotoMono-Bold.ttf-ea008af97d359b7630bd271235703cae.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf" +dest_files=["res://.godot/imported/RobotoMono-Bold.ttf-ea008af97d359b7630bd271235703cae.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf new file mode 100644 index 0000000..4bfe29a Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf.import new file mode 100644 index 0000000..e7dab2b --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://cnrsmyjdnlikm" +path="res://.godot/imported/RobotoMono-BoldItalic.ttf-6e10905211cda810d470782293480777.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf" +dest_files=["res://.godot/imported/RobotoMono-BoldItalic.ttf-6e10905211cda810d470782293480777.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf new file mode 100644 index 0000000..d535884 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf.import new file mode 100644 index 0000000..a8dc5f8 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://c8ey5njg4eh6d" +path="res://.godot/imported/RobotoMono-ExtraLight.ttf-c8ac954f2ab584e7652e58ccd95cc705.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf" +dest_files=["res://.godot/imported/RobotoMono-ExtraLight.ttf-c8ac954f2ab584e7652e58ccd95cc705.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf new file mode 100644 index 0000000..b28960a Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf.import new file mode 100644 index 0000000..4011502 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://pghouxn0ujr7" +path="res://.godot/imported/RobotoMono-ExtraLightItalic.ttf-06133dd8b521ead6203b317979387344.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf" +dest_files=["res://.godot/imported/RobotoMono-ExtraLightItalic.ttf-06133dd8b521ead6203b317979387344.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf new file mode 100644 index 0000000..4ee4dc4 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf.import new file mode 100644 index 0000000..40dab27 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://hp3750m8e3nq" +path="res://.godot/imported/RobotoMono-Italic.ttf-328fe6d9b2ac5d629c43c335b916d307.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf" +dest_files=["res://.godot/imported/RobotoMono-Italic.ttf-328fe6d9b2ac5d629c43c335b916d307.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf new file mode 100644 index 0000000..276af4c Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf.import new file mode 100644 index 0000000..cddd89e --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://dm1xiq8pmd6xk" +path="res://.godot/imported/RobotoMono-Light.ttf-638f745780c834176c3bf9969f0e408e.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf" +dest_files=["res://.godot/imported/RobotoMono-Light.ttf-638f745780c834176c3bf9969f0e408e.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf new file mode 100644 index 0000000..a2801c2 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf.import new file mode 100644 index 0000000..4216e5d --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://crphq80yxhgic" +path="res://.godot/imported/RobotoMono-LightItalic.ttf-473f0d613e289d058b8c392d0c5242bc.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf" +dest_files=["res://.godot/imported/RobotoMono-LightItalic.ttf-473f0d613e289d058b8c392d0c5242bc.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf new file mode 100644 index 0000000..8461be7 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf.import new file mode 100644 index 0000000..342bafe --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://csdprnrpwr3xp" +path="res://.godot/imported/RobotoMono-Medium.ttf-f165ecef77d89557a95acac0927f13c4.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf" +dest_files=["res://.godot/imported/RobotoMono-Medium.ttf-f165ecef77d89557a95acac0927f13c4.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf new file mode 100644 index 0000000..a3bfaa1 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf.import new file mode 100644 index 0000000..604eddb --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://cdxcb7jq16o5k" +path="res://.godot/imported/RobotoMono-MediumItalic.ttf-40c40d791914284c8092585e839f0cd1.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf" +dest_files=["res://.godot/imported/RobotoMono-MediumItalic.ttf-40c40d791914284c8092585e839f0cd1.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf new file mode 100644 index 0000000..7c4ce36 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf.import new file mode 100644 index 0000000..3d8905f --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://ck2sp0iypcks" +path="res://.godot/imported/RobotoMono-Regular.ttf-f5a7315540116b55ba9e010120cbfb0c.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf" +dest_files=["res://.godot/imported/RobotoMono-Regular.ttf-f5a7315540116b55ba9e010120cbfb0c.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf new file mode 100644 index 0000000..15ee6c6 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf.import new file mode 100644 index 0000000..e264b7e --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://bexpf232jnjbc" +path="res://.godot/imported/RobotoMono-SemiBold.ttf-6012d0b71d40b9767a7b6a480fe0d4b7.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf" +dest_files=["res://.godot/imported/RobotoMono-SemiBold.ttf-6012d0b71d40b9767a7b6a480fe0d4b7.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf new file mode 100644 index 0000000..8e21497 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf.import new file mode 100644 index 0000000..d135b44 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://xuc8ovbe0rku" +path="res://.godot/imported/RobotoMono-SemiBoldItalic.ttf-12b525223c8f2dfb78bca4e7ecaf3ca5.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf" +dest_files=["res://.godot/imported/RobotoMono-SemiBoldItalic.ttf-12b525223c8f2dfb78bca4e7ecaf3ca5.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf new file mode 100644 index 0000000..ee8a3fd Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf.import new file mode 100644 index 0000000..e519d15 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://cnc1pdajlxyxp" +path="res://.godot/imported/RobotoMono-Thin.ttf-a3a6620deea1a01e153a2a60c778e675.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf" +dest_files=["res://.godot/imported/RobotoMono-Thin.ttf-a3a6620deea1a01e153a2a60c778e675.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf new file mode 100644 index 0000000..40b01e4 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf.import new file mode 100644 index 0000000..f9aaeda --- /dev/null +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf.import @@ -0,0 +1,33 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://cvh21iixbcfww" +path="res://.godot/imported/RobotoMono-ThinItalic.ttf-e9ceff3e4cdfbfedd19dbb3d3bba724a.fontdata" + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf" +dest_files=["res://.godot/imported/RobotoMono-ThinItalic.ttf-e9ceff3e4cdfbfedd19dbb3d3bba724a.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +hinting=1 +subpixel_positioning=1 +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png b/addons/gdUnit4/src/update/assets/horizontal-line2.png new file mode 100644 index 0000000..66aa098 Binary files /dev/null and b/addons/gdUnit4/src/update/assets/horizontal-line2.png differ diff --git a/addons/gdUnit4/src/update/assets/horizontal-line2.png.import b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import new file mode 100644 index 0000000..949745c --- /dev/null +++ b/addons/gdUnit4/src/update/assets/horizontal-line2.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgaa5faajesgv" +path="res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/horizontal-line2.png" +dest_files=["res://.godot/imported/horizontal-line2.png-92618e6ee5cc9002847547a8c9deadbc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/update/assets/progress-background.png b/addons/gdUnit4/src/update/assets/progress-background.png new file mode 100644 index 0000000..670013a Binary files /dev/null and b/addons/gdUnit4/src/update/assets/progress-background.png differ diff --git a/addons/gdUnit4/src/update/assets/progress-background.png.import b/addons/gdUnit4/src/update/assets/progress-background.png.import new file mode 100644 index 0000000..3f105e8 --- /dev/null +++ b/addons/gdUnit4/src/update/assets/progress-background.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cwxuep3lbnu3p" +path="res://.godot/imported/progress-background.png-20022b3af2be583c006365edbc69d914.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/update/assets/progress-background.png" +dest_files=["res://.godot/imported/progress-background.png-20022b3af2be583c006365edbc69d914.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/test/GdUnitAwaiterTest.gd b/addons/gdUnit4/test/GdUnitAwaiterTest.gd new file mode 100644 index 0000000..54f0e3a --- /dev/null +++ b/addons/gdUnit4/test/GdUnitAwaiterTest.gd @@ -0,0 +1,86 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitAwaiterTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/GdUnitAwaiter.gd' + +signal test_signal_a() +signal test_signal_b() +signal test_signal_c(value) +signal test_signal_d(value_a, value_b) + + +func after_test(): + for node in get_children(): + if node is Timer: + remove_child(node) + node.stop() + node.free() + + +func install_signal_emitter(signal_name :String, signal_args: Array = [], time_out : float = 0.020): + var timer := Timer.new() + add_child(timer) + timer.timeout.connect(Callable(self, "emit_test_signal").bind(signal_name, signal_args)) + timer.one_shot = true + timer.start(time_out) + + +func emit_test_signal(signal_name :String, signal_args: Array): + match signal_args.size(): + 0: emit_signal(signal_name) + 1: emit_signal(signal_name, signal_args[0]) + 2: emit_signal(signal_name, signal_args[0], signal_args[1]) + 3: emit_signal(signal_name, signal_args[0], signal_args[1], signal_args[2]) + + +func test_await_signal_on() -> void: + install_signal_emitter("test_signal_a") + await await_signal_on(self, "test_signal_a", [], 100) + + install_signal_emitter("test_signal_b") + await await_signal_on(self, "test_signal_b", [], 100) + + install_signal_emitter("test_signal_c", []) + await await_signal_on(self, "test_signal_c", [], 100) + + install_signal_emitter("test_signal_c", ["abc"]) + await await_signal_on(self, "test_signal_c", ["abc"], 100) + + install_signal_emitter("test_signal_c", ["abc", "eee"]) + await await_signal_on(self, "test_signal_c", ["abc", "eee"], 100) + + +func test_await_signal_on_manysignals_emitted() -> void: + # emits many different signals + install_signal_emitter("test_signal_a") + install_signal_emitter("test_signal_a") + install_signal_emitter("test_signal_a") + install_signal_emitter("test_signal_c", ["xxx"]) + # the signal we want to wait for + install_signal_emitter("test_signal_c", ["abc"], .200) + install_signal_emitter("test_signal_c", ["yyy"], .100) + # we only wait for 'test_signal_c("abc")' is emitted + await await_signal_on(self, "test_signal_c", ["abc"], 300) + + +func test_await_signal_on_never_emitted_timedout() -> void: + ( + # we wait for 'test_signal_c("yyy")' which is never emitted + await assert_failure_await(func x(): await await_signal_on(self, "test_signal_c", ["yyy"], 200)) + ).has_line(73)\ + .has_message("await_signal_on(test_signal_c, [\"yyy\"]) timed out after 200ms") + + +func test_await_signal_on_invalid_source_timedout() -> void: + ( + # we wait for a signal on a already freed instance + await assert_failure_await(func x(): await await_signal_on(invalid_node(), "tree_entered", [], 300)) + ).has_line(81).has_message(GdAssertMessages.error_await_signal_on_invalid_instance(null, "tree_entered", [])) + + +func invalid_node() -> Node: + return null diff --git a/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd b/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd new file mode 100644 index 0000000..e57484e --- /dev/null +++ b/addons/gdUnit4/test/GdUnitScanTestSuitePerformanceTest.gd @@ -0,0 +1,13 @@ +extends GdUnitTestSuite + + +func test_load_performance() -> void: + var time = LocalTime.now() + prints("Scan for test suites.") + var test_suites := GdUnitTestSuiteScanner.new().scan("res://addons/gdUnit4/test/") + assert_int(time.elapsed_since_ms())\ + .override_failure_message("Expecting the loading time overall is less than 10s")\ + .is_less(10*1000) + prints("Scanning of %d test suites took" % test_suites.size(), time.elapsed_since()) + for ts in test_suites: + ts.free() diff --git a/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd b/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd new file mode 100644 index 0000000..cec2975 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitScanTestSuiteTest.gd @@ -0,0 +1,29 @@ +# This test verifys only the scanner functionallity +# to find all given tests by pattern 'func test_():' +extends GdUnitTestSuite + + +func test_example(): + assert_that("This is a example message").has_length(25) + assert_that("This is a example message").starts_with("This is a ex") + + +func test_b(): + pass + + +# test name starts with same name e.g. test_b vs test_b2 +func test_b2(): + pass + + +# test scanning success with invalid formatting +func test_b21 ( ) : + pass + + +# finally verify all tests are found +func after(): + assert_array(get_children())\ + .extract("get_name")\ + .contains_exactly(["test_example", "test_b", "test_b2", "test_b21"]) diff --git a/addons/gdUnit4/test/GdUnitScriptTypeTest.gd b/addons/gdUnit4/test/GdUnitScriptTypeTest.gd new file mode 100644 index 0000000..badc982 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitScriptTypeTest.gd @@ -0,0 +1,17 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitScriptTypeTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitScriptType.gd' + + +func test_type_of() -> void: + assert_str(GdUnitScriptType.type_of(null)).is_equal(GdUnitScriptType.UNKNOWN) + assert_str(GdUnitScriptType.type_of(ClassDB.instantiate("GDScript"))).is_equal(GdUnitScriptType.GD) + #if GdUnit4CSharpApiLoader.is_mono_supported(): + # assert_str(GdUnitScriptType.type_of(ClassDB.instantiate("CSharpScript"))).is_equal(GdUnitScriptType.CS) + #assert_str(GdUnitScriptType.type_of(ClassDB.instantiate("VisualScript"))).is_equal(GdUnitScriptType.VS) + #assert_str(GdUnitScriptType.type_of(ClassDB.instantiate("NativeScript"))).is_equal(GdUnitScriptType.NATIVE) diff --git a/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd b/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd new file mode 100644 index 0000000..fee6900 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestCaseTimeoutTest.gd @@ -0,0 +1,125 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite + +const SECOND :int = 1000 +const MINUTE :int = SECOND*60 + +var _before_arg +var _test_arg + + +func before(): + # use some variables to test clone test suite works as expected + _before_arg = "---before---" + + +func before_test(): + # set failing test to success if failed by timeout + discard_error_interupted_by_timeout() + _test_arg = "abc" + + +# without custom timeout should execute the complete test +func test_timeout_after_test_completes(): + assert_str(_before_arg).is_equal("---before---") + var counter := 0 + await await_millis(1000) + prints("A","1s") + counter += 1 + await await_millis(1000) + prints("A","2s") + counter += 1 + await await_millis(1000) + prints("A","3s") + counter += 1 + await await_millis(1000) + prints("A","5s") + counter += 2 + prints("A","end test test_timeout_after_test_completes") + assert_int(counter).is_equal(5) + + +# set test timeout to 2s +@warning_ignore("unused_parameter") +func test_timeout_2s(timeout=2000): + assert_str(_before_arg).is_equal("---before---") + prints("B", "0s") + await await_millis(1000) + prints("B", "1s") + await await_millis(1000) + prints("B", "2s") + await await_millis(1000) + # this line should not reach if timeout aborts the test case after 2s + fail("The test case must be interupted by a timeout after 2s") + prints("B", "3s") + prints("B", "end") + + +# set test timeout to 4s +@warning_ignore("unused_parameter") +func test_timeout_4s(timeout=4000): + assert_str(_before_arg).is_equal("---before---") + prints("C", "0s") + await await_millis(1000) + prints("C", "1s") + await await_millis(1000) + prints("C", "2s") + await await_millis(1000) + prints("C", "3s") + await await_millis(4000) + # this line should not reach if timeout aborts the test case after 4s + fail("The test case must be interupted by a timeout after 4s") + prints("C", "7s") + prints("C", "end") + + +@warning_ignore("unused_parameter") +func test_timeout_single_yield_wait(timeout=3000): + assert_str(_before_arg).is_equal("---before---") + prints("D", "0s") + await await_millis(6000) + prints("D", "6s") + # this line should not reach if timeout aborts the test case after 3s + fail("The test case must be interupted by a timeout after 3s") + prints("D", "end test test_timeout") + + +@warning_ignore("unused_parameter") +func test_timeout_long_running_test_abort(timeout=4000): + assert_str(_before_arg).is_equal("---before---") + prints("E", "0s") + var start_time := Time.get_ticks_msec() + var sec_start_time := Time.get_ticks_msec() + + # simulate long running function + while true: + var elapsed_time := Time.get_ticks_msec() - start_time + var sec_time = Time.get_ticks_msec() - sec_start_time + + if sec_time > 1000: + sec_start_time = Time.get_ticks_msec() + prints("E", LocalTime.elapsed(elapsed_time)) + + # give system time to check for timeout + await await_millis(200) + + # exit while after 4500ms inclusive 500ms offset + if elapsed_time > 4500: + break + + # this line should not reach if timeout aborts the test case after 4s + fail("The test case must be abort interupted by a timeout 4s") + prints("F", "end test test_timeout") + + +@warning_ignore("unused_parameter", "unused_variable") +func test_timeout_fuzzer(fuzzer := Fuzzers.rangei(-23, 22), timeout=2000): + discard_error_interupted_by_timeout() + var value = fuzzer.next_value() + # wait each iteration 200ms + await await_millis(200) + # we expects the test is interupped after 10 iterations because each test takes 200ms + # and the test should not longer run than 2000ms + assert_int(fuzzer.iteration_index())\ + .override_failure_message("The test must be interupted after around 10 iterations")\ + .is_less_equal(10) diff --git a/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd b/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd new file mode 100644 index 0000000..6fbf138 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestCaseTimingTest.gd @@ -0,0 +1,96 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite + +var _iteration_timer_start = 0 +var _test_values_current :Dictionary +var _test_values_expected :Dictionary + +const SECOND:int = 1000 +const MINUTE:int = SECOND*60 + +class TestCaseStatistics: + var _test_before_calls :int + var _test_after_calls :int + + + func _init(before_calls := 0, after_calls := 0): + _test_before_calls = before_calls + _test_after_calls = after_calls + + +func before(): + _test_values_current = { + "test_2s" : TestCaseStatistics.new(), + "test_multi_yielding" : TestCaseStatistics.new(), + "test_multi_yielding_with_fuzzer" : TestCaseStatistics.new() + } + _test_values_expected = { + "test_2s" : TestCaseStatistics.new(1, 1), + "test_multi_yielding" : TestCaseStatistics.new(1, 1), + "test_multi_yielding_with_fuzzer" : TestCaseStatistics.new(5 , 5) + } + + +func after(): + for test_case in _test_values_expected.keys(): + var current := _test_values_current[test_case] as TestCaseStatistics + var expected := _test_values_expected[test_case] as TestCaseStatistics + assert_int(current._test_before_calls)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [expected._test_before_calls, current._test_before_calls, test_case])\ + .is_equal(expected._test_before_calls) + assert_int(current._test_after_calls)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [expected._test_before_calls, current._test_before_calls, test_case])\ + .is_equal(expected._test_after_calls) + + +func before_test(): + var current = _test_values_current[__active_test_case] as TestCaseStatistics + current._test_before_calls +=1 + + +func after_test(): + var current = _test_values_current[__active_test_case] as TestCaseStatistics + current._test_after_calls +=1 + + +func test_2s(): + var timer_start := Time.get_ticks_msec() + await await_millis(2000) + # subtract an offset of 100ms because the time is not accurate + assert_int(Time.get_ticks_msec()-timer_start).is_between(2*SECOND-100, 2*SECOND+100) + + +func test_multi_yielding(): + var timer_start := Time.get_ticks_msec() + prints("test_yielding") + await get_tree().process_frame + prints("4") + await get_tree().create_timer(1.0).timeout + prints("3") + await get_tree().create_timer(1.0).timeout + prints("2") + await get_tree().create_timer(1.0).timeout + prints("1") + await get_tree().create_timer(1.0).timeout + prints("Go") + assert_int(Time.get_ticks_msec()-timer_start).is_greater_equal(4*(SECOND-50)) + + +func test_multi_yielding_with_fuzzer(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations = 5): + if fuzzer.iteration_index() > 5: + fail("Should only called 5 times") + if fuzzer.iteration_index() == 1: + _iteration_timer_start = Time.get_ticks_msec() + prints("test iteration %d" % fuzzer.iteration_index()) + prints("4") + await get_tree().create_timer(1.0).timeout + prints("3") + await get_tree().create_timer(1.0).timeout + prints("2") + await get_tree().create_timer(1.0).timeout + prints("1") + await get_tree().create_timer(1.0).timeout + prints("Go") + if fuzzer.iteration_index() == 5: + # elapsed time must be fuzzer_iterations times * 4s = 40s (using 3,9s because of inaccurate timings) + assert_int(Time.get_ticks_msec()-_iteration_timer_start).is_greater_equal(3900*fuzzer_iterations) diff --git a/addons/gdUnit4/test/GdUnitTestResourceLoader.gd b/addons/gdUnit4/test/GdUnitTestResourceLoader.gd new file mode 100644 index 0000000..665c1ee --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestResourceLoader.gd @@ -0,0 +1,77 @@ +class_name GdUnitTestResourceLoader +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +enum { + GD_SUITE, + CS_SUITE +} + + +static func load_test_suite(resource_path :String, script_type = GD_SUITE) -> Node: + match script_type: + GD_SUITE: + return load_test_suite_gd(resource_path) + CS_SUITE: + return load_test_suite_cs(resource_path) + assert("type '%s' is not implemented" % script_type) + return null + + +static func load_test_suite_gd(resource_path :String) -> GdUnitTestSuite: + var script := load_gd_script(resource_path) + var test_suite :GdUnitTestSuite = script.new() + test_suite.set_name(resource_path.get_file().replace(".resource", "").replace(".gd", "")) + # complete test suite wiht parsed test cases + var suite_scanner := GdUnitTestSuiteScanner.new() + var test_case_names := suite_scanner._extract_test_case_names(script) + # add test cases to test suite and parse test case line nummber + suite_scanner._parse_and_add_test_cases(test_suite, script, test_case_names) + return test_suite + + +static func load_test_suite_cs(resource_path :String) -> Node: + if not GdUnit4CSharpApiLoader.is_mono_supported(): + return null + var script = ClassDB.instantiate("CSharpScript") + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + script.resource_path = resource_path + script.reload() + return null + + +static func load_cs_script(resource_path :String, debug_write := false) -> Script: + if not GdUnit4CSharpApiLoader.is_mono_supported(): + return null + var script = ClassDB.instantiate("CSharpScript") + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + script.resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % resource_path.get_file().replace(".resource", ".cs") + if debug_write: + print_debug("save resource:", script.resource_path) + DirAccess.remove_absolute(script.resource_path) + var err := ResourceSaver.save(script, script.resource_path) + if err != OK: + print_debug("Can't save debug resource", script.resource_path, "Error:", error_string(err)) + script.take_over_path(script.resource_path) + else: + script.take_over_path(resource_path) + script.reload() + return script + + +static func load_gd_script(resource_path :String, debug_write := false) -> GDScript: + var script := GDScript.new() + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + script.resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % resource_path.get_file().replace(".resource", ".gd") + if debug_write: + print_debug("save resource:", script.resource_path) + DirAccess.remove_absolute(script.resource_path) + var err := ResourceSaver.save(script, script.resource_path) + if err != OK: + print_debug("Can't save debug resource", script.resource_path, "Error:", error_string(err)) + script.take_over_path(script.resource_path) + else: + script.take_over_path(resource_path) + script.reload() + return script diff --git a/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd b/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd new file mode 100644 index 0000000..d75de1c --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestResourceLoaderTest.gd @@ -0,0 +1,16 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitTestResourceLoaderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/test/GdUnitTestResourceLoader.gd' + + +func test_load_test_suite_gd() -> void: + var resource_path = "res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource" + var test_suite := GdUnitTestResourceLoader.load_test_suite_gd(resource_path) + assert_that(test_suite).is_not_null() + assert_object(test_suite).is_instanceof(GdUnitTestSuite) + test_suite.free() diff --git a/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd b/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd new file mode 100644 index 0000000..f2770c0 --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestSuitePerformanceTest.gd @@ -0,0 +1,13 @@ +extends GdUnitTestSuite + + +func test_testsuite_loading_performance(): + var time := LocalTime.now() + var reload_counter := 100 + for i in range(1, reload_counter): + ResourceLoader.load("res://addons/gdUnit4/src/GdUnitTestSuite.gd", "GDScript", ResourceLoader.CACHE_MODE_IGNORE) + var error_message := "Expecting the loading time of test-suite is less than 50ms\n But was %s" % (time.elapsed_since_ms() / reload_counter) + assert_int(time.elapsed_since_ms()/ reload_counter)\ + .override_failure_message(error_message)\ + .is_less(50) + prints("loading takes %d ms" % (time.elapsed_since_ms() / reload_counter)) diff --git a/addons/gdUnit4/test/GdUnitTestSuiteTest.gd b/addons/gdUnit4/test/GdUnitTestSuiteTest.gd new file mode 100644 index 0000000..c7c8f6a --- /dev/null +++ b/addons/gdUnit4/test/GdUnitTestSuiteTest.gd @@ -0,0 +1,48 @@ +# GdUnit generated TestSuite +class_name GdUnitTestSuiteTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/GdUnitTestSuite.gd' +const GdUnitAssertImpl = preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + +var _events :Array[GdUnitEvent] = [] + + +func collect_report(event :GdUnitEvent): + _events.push_back(event) + + +func before(): + # register to receive test reports + GdUnitSignals.instance().gdunit_event.connect(collect_report) + + +func after(): + # verify the test case `test_unknown_argument_in_test_case` was skipped + assert_array(_events).extractv(extr("type"), extr("is_skipped"), extr("test_name"))\ + .contains([tuple(GdUnitEvent.TESTCASE_AFTER, true, "test_unknown_argument_in_test_case")]) + GdUnitSignals.instance().gdunit_event.disconnect(collect_report) + + +func test_assert_that_types() -> void: + assert_object(assert_that(true)).is_instanceof(GdUnitBoolAssert) + assert_object(assert_that(1)).is_instanceof(GdUnitIntAssert) + assert_object(assert_that(3.12)).is_instanceof(GdUnitFloatAssert) + assert_object(assert_that("abc")).is_instanceof(GdUnitStringAssert) + assert_object(assert_that(Vector2.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector2i.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector3.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector3i.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector4.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that(Vector4i.ONE)).is_instanceof(GdUnitVectorAssert) + assert_object(assert_that([])).is_instanceof(GdUnitArrayAssert) + assert_object(assert_that({})).is_instanceof(GdUnitDictionaryAssert) + assert_object(assert_that(GdUnitResult.new())).is_instanceof(GdUnitObjectAssert) + # all not a built-in types mapped to default GdUnitAssert + assert_object(assert_that(Color.RED)).is_instanceof(GdUnitAssertImpl) + assert_object(assert_that(Plane.PLANE_XY)).is_instanceof(GdUnitAssertImpl) + + +func test_unknown_argument_in_test_case(_invalid_arg) -> void: + fail("This test case should be not executed, it must be skipped.") diff --git a/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd b/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd new file mode 100644 index 0000000..1698977 --- /dev/null +++ b/addons/gdUnit4/test/asserts/CallBackValueProviderTest.gd @@ -0,0 +1,23 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name CallBackValueProviderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/CallBackValueProvider.gd' + + +func next_value() -> String: + return "a value" + + +func test_get_value() -> void: + var vp := CallBackValueProvider.new(self, "next_value") + assert_str(await vp.get_value()).is_equal("a value") + + +func test_construct_invalid() -> void: + var vp := CallBackValueProvider.new(self, "invalid_func", Array(), false) + # will return null because of invalid function name + assert_str(await vp.get_value()).is_null() diff --git a/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd new file mode 100644 index 0000000..d70dbb6 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitArrayAssertImplTest.gd @@ -0,0 +1,777 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd' + +var _saved_report_assert_warnings + + +func before(): + _saved_report_assert_warnings = ProjectSettings.get_setting(GdUnitSettings.REPORT_ASSERT_WARNINGS) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_WARNINGS, false) + + +func after(): + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_WARNINGS, _saved_report_assert_warnings) + + +func test_is_null() -> void: + assert_array(null).is_null() + + assert_failure(func(): assert_array([]).is_null()) \ + .is_failed() \ + .has_message("Expecting: '' but was ''") + + +func test_is_not_null() -> void: + assert_array([]).is_not_null() + + assert_failure(func(): assert_array(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal(): + assert_array([1, 2, 3, 4, 2, 5]).is_equal([1, 2, 3, 4, 2, 5]) + # should fail because the array not contains same elements and has diff size + assert_failure(func(): assert_array([1, 2, 4, 5]).is_equal([1, 2, 3, 4, 2, 5])) \ + .is_failed() + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).is_equal([1, 2, 3, 4])) \ + .is_failed() + # current array is bigger than expected + assert_failure(func(): assert_array([1, 2222, 3, 4, 5, 6]).is_equal([1, 2, 3, 4])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4]' + but was + '[1, 2222, 3, 4, 5, 6]' + + Differences found: + Index Current Expected 1 2222 2 4 5 5 6 """ + .dedent().trim_prefix("\n")) + + # expected array is bigger than current + assert_failure(func(): assert_array([1, 222, 3, 4]).is_equal([1, 2, 3, 4, 5, 6])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5, 6]' + but was + '[1, 222, 3, 4]' + + Differences found: + Index Current Expected 1 222 2 4 5 5 6 """ + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array(null).is_equal([1, 2, 3])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3]' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_equal_big_arrays(): + var expeted := Array() + expeted.resize(1000) + for i in 1000: + expeted[i] = i + var current := expeted.duplicate() + current[10] = "invalid" + current[40] = "invalid" + current[100] = "invalid" + current[888] = "invalid" + + assert_failure(func(): assert_array(current).is_equal(expeted)) \ + .is_failed() \ + .has_message(""" + Expecting: + '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, ...]' + but was + '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, invalid, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, ...]' + + Differences found: + Index Current Expected 10 invalid 10 40 invalid 40 100 invalid 100 888 invalid 888 """ + .dedent().trim_prefix("\n")) + + +func test_is_equal_ignoring_case(): + assert_array(["this", "is", "a", "message"]).is_equal_ignoring_case(["This", "is", "a", "Message"]) + # should fail because the array not contains same elements + assert_failure(func(): assert_array(["this", "is", "a", "message"]).is_equal_ignoring_case(["This", "is", "an", "Message"])) \ + .is_failed() + assert_failure(func(): assert_array(null).is_equal_ignoring_case(["This", "is"])) \ + .is_failed() \ + .has_message(""" + Expecting: + '["This", "is"]' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_not_equal(): + assert_array(null).is_not_equal([1, 2, 3]) + assert_array([1, 2, 3, 4, 5]).is_not_equal([1, 2, 3, 4, 5, 6]) + # should fail because the array contains same elements + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).is_not_equal([1, 2, 3, 4, 5])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + not equal to + '[1, 2, 3, 4, 5]'""" + .dedent().trim_prefix("\n")) + + +func test_is_not_equal_ignoring_case(): + assert_array(null).is_not_equal_ignoring_case(["This", "is", "an", "Message"]) + assert_array(["this", "is", "a", "message"]).is_not_equal_ignoring_case(["This", "is", "an", "Message"]) + # should fail because the array contains same elements ignoring case sensitive + assert_failure(func(): assert_array(["this", "is", "a", "message"]).is_not_equal_ignoring_case(["This", "is", "a", "Message"])) \ + .is_failed() \ + .has_message(""" + Expecting: + '["This", "is", "a", "Message"]' + not equal to (case insensitiv) + '["this", "is", "a", "message"]'""" + .dedent().trim_prefix("\n")) + + +func test_is_empty(): + assert_array([]).is_empty() + + assert_failure(func(): assert_array([1, 2, 3]).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + '[1, 2, 3]'""" + .dedent().trim_prefix("\n")) + assert_failure(func(): assert_array(null).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_is_not_empty(): + assert_array(null).is_not_empty() + assert_array([1]).is_not_empty() + + assert_failure(func(): assert_array([]).is_not_empty()) \ + .is_failed() \ + .has_message("Expecting:\n must not be empty") + + +func test_is_same() -> void: + var value := [0] + assert_array(value).is_same(value) + + assert_failure(func(): assert_array(value).is_same(value.duplicate()))\ + .is_failed()\ + .has_message("Expecting:\n '[0]'\n to refer to the same object\n '[0]'") + + +func test_is_not_same() -> void: + assert_array([0]).is_not_same([0]) + var value := [0] + assert_failure(func(): assert_array(value).is_not_same(value))\ + .is_failed()\ + .has_message("Expecting not same:\n '[0]'") + + +func test_has_size(): + assert_array([1, 2, 3, 4, 5]).has_size(5) + assert_array(["a", "b", "c", "d", "e", "f"]).has_size(6) + + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).has_size(4)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '4' + but was + '5'""" + .dedent().trim_prefix("\n")) + assert_failure(func(): assert_array(null).has_size(4)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '4' + but was + ''""" + .dedent().trim_prefix("\n")) + + +func test_contains(): + assert_array([1, 2, 3, 4, 5]).contains([]) + assert_array([1, 2, 3, 4, 5]).contains([5, 2]) + assert_array([1, 2, 3, 4, 5]).contains([5, 4, 3, 2, 1]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains([TestObj.new("A", 0)]) + + # should fail because the array not contains 7 and 6 + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).contains([2, 7, 6])) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '[1, 2, 3, 4, 5]' + do contains (in any order) + '[2, 7, 6]' + but could not find elements: + '[7, 6]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array(null).contains([2, 7, 6])) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '' + do contains (in any order) + '[2, 7, 6]' + but could not find elements: + '[2, 7, 6]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array([valueA, valueB]).contains([TestObj.new("C", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '[class:A, class:B]' + do contains (in any order) + '[class:C]' + but could not find elements: + '[class:C]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_exactly(): + assert_array([1, 2, 3, 4, 5]).contains_exactly([1, 2, 3, 4, 5]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_exactly([TestObj.new("A", 0), valueB]) + + # should fail because the array contains the same elements but in a different order + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).contains_exactly([1, 4, 3, 2, 5])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5]' + do contains (in same order) + '[1, 4, 3, 2, 5]' + but has different order at position '1' + '2' vs '4'""" + .dedent().trim_prefix("\n")) + + # should fail because the array contains more elements and in a different order + assert_failure(func(): assert_array([1, 2, 3, 4, 5, 6, 7]).contains_exactly([1, 4, 3, 2, 5])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5, 6, 7]' + do contains (in same order) + '[1, 4, 3, 2, 5]' + but some elements where not expected: + '[6, 7]'""" + .dedent().trim_prefix("\n")) + + # should fail because the array contains less elements and in a different order + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).contains_exactly([1, 4, 3, 2, 5, 6, 7])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 3, 4, 5]' + do contains (in same order) + '[1, 4, 3, 2, 5, 6, 7]' + but could not find elements: + '[6, 7]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array(null).contains_exactly([1, 4, 3])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains (in same order) + '[1, 4, 3]' + but could not find elements: + '[1, 4, 3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array([valueA, valueB]).contains_exactly([valueB, TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[class:A, class:B]' + do contains (in same order) + '[class:B, class:A]' + but has different order at position '0' + 'class:A' vs 'class:B'""" + .dedent().trim_prefix("\n")) + + +func test_contains_exactly_in_any_order(): + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order([1, 2, 3, 4, 5]) + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order([5, 3, 2, 4, 1]) + assert_array([1, 2, 3, 4, 5]).contains_exactly_in_any_order([5, 1, 2, 4, 3]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_exactly_in_any_order([valueB, TestObj.new("A", 0)]) + + # should fail because the array contains not exactly the same elements in any order + assert_failure(func(): assert_array([1, 2, 6, 4, 5]).contains_exactly_in_any_order([5, 3, 2, 4, 1, 9, 10])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 6, 4, 5]' + do contains exactly (in any order) + '[5, 3, 2, 4, 1, 9, 10]' + but some elements where not expected: + '[6]' + and could not find elements: + '[3, 9, 10]'""" + .dedent().trim_prefix("\n")) + + #should fail because the array contains the same elements but in a different order + assert_failure(func(): assert_array([1, 2, 6, 9, 10, 4, 5]).contains_exactly_in_any_order([5, 3, 2, 4, 1])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[1, 2, 6, 9, 10, 4, 5]' + do contains exactly (in any order) + '[5, 3, 2, 4, 1]' + but some elements where not expected: + '[6, 9, 10]' + and could not find elements: + '[3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array(null).contains_exactly_in_any_order([1, 4, 3])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains exactly (in any order) + '[1, 4, 3]' + but could not find elements: + '[1, 4, 3]'""" + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array([valueA, valueB]).contains_exactly_in_any_order([valueB, TestObj.new("C", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '[class:A, class:B]' + do contains exactly (in any order) + '[class:B, class:C]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:C]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same(): + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + + assert_array([valueA, valueB]).contains_same([valueA]) + + assert_failure(func(): assert_array([valueA, valueB]).contains_same([TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME elements: + '[class:A, class:B]' + do contains (in any order) + '[class:A]' + but could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same_exactly(): + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_same_exactly([valueA, valueB]) + + assert_failure(func(): assert_array([valueA, valueB]).contains_same_exactly([valueB, valueA])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME exactly elements: + '[class:A, class:B]' + do contains (in same order) + '[class:B, class:A]' + but has different order at position '0' + 'class:A' vs 'class:B'""" + .dedent().trim_prefix("\n")) + + assert_failure(func(): assert_array([valueA, valueB]).contains_same_exactly([TestObj.new("A", 0), valueB])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME exactly elements: + '[class:A, class:B]' + do contains (in same order) + '[class:A, class:B]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_same_exactly_in_any_order(): + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).contains_same_exactly_in_any_order([valueB, valueA]) + + assert_failure(func(): assert_array([valueA, valueB]).contains_same_exactly_in_any_order([valueB, TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME exactly elements: + '[class:A, class:B]' + do contains exactly (in any order) + '[class:B, class:A]' + but some elements where not expected: + '[class:A]' + and could not find elements: + '[class:A]'""" + .dedent().trim_prefix("\n")) + + +func test_not_contains(): + assert_array([]).not_contains([0]) + assert_array([1, 2, 3, 4, 5]).not_contains([0]) + assert_array([1, 2, 3, 4, 5]).not_contains([0, 6]) + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + assert_array([valueA, valueB]).not_contains([TestObj.new("C", 0)]) + + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).not_contains([5]))\ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[5]' + but found elements: + '[5]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).not_contains([1, 4, 6])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[1, 4, 6]' + but found elements: + '[1, 4]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func(): assert_array([1, 2, 3, 4, 5]).not_contains([6, 4, 1])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[1, 2, 3, 4, 5]' + do not contains + '[6, 4, 1]' + but found elements: + '[4, 1]'""" + .dedent().trim_prefix("\n") + ) + + assert_failure(func(): assert_array([valueA, valueB]).not_contains([TestObj.new("A", 0)])) \ + .is_failed() \ + .has_message(""" + Expecting: + '[class:A, class:B]' + do not contains + '[class:A]' + but found elements: + '[class:A]'""" + .dedent().trim_prefix("\n") + ) + + +func test_not_contains_same(): + var valueA := TestObj.new("A", 0) + var valueB := TestObj.new("B", 0) + var valueC := TestObj.new("B", 0) + assert_array([valueA, valueB]).not_contains_same([valueC]) + + assert_failure(func(): assert_array([valueA, valueB]).not_contains_same([valueB])) \ + .is_failed() \ + .has_message(""" + Expecting SAME: + '[class:A, class:B]' + do not contains + '[class:B]' + but found elements: + '[class:B]'""" + .dedent().trim_prefix("\n") + ) + + +func test_fluent(): + assert_array([])\ + .has_size(0)\ + .is_empty()\ + .is_not_null()\ + .contains([])\ + .contains_exactly([]) + + +func test_must_fail_has_invlalid_type(): + assert_failure(func(): assert_array(1)) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + assert_failure(func(): assert_array(1.3)) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + assert_failure(func(): assert_array(true)) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + assert_failure(func(): assert_array(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitArrayAssert inital error, unexpected type ") + + +func test_extract() -> void: + # try to extract checked base types + assert_array([1, false, 3.14, null, Color.ALICE_BLUE]).extract("get_class") \ + .contains_exactly(["n.a.", "n.a.", "n.a.", null, "n.a."]) + # extracting by a func without arguments + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("get_class") \ + .contains_exactly(["RefCounted", "n.a.", "AStar3D", "Node"]) + # extracting by a func with arguments + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("has_signal", ["tree_entered"]) \ + .contains_exactly([false, "n.a.", false, true]) + + # try extract checked object via a func that not exists + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("invalid_func") \ + .contains_exactly(["n.a.", "n.a.", "n.a.", "n.a."]) + # try extract checked object via a func that has no return value + assert_array([RefCounted.new(), 2, AStar3D.new(), auto_free(Node.new())]).extract("remove_meta", [""]) \ + .contains_exactly([null, "n.a.", null, null]) + + assert_failure(func(): assert_array(null).extract("get_class").contains_exactly(["AStar3D", "Node"])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains (in same order) + '["AStar3D", "Node"]' + but could not find elements: + '["AStar3D", "Node"]'""" + .dedent().trim_prefix("\n")) + + +class TestObj: + var _name :String + var _value + var _x + + func _init(name :String, value, x = null): + _name = name + _value = value + _x = x + + func get_name() -> String: + return _name + + func get_value(): + return _value + + func get_x(): + return _x + + func get_x1() -> String: + return "x1" + + func get_x2() -> String: + return "x2" + + func get_x3() -> String: + return "x3" + + func get_x4() -> String: + return "x4" + + func get_x5() -> String: + return "x5" + + func get_x6() -> String: + return "x6" + + func get_x7() -> String: + return "x7" + + func get_x8() -> String: + return "x8" + + func get_x9() -> String: + return "x9" + + func _to_string() -> String: + return "class:" + _name + +func test_extractv() -> void: + # single extract + assert_array([1, false, 3.14, null, Color.ALICE_BLUE])\ + .extractv(extr("get_class"))\ + .contains_exactly(["n.a.", "n.a.", "n.a.", null, "n.a."]) + # tuple of two + assert_array([TestObj.new("A", 10), TestObj.new("B", "foo"), Color.ALICE_BLUE, TestObj.new("C", 11)])\ + .extractv(extr("get_name"), extr("get_value"))\ + .contains_exactly([tuple("A", 10), tuple("B", "foo"), tuple("n.a.", "n.a."), tuple("C", 11)]) + # tuple of three + assert_array([TestObj.new("A", 10), TestObj.new("B", "foo", "bar"), TestObj.new("C", 11, 42)])\ + .extractv(extr("get_name"), extr("get_value"), extr("get_x"))\ + .contains_exactly([tuple("A", 10, null), tuple("B", "foo", "bar"), tuple("C", 11, 42)]) + + assert_failure(func(): + assert_array(null) \ + .extractv(extr("get_name"), extr("get_value"), extr("get_x")) \ + .contains_exactly([tuple("A", 10, null), tuple("B", "foo", "bar"), tuple("C", 11, 42)])) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '' + do contains (in same order) + '[tuple(["A", 10, ]), tuple(["B", "foo", "bar"]), tuple(["C", 11, 42])]' + but could not find elements: + '[tuple(["A", 10, ]), tuple(["B", "foo", "bar"]), tuple(["C", 11, 42])]'""" + .dedent().trim_prefix("\n")) + + +func test_extractv_chained_func() -> void: + var root_a = TestObj.new("root_a", null) + var obj_a = TestObj.new("A", root_a) + var obj_b = TestObj.new("B", root_a) + var obj_c = TestObj.new("C", root_a) + var root_b = TestObj.new("root_b", root_a) + var obj_x = TestObj.new("X", root_b) + var obj_y = TestObj.new("Y", root_b) + + assert_array([obj_a, obj_b, obj_c, obj_x, obj_y])\ + .extractv(extr("get_name"), extr("get_value.get_name"))\ + .contains_exactly([ + tuple("A", "root_a"), + tuple("B", "root_a"), + tuple("C", "root_a"), + tuple("X", "root_b"), + tuple("Y", "root_b") + ]) + + +func test_extract_chained_func() -> void: + var root_a = TestObj.new("root_a", null) + var obj_a = TestObj.new("A", root_a) + var obj_b = TestObj.new("B", root_a) + var obj_c = TestObj.new("C", root_a) + var root_b = TestObj.new("root_b", root_a) + var obj_x = TestObj.new("X", root_b) + var obj_y = TestObj.new("Y", root_b) + + assert_array([obj_a, obj_b, obj_c, obj_x, obj_y])\ + .extract("get_value.get_name")\ + .contains_exactly([ + "root_a", + "root_a", + "root_a", + "root_b", + "root_b", + ]) + + +func test_extractv_max_args() -> void: + assert_array([TestObj.new("A", 10), TestObj.new("B", "foo", "bar"), TestObj.new("C", 11, 42)])\ + .extractv(\ + extr("get_name"), + extr("get_x1"), + extr("get_x2"), + extr("get_x3"), + extr("get_x4"), + extr("get_x5"), + extr("get_x6"), + extr("get_x7"), + extr("get_x8"), + extr("get_x9"))\ + .contains_exactly([ + tuple("A", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9"), + tuple("B", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9"), + tuple("C", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9")]) + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_array([]) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_array([]).is_empty() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_array([]).is_not_empty()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_array([]).is_empty() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() + + +class ExampleTestClass extends RefCounted: + var _childs := Array() + var _parent = null + + + func add_child(child :ExampleTestClass) -> ExampleTestClass: + _childs.append(child) + child._parent = self + return self + + + func dispose(): + _parent = null + _childs.clear() + + +func test_contains_exactly_stuck() -> void: + var example_a := ExampleTestClass.new()\ + .add_child(ExampleTestClass.new())\ + .add_child(ExampleTestClass.new()) + var example_b := ExampleTestClass.new()\ + .add_child(ExampleTestClass.new())\ + .add_child(ExampleTestClass.new()) + # this test was stuck and ends after a while into an aborted test case + # https://github.com/MikeSchulze/gdUnit3/issues/244 + assert_failure(func(): assert_array([example_a, example_b]).contains_exactly([example_a, example_b, example_a]))\ + .is_failed() + # manual free because of cross references + example_a.dispose() + example_b.dispose() diff --git a/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd new file mode 100644 index 0000000..dda3b2d --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitAssertImplTest.gd @@ -0,0 +1,118 @@ +# GdUnit generated TestSuite +class_name GdUnitAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd' + + +func before(): + assert_int(GdUnitAssertions.get_line_number()).is_equal(10) + assert_failure(func(): assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(11) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func after(): + assert_failure(func(): assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(18) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func before_test(): + assert_failure(func(): assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(25) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func after_test(): + assert_failure(func(): assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(32) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number(): + # test to return the current line number for an failure + assert_failure(func(): assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(40) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number_yielded(): + # test to return the current line number after using yield + await get_tree().create_timer(0.100).timeout + assert_failure(func(): assert_int(10).is_equal(42)) \ + .is_failed() \ + .has_line(49) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number_multiline(): + # test to return the current line number for an failure + # https://github.com/godotengine/godot/issues/43326 + assert_failure(func(): assert_int(10)\ + .is_not_negative()\ + .is_equal(42)) \ + .is_failed() \ + .has_line(58) \ + .has_message("Expecting:\n '42'\n but was\n '10'") + + +func test_get_line_number_verify(): + var obj = mock(RefCounted) + assert_failure(func(): verify(obj, 1).get_reference_count()) \ + .is_failed() \ + .has_line(68) \ + .has_message("Expecting interaction on:\n 'get_reference_count()' 1 time's\nBut found interactions on:\n") + + +func test_is_null(): + assert_that(null).is_null() + + assert_failure(func(): assert_that(Color.RED).is_null()) \ + .is_failed() \ + .has_line(77) \ + .starts_with_message("Expecting: '' but was 'Color(1, 0, 0, 1)'") + + +func test_is_not_null(): + assert_that(Color.RED).is_not_null() + + assert_failure(func(): assert_that(null).is_not_null()) \ + .is_failed() \ + .has_line(86) \ + .has_message("Expecting: not to be ''") + + +func test_is_equal(): + assert_that(Color.RED).is_equal(Color.RED) + assert_that(Plane.PLANE_XY).is_equal(Plane.PLANE_XY) + + assert_failure(func(): assert_that(Color.RED).is_equal(Color.GREEN)) \ + .is_failed() \ + .has_line(96) \ + .has_message("Expecting:\n 'Color(0, 1, 0, 1)'\n but was\n 'Color(1, 0, 0, 1)'") + + +func test_is_not_equal(): + assert_that(Color.RED).is_not_equal(Color.GREEN) + assert_that(Plane.PLANE_XY).is_not_equal(Plane.PLANE_XZ) + + assert_failure(func(): assert_that(Color.RED).is_not_equal(Color.RED)) \ + .is_failed() \ + .has_line(106) \ + .has_message("Expecting:\n 'Color(1, 0, 0, 1)'\n not equal to\n 'Color(1, 0, 0, 1)'") + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_that(Color.RED) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_line(113) \ + .has_message("Custom failure message") diff --git a/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd new file mode 100644 index 0000000..39b327b --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitBoolAssertImplTest.gd @@ -0,0 +1,117 @@ +# GdUnit generated TestSuite +class_name GdUnitBoolAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd' + + +func test_is_true(): + assert_bool(true).is_true() + + assert_failure(func(): assert_bool(false).is_true())\ + .is_failed() \ + .has_message("Expecting: 'true' but is 'false'") + assert_failure(func(): assert_bool(null).is_true()) \ + .is_failed() \ + .has_message("Expecting: 'true' but is ''") + + +func test_isFalse(): + assert_bool(false).is_false() + + assert_failure(func(): assert_bool(true).is_false()) \ + .is_failed() \ + .has_message("Expecting: 'false' but is 'true'") + assert_failure(func(): assert_bool(null).is_false()) \ + .is_failed() \ + .has_message("Expecting: 'false' but is ''") + + +func test_is_null(): + assert_bool(null).is_null() + # should fail because the current is not null + assert_failure(func(): assert_bool(true).is_null())\ + .is_failed() \ + .starts_with_message("Expecting: '' but was 'true'") + + +func test_is_not_null(): + assert_bool(true).is_not_null() + # should fail because the current is null + assert_failure(func(): assert_bool(null).is_not_null())\ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal(): + assert_bool(true).is_equal(true) + assert_bool(false).is_equal(false) + + assert_failure(func(): assert_bool(true).is_equal(false)) \ + .is_failed() \ + .has_message("Expecting:\n 'false'\n but was\n 'true'") + assert_failure(func(): assert_bool(null).is_equal(false)) \ + .is_failed() \ + .has_message("Expecting:\n 'false'\n but was\n ''") + + +func test_is_not_equal(): + assert_bool(null).is_not_equal(false) + assert_bool(true).is_not_equal(false) + assert_bool(false).is_not_equal(true) + + assert_failure(func(): assert_bool(true).is_not_equal(true)) \ + .is_failed() \ + .has_message("Expecting:\n 'true'\n not equal to\n 'true'") + + +func test_fluent(): + assert_bool(true).is_true().is_equal(true).is_not_equal(false) + + +func test_must_fail_has_invlalid_type(): + assert_failure(func(): assert_bool(1)) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + assert_failure(func(): assert_bool(3.13)) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + assert_failure(func(): assert_bool("foo")) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + assert_failure(func(): assert_bool(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitBoolAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_bool(true) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_bool(true).is_true() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_bool(true).is_false()).is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_bool(true).is_true() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd new file mode 100644 index 0000000..7d66191 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitDictionaryAssertImplTest.gd @@ -0,0 +1,470 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd' + + +func test_must_fail_has_invlalid_type() -> void: + assert_failure(func(): assert_dict(1)) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func(): assert_dict(1.3)) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func(): assert_dict(true)) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func(): assert_dict("abc")) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func(): assert_dict([])) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + assert_failure(func(): assert_dict(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitDictionaryAssert inital error, unexpected type ") + + +func test_is_null() -> void: + assert_dict(null).is_null() + + assert_failure(func(): assert_dict({}).is_null()) \ + .is_failed() \ + .has_message("Expecting: '' but was '{ }'") + + +func test_is_not_null() -> void: + assert_dict({}).is_not_null() + + assert_failure(func(): assert_dict(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal() -> void: + assert_dict({}).is_equal({}) + assert_dict({1:1}).is_equal({1:1}) + assert_dict({1:1, "key_a": "value_a"}).is_equal({1:1, "key_a": "value_a" }) + # different order is also equals + assert_dict({"key_a": "value_a", 1:1}).is_equal({1:1, "key_a": "value_a" }) + + # should fail + assert_failure(func(): assert_dict(null).is_equal({1:1})) \ + .is_failed() \ + .has_message(""" + Expecting: + '{ + 1: 1 + }' + but was + ''""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func(): assert_dict({}).is_equal({1:1})).is_failed() + assert_failure(func(): assert_dict({1:1}).is_equal({})).is_failed() + assert_failure(func(): assert_dict({1:1}).is_equal({1:2})).is_failed() + assert_failure(func(): assert_dict({1:2}).is_equal({1:1})).is_failed() + assert_failure(func(): assert_dict({1:1}).is_equal({1:1, "key_a": "value_a"})).is_failed() + assert_failure(func(): assert_dict({1:1, "key_a": "value_a"}).is_equal({1:1})).is_failed() + assert_failure(func(): assert_dict({1:1, "key_a": "value_a"}).is_equal({1:1, "key_b": "value_b"})).is_failed() + assert_failure(func(): assert_dict({1:1, "key_b": "value_b"}).is_equal({1:1, "key_a": "value_a"})).is_failed() + assert_failure(func(): assert_dict({"key_a": "value_a", 1:1}).is_equal({1:1, "key_b": "value_b"})).is_failed() + assert_failure(func(): assert_dict({1:1, "key_b": "value_b"}).is_equal({"key_a": "value_a", 1:1})) \ + .is_failed() \ + .has_message(""" + Expecting: + '{ + 1: 1, + "key_a": "value_a" + }' + but was + '{ + 1: 1, + "key_ab": "value_ab" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_not_equal() -> void: + assert_dict(null).is_not_equal({}) + assert_dict({}).is_not_equal(null) + assert_dict({}).is_not_equal({1:1}) + assert_dict({1:1}).is_not_equal({}) + assert_dict({1:1}).is_not_equal({1:2}) + assert_dict({2:1}).is_not_equal({1:1}) + assert_dict({1:1}).is_not_equal({1:1, "key_a": "value_a"}) + assert_dict({1:1, "key_a": "value_a"}).is_not_equal({1:1}) + assert_dict({1:1, "key_a": "value_a"}).is_not_equal({1:1, "key_b": "value_b"}) + + # should fail + assert_failure(func(): assert_dict({}).is_not_equal({})).is_failed() + assert_failure(func(): assert_dict({1:1}).is_not_equal({1:1})).is_failed() + assert_failure(func(): assert_dict({1:1, "key_a": "value_a"}).is_not_equal({1:1, "key_a": "value_a"})).is_failed() + assert_failure(func(): assert_dict({"key_a": "value_a", 1:1}).is_not_equal({1:1, "key_a": "value_a"})) \ + .is_failed() \ + .has_message(""" + Expecting: + '{ + 1: 1, + "key_a": "value_a" + }' + not equal to + '{ + 1: 1, + "key_a": "value_a" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_same() -> void: + var dict_a := {} + var dict_b := {"key"="value", "key2"="value"} + var dict_c := {1:1, "key_a": "value_a"} + var dict_d := {"key_a": "value_a", 1:1} + assert_dict(dict_a).is_same(dict_a) + assert_dict(dict_b).is_same(dict_b) + assert_dict(dict_c).is_same(dict_c) + assert_dict(dict_d).is_same(dict_d) + + assert_failure( func(): assert_dict({}).is_same({})) \ + .is_failed()\ + .has_message(""" + Expecting: + '{ }' + to refer to the same object + '{ }'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure( func(): assert_dict({1:1, "key_a": "value_a"}).is_same({1:1, "key_a": "value_a" })) \ + .is_failed()\ + .has_message(""" + Expecting: + '{ + 1: 1, + "key_a": "value_a" + }' + to refer to the same object + '{ + 1: 1, + "key_a": "value_a" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_not_same() -> void: + var dict_a := {} + var dict_b := {} + var dict_c := {1:1, "key_a": "value_a"} + var dict_d := {1:1, "key_a": "value_a"} + assert_dict(dict_a).is_not_same(dict_b).is_not_same(dict_c).is_not_same(dict_d) + assert_dict(dict_b).is_not_same(dict_a).is_not_same(dict_c).is_not_same(dict_d) + assert_dict(dict_c).is_not_same(dict_a).is_not_same(dict_b).is_not_same(dict_d) + assert_dict(dict_d).is_not_same(dict_a).is_not_same(dict_b).is_not_same(dict_c) + + assert_failure( func(): assert_dict(dict_a).is_not_same(dict_a)) \ + .is_failed()\ + .has_message(""" + Expecting not same: + '{ }'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure( func(): assert_dict(dict_c).is_not_same(dict_c)) \ + .is_failed()\ + .has_message(""" + Expecting not same: + '{ + 1: 1, + "key_a": "value_a" + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_empty() -> void: + assert_dict({}).is_empty() + + assert_failure(func(): assert_dict(null).is_empty()) \ + .is_failed() \ + .has_message("Expecting:\n" + + " must be empty but was\n" + + " ''") + assert_failure(func(): assert_dict({1:1}).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + '{ + 1: 1 + }'""" + .dedent() + .trim_prefix("\n") + ) + + +func test_is_not_empty() -> void: + assert_dict({1:1}).is_not_empty() + assert_dict({1:1, "key_a": "value_a"}).is_not_empty() + + assert_failure(func(): assert_dict(null).is_not_empty()) \ + .is_failed() \ + .has_message("Expecting:\n" + + " must not be empty") + assert_failure(func(): assert_dict({}).is_not_empty()).is_failed() + + +func test_has_size() -> void: + assert_dict({}).has_size(0) + assert_dict({1:1}).has_size(1) + assert_dict({1:1, 2:1}).has_size(2) + assert_dict({1:1, 2:1, 3:1}).has_size(3) + + assert_failure(func(): assert_dict(null).has_size(0))\ + .is_failed() \ + .has_message("Expecting: not to be ''") + assert_failure(func(): assert_dict(null).has_size(1)).is_failed() + assert_failure(func(): assert_dict({}).has_size(1)).is_failed() + assert_failure(func(): assert_dict({1:1}).has_size(0)).is_failed() + assert_failure(func(): assert_dict({1:1}).has_size(2)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '2' + but was + '1'""" + .dedent() + .trim_prefix("\n") + ) + +class TestObj: + var _name :String + var _value :int + + func _init(name :String = "Foo", value :int = 0): + _name = name + _value = value + + func _to_string() -> String: + return "class:%s:%d" % [_name, _value] + + +func test_contains_keys() -> void: + var key_a := TestObj.new() + var key_b := TestObj.new() + var key_c := TestObj.new() + var key_d := TestObj.new("D") + + assert_dict({1:1, 2:2, 3:3}).contains_keys([2]) + assert_dict({1:1, 2:2, "key_a": "value_a"}).contains_keys([2, "key_a"]) + assert_dict({key_a:1, key_b:2, key_c:3}).contains_keys([key_a, key_b]) + assert_dict({key_a:1, key_c:3 }).contains_keys([key_b]) + assert_dict({key_a:1, 3:3}).contains_keys([key_a, key_b]) + + + assert_failure(func(): assert_dict({1:1, 3:3}).contains_keys([2])) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[1, 3]' + to contains: + '[2]' + but can't find key's: + '[2]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func(): assert_dict({1:1, 3:3}).contains_keys([1, 4])) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[1, 3]' + to contains: + '[1, 4]' + but can't find key's: + '[4]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func(): assert_dict(null).contains_keys([1, 4])) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + assert_failure(func(): assert_dict({key_a:1, 3:3}).contains_keys([key_a, key_d])) \ + .is_failed() \ + .has_message(""" + Expecting contains keys: + '[class:Foo:0, 3]' + to contains: + '[class:Foo:0, class:D:0]' + but can't find key's: + '[class:D:0]'""" + .dedent().trim_prefix("\n")) + + +func test_contains_key_value() -> void: + assert_dict({1:1}).contains_key_value(1, 1) + assert_dict({1:1, 2:2, 3:3}).contains_key_value(3, 3).contains_key_value(1, 1) + + assert_failure(func(): assert_dict({1:1}).contains_key_value(1, 2)) \ + .is_failed() \ + .has_message(""" + Expecting contains key and value: + '1' : '2' + but contains + '1' : '1'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func(): assert_dict(null).contains_key_value(1, 2)) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_not_contains_keys() -> void: + assert_dict({}).not_contains_keys([2]) + assert_dict({1:1, 3:3}).not_contains_keys([2]) + assert_dict({1:1, 3:3}).not_contains_keys([2, 4]) + + assert_failure(func(): assert_dict({1:1, 2:2, 3:3}).not_contains_keys([2, 4])) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains keys: + '[1, 2, 3]' + do not contains: + '[2, 4]' + but contains key's: + '[2]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func(): assert_dict({1:1, 2:2, 3:3}).not_contains_keys([1, 2, 3, 4])) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains keys: + '[1, 2, 3]' + do not contains: + '[1, 2, 3, 4]' + but contains key's: + '[1, 2, 3]'""" + .dedent() + .trim_prefix("\n") + ) + assert_failure(func(): assert_dict(null).not_contains_keys([1, 4])) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_contains_same_keys() -> void: + var key_a := TestObj.new() + var key_b := TestObj.new() + var key_c := TestObj.new() + + assert_dict({1:1, 2:2, 3:3}).contains_same_keys([2]) + assert_dict({1:1, 2:2, "key_a": "value_a"}).contains_same_keys([2, "key_a"]) + assert_dict({key_a:1, key_b:2, 3:3}).contains_same_keys([key_b]) + assert_dict({key_a:1, key_b:2, 3:3}).contains_same_keys([key_a, key_b]) + + assert_failure(func(): assert_dict({key_a:1, key_c:3 }).contains_same_keys([key_a, key_b])) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME keys: + '[class:Foo:0, class:Foo:0]' + to contains: + '[class:Foo:0, class:Foo:0]' + but can't find key's: + '[class:Foo:0]'""" + .dedent().trim_prefix("\n") + ) + + +func test_contains_same_key_value() -> void: + var key_a := TestObj.new("A") + var key_b := TestObj.new("B") + var key_c := TestObj.new("C") + var key_d := TestObj.new("A") + + assert_dict({key_a:1, key_b:2, key_c:3})\ + .contains_same_key_value(key_a, 1)\ + .contains_same_key_value(key_b, 2) + + assert_failure(func(): assert_dict({key_a:1, key_b:2, key_c:3}).contains_same_key_value(key_a, 2)) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME key and value: + : '2' + but contains + : '1'""" + .dedent().trim_prefix("\n") + ) + assert_failure(func(): assert_dict({key_a:1, key_b:2, key_c:3}).contains_same_key_value(key_d, 1)) \ + .is_failed() \ + .has_message(""" + Expecting contains SAME key and value: + : '1' + but contains + : '[class:A:0, class:B:0, class:C:0]'""" + .dedent().trim_prefix("\n") + ) + + +func test_not_contains_same_keys() -> void: + var key_a := TestObj.new("A") + var key_b := TestObj.new("B") + var key_c := TestObj.new("C") + var key_d := TestObj.new("A") + + assert_dict({}).not_contains_same_keys([key_a]) + assert_dict({key_a:1, key_b:2}).not_contains_same_keys([key_c, key_d]) + + assert_failure(func(): assert_dict({key_a:1, key_b:2}).not_contains_same_keys([key_c, key_b])) \ + .is_failed() \ + .has_message(""" + Expecting NOT contains SAME keys + '[class:A:0, class:B:0]' + do not contains: + '[class:C:0, class:B:0]' + but contains key's: + '[class:B:0]'""" + .dedent().trim_prefix("\n") + ) + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_dict({1:1}) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_dict({}).is_empty() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_dict({}).is_not_empty()).is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_dict({}).is_empty() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd new file mode 100644 index 0000000..3fbde29 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFailureAssertImplTest.gd @@ -0,0 +1,79 @@ +# GdUnit generated TestSuite +class_name GdUnitFailureAssertImplTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd' + + +func last_assert() -> Variant: + return GdUnitThreadManager.get_current_context().get_assert() + + +func test_has_line() -> void: + assert_failure(func(): assert_bool(true).is_false()) \ + .is_failed() \ + .has_line(16) + + +func test_has_message() -> void: + assert_failure(func(): assert_bool(true).is_true()) \ + .is_success() + assert_failure(func(): assert_bool(true).is_false()) \ + .is_failed()\ + .has_message("Expecting: 'false' but is 'true'") + + +func test_starts_with_message() -> void: + assert_failure(func(): assert_bool(true).is_false()) \ + .is_failed()\ + .starts_with_message("Expecting: 'false' bu") + + +func test_assert_failure_on_invalid_cb() -> void: + assert_failure(func(): prints())\ + .is_failed()\ + .has_message("Invalid Callable! It must be a callable of 'GdUnitAssert'") + + +@warning_ignore("unused_parameter") +func test_assert_failure_on_assert(test_name :String, assert_type, value, test_parameters = [ + ["GdUnitBoolAssert", GdUnitBoolAssert, true], + ["GdUnitStringAssert", GdUnitStringAssert, "value"], + ["GdUnitIntAssert", GdUnitIntAssert, 42], + ["GdUnitFloatAssert", GdUnitFloatAssert, 42.0], + ["GdUnitObjectAssert", GdUnitObjectAssert, RefCounted.new()], + ["GdUnitVectorAssert", GdUnitVectorAssert, Vector2.ZERO], + ["GdUnitVectorAssert", GdUnitVectorAssert, Vector3.ZERO], + ["GdUnitArrayAssert", GdUnitArrayAssert, Array()], + ["GdUnitDictionaryAssert", GdUnitDictionaryAssert, {}], +]) -> void: + var instance := assert_failure(func(): assert_that(value)) + assert_object(last_assert()).is_instanceof(assert_type) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_file() -> void: + var instance := assert_failure(func(): assert_file("res://foo.gd")) + assert_object(last_assert()).is_instanceof(GdUnitFileAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_func() -> void: + var instance := assert_failure(func(): assert_func(RefCounted.new(), "_to_string")) + assert_object(last_assert()).is_instanceof(GdUnitFuncAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_signal() -> void: + var instance := assert_failure(func(): assert_signal(null)) + assert_object(last_assert()).is_instanceof(GdUnitSignalAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) + + +func test_assert_failure_on_assert_result() -> void: + var instance := assert_failure(func(): assert_result(null)) + assert_object(last_assert()).is_instanceof(GdUnitResultAssert) + assert_object(instance).is_instanceof(GdUnitFailureAssert) diff --git a/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd new file mode 100644 index 0000000..4a299c0 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFloatAssertImplTest.gd @@ -0,0 +1,246 @@ +# GdUnit generated TestSuite +class_name GdUnitFloatAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd' + + +func test_is_null(): + assert_float(null).is_null() + + assert_failure(func(): assert_float(23.2).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was '23.200000'") + + +func test_is_not_null(): + assert_float(23.2).is_not_null() + + assert_failure(func(): assert_float(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal(): + assert_float(23.2).is_equal(23.2) + + assert_failure(func(): assert_float(23.2).is_equal(23.4)) \ + .is_failed() \ + .has_message("Expecting:\n '23.400000'\n but was\n '23.200000'") + assert_failure(func(): assert_float(null).is_equal(23.4)) \ + .is_failed() \ + .has_message("Expecting:\n '23.400000'\n but was\n ''") + + +func test_is_not_equal(): + assert_float(null).is_not_equal(23.4) + assert_float(23.2).is_not_equal(23.4) + + assert_failure(func(): assert_float(23.2).is_not_equal(23.2)) \ + .is_failed() \ + .has_message("Expecting:\n '23.200000'\n not equal to\n '23.200000'") + + +func test_is_equal_approx() -> void: + assert_float(23.2).is_equal_approx(23.2, 0.01) + assert_float(23.19).is_equal_approx(23.2, 0.01) + assert_float(23.20).is_equal_approx(23.2, 0.01) + assert_float(23.21).is_equal_approx(23.2, 0.01) + + assert_failure(func(): assert_float(23.18).is_equal_approx(23.2, 0.01)) \ + .is_failed() \ + .has_message("Expecting:\n '23.180000'\n in range between\n '23.190000' <> '23.210000'") + assert_failure(func(): assert_float(23.22).is_equal_approx(23.2, 0.01)) \ + .is_failed() \ + .has_message("Expecting:\n '23.220000'\n in range between\n '23.190000' <> '23.210000'") + assert_failure(func(): assert_float(null).is_equal_approx(23.2, 0.01)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '23.190000' <> '23.210000'") + + + +func test_is_less_(): + assert_failure(func(): assert_float(23.2).is_less(23.2)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23.200000' but was '23.200000'") + +func test_is_less(): + assert_float(23.2).is_less(23.4) + assert_float(23.2).is_less(26.0) + + assert_failure(func(): assert_float(23.2).is_less(23.2)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23.200000' but was '23.200000'") + assert_failure(func(): assert_float(null).is_less(23.2)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23.200000' but was ''") + + +func test_is_less_equal(): + assert_float(23.2).is_less_equal(23.4) + assert_float(23.2).is_less_equal(23.2) + + assert_failure(func(): assert_float(23.2).is_less_equal(23.1)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '23.100000' but was '23.200000'") + assert_failure(func(): assert_float(null).is_less_equal(23.1)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '23.100000' but was ''") + + +func test_is_greater(): + assert_float(23.2).is_greater(23.0) + assert_float(23.4).is_greater(22.1) + + assert_failure(func(): assert_float(23.2).is_greater(23.2)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23.200000' but was '23.200000'") + assert_failure(func(): assert_float(null).is_greater(23.2)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23.200000' but was ''") + + +func test_is_greater_equal(): + assert_float(23.2).is_greater_equal(20.2) + assert_float(23.2).is_greater_equal(23.2) + + assert_failure(func(): assert_float(23.2).is_greater_equal(23.3)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '23.300000' but was '23.200000'") + assert_failure(func(): assert_float(null).is_greater_equal(23.3)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '23.300000' but was ''") + + +func test_is_negative(): + assert_float(-13.2).is_negative() + + assert_failure(func(): assert_float(13.2).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '13.200000' be negative") + assert_failure(func(): assert_float(null).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be negative") + + +func test_is_not_negative(): + assert_float(13.2).is_not_negative() + + assert_failure(func(): assert_float(-13.2).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '-13.200000' be not negative") + assert_failure(func(): assert_float(null).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be not negative") + + +func test_is_zero(): + assert_float(0.0).is_zero() + + assert_failure(func(): assert_float(0.00001).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is '0.000010'") + assert_failure(func(): assert_float(null).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is ''") + + +func test_is_not_zero(): + assert_float(0.00001).is_not_zero() + + assert_failure(func(): assert_float(0.000001).is_not_zero()) \ + .is_failed() \ + .has_message("Expecting:\n not equal to 0") + assert_failure(func(): assert_float(null).is_not_zero()) \ + .is_failed() \ + .has_message("Expecting:\n not equal to 0") + + +func test_is_in(): + assert_float(5.2).is_in([5.1, 5.2, 5.3, 5.4]) + # this assertion fail because 5.5 is not in [5.1, 5.2, 5.3, 5.4] + assert_failure(func(): assert_float(5.5).is_in([5.1, 5.2, 5.3, 5.4])) \ + .is_failed() \ + .has_message("Expecting:\n '5.500000'\n is in\n '[5.1, 5.2, 5.3, 5.4]'") + assert_failure(func(): assert_float(null).is_in([5.1, 5.2, 5.3, 5.4])) \ + .is_failed() \ + .has_message("Expecting:\n ''\n is in\n '[5.1, 5.2, 5.3, 5.4]'") + + +func test_is_not_in(): + assert_float(null).is_not_in([5.1, 5.3, 5.4]) + assert_float(5.2).is_not_in([5.1, 5.3, 5.4]) + # this assertion fail because 5.2 is not in [5.1, 5.2, 5.3, 5.4] + assert_failure(func(): assert_float(5.2).is_not_in([5.1, 5.2, 5.3, 5.4])) \ + .is_failed() \ + .has_message("Expecting:\n '5.200000'\n is not in\n '[5.1, 5.2, 5.3, 5.4]'") + + +func test_is_between(): + assert_float(-20.0).is_between(-20.0, 20.9) + assert_float(10.0).is_between(-20.0, 20.9) + assert_float(20.9).is_between(-20.0, 20.9) + + +func test_is_between_must_fail(): + assert_failure(func(): assert_float(-10.0).is_between(-9.0, 0.0)) \ + .is_failed() \ + .has_message("Expecting:\n '-10.000000'\n in range between\n '-9.000000' <> '0.000000'") + assert_failure(func(): assert_float(0.0).is_between(1, 10)) \ + .is_failed() \ + .has_message("Expecting:\n '0.000000'\n in range between\n '1.000000' <> '10.000000'") + assert_failure(func(): assert_float(10.0).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n '10.000000'\n in range between\n '11.000000' <> '21.000000'") + assert_failure(func(): assert_float(null).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '11.000000' <> '21.000000'") + + +func test_must_fail_has_invlalid_type(): + assert_failure(func(): assert_float(1)) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + assert_failure(func(): assert_float(true)) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + assert_failure(func(): assert_float("foo")) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + assert_failure(func(): assert_float(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitFloatAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_float(3.14) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_float(0.0).is_zero() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_float(1.0).is_zero()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_float(0.0).is_zero() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd new file mode 100644 index 0000000..8678a6f --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitFuncAssertImplTest.gd @@ -0,0 +1,368 @@ +# GdUnit generated TestSuite +class_name GdUnitFuncAssertImplTest +extends GdUnitTestSuite +@warning_ignore("unused_parameter") + + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd' +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +# we need to skip await fail test because of an bug in Godot 4.0 stable +func is_skip_fail_await() -> bool: + return Engine.get_version_info().hex < 0x40002 + + +class TestValueProvider: + var _max_iterations :int + var _current_itteration := 0 + + func _init(iterations := 0): + _max_iterations = iterations + + func bool_value() -> bool: + _current_itteration += 1 + if _current_itteration == _max_iterations: + return true + return false + + func int_value() -> int: + return 0 + + func float_value() -> float: + return 0.0 + + func string_value() -> String: + return "value" + + func object_value() -> Object: + return Resource.new() + + func array_value() -> Array: + return [] + + func dict_value() -> Dictionary: + return {} + + func vec2_value() -> Vector2: + return Vector2.ONE + + func vec3_value() -> Vector3: + return Vector3.ONE + + func no_value() -> void: + pass + + func unknown_value(): + return Vector3.ONE + + +class ValueProvidersWithArguments: + + func is_type(_type :int) -> bool: + return true + + func get_index(_instance :Object, _name :String) -> int: + return 1 + + func get_index2(_instance :Object, _name :String, _recursive := false) -> int: + return 1 + + +class TestIterativeValueProvider: + var _max_iterations :int + var _current_itteration := 0 + var _inital_value + var _final_value + + func _init(inital_value, iterations :int, final_value): + _max_iterations = iterations + _inital_value = inital_value + _final_value = final_value + + func bool_value() -> bool: + _current_itteration += 1 + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func int_value() -> int: + _current_itteration += 1 + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func obj_value() -> Variant: + _current_itteration += 1 + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func has_type(type :int, _recursive :bool = true) -> int: + _current_itteration += 1 + #await Engine.get_main_loop().idle_frame + if type == _current_itteration: + return _final_value + return _inital_value + + func await_value() -> int: + _current_itteration += 1 + await Engine.get_main_loop().process_frame + prints("yielded_value", _current_itteration) + if _current_itteration >= _max_iterations: + return _final_value + return _inital_value + + func reset() -> void: + _current_itteration = 0 + + func iteration() -> int: + return _current_itteration + + +@warning_ignore("unused_parameter") +func test_is_null(timeout = 2000) -> void: + var value_provider := TestIterativeValueProvider.new(RefCounted.new(), 5, null) + # without default timeout od 2000ms + assert_func(value_provider, "obj_value").is_not_null() + await assert_func(value_provider, "obj_value").is_null() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "obj_value").is_not_null() + await assert_func(value_provider, "obj_value").wait_until(5000).is_null() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + if is_skip_fail_await(): + return + value_provider = TestIterativeValueProvider.new(RefCounted.new(), 1, RefCounted.new()) + ( + await assert_failure_await(func(): await assert_func(value_provider, "obj_value", []).wait_until(100).is_null()) + ).has_message("Expected: is null but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_not_null(timeout = 2000) -> void: + var value_provider := TestIterativeValueProvider.new(null, 5, RefCounted.new()) + # without default timeout od 2000ms + assert_func(value_provider, "obj_value").is_null() + await assert_func(value_provider, "obj_value").is_not_null() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "obj_value").is_null() + await assert_func(value_provider, "obj_value").wait_until(5000).is_not_null() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + value_provider = TestIterativeValueProvider.new(null, 1, null) + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(value_provider, "obj_value", []).wait_until(100).is_not_null()) + ).has_message("Expected: is not null but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_true(timeout = 2000) -> void: + var value_provider := TestIterativeValueProvider.new(false, 5, true) + # without default timeout od 2000ms + assert_func(value_provider, "bool_value").is_false() + await assert_func(value_provider, "bool_value").is_true() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "bool_value").is_false() + await assert_func(value_provider, "bool_value").wait_until(5000).is_true() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + value_provider = TestIterativeValueProvider.new(false, 1, false) + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(value_provider, "bool_value", []).wait_until(100).is_true()) + ).has_message("Expected: is true but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_false(timeout = 2000) -> void: + var value_provider := TestIterativeValueProvider.new(true, 5, false) + # without default timeout od 2000ms + assert_func(value_provider, "bool_value").is_true() + await assert_func(value_provider, "bool_value").is_false() + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "bool_value").is_true() + await assert_func(value_provider, "bool_value").wait_until(5000).is_false() + assert_int(value_provider.iteration()).is_equal(5) + + # failure case + value_provider = TestIterativeValueProvider.new(true, 1, true) + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(value_provider, "bool_value", []).wait_until(100).is_false()) + ).has_message("Expected: is false but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_equal(timeout = 2000) -> void: + var value_provider := TestIterativeValueProvider.new(42, 5, 23) + # without default timeout od 2000ms + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").is_equal(23) + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").wait_until(5000).is_equal(23) + assert_int(value_provider.iteration()).is_equal(5) + + # failing case + value_provider = TestIterativeValueProvider.new(23, 1, 23) + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(value_provider, "int_value", []).wait_until(100).is_equal(25)) + ).has_message("Expected: is equal '25' but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_not_equal(timeout = 2000) -> void: + var value_provider := TestIterativeValueProvider.new(42, 5, 23) + # without default timeout od 2000ms + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").is_not_equal(42) + assert_int(value_provider.iteration()).is_equal(5) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "int_value").is_equal(42) + await assert_func(value_provider, "int_value").wait_until(5000).is_not_equal(42) + assert_int(value_provider.iteration()).is_equal(5) + + # failing case + value_provider = TestIterativeValueProvider.new(23, 1, 23) + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(value_provider, "int_value", []).wait_until(100).is_not_equal(23)) + ).has_message("Expected: is not equal '23' but timed out after 100ms") + + +@warning_ignore("unused_parameter") +func test_is_equal_wiht_func_arg(timeout = 1300) -> void: + var value_provider := TestIterativeValueProvider.new(42, 10, 23) + # without default timeout od 2000ms + assert_func(value_provider, "has_type", [1]).is_equal(42) + await assert_func(value_provider, "has_type", [10]).is_equal(23) + assert_int(value_provider.iteration()).is_equal(10) + + # with a timeout of 5s + value_provider.reset() + assert_func(value_provider, "has_type", [1]).is_equal(42) + await assert_func(value_provider, "has_type", [10]).wait_until(5000).is_equal(23) + assert_int(value_provider.iteration()).is_equal(10) + + +# abort test after 500ms to fail +@warning_ignore("unused_parameter") +func test_timeout_and_assert_fails(timeout = 500) -> void: + # disable temporary the timeout errors for this test + discard_error_interupted_by_timeout() + var value_provider := TestIterativeValueProvider.new(1, 10, 10) + # wait longer than test timeout, the value will be never '42' + await assert_func(value_provider, "int_value").wait_until(1000).is_equal(42) + fail("The test must be interrupted after 500ms") + + +func timed_function() -> Color: + var color = Color.RED + await await_millis(20) + color = Color.GREEN + await await_millis(20) + color = Color.BLUE + await await_millis(20) + color = Color.BLACK + return color + + +func test_timer_yielded_function() -> void: + await assert_func(self, "timed_function").is_equal(Color.BLACK) + # will be never red + await assert_func(self, "timed_function").wait_until(100).is_not_equal(Color.RED) + # failure case + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(self, "timed_function", []).wait_until(100).is_equal(Color.RED)) + ).has_message("Expected: is equal 'Color(1, 0, 0, 1)' but timed out after 100ms") + + +func test_timer_yielded_function_timeout() -> void: + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(self, "timed_function", []).wait_until(40).is_equal(Color.BLACK)) + ).has_message("Expected: is equal 'Color()' but timed out after 40ms") + + +func yielded_function() -> Color: + var color = Color.RED + await get_tree().process_frame + color = Color.GREEN + await get_tree().process_frame + color = Color.BLUE + await get_tree().process_frame + color = Color.BLACK + return color + + +func test_idle_frame_yielded_function() -> void: + await assert_func(self, "yielded_function").is_equal(Color.BLACK) + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(self, "yielded_function", []).wait_until(500).is_equal(Color.RED)) + ).has_message("Expected: is equal 'Color(1, 0, 0, 1)' but timed out after 500ms") + + +func test_has_failure_message() -> void: + if is_skip_fail_await(): + return + var value_provider := TestIterativeValueProvider.new(10, 1, 10) + ( + await assert_failure_await(func(): await assert_func(value_provider, "int_value", []).wait_until(500).is_equal(42)) + ).has_message("Expected: is equal '42' but timed out after 500ms") + + +func test_override_failure_message() -> void: + if is_skip_fail_await(): + return + var value_provider := TestIterativeValueProvider.new(10, 1, 20) + ( + await assert_failure_await(func(): await assert_func(value_provider, "int_value", []) \ + .override_failure_message("Custom failure message") \ + .wait_until(100) \ + .is_equal(42)) + ).has_message("Custom failure message") + + +@warning_ignore("unused_parameter") +func test_invalid_function(timeout = 100): + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_func(self, "invalid_func_name", [])\ + .wait_until(1000)\ + .is_equal(42)) + ).starts_with_message("The function 'invalid_func_name' do not exists checked instance") diff --git a/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd new file mode 100644 index 0000000..766723e --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd @@ -0,0 +1,137 @@ +# GdUnit generated TestSuite +class_name GdUnitGodotErrorAssertImplTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd' + + +class GodotErrorTestClass: + + func test(value :int) -> void: + match value: + 0: + @warning_ignore("assert_always_true") + assert(true, "no error" ) + 1: # failing assert + await Engine.get_main_loop().process_frame + if OS.is_debug_build(): + # do not break the debug session we simmulate a assert by writing the error manually + prints(""" + USER SCRIPT ERROR: Assertion failed: this is an assert error + at: GodotErrorTestClass.test (res://addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd:18) + """.dedent()) + else: + assert(false, "this is an assert error" ) + 2: # push_warning + push_warning('this is an push_warning') + 3: # push_error + push_error('this is an push_error') + pass + 4: # runtime error + if OS.is_debug_build(): + # do not break the debug session we simmulate a assert by writing the error manually + prints(""" + USER SCRIPT ERROR: Division by zero error in operator '/'. + at: GodotErrorTestClass.test (res://addons/gdUnit4/test/asserts/GdUnitGodotErrorAssertImplTest.gd:32) + """.dedent()) + else: + var a = 0 + @warning_ignore("integer_division") + @warning_ignore("unused_variable") + var x = 1/a + + +var _save_is_report_push_errors :bool +var _save_is_report_script_errors :bool + + +# skip see https://github.com/godotengine/godot/issues/80292 +@warning_ignore('unused_parameter') +func before(do_skip=Engine.get_version_info().hex < 0x40100, skip_reason="Exclude this test suite for Godot versions <= 4.1.x"): + _save_is_report_push_errors = GdUnitSettings.is_report_push_errors() + _save_is_report_script_errors = GdUnitSettings.is_report_script_errors() + # disable default error reporting for testing + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, false) + + +func after(): + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, _save_is_report_push_errors) + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, _save_is_report_script_errors) + + +func after_test(): + # Cleanup report artifacts + GdUnitThreadManager.get_current_context().get_execution_context().error_monitor._entries.clear() + + +func test_invalid_callable() -> void: + assert_failure(func(): assert_error(Callable()).is_success())\ + .is_failed()\ + .has_message("Invalid Callable 'null::null'") + + +func test_is_success() -> void: + await assert_error(func (): await GodotErrorTestClass.new().test(0)).is_success() + + var assert_ = await assert_failure_await(func(): + await assert_error(func (): await GodotErrorTestClass.new().test(1)).is_success()) + assert_.is_failed().has_message(""" + Expecting: no error's are ocured. + but found: 'Assertion failed: this is an assert error' + """.dedent().trim_prefix("\n")) + + +func test_is_assert_failed() -> void: + await assert_error(func (): await GodotErrorTestClass.new().test(1))\ + .is_runtime_error('Assertion failed: this is an assert error') + + var assert_ = await assert_failure_await(func(): + await assert_error(func (): GodotErrorTestClass.new().test(0)).is_runtime_error('Assertion failed: this is an assert error')) + assert_.is_failed().has_message(""" + Expecting: a runtime error is triggered. + message: 'Assertion failed: this is an assert error' + found: no errors + """.dedent().trim_prefix("\n")) + + +func test_is_push_warning() -> void: + await assert_error(func (): GodotErrorTestClass.new().test(2))\ + .is_push_warning('this is an push_warning') + + var assert_ = await assert_failure_await(func(): + await assert_error(func (): GodotErrorTestClass.new().test(0)).is_push_warning('this is an push_warning')) + assert_.is_failed().has_message(""" + Expecting: push_warning() is called. + message: 'this is an push_warning' + found: no errors + """.dedent().trim_prefix("\n")) + + +func test_is_push_error() -> void: + await assert_error(func (): GodotErrorTestClass.new().test(3))\ + .is_push_error('this is an push_error') + + var assert_ = await assert_failure_await(func(): + await assert_error(func (): GodotErrorTestClass.new().test(0)).is_push_error('this is an push_error')) + assert_.is_failed().has_message(""" + Expecting: push_error() is called. + message: 'this is an push_error' + found: no errors + """.dedent().trim_prefix("\n")) + + +func test_is_runtime_error() -> void: + await assert_error(func (): GodotErrorTestClass.new().test(4))\ + .is_runtime_error("Division by zero error in operator '/'.") + + var assert_ = await assert_failure_await(func(): + await assert_error(func (): GodotErrorTestClass.new().test(0)).is_runtime_error("Division by zero error in operator '/'.")) + assert_.is_failed().has_message(""" + Expecting: a runtime error is triggered. + message: 'Division by zero error in operator '/'.' + found: no errors + """.dedent().trim_prefix("\n")) diff --git a/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd b/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd new file mode 100644 index 0000000..cb58348 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitGodotErrorWithAssertTest.gd @@ -0,0 +1,40 @@ +extends GdUnitTestSuite + + +var _catched_events :Array[GdUnitEvent] = [] + + +func test_assert_method_with_enabled_global_error_report() -> void: + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, true) + await assert_error(do_a_fail).is_runtime_error('Assertion failed: test') + + +func test_assert_method_with_disabled_global_error_report() -> void: + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, false) + await assert_error(do_a_fail).is_runtime_error('Assertion failed: test') + + +@warning_ignore("assert_always_false") +func do_a_fail(): + if OS.is_debug_build(): + # On debug level we need to simulate the assert log entry, otherwise we stuck on a breakpoint + prints(""" + USER SCRIPT ERROR: Assertion failed: test + at: do_a_fail (res://addons/gdUnit4/test/asserts/GdUnitErrorAssertTest.gd:20)""") + else: + assert(3 == 1, 'test') + + +func catch_test_events(event :GdUnitEvent) -> void: + _catched_events.append(event) + + +func before() -> void: + GdUnitSignals.instance().gdunit_event.connect(catch_test_events) + + +func after() -> void: + # We expect no errors or failures, as we caught already the assert error by using the assert `assert_error` on the test case + assert_array(_catched_events).extractv(extr("error_count"), extr("failed_count"))\ + .contains_exactly([tuple(0, 0), tuple(0,0), tuple(0,0), tuple(0,0)]) + GdUnitSignals.instance().gdunit_event.disconnect(catch_test_events) diff --git a/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd new file mode 100644 index 0000000..9ad4e73 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitIntAssertImplTest.gd @@ -0,0 +1,242 @@ +# GdUnit generated TestSuite +class_name GdUnitIntAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd' + + +func test_is_null(): + assert_int(null).is_null() + + assert_failure(func(): assert_int(23).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was '23'") + + +func test_is_not_null(): + assert_int(23).is_not_null() + + assert_failure(func(): assert_int(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal(): + assert_int(23).is_equal(23) + + assert_failure(func(): assert_int(23).is_equal(42)) \ + .is_failed() \ + .has_message("Expecting:\n '42'\n but was\n '23'") + assert_failure(func(): assert_int(null).is_equal(42)) \ + .is_failed() \ + .has_message("Expecting:\n '42'\n but was\n ''") + + +func test_is_not_equal(): + assert_int(null).is_not_equal(42) + assert_int(23).is_not_equal(42) + + assert_failure(func(): assert_int(23).is_not_equal(23)) \ + .is_failed() \ + .has_message("Expecting:\n '23'\n not equal to\n '23'") + + +func test_is_less(): + assert_int(23).is_less(42) + assert_int(23).is_less(24) + + assert_failure(func(): assert_int(23).is_less(23)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23' but was '23'") + assert_failure(func(): assert_int(null).is_less(23)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '23' but was ''") + + +func test_is_less_equal(): + assert_int(23).is_less_equal(42) + assert_int(23).is_less_equal(23) + + assert_failure(func(): assert_int(23).is_less_equal(22)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '22' but was '23'") + assert_failure(func(): assert_int(null).is_less_equal(22)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '22' but was ''") + + +func test_is_greater(): + assert_int(23).is_greater(20) + assert_int(23).is_greater(22) + + assert_failure(func(): assert_int(23).is_greater(23)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23' but was '23'") + assert_failure(func(): assert_int(null).is_greater(23)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '23' but was ''") + + +func test_is_greater_equal(): + assert_int(23).is_greater_equal(20) + assert_int(23).is_greater_equal(23) + + assert_failure(func(): assert_int(23).is_greater_equal(24)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '24' but was '23'") + assert_failure(func(): assert_int(null).is_greater_equal(24)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '24' but was ''") + + +func test_is_even(): + assert_int(12).is_even() + + assert_failure(func(): assert_int(13).is_even()) \ + .is_failed() \ + .has_message("Expecting:\n '13' must be even") + assert_failure(func(): assert_int(null).is_even()) \ + .is_failed() \ + .has_message("Expecting:\n '' must be even") + + +func test_is_odd(): + assert_int(13).is_odd() + + assert_failure(func(): assert_int(12).is_odd()) \ + .is_failed() \ + .has_message("Expecting:\n '12' must be odd") + assert_failure(func(): assert_int(null).is_odd()) \ + .is_failed() \ + .has_message("Expecting:\n '' must be odd") + + +func test_is_negative(): + assert_int(-13).is_negative() + + assert_failure(func(): assert_int(13).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '13' be negative") + assert_failure(func(): assert_int(null).is_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be negative") + + +func test_is_not_negative(): + assert_int(13).is_not_negative() + + assert_failure(func(): assert_int(-13).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '-13' be not negative") + assert_failure(func(): assert_int(null).is_not_negative()) \ + .is_failed() \ + .has_message("Expecting:\n '' be not negative") + + +func test_is_zero(): + assert_int(0).is_zero() + + assert_failure(func(): assert_int(1).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is '1'") + assert_failure(func(): assert_int(null).is_zero()) \ + .is_failed() \ + .has_message("Expecting:\n equal to 0 but is ''") + + +func test_is_not_zero(): + assert_int(null).is_not_zero() + assert_int(1).is_not_zero() + + assert_failure(func(): assert_int(0).is_not_zero()) \ + .is_failed() \ + .has_message("Expecting:\n not equal to 0") + + +func test_is_in(): + assert_int(5).is_in([3, 4, 5, 6]) + # this assertion fail because 7 is not in [3, 4, 5, 6] + assert_failure(func(): assert_int(7).is_in([3, 4, 5, 6])) \ + .is_failed() \ + .has_message("Expecting:\n '7'\n is in\n '[3, 4, 5, 6]'") + assert_failure(func(): assert_int(null).is_in([3, 4, 5, 6])) \ + .is_failed() \ + .has_message("Expecting:\n ''\n is in\n '[3, 4, 5, 6]'") + + +func test_is_not_in(): + assert_int(null).is_not_in([3, 4, 6, 7]) + assert_int(5).is_not_in([3, 4, 6, 7]) + # this assertion fail because 7 is not in [3, 4, 5, 6] + assert_failure(func(): assert_int(5).is_not_in([3, 4, 5, 6])) \ + .is_failed() \ + .has_message("Expecting:\n '5'\n is not in\n '[3, 4, 5, 6]'") + + +func test_is_between(fuzzer = Fuzzers.rangei(-20, 20)): + var value = fuzzer.next_value() as int + assert_int(value).is_between(-20, 20) + + +func test_is_between_must_fail(): + assert_failure(func(): assert_int(-10).is_between(-9, 0)) \ + .is_failed() \ + .has_message("Expecting:\n '-10'\n in range between\n '-9' <> '0'") + assert_failure(func(): assert_int(0).is_between(1, 10)) \ + .is_failed() \ + .has_message("Expecting:\n '0'\n in range between\n '1' <> '10'") + assert_failure(func(): assert_int(10).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n '10'\n in range between\n '11' <> '21'") + assert_failure(func(): assert_int(null).is_between(11, 21)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '11' <> '21'") + + +func test_must_fail_has_invlalid_type(): + assert_failure(func(): assert_int(3.3)) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + assert_failure(func(): assert_int(true)) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + assert_failure(func(): assert_int("foo")) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + assert_failure(func(): assert_int(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitIntAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_int(314)\ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_int(0).is_zero() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_int(1).is_zero()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_int(0).is_zero() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd new file mode 100644 index 0000000..1abd80e --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitObjectAssertImplTest.gd @@ -0,0 +1,178 @@ +# GdUnit generated TestSuite +class_name GdUnitObjectAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd' + + +func test_is_equal(): + assert_object(Mesh.new()).is_equal(Mesh.new()) + + assert_failure(func(): assert_object(Mesh.new()).is_equal(Skin.new())) \ + .is_failed() + assert_failure(func(): assert_object(null).is_equal(Skin.new())) \ + .is_failed() \ + .has_message("Expecting:\n" + + " \n" + + " but was\n" + + " ''") + + +func test_is_not_equal(): + assert_object(null).is_not_equal(Skin.new()) + assert_object(Mesh.new()).is_not_equal(Skin.new()) + + assert_failure(func(): assert_object(Mesh.new()).is_not_equal(Mesh.new())) \ + .is_failed() + + +func test_is_instanceof(): + # engine class test + assert_object(auto_free(Path3D.new())).is_instanceof(Node) + assert_object(auto_free(Camera3D.new())).is_instanceof(Camera3D) + # script class test + assert_object(auto_free(Udo.new())).is_instanceof(Person) + # inner class test + assert_object(auto_free(CustomClass.InnerClassA.new())).is_instanceof(Node) + assert_object(auto_free(CustomClass.InnerClassB.new())).is_instanceof(CustomClass.InnerClassA) + + assert_failure(func(): assert_object(auto_free(Path3D.new())).is_instanceof(Tree)) \ + .is_failed() \ + .has_message("Expected instance of:\n 'Tree'\n But it was 'Path3D'") + assert_failure(func(): assert_object(null).is_instanceof(Tree)) \ + .is_failed() \ + .has_message("Expected instance of:\n 'Tree'\n But it was ''") + + +func test_is_not_instanceof(): + assert_object(null).is_not_instanceof(Tree) + # engine class test + assert_object(auto_free(Path3D.new())).is_not_instanceof(Tree) + # script class test + assert_object(auto_free(City.new())).is_not_instanceof(Person) + # inner class test + assert_object(auto_free(CustomClass.InnerClassA.new())).is_not_instanceof(Tree) + assert_object(auto_free(CustomClass.InnerClassB.new())).is_not_instanceof(CustomClass.InnerClassC) + + assert_failure(func(): assert_object(auto_free(Path3D.new())).is_not_instanceof(Node)) \ + .is_failed() \ + .has_message("Expected not be a instance of ") + + +func test_is_null(): + assert_object(null).is_null() + + assert_failure(func(): assert_object(auto_free(Node.new())).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was ") + + +func test_is_not_null(): + assert_object(auto_free(Node.new())).is_not_null() + + assert_failure(func(): assert_object(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_same(): + var obj1 = auto_free(Node.new()) + var obj2 = obj1 + var obj3 = auto_free(obj1.duplicate()) + assert_object(obj1).is_same(obj1) + assert_object(obj1).is_same(obj2) + assert_object(obj2).is_same(obj1) + + assert_failure(func(): assert_object(null).is_same(obj1)) \ + .is_failed() \ + .has_message("Expecting:\n" + + " \n" + + " to refer to the same object\n" + + " ''") + assert_failure(func(): assert_object(obj1).is_same(obj3)) \ + .is_failed() + assert_failure(func(): assert_object(obj3).is_same(obj1)) \ + .is_failed() + assert_failure(func(): assert_object(obj3).is_same(obj2)) \ + .is_failed() + + +func test_is_not_same(): + var obj1 = auto_free(Node.new()) + var obj2 = obj1 + var obj3 = auto_free(obj1.duplicate()) + assert_object(null).is_not_same(obj1) + assert_object(obj1).is_not_same(obj3) + assert_object(obj3).is_not_same(obj1) + assert_object(obj3).is_not_same(obj2) + + assert_failure(func(): assert_object(obj1).is_not_same(obj1)) \ + .is_failed() \ + .has_message(""" + Expecting not same: + """ + .dedent() + .trim_prefix("\n")) + assert_failure(func(): assert_object(obj1).is_not_same(obj2)) \ + .is_failed() \ + .has_message(""" + Expecting not same: + """ + .dedent() + .trim_prefix("\n")) + assert_failure(func(): assert_object(obj2).is_not_same(obj1)) \ + .is_failed() \ + .has_message(""" + Expecting not same: + """ + .dedent() + .trim_prefix("\n")) + + +func test_must_fail_has_invlalid_type(): + assert_failure(func(): assert_object(1)) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + assert_failure(func(): assert_object(1.3)) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + assert_failure(func(): assert_object(true)) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + assert_failure(func(): assert_object("foo")) \ + .is_failed() \ + .has_message("GdUnitObjectAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_object(auto_free(Node.new())) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_object(null).is_null() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_object(RefCounted.new()).is_null()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_object(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() + + # should abort here because we had an failing assert + if is_failure(): + return + assert_bool(true).override_failure_message("This line shold never be called").is_false() diff --git a/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd b/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd new file mode 100644 index 0000000..26c2543 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitPackedArrayAssertTest.gd @@ -0,0 +1,338 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd' + + +@warning_ignore("unused_parameter") +func test_is_array_assert(_test :String, array, test_parameters = [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + var assert_ = assert_that(array) + assert_object(assert_).is_instanceof(GdUnitArrayAssert) + + +@warning_ignore("unused_parameter") +func test_is_null(_test :String, value, test_parameters = [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + assert_array(null).is_null() + assert_failure(func(): assert_array(value).is_null()) \ + .is_failed() \ + .has_message("Expecting: '' but was '%s'" % GdDefaultValueDecoder.decode(value)) + + +@warning_ignore("unused_parameter") +func test_is_not_null(_test :String, array, test_parameters = [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + assert_array(array).is_not_null() + + assert_failure(func(): assert_array(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +@warning_ignore("unused_parameter") +func test_is_equal(_test :String, array, test_parameters = [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + var other = array.duplicate() + assert_array(array).is_equal(other) + # should fail because the array not contains same elements and has diff size + other.append(array[2]) + assert_failure(func(): assert_array(array).is_equal(other)) \ + .is_failed() \ + .has_message(""" + Expecting: + '%s' + but was + '%s' + + Differences found: + Index Current Expected 5 $value """ + .dedent() + .trim_prefix("\n") + .replace("$value", str(array[2]) ) % [GdArrayTools.as_string(other, false), GdArrayTools.as_string(array, false)]) + + +@warning_ignore("unused_parameter") +func test_is_not_equal(_test :String, array, test_parameters = [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + var other = array.duplicate() + other.append(array[2]) + assert_array(array).is_not_equal(other) + # should fail because the array contains same elements + assert_failure(func(): assert_array(array).is_not_equal(array.duplicate())) \ + .is_failed() \ + .has_message(""" + Expecting: + '%s' + not equal to + '%s'""" + .dedent() + .trim_prefix("\n") % [GdDefaultValueDecoder.decode(array), GdDefaultValueDecoder.decode(array)]) + + +@warning_ignore("unused_parameter") +func test_is_empty(_test :String, array, test_parameters = [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + var empty = array.duplicate() + empty.clear() + assert_array(empty).is_empty() + # should fail because the array is not empty + assert_failure(func(): assert_array(array).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + '%s'""" + .dedent() + .trim_prefix("\n") % GdDefaultValueDecoder.decode(array)) + + +@warning_ignore("unused_parameter") +func test_is_not_empty(_test :String, array, test_parameters = [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).is_not_empty() + # should fail because the array is empty + var empty = array.duplicate() + empty.clear() + assert_failure(func(): assert_array(empty).is_not_empty()) \ + .is_failed() \ + .has_message("Expecting:\n must not be empty") + + +@warning_ignore("unused_parameter") +func test_is_same(value, test_parameters = [ + [[0]], + [PackedByteArray([0])], + [PackedFloat32Array([0.0])], + [PackedFloat64Array([0.0])], + [PackedInt32Array([0])], + [PackedInt64Array([0])], + [PackedStringArray([""])], + [PackedColorArray([Color.RED])], + [PackedVector2Array([Vector2.ZERO])], + [PackedVector3Array([Vector3.ZERO])], +]) -> void: + assert_array(value).is_same(value) + + var v := GdDefaultValueDecoder.decode(value) + assert_failure(func(): assert_array(value).is_same(value.duplicate()))\ + .is_failed()\ + .has_message(""" + Expecting: + '%s' + to refer to the same object + '%s'""" + .dedent() + .trim_prefix("\n") % [v, v]) + + +@warning_ignore("unused_parameter") +func test_is_not_same(value, test_parameters = [ + [[0]], + [PackedByteArray([0])], + [PackedFloat32Array([0.0])], + [PackedFloat64Array([0.0])], + [PackedInt32Array([0])], + [PackedInt64Array([0])], + [PackedStringArray([""])], + [PackedColorArray([Color.RED])], + [PackedVector2Array([Vector2.ZERO])], + [PackedVector3Array([Vector3.ZERO])], +]) -> void: + assert_array(value).is_not_same(value.duplicate()) + + assert_failure(func(): assert_array(value).is_not_same(value))\ + .is_failed()\ + .has_message("Expecting not same:\n '%s'" % GdDefaultValueDecoder.decode(value)) + + +@warning_ignore("unused_parameter") +func test_has_size(_test :String, array, test_parameters = [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).has_size(5) + # should fail because the array has a size of 5 + assert_failure(func(): assert_array(array).has_size(4)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '4' + but was + '5'""" + .dedent() + .trim_prefix("\n")) + + +@warning_ignore("unused_parameter") +func test_contains(_test :String, array, test_parameters = [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).contains([array[1], array[3], array[4]]) + # should fail because the array not contains 7 and 6 + var do_contains := [array[1], 7, 6] + assert_failure(func(): assert_array(array).contains(do_contains)) \ + .is_failed() \ + .has_message(""" + Expecting contains elements: + '$source' + do contains (in any order) + '$contains' + but could not find elements: + '[7, 6]'""" + .dedent() + .trim_prefix("\n") + .replace("$source", GdDefaultValueDecoder.decode(array)) + .replace("$contains", GdDefaultValueDecoder.decode(do_contains)) + ) + + +@warning_ignore("unused_parameter") +func test_contains_exactly(_test :String, array, test_parameters = [ + ["Array", Array([1, 2, 3, 4, 5])], + ["PackedByteArray", PackedByteArray([1, 2, 3, 4, 5])], + ["PackedInt32Array", PackedInt32Array([1, 2, 3, 4, 5])], + ["PackedInt64Array", PackedInt64Array([1, 2, 3, 4, 5])], + ["PackedFloat32Array", PackedFloat32Array([1, 2, 3, 4, 5])], + ["PackedFloat64Array", PackedFloat64Array([1, 2, 3, 4, 5])], + ["PackedStringArray", PackedStringArray([1, 2, 3, 4, 5])], + ["PackedVector2Array", PackedVector2Array([Vector2.ZERO, Vector2.LEFT, Vector2.RIGHT, Vector2.UP, Vector2.DOWN])], + ["PackedVector3Array", PackedVector3Array([Vector3.ZERO, Vector3.LEFT, Vector3.RIGHT, Vector3.UP, Vector3.DOWN])], + ["PackedColorArray", PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.BLACK])] ] + ) -> void: + + assert_array(array).contains_exactly(array.duplicate()) + # should fail because the array not contains same elements but in different order + var shuffled = array.duplicate() + shuffled[1] = array[3] + shuffled[3] = array[1] + assert_failure(func(): assert_array(array).contains_exactly(shuffled)) \ + .is_failed() \ + .has_message(""" + Expecting contains exactly elements: + '$source' + do contains (in same order) + '$contains' + but has different order at position '1' + '$A' vs '$B'""" + .dedent() + .trim_prefix("\n") + .replace("$A", GdDefaultValueDecoder.decode(array[1])) + .replace("$B", GdDefaultValueDecoder.decode(array[3])) + .replace("$source", GdDefaultValueDecoder.decode(array)) + .replace("$contains", GdDefaultValueDecoder.decode(shuffled)) + ) + +@warning_ignore("unused_parameter") +func test_override_failure_message(_test :String, array, test_parameters = [ + ["Array", Array()], + ["PackedByteArray", PackedByteArray()], + ["PackedInt32Array", PackedInt32Array()], + ["PackedInt64Array", PackedInt64Array()], + ["PackedFloat32Array", PackedFloat32Array()], + ["PackedFloat64Array", PackedFloat64Array()], + ["PackedStringArray", PackedStringArray()], + ["PackedVector2Array", PackedVector2Array()], + ["PackedVector3Array", PackedVector3Array()], + ["PackedColorArray", PackedColorArray()] ] + ) -> void: + + assert_failure(func(): assert_array(array) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") diff --git a/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd new file mode 100644 index 0000000..94d009a --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitResultAssertImplTest.gd @@ -0,0 +1,143 @@ +# GdUnit generated TestSuite +class_name GdUnitResultAssertImplTest +extends GdUnitTestSuite + + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd' + + +func test_is_null(): + assert_result(null).is_null() + + assert_failure(func(): assert_result(GdUnitResult.success("")).is_null()) \ + .is_failed() \ + .has_message('Expecting: \'\' but was <{ "state": 0, "value": "\\"\\"", "warn_msg": "", "err_msg": "" }>') + + +func test_is_not_null(): + assert_result(GdUnitResult.success("")).is_not_null() + + assert_failure(func(): assert_result(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_empty(): + assert_result(GdUnitResult.empty()).is_empty() + + assert_failure(func(): assert_result(GdUnitResult.warn("a warning")).is_empty()) \ + .is_failed() \ + .has_message("Expecting the result must be a EMPTY but was WARNING:\n 'a warning'") + assert_failure(func(): assert_result(GdUnitResult.error("a error")).is_empty()) \ + .is_failed() \ + .has_message("Expecting the result must be a EMPTY but was ERROR:\n 'a error'") + assert_failure(func(): assert_result(null).is_empty()) \ + .is_failed() \ + .has_message("Expecting the result must be a EMPTY but was .") + + +func test_is_success(): + assert_result(GdUnitResult.success("")).is_success() + + assert_failure(func(): assert_result(GdUnitResult.warn("a warning")).is_success()) \ + .is_failed() \ + .has_message("Expecting the result must be a SUCCESS but was WARNING:\n 'a warning'") + assert_failure(func(): assert_result(GdUnitResult.error("a error")).is_success()) \ + .is_failed() \ + .has_message("Expecting the result must be a SUCCESS but was ERROR:\n 'a error'") + assert_failure(func(): assert_result(null).is_success()) \ + .is_failed() \ + .has_message("Expecting the result must be a SUCCESS but was .") + + +func test_is_warning(): + assert_result(GdUnitResult.warn("a warning")).is_warning() + + assert_failure(func(): assert_result(GdUnitResult.success("value")).is_warning()) \ + .is_failed() \ + .has_message("Expecting the result must be a WARNING but was SUCCESS.") + assert_failure(func(): assert_result(GdUnitResult.error("a error")).is_warning()) \ + .is_failed() \ + .has_message("Expecting the result must be a WARNING but was ERROR:\n 'a error'") + assert_failure(func(): assert_result(null).is_warning()) \ + .is_failed() \ + .has_message("Expecting the result must be a WARNING but was .") + + +func test_is_error(): + assert_result(GdUnitResult.error("a error")).is_error() + + assert_failure(func(): assert_result(GdUnitResult.success("")).is_error()) \ + .is_failed() \ + .has_message("Expecting the result must be a ERROR but was SUCCESS.") + assert_failure(func(): assert_result(GdUnitResult.warn("a warning")).is_error()) \ + .is_failed() \ + .has_message("Expecting the result must be a ERROR but was WARNING:\n 'a warning'") + assert_failure(func(): assert_result(null).is_error()) \ + .is_failed() \ + .has_message("Expecting the result must be a ERROR but was .") + + +func test_contains_message(): + assert_result(GdUnitResult.error("a error")).contains_message("a error") + assert_result(GdUnitResult.warn("a warning")).contains_message("a warning") + + assert_failure(func(): assert_result(GdUnitResult.success("")).contains_message("Error 500")) \ + .is_failed() \ + .has_message("Expecting:\n 'Error 500'\n but the GdUnitResult is a success.") + assert_failure(func(): assert_result(GdUnitResult.warn("Warning xyz!")).contains_message("Warning aaa!")) \ + .is_failed() \ + .has_message("Expecting:\n 'Warning aaa!'\n but was\n 'Warning xyz!'.") + assert_failure(func(): assert_result(GdUnitResult.error("Error 410")).contains_message("Error 500")) \ + .is_failed() \ + .has_message("Expecting:\n 'Error 500'\n but was\n 'Error 410'.") + assert_failure(func(): assert_result(null).contains_message("Error 500")) \ + .is_failed() \ + .has_message("Expecting:\n 'Error 500'\n but was\n ''.") + + +func test_is_value(): + assert_result(GdUnitResult.success("")).is_value("") + var result_value = auto_free(Node.new()) + assert_result(GdUnitResult.success(result_value)).is_value(result_value) + + assert_failure(func(): assert_result(GdUnitResult.success("")).is_value("abc")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n 'abc'\n but was\n ''.") + assert_failure(func(): assert_result(GdUnitResult.success("abc")).is_value("")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n ''\n but was\n 'abc'.") + assert_failure(func(): assert_result(GdUnitResult.success(result_value)).is_value("")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n ''\n but was\n .") + assert_failure(func(): assert_result(null).is_value("")) \ + .is_failed() \ + .has_message("Expecting to contain same value:\n ''\n but was\n ''.") + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_result(GdUnitResult.success("")) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_result(null).is_null() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_result(RefCounted.new()).is_null()).is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_result(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() diff --git a/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd new file mode 100644 index 0000000..1c635c6 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitSignalAssertImplTest.gd @@ -0,0 +1,228 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitSignalAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd' +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +class TestEmitter extends Node: + signal test_signal_counted(value) + signal test_signal(value :int) + signal test_signal_unused() + + var _trigger_count :int + var _count := 0 + + func _init(trigger_count := 10): + _trigger_count = trigger_count + + func _process(_delta): + if _count >= _trigger_count: + test_signal_counted.emit(_count) + + if _count == 20: + test_signal.emit(10) + test_signal.emit(20) + _count += 1 + + func reset_trigger(trigger_count := 10) -> void: + _trigger_count = trigger_count + + +var signal_emitter :TestEmitter + + +func before_test(): + signal_emitter = auto_free(TestEmitter.new()) + add_child(signal_emitter) + + +# we need to skip await fail test because of an bug in Godot 4.0 stable +func is_skip_fail_await() -> bool: + return Engine.get_version_info().hex < 0x40002 + + +func test_invalid_arg() -> void: + ( + await assert_failure_await(func(): await assert_signal(null).wait_until(50).is_emitted("test_signal_counted")) + ).has_message("Can't wait for signal checked a NULL object.") + ( + await assert_failure_await(func(): await assert_signal(null).wait_until(50).is_not_emitted("test_signal_counted")) + ).has_message("Can't wait for signal checked a NULL object.") + + +func test_unknown_signal() -> void: + ( + await assert_failure_await(func(): await assert_signal(signal_emitter).wait_until(50).is_emitted("unknown")) + ).has_message("Can't wait for non-existion signal 'unknown' checked object 'Node'.") + + +func test_signal_is_emitted_without_args() -> void: + # wait until signal 'test_signal_counted' without args + await assert_signal(signal_emitter).is_emitted("test_signal", [10]) + await assert_signal(signal_emitter).is_emitted("test_signal", [20]) + # wait until signal 'test_signal_unused' where is never emitted + + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_signal(signal_emitter).wait_until(500).is_emitted("test_signal_unused")) + ).has_message("Expecting emit signal: 'test_signal_unused()' but timed out after 500ms") + + +func test_signal_is_emitted_with_args() -> void: + # wait until signal 'test_signal_counted' is emitted with value 20 + await assert_signal(signal_emitter).is_emitted("test_signal_counted", [20]) + + if is_skip_fail_await(): + return + ( + await assert_failure_await(func(): await assert_signal(signal_emitter).wait_until(50).is_emitted("test_signal_counted", [500])) + ).has_message("Expecting emit signal: 'test_signal_counted([500])' but timed out after 50ms") + + +func test_signal_is_emitted_use_argument_matcher() -> void: + # wait until signal 'test_signal_counted' is emitted by using any_int() matcher for signal arguments + await assert_signal(signal_emitter).is_emitted("test_signal_counted", [any_int()]) + + # should also work with any() matcher + signal_emitter.reset_trigger() + await assert_signal(signal_emitter).is_emitted("test_signal_counted", [any()]) + + # should fail because the matcher uses the wrong type + signal_emitter.reset_trigger() + ( + await assert_failure_await( func(): await assert_signal(signal_emitter).wait_until(50).is_emitted("test_signal_counted", [any_string()])) + ).has_message("Expecting emit signal: 'test_signal_counted([any_string()])' but timed out after 50ms") + + +func test_signal_is_not_emitted() -> void: + # wait to verify signal 'test_signal_counted()' is not emitted until the first 50ms + await assert_signal(signal_emitter).wait_until(50).is_not_emitted("test_signal_counted") + # wait to verify signal 'test_signal_counted(50)' is not emitted until the NEXT first 80ms + await assert_signal(signal_emitter).wait_until(30).is_not_emitted("test_signal_counted", [50]) + + if is_skip_fail_await(): + return + # until the next 500ms the signal is emitted and ends in a failure + ( + await assert_failure_await(func(): await assert_signal(signal_emitter).wait_until(1000).is_not_emitted("test_signal_counted", [50])) + ).starts_with_message("Expecting do not emit signal: 'test_signal_counted([50])' but is emitted after") + + +func test_override_failure_message() -> void: + if is_skip_fail_await(): + return + + ( + await assert_failure_await(func(): await assert_signal(signal_emitter) \ + .override_failure_message("Custom failure message")\ + .wait_until(100)\ + .is_emitted("test_signal_unused")) + ).has_message("Custom failure message") + + +func test_node_changed_emitting_signals(): + var node :Node2D = auto_free(Node2D.new()) + add_child(node) + + await assert_signal(node).wait_until(200).is_emitted("draw") + + node.visible = false; + await assert_signal(node).wait_until(200).is_emitted("visibility_changed") + + # expecting to fail, we not changed the visibility + #node.visible = true; + if not is_skip_fail_await(): + ( + await assert_failure_await(func(): await assert_signal(node).wait_until(200).is_emitted("visibility_changed")) + ).has_message("Expecting emit signal: 'visibility_changed()' but timed out after 200ms") + + node.show() + await assert_signal(node).wait_until(200).is_emitted("draw") + + +func test_is_signal_exists() -> void: + var node :Node2D = auto_free(Node2D.new()) + + assert_signal(node).is_signal_exists("visibility_changed")\ + .is_signal_exists("draw")\ + .is_signal_exists("visibility_changed")\ + .is_signal_exists("tree_entered")\ + .is_signal_exists("tree_exiting")\ + .is_signal_exists("tree_exited") + + if is_skip_fail_await(): + return + + ( + await assert_failure_await(func(): assert_signal(node).is_signal_exists("not_existing_signal")) + ).has_message("The signal 'not_existing_signal' not exists checked object 'Node2D'.") + + +class MyEmitter extends Node: + + signal my_signal_a + signal my_signal_b(value :String) + + + func do_emit_a() -> void: + my_signal_a.emit() + + + func do_emit_b() -> void: + my_signal_b.emit("foo") + + +func test_monitor_signals() -> void: + # start to watch on the emitter to collect all emitted signals + var emitter_a := monitor_signals(MyEmitter.new()) + var emitter_b := monitor_signals(MyEmitter.new()) + + # verify the signals are not emitted initial + await assert_signal(emitter_a).wait_until(50).is_not_emitted('my_signal_a') + await assert_signal(emitter_a).wait_until(50).is_not_emitted('my_signal_b') + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_a') + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_b') + + # emit signal `my_signal_a` on emitter_a + emitter_a.do_emit_a() + await assert_signal(emitter_a).is_emitted('my_signal_a') + + # emit signal `my_signal_b` on emitter_a + emitter_a.do_emit_b() + await assert_signal(emitter_a).is_emitted('my_signal_b', ["foo"]) + # verify emitter_b still has nothing emitted + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_a') + await assert_signal(emitter_b).wait_until(50).is_not_emitted('my_signal_b') + + # now verify emitter b + emitter_b.do_emit_a() + await assert_signal(emitter_b).wait_until(50).is_emitted('my_signal_a') + + +class ExampleResource extends Resource: + @export var title := "Title": + set(new_value): + title = new_value + changed.emit() + + + func change_title(p_title: String) -> void: + title = p_title + + +func test_monitor_signals_on_resource_set() -> void: + var sut = ExampleResource.new() + var emitter := monitor_signals(sut) + + sut.change_title("Some title") + + # title change should emit "changed" signal + await assert_signal(emitter).is_emitted("changed") + assert_str(sut.title).is_equal("Some title") + diff --git a/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd new file mode 100644 index 0000000..a5dd4dd --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitStringAssertImplTest.gd @@ -0,0 +1,450 @@ +# GdUnit generated TestSuite +class_name GdUnitStringAssertImplTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd' + + + +func test_is_null(): + assert_str(null).is_null() + + assert_failure(func(): assert_str("abc").is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was 'abc'") + + +func test_is_not_null(): + assert_str("abc").is_not_null() + assert_str(&"abc").is_not_null() + + assert_failure(func(): assert_str(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + + +func test_is_equal(): + assert_str("This is a test message").is_equal("This is a test message") + assert_str("abc").is_equal("abc") + assert_str("abc").is_equal(&"abc") + assert_str(&"abc").is_equal("abc") + assert_str(&"abc").is_equal(&"abc") + + assert_failure(func(): assert_str("This is a test message").is_equal("This is a test Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test Message' + but was + 'This is a test Mmessage'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).is_equal("This is a test Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test Message' + but was + ''""".dedent().trim_prefix("\n")) + + +func test_is_equal_pipe_character() -> void: + assert_failure(func(): assert_str("AAA|BBB|CCC").is_equal("AAA|BBB.CCC")) \ + .is_failed() + + +func test_is_equal_ignoring_case(): + assert_str("This is a test message").is_equal_ignoring_case("This is a test Message") + assert_str("This is a test message").is_equal_ignoring_case(&"This is a test Message") + assert_str(&"This is a test message").is_equal_ignoring_case("This is a test Message") + assert_str(&"This is a test message").is_equal_ignoring_case(&"This is a test Message") + + assert_failure(func(): assert_str("This is a test message").is_equal_ignoring_case("This is a Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a Message' + but was + 'This is a test Mmessage' (ignoring case)""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).is_equal_ignoring_case("This is a Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a Message' + but was + '' (ignoring case)""".dedent().trim_prefix("\n")) + + +func test_is_not_equal(): + assert_str(null).is_not_equal("This is a test Message") + assert_str("This is a test message").is_not_equal("This is a test Message") + assert_str("This is a test message").is_not_equal(&"This is a test Message") + assert_str(&"This is a test message").is_not_equal("This is a test Message") + assert_str(&"This is a test message").is_not_equal(&"This is a test Message") + + assert_failure(func(): assert_str("This is a test message").is_not_equal("This is a test message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not equal to + 'This is a test message'""".dedent().trim_prefix("\n")) + + +func test_is_not_equal_ignoring_case(): + assert_str(null).is_not_equal_ignoring_case("This is a Message") + assert_str("This is a test message").is_not_equal_ignoring_case("This is a Message") + assert_str("This is a test message").is_not_equal_ignoring_case(&"This is a Message") + assert_str(&"This is a test message").is_not_equal_ignoring_case("This is a Message") + assert_str(&"This is a test message").is_not_equal_ignoring_case(&"This is a Message") + + assert_failure(func(): assert_str("This is a test message").is_not_equal_ignoring_case("This is a test Message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test Message' + not equal to + 'This is a test message'""".dedent().trim_prefix("\n")) + + +func test_is_empty(): + assert_str("").is_empty() + assert_str(&"").is_empty() + + assert_failure(func(): assert_str(" ").is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + ' '""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str("abc").is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + 'abc'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(&"abc").is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + 'abc'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).is_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must be empty but was + ''""".dedent().trim_prefix("\n")) + + +func test_is_not_empty(): + assert_str(" ").is_not_empty() + assert_str(" ").is_not_empty() + assert_str("abc").is_not_empty() + assert_str(&"abc").is_not_empty() + + assert_failure(func(): assert_str("").is_not_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must not be empty""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).is_not_empty()) \ + .is_failed() \ + .has_message(""" + Expecting: + must not be empty""".dedent().trim_prefix("\n")) + + +func test_contains(): + assert_str("This is a test message").contains("a test") + assert_str("This is a test message").contains(&"a test") + assert_str(&"This is a test message").contains("a test") + assert_str(&"This is a test message").contains(&"a test") + # must fail because of camel case difference + assert_failure(func(): assert_str("This is a test message").contains("a Test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + do contains + 'a Test'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).contains("a Test")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + do contains + 'a Test'""".dedent().trim_prefix("\n")) + + +func test_not_contains(): + assert_str(null).not_contains("a tezt") + assert_str("This is a test message").not_contains("a tezt") + assert_str("This is a test message").not_contains(&"a tezt") + assert_str(&"This is a test message").not_contains("a tezt") + assert_str(&"This is a test message").not_contains(&"a tezt") + + assert_failure(func(): assert_str("This is a test message").not_contains("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not do contain + 'a test'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(&"This is a test message").not_contains("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not do contain + 'a test'""".dedent().trim_prefix("\n")) + + +func test_contains_ignoring_case(): + assert_str("This is a test message").contains_ignoring_case("a Test") + assert_str("This is a test message").contains_ignoring_case(&"a Test") + assert_str(&"This is a test message").contains_ignoring_case("a Test") + assert_str(&"This is a test message").contains_ignoring_case(&"a Test") + + assert_failure(func(): assert_str("This is a test message").contains_ignoring_case("a Tesd")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + contains + 'a Tesd' + (ignoring case)""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).contains_ignoring_case("a Tesd")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + contains + 'a Tesd' + (ignoring case)""".dedent().trim_prefix("\n")) + + +func test_not_contains_ignoring_case(): + assert_str(null).not_contains_ignoring_case("a Test") + assert_str("This is a test message").not_contains_ignoring_case("a Tezt") + assert_str("This is a test message").not_contains_ignoring_case(&"a Tezt") + assert_str(&"This is a test message").not_contains_ignoring_case("a Tezt") + assert_str(&"This is a test message").not_contains_ignoring_case(&"a Tezt") + + assert_failure(func(): assert_str("This is a test message").not_contains_ignoring_case("a Test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + not do contains + 'a Test' + (ignoring case)""".dedent().trim_prefix("\n")) + + +func test_starts_with(): + assert_str("This is a test message").starts_with("This is") + assert_str("This is a test message").starts_with(&"This is") + assert_str(&"This is a test message").starts_with("This is") + assert_str(&"This is a test message").starts_with(&"This is") + + assert_failure(func(): assert_str("This is a test message").starts_with("This iss")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to start with + 'This iss'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str("This is a test message").starts_with("this is")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to start with + 'this is'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str("This is a test message").starts_with("test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to start with + 'test'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).starts_with("test")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + to start with + 'test'""".dedent().trim_prefix("\n")) + + +func test_ends_with(): + assert_str("This is a test message").ends_with("test message") + assert_str("This is a test message").ends_with(&"test message") + assert_str(&"This is a test message").ends_with("test message") + assert_str(&"This is a test message").ends_with(&"test message") + + assert_failure(func(): assert_str("This is a test message").ends_with("tes message")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to end with + 'tes message'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str("This is a test message").ends_with("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + 'This is a test message' + to end with + 'a test'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).ends_with("a test")) \ + .is_failed() \ + .has_message(""" + Expecting: + '' + to end with + 'a test'""".dedent().trim_prefix("\n")) + + +func test_has_lenght(): + assert_str("This is a test message").has_length(22) + assert_str(&"This is a test message").has_length(22) + assert_str("").has_length(0) + assert_str(&"").has_length(0) + + assert_failure(func(): assert_str("This is a test message").has_length(23)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '23' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).has_length(23)) \ + .is_failed() \ + .has_message(""" + Expecting size: + '23' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_lenght_less_than(): + assert_str("This is a test message").has_length(23, Comparator.LESS_THAN) + assert_str("This is a test message").has_length(42, Comparator.LESS_THAN) + assert_str(&"This is a test message").has_length(42, Comparator.LESS_THAN) + + assert_failure(func(): assert_str("This is a test message").has_length(22, Comparator.LESS_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than: + '22' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).has_length(22, Comparator.LESS_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than: + '22' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_lenght_less_equal(): + assert_str("This is a test message").has_length(22, Comparator.LESS_EQUAL) + assert_str("This is a test message").has_length(23, Comparator.LESS_EQUAL) + assert_str(&"This is a test message").has_length(23, Comparator.LESS_EQUAL) + + assert_failure(func(): assert_str("This is a test message").has_length(21, Comparator.LESS_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than or equal: + '21' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).has_length(21, Comparator.LESS_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be less than or equal: + '21' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_lenght_greater_than(): + assert_str("This is a test message").has_length(21, Comparator.GREATER_THAN) + assert_str(&"This is a test message").has_length(21, Comparator.GREATER_THAN) + + assert_failure(func(): assert_str("This is a test message").has_length(22, Comparator.GREATER_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than: + '22' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).has_length(22, Comparator.GREATER_THAN)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than: + '22' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_has_lenght_greater_equal(): + assert_str("This is a test message").has_length(21, Comparator.GREATER_EQUAL) + assert_str("This is a test message").has_length(22, Comparator.GREATER_EQUAL) + assert_str(&"This is a test message").has_length(22, Comparator.GREATER_EQUAL) + + assert_failure(func(): assert_str("This is a test message").has_length(23, Comparator.GREATER_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than or equal: + '23' but was '22' in + 'This is a test message'""".dedent().trim_prefix("\n")) + assert_failure(func(): assert_str(null).has_length(23, Comparator.GREATER_EQUAL)) \ + .is_failed() \ + .has_message(""" + Expecting size to be greater than or equal: + '23' but was '' in + ''""".dedent().trim_prefix("\n")) + + +func test_fluentable(): + assert_str("value a").is_not_equal("a")\ + .is_equal("value a")\ + .has_length(7)\ + .is_equal("value a") + + +func test_must_fail_has_invlalid_type(): + assert_failure(func(): assert_str(1)) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + assert_failure(func(): assert_str(1.3)) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + assert_failure(func(): assert_str(true)) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + assert_failure(func(): assert_str(Resource.new())) \ + .is_failed() \ + .has_message("GdUnitStringAssert inital error, unexpected type ") + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_str("")\ + .override_failure_message("Custom failure message")\ + .is_null())\ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_str(null).is_null() + assert_bool(is_failure()).is_false() + + # checked failed assert + assert_failure(func(): assert_str(RefCounted.new()).is_null()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_str(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() diff --git a/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd b/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd new file mode 100644 index 0000000..7c04da9 --- /dev/null +++ b/addons/gdUnit4/test/asserts/GdUnitVectorAssertImplTest.gd @@ -0,0 +1,367 @@ +# GdUnit generated TestSuite +class_name GdUnitVectorAssertImplTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd' + + +var _test_seta =[ + [null], + [Vector2.ONE], + [Vector2i.ONE], + [Vector3.ONE], + [Vector3i.ONE], + [Vector4.ONE], + [Vector4i.ONE], +] + + +@warning_ignore("unused_parameter") +func test_supported_types(value, test_parameters = _test_seta): + assert_object(assert_vector(value))\ + .is_not_null()\ + .is_instanceof(GdUnitVectorAssert) + + +@warning_ignore("unused_parameter") +func test_unsupported_types(value, details :String, test_parameters =[ + [true, 'bool'], + [42, 'int'], + [42.0, 'float'], + ['foo', 'String'], +] ): + assert_failure(func(): assert_vector(value))\ + .is_failed()\ + .has_message("GdUnitVectorAssert error, the type <%s> is not supported." % details) + + +@warning_ignore("unused_parameter") +func test_is_null(value, test_parameters = _test_seta): + if value == null: + assert_vector(null).is_null() + else: + assert_failure(func(): assert_vector(value).is_null()) \ + .is_failed() \ + .starts_with_message("Expecting: '' but was '%s'" % value) + + +@warning_ignore("unused_parameter") +func test_is_not_null(value, test_parameters = _test_seta): + if value == null: + assert_failure(func(): assert_vector(null).is_not_null()) \ + .is_failed() \ + .has_message("Expecting: not to be ''") + else: + assert_vector(value).is_not_null() + + +@warning_ignore("unused_parameter") +func test_is_equal() -> void: + assert_vector(Vector2.ONE).is_equal(Vector2.ONE) + assert_vector(Vector2.LEFT).is_equal(Vector2.LEFT) + assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001)) + + # is not equal + assert_failure(func(): assert_vector(Vector2.ONE).is_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting:\n '(1.2, 1.000001)'\n but was\n '(1, 1)'") + # is null + assert_failure(func(): assert_vector(null).is_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting:\n '(1.2, 1.000001)'\n but was\n ''") + # comparing different vector types + assert_failure(func(): assert_vector(Vector2.ONE).is_equal(Vector3.ONE)) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_equal_over_all_types(value, test_parameters = _test_seta) -> void: + assert_vector(value).is_equal(value) + + +func test_is_not_equal() -> void: + assert_vector(null).is_not_equal(Vector2.LEFT) + assert_vector(Vector2.ONE).is_not_equal(Vector2.LEFT) + assert_vector(Vector2.LEFT).is_not_equal(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_not_equal(Vector2(1.2, 1.000002)) + + assert_failure(func(): assert_vector(Vector2(1.2, 1.000001)).is_not_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting:\n '(1.2, 1.000001)'\n not equal to\n '(1.2, 1.000001)'") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000001)).is_not_equal(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_not_equal_over_all_types(value, test_parameters = _test_seta) -> void: + var expected = Vector2.LEFT if value == null else value * 2 + assert_vector(value).is_not_equal(expected) + + +func test_is_equal_approx() -> void: + assert_vector(Vector2.ONE).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004)) + assert_vector(Vector2(0.996, 0.996)).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004)) + assert_vector(Vector2(1.004, 1.004)).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004)) + + assert_failure(func(): assert_vector(Vector2(1.005, 1)).is_equal_approx(Vector2.ONE, Vector2(0.004, 0.004))) \ + .is_failed() \ + .has_message("Expecting:\n '(1.005, 1)'\n in range between\n '(0.996, 0.996)' <> '(1.004, 1.004)'") + assert_failure(func(): assert_vector(Vector2(1, 0.995)).is_equal_approx(Vector2.ONE, Vector2(0, 0.004))) \ + .is_failed() \ + .has_message("Expecting:\n '(1, 0.995)'\n in range between\n '(1, 0.996)' <> '(1, 1.004)'") + assert_failure(func(): assert_vector(null).is_equal_approx(Vector2.ONE, Vector2(0, 0.004))) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '(1, 0.996)' <> '(1, 1.004)'") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000001)).is_equal_approx(Vector3.ONE, Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + assert_failure(func(): assert_vector(Vector2(0.878431, 0.505882)).is_equal_approx(Vector2(0.878431, 0.105882), Vector2(0.000001, 0.000001))) \ + .is_failed() \ + .has_message(""" + Expecting: + '(0.878431, 0.505882)' + in range between + '(0.87843, 0.105881)' <> '(0.878432, 0.105883)'""" + .dedent().trim_prefix("\n") + ) + assert_failure(func(): assert_vector(Vector3(0.0, 0.878431, 0.505882)).is_equal_approx(Vector3(0.0, 0.878431, 0.105882), Vector3(0.000001, 0.000001, 0.000001))) \ + .is_failed() \ + .has_message(""" + Expecting: + '(0, 0.878431, 0.505882)' + in range between + '(-0.000001, 0.87843, 0.105881)' <> '(0.000001, 0.878432, 0.105883)'""" + .dedent().trim_prefix("\n") + ) + + +@warning_ignore("unused_parameter") +func test_is_equal_approx_over_all_types(value, expected, approx, test_parameters = [ + [Vector2(0.996, 1.004), Vector2.ONE, Vector2(0.004, 0.004)], + [Vector2i(9, 11), Vector2i(10, 10), Vector2i(1, 1)], + [Vector3(0.996, 0.996, 1.004), Vector3.ONE, Vector3(0.004, 0.004, 0.004)], + [Vector3i(10, 9, 11), Vector3i(10, 10, 10), Vector3i(1, 1, 1)], + [Vector4(0.996, 0.996, 1.004, 1.004), Vector4.ONE, Vector4(0.004, 0.004, 0.004, 0.004)], + [Vector4i(10, 9, 11, 9), Vector4i(10, 10, 10, 10), Vector4i(1, 1, 1, 1)] +]) -> void: + assert_vector(value).is_equal_approx(expected, approx) + + +func test_is_less() -> void: + assert_vector(Vector2.LEFT).is_less(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_less(Vector2(1.2, 1.000002)) + + assert_failure(func(): assert_vector(Vector2.ONE).is_less(Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '(1, 1)' but was '(1, 1)'") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000001)).is_less(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '(1.2, 1.000001)' but was '(1.2, 1.000001)'") + assert_failure(func(): assert_vector(null).is_less(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than:\n '(1.2, 1.000001)' but was ''") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000001)).is_less(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_less_over_all_types(value, expected, test_parameters = [ + [Vector2(1.0, 1.0), Vector2(1.0001, 1.0001)], + [Vector2i(1, 1), Vector2i(2, 1)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0001, 1.0001, 1.0)], + [Vector3i(1, 1, 1), Vector3i(2, 1, 1)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0001, 1.0001, 1.0, 1.0)], + [Vector4i(1, 1, 1, 1), Vector4i(2, 1, 1, 1)], +]) -> void: + assert_vector(value).is_less(expected) + + +func test_is_less_equal() -> void: + assert_vector(Vector2.ONE).is_less_equal(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_less_equal(Vector2(1.2, 1.000001)) + assert_vector(Vector2(1.2, 1.000001)).is_less_equal(Vector2(1.2, 1.000002)) + + assert_failure(func(): assert_vector(Vector2.ONE).is_less_equal(Vector2.ZERO)) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '(0, 0)' but was '(1, 1)'") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000002)).is_less_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '(1.2, 1.000001)' but was '(1.2, 1.000002)'") + assert_failure(func(): assert_vector(null).is_less_equal(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be less than or equal:\n '(1.2, 1.000001)' but was ''") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000002)).is_less_equal(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_less_equal_over_all_types(value, expected, test_parameters = [ + [Vector2(1.0, 1.0), Vector2(1.0001, 1.0001)], + [Vector2(1.0, 1.0), Vector2(1.0, 1.0)], + [Vector2i(1, 1), Vector2i(2, 1)], + [Vector2i(1, 1), Vector2i(1, 1)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0001, 1.0001, 1.0)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3i(1, 1, 1), Vector3i(2, 1, 1)], + [Vector3i(1, 1, 1), Vector3i(1, 1, 1)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0001, 1.0001, 1.0, 1.0)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4i(1, 1, 1, 1), Vector4i(2, 1, 1, 1)], + [Vector4i(1, 1, 1, 1), Vector4i(1, 1, 1, 1)], +]) -> void: + assert_vector(value).is_less_equal(expected) + + +func test_is_greater() -> void: + assert_vector(Vector2.ONE).is_greater(Vector2.RIGHT) + assert_vector(Vector2(1.2, 1.000002)).is_greater(Vector2(1.2, 1.000001)) + + assert_failure(func(): assert_vector(Vector2.ZERO).is_greater(Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '(1, 1)' but was '(0, 0)'") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000001)).is_greater(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '(1.2, 1.000001)' but was '(1.2, 1.000001)'") + assert_failure(func(): assert_vector(null).is_greater(Vector2(1.2, 1.000001))) \ + .is_failed() \ + .has_message("Expecting to be greater than:\n '(1.2, 1.000001)' but was ''") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000001)).is_greater(Vector3(1.2, 1.000001, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_greater_over_all_types(value, expected, test_parameters = [ + [Vector2(1.0001, 1.0001), Vector2(1.0, 1.0)], + [Vector2i(2, 1), Vector2i(1, 1)], + [Vector3(1.0001, 1.0001, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3i(2, 1, 1), Vector3i(1, 1, 1)], + [Vector4(1.0001, 1.0001, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4i(2, 1, 1, 1), Vector4i(1, 1, 1, 1)], +]) -> void: + assert_vector(value).is_greater(expected) + + +func test_is_greater_equal() -> void: + assert_vector(Vector2.ONE*2).is_greater_equal(Vector2.ONE) + assert_vector(Vector2.ONE).is_greater_equal(Vector2.ONE) + assert_vector(Vector2(1.2, 1.000001)).is_greater_equal(Vector2(1.2, 1.000001)) + assert_vector(Vector2(1.2, 1.000002)).is_greater_equal(Vector2(1.2, 1.000001)) + + assert_failure(func(): assert_vector(Vector2.ZERO).is_greater_equal(Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '(1, 1)' but was '(0, 0)'") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000002)).is_greater_equal(Vector2(1.2, 1.000003))) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '(1.2, 1.000003)' but was '(1.2, 1.000002)'") + assert_failure(func(): assert_vector(null).is_greater_equal(Vector2(1.2, 1.000003))) \ + .is_failed() \ + .has_message("Expecting to be greater than or equal:\n '(1.2, 1.000003)' but was ''") + assert_failure(func(): assert_vector(Vector2(1.2, 1.000002)).is_greater_equal(Vector3(1.2, 1.000003, 1.0))) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_greater_equal_over_all_types(value, expected, test_parameters = [ + [Vector2(1.0001, 1.0001), Vector2(1.0, 1.0)], + [Vector2(1.0, 1.0), Vector2(1.0, 1.0)], + [Vector2i(2, 1), Vector2i(1, 1)], + [Vector2i(1, 1), Vector2i(1, 1)], + [Vector3(1.0001, 1.0001, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3(1.0, 1.0, 1.0), Vector3(1.0, 1.0, 1.0)], + [Vector3i(2, 1, 1), Vector3i(1, 1, 1)], + [Vector3i(1, 1, 1), Vector3i(1, 1, 1)], + [Vector4(1.0001, 1.0001, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.0, 1.0, 1.0, 1.0)], + [Vector4i(2, 1, 1, 1), Vector4i(1, 1, 1, 1)], + [Vector4i(1, 1, 1, 1), Vector4i(1, 1, 1, 1)], +]) -> void: + assert_vector(value).is_greater_equal(expected) + + +func test_is_between(fuzzer = Fuzzers.rangev2(Vector2.ZERO, Vector2.ONE)): + var value :Vector2 = fuzzer.next_value() + assert_vector(value).is_between(Vector2.ZERO, Vector2.ONE) + + assert_failure(func(): assert_vector(Vector2(1, 1.00001)).is_between(Vector2.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting:\n '(1, 1.00001)'\n in range between\n '(0, 0)' <> '(1, 1)'") + assert_failure(func(): assert_vector(null).is_between(Vector2.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting:\n ''\n in range between\n '(0, 0)' <> '(1, 1)'") + assert_failure(func(): assert_vector(Vector2(1, 1.00001)).is_between(Vector2.ZERO, Vector3.ONE)) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_between_over_all_types(value, from, to, test_parameters = [ + [Vector2(1.2, 1.2), Vector2(1.0, 1.0), Vector2(1.2, 1.2)], + [Vector2i(1, 1), Vector2i(1, 1), Vector2i(2, 2)], + [Vector3(1.2, 1.2, 1.2), Vector3(1.0, 1.0, 1.0), Vector3(1.2, 1.2, 1.2)], + [Vector3i(1, 1, 1), Vector3i(1, 1, 1), Vector3i(2, 2, 2)], + [Vector4(1.2, 1.2, 1.2, 1.2), Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.2, 1.2, 1.2, 1.2)], + [Vector4i(1, 1, 1, 1), Vector4i(1, 1, 1, 1), Vector4i(2, 2, 2, 2)], +]) -> void: + assert_vector(value).is_between(from, to) + + +func test_is_not_between(fuzzer = Fuzzers.rangev2(Vector2.ONE, Vector2.ONE*2)): + var value :Vector2 = fuzzer.next_value() + assert_vector(null).is_not_between(Vector2.ZERO, Vector2.ONE) + assert_vector(value).is_not_between(Vector2.ZERO, Vector2.ONE) + + assert_failure(func(): assert_vector(Vector2.ONE).is_not_between(Vector2.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Expecting:\n '(1, 1)'\n not in range between\n '(0, 0)' <> '(1, 1)'") + assert_failure(func(): assert_vector(Vector2.ONE).is_not_between(Vector3.ZERO, Vector2.ONE)) \ + .is_failed() \ + .has_message("Unexpected type comparison:\n Expecting type 'Vector2' but is 'Vector3'") + + +@warning_ignore("unused_parameter") +func test_is_not_between_over_all_types(value, from, to, test_parameters = [ + [Vector2(3.2, 1.2), Vector2(1.0, 1.0), Vector2(1.2, 1.2)], + [Vector2i(3, 1), Vector2i(1, 1), Vector2i(2, 2)], + [Vector3(3.2, 1.2, 1.2), Vector3(1.0, 1.0, 1.0), Vector3(1.2, 1.2, 1.2)], + [Vector3i(3, 1, 1), Vector3i(1, 1, 1), Vector3i(2, 2, 2)], + [Vector4(3.2, 1.2, 1.2, 1.2), Vector4(1.0, 1.0, 1.0, 1.0), Vector4(1.2, 1.2, 1.2, 1.2)], + [Vector4i(3, 1, 1, 1), Vector4i(1, 1, 1, 1), Vector4i(2, 2, 2, 2)], +]) -> void: + assert_vector(value).is_not_between(from, to) + + +func test_override_failure_message() -> void: + assert_failure(func(): assert_vector(Vector2.ONE) \ + .override_failure_message("Custom failure message") \ + .is_null()) \ + .is_failed() \ + .has_message("Custom failure message") + + +# tests if an assert fails the 'is_failure' reflects the failure status +func test_is_failure() -> void: + # initial is false + assert_bool(is_failure()).is_false() + + # checked success assert + assert_vector(null).is_null() + assert_bool(is_failure()).is_false() + + # checked faild assert + assert_failure(func(): assert_vector(RefCounted.new()).is_null()) \ + .is_failed() + assert_bool(is_failure()).is_true() + + # checked next success assert + assert_vector(null).is_null() + # is true because we have an already failed assert + assert_bool(is_failure()).is_true() diff --git a/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd b/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd new file mode 100644 index 0000000..bc2207b --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdArgumentParserTest.gd @@ -0,0 +1,120 @@ +# GdUnit generated TestSuite +class_name CmdArgumentParserTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdArgumentParser.gd' + +var option_a := CmdOption.new("-a", "some help text a", "some description a") +var option_f := CmdOption.new("-f, --foo", "some help text foo", "some description foo") +var option_b := CmdOption.new("-b, --bar", "-b ", "comand with required argument", TYPE_STRING) +var option_c := CmdOption.new("-c, --calc", "-c [value]", "command with optional argument", TYPE_STRING, true) +var option_x := CmdOption.new("-x", "some help text x", "some description x") + +var _cmd_options :CmdOptions + + +func before(): + # setup command options + _cmd_options = CmdOptions.new([ + option_a, + option_f, + option_b, + option_c, + ], + # advnaced options + [ + option_x, + ]) + + +func test_parse_success(): + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + assert_result(parser.parse([])).is_empty() + # check with godot cmd argumnents before tool argument + assert_result(parser.parse(["-d", "dir/dir/CmdTool.gd"])).is_empty() + + # if valid argument set than don't show the help by default + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + ]) + + +func test_parse_success_required_arg(): + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-a", "-b", "valueA", "-b", "valueB"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + CmdCommand.new("-b", ["valueA", "valueB"]), + ]) + + # useing command long term + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-a", "--bar", "value"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + CmdCommand.new("-b", ["value"]) + ]) + + +func test_parse_success_optional_arg(): + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + # without argument + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c"), + CmdCommand.new("-a") + ]) + + # without argument at end + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-a", "-c"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-a"), + CmdCommand.new("-c") + ]) + + # with argument + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "argument", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c", ["argument"]), + CmdCommand.new("-a") + ]) + + +func test_parse_success_repead_cmd_args(): + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + # without argument + var result := parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "argument", "-a"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c", ["argument"]), + CmdCommand.new("-a") + ]) + + # with repeading commands argument + result = parser.parse(["-d", "dir/dir/CmdTool.gd", "-c", "argument1", "-a", "-c", "argument2", "-c", "argument3"]) + assert_result(result).is_success() + assert_array(result.value()).contains_exactly([ + CmdCommand.new("-c", ["argument1", "argument2", "argument3"]), + CmdCommand.new("-a") + ]) + + +func test_parse_error(): + var parser := CmdArgumentParser.new(_cmd_options, "CmdTool.gd") + + assert_result(parser.parse([])).is_empty() + + # if invalid arguemens set than return with error and show the help by default + assert_result(parser.parse(["-d", "dir/dir/CmdTool.gd", "-unknown"])).is_error()\ + .contains_message("Unknown '-unknown' command!") diff --git a/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd b/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd new file mode 100644 index 0000000..4630309 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdCommandHandlerTest.gd @@ -0,0 +1,123 @@ +# GdUnit generated TestSuite +class_name CmdCommandHandlerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdCommandHandler.gd' + +var _cmd_options: CmdOptions +var _cmd_instance: TestCommands + + +# small example of command class +class TestCommands: + func cmd_a() -> String: + return "cmd_a" + + func cmd_foo() -> String: + return "cmd_foo" + + func cmd_bar(value :String) -> String: + return value + + func cmd_bar2(value_a: String, value_b: String) -> Array: + return [value_a, value_b] + + func cmd_x() -> String: + return "cmd_x" + + +func before() -> void: + # setup command options + _cmd_options = CmdOptions.new([ + CmdOption.new("-a", "some help text a", "some description a"), + CmdOption.new("-f, --foo", "some help text foo", "some description foo"), + CmdOption.new("-b, --bar", "some help text bar", "some description bar"), + CmdOption.new("-b2, --bar2", "some help text bar", "some description bar"), + ], + # advnaced options + [ + CmdOption.new("-x", "some help text x", "some description x"), + ]) + _cmd_instance = TestCommands.new() + + +func test__validate_no_registerd_commands() -> void: + var cmd_handler := CmdCommandHandler.new(_cmd_options) + + assert_result(cmd_handler._validate()).is_success() + + +func test__validate_registerd_commands() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", Callable(_cmd_instance, "cmd_a")) + cmd_handler.register_cb("-f", Callable(_cmd_instance, "cmd_foo")) + cmd_handler.register_cb("-b", Callable(_cmd_instance, "cmd_bar")) + + assert_result(cmd_handler._validate()).is_success() + + +func test__validate_registerd_unknown_commands() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", Callable(_cmd_instance, "cmd_a")) + cmd_handler.register_cb("-d", Callable(_cmd_instance, "cmd_foo")) + cmd_handler.register_cb("-b", Callable(_cmd_instance, "cmd_bar")) + cmd_handler.register_cb("-y", Callable(_cmd_instance, "cmd_x")) + + assert_result(cmd_handler._validate())\ + .is_error()\ + .contains_message("The command '-d' is unknown, verify your CmdOptions!\nThe command '-y' is unknown, verify your CmdOptions!") + + +func test__validate_registerd_invalid_callbacks() -> void: + var cmd_handler := CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", Callable(_cmd_instance, "cmd_a")) + cmd_handler.register_cb("-f") + cmd_handler.register_cb("-b", Callable(_cmd_instance, "cmd_not_exists")) + + assert_result(cmd_handler._validate())\ + .is_error()\ + .contains_message("Invalid function reference for command '-b', Check the function reference!") + + +func test__validate_registerd_register_same_callback_twice() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + cmd_handler.register_cb("-a", Callable(_cmd_instance, "cmd_a")) + cmd_handler.register_cb("-b", Callable(_cmd_instance, "cmd_a")) + if cmd_handler._enhanced_fr_test: + assert_result(cmd_handler._validate())\ + .is_error()\ + .contains_message("The function reference 'cmd_a' already registerd for command '-a'!") + + +func test_execute_no_commands() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + assert_result(cmd_handler.execute([])).is_success() + + +func test_execute_commands_no_cb_registered() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + assert_result(cmd_handler.execute([CmdCommand.new("-a")])).is_success() + + +func test_execute_commands_with_cb_registered() -> void: + var cmd_handler: = CmdCommandHandler.new(_cmd_options) + var cmd_spy = spy(_cmd_instance) + + cmd_handler.register_cb("-a", Callable(cmd_spy, "cmd_a")) + cmd_handler.register_cb("-b", Callable(cmd_spy, "cmd_bar")) + cmd_handler.register_cbv("-b2", Callable(cmd_spy, "cmd_bar2")) + + assert_result(cmd_handler.execute([CmdCommand.new("-a")])).is_success() + verify(cmd_spy).cmd_a() + verify_no_more_interactions(cmd_spy) + + reset(cmd_spy) + assert_result(cmd_handler.execute([ + CmdCommand.new("-a"), + CmdCommand.new("-b", ["some_value"]), + CmdCommand.new("-b2", ["value1", "value2"])])).is_success() + verify(cmd_spy).cmd_a() + verify(cmd_spy).cmd_bar("some_value") + verify(cmd_spy).cmd_bar2("value1", "value2") + verify_no_more_interactions(cmd_spy) diff --git a/addons/gdUnit4/test/cmd/CmdCommandTest.gd b/addons/gdUnit4/test/cmd/CmdCommandTest.gd new file mode 100644 index 0000000..32cfaa6 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdCommandTest.gd @@ -0,0 +1,32 @@ +# GdUnit generated TestSuite +class_name CmdCommandTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdCommand.gd' + + +func test_create(): + var cmd_a := CmdCommand.new("cmd_a") + assert_str(cmd_a.name()).is_equal("cmd_a") + assert_array(cmd_a.arguments()).is_empty() + + var cmd_b := CmdCommand.new("cmd_b", ["arg1"]) + assert_str(cmd_b.name()).is_equal("cmd_b") + assert_array(cmd_b.arguments()).contains_exactly(["arg1"]) + + assert_object(cmd_a).is_not_equal(cmd_b) + + +func test_add_argument(): + var cmd_a := CmdCommand.new("cmd_a") + cmd_a.add_argument("arg1") + cmd_a.add_argument("arg2") + assert_str(cmd_a.name()).is_equal("cmd_a") + assert_array(cmd_a.arguments()).contains_exactly(["arg1", "arg2"]) + + var cmd_b := CmdCommand.new("cmd_b", ["arg1"]) + cmd_b.add_argument("arg2") + cmd_b.add_argument("arg3") + assert_str(cmd_b.name()).is_equal("cmd_b") + assert_array(cmd_b.arguments()).contains_exactly(["arg1", "arg2", "arg3"]) diff --git a/addons/gdUnit4/test/cmd/CmdConsoleTest.gd b/addons/gdUnit4/test/cmd/CmdConsoleTest.gd new file mode 100644 index 0000000..3fdb150 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdConsoleTest.gd @@ -0,0 +1,85 @@ +# GdUnit generated TestSuite +class_name CmdConsoleTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdConsole.gd' + + +func test_print_color_default() -> void: + var console :CmdConsole = spy(CmdConsole.new()) + + console.print_color("test message", Color.RED) + verify(console).color(Color.RED) + verify(console).end_color() + verify(console).printl("test message") + verify(console).bold(false) + verify(console).italic(false) + verify(console).underline(false) + verify(console, 0).new_line() + + reset(console) + console.print_color("test message2", Color.BLUE) + verify(console).color(Color.BLUE) + verify(console).end_color() + verify(console).printl("test message2") + verify(console).bold(false) + verify(console).italic(false) + verify(console).underline(false) + verify(console, 0).new_line() + + +func test_print_color_with_flags() -> void: + var console :CmdConsole = spy(CmdConsole.new()) + + # bold + console.print_color("test message", Color.RED, CmdConsole.BOLD) + verify(console).bold(true) + verify(console).italic(false) + verify(console).underline(false) + reset(console) + + # italic + console.print_color("test message", Color.RED, CmdConsole.ITALIC) + verify(console).bold(false) + verify(console).italic(true) + verify(console).underline(false) + reset(console) + + # underline + console.print_color("test message", Color.RED, CmdConsole.UNDERLINE) + verify(console).bold(false) + verify(console).italic(false) + verify(console).underline(true) + reset(console) + + # combile italic & underline + console.print_color("test message", Color.RED, CmdConsole.ITALIC|CmdConsole.UNDERLINE) + verify(console).bold(false) + verify(console).italic(true) + verify(console).underline(true) + reset(console) + + # combile bold & italic + console.print_color("test message", Color.RED, CmdConsole.BOLD|CmdConsole.ITALIC) + verify(console).bold(true) + verify(console).italic(true) + verify(console).underline(false) + reset(console) + + # combile all + console.print_color("test message", Color.RED, CmdConsole.BOLD|CmdConsole.ITALIC|CmdConsole.UNDERLINE) + verify(console).bold(true) + verify(console).italic(true) + verify(console).underline(true) + reset(console) + + +func test_prints_color() -> void: + var console :CmdConsole = spy(CmdConsole.new()) + + console.prints_color("test message", Color.RED, CmdConsole.BOLD|CmdConsole.ITALIC) + # verify prints delegates to print_color + verify(console).print_color("test message", Color.RED, CmdConsole.BOLD|CmdConsole.ITALIC) + # and adds a new line + verify(console).new_line() diff --git a/addons/gdUnit4/test/cmd/CmdOptionTest.gd b/addons/gdUnit4/test/cmd/CmdOptionTest.gd new file mode 100644 index 0000000..f9fcf83 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdOptionTest.gd @@ -0,0 +1,51 @@ +# GdUnit generated TestSuite +class_name CmdOptionTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdOption.gd' + + +func test_commands(): + assert_array(CmdOption.new("-a", "help a", "describe a").commands())\ + .contains_exactly(["-a"]) + assert_array(CmdOption.new("-a, --aaa", "help a", "describe a").commands())\ + .contains_exactly(["-a", "--aaa"]) + # containing space or tabs + assert_array(CmdOption.new("-b , --bb ", "help a", "describe a")\ + .commands()).contains_exactly(["-b", "--bb"]) + + +func test_short_command(): + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").short_command()).is_equal("-a") + + +func test_help(): + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").help()).is_equal("help a") + + +func test_description(): + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").description()).is_equal("describe a") + + +func test_type(): + assert_int(CmdOption.new("-a", "", "").type()).is_equal(TYPE_NIL) + assert_int(CmdOption.new("-a", "", "", TYPE_STRING).type()).is_equal(TYPE_STRING) + assert_int(CmdOption.new("-a", "", "", TYPE_BOOL).type()).is_equal(TYPE_BOOL) + + +func test_is_argument_optional(): + assert_bool(CmdOption.new("-a", "", "").is_argument_optional()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_BOOL, false).is_argument_optional()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_BOOL, true).is_argument_optional()).is_true() + + +func test_has_argument(): + assert_bool(CmdOption.new("-a", "", "").has_argument()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_NIL).has_argument()).is_false() + assert_bool(CmdOption.new("-a", "", "", TYPE_BOOL).has_argument()).is_true() + + +func test_describe(): + assert_str(CmdOption.new("-a, --aaa", "help a", "describe a").describe())\ + .is_equal(' ["-a", "--aaa"] describe a \n help a\n') diff --git a/addons/gdUnit4/test/cmd/CmdOptionsTest.gd b/addons/gdUnit4/test/cmd/CmdOptionsTest.gd new file mode 100644 index 0000000..911b2e2 --- /dev/null +++ b/addons/gdUnit4/test/cmd/CmdOptionsTest.gd @@ -0,0 +1,57 @@ +# GdUnit generated TestSuite +class_name CmdOptionsTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/cmd/CmdOptions.gd' + + +var option_a := CmdOption.new("-a", "some help text a", "some description a") +var option_f := CmdOption.new("-f, --foo", "some help text foo", "some description foo") +var option_b := CmdOption.new("-b, --bar", "some help text bar", "some description bar") +var option_x := CmdOption.new("-x", "some help text x", "some description x") + +var _cmd_options :CmdOptions + + +func before(): + # setup command options + _cmd_options = CmdOptions.new([ + option_a, + option_f, + option_b, + ], + # advnaced options + [ + option_x, + ]) + + +func test_get_option(): + assert_object(_cmd_options.get_option("-a")).is_same(option_a) + assert_object(_cmd_options.get_option("-f")).is_same(option_f) + assert_object(_cmd_options.get_option("--foo")).is_same(option_f) + assert_object(_cmd_options.get_option("-b")).is_same(option_b) + assert_object(_cmd_options.get_option("--bar")).is_same(option_b) + assert_object(_cmd_options.get_option("-x")).is_same(option_x) + # for not existsing command + assert_object(_cmd_options.get_option("-z")).is_null() + + +func test_default_options(): + assert_array(_cmd_options.default_options()).contains_exactly([ + option_a, + option_f, + option_b]) + + +func test_advanced_options(): + assert_array(_cmd_options.advanced_options()).contains_exactly([option_x]) + + +func test_options(): + assert_array(_cmd_options.options()).contains_exactly([ + option_a, + option_f, + option_b, + option_x]) diff --git a/addons/gdUnit4/test/core/ExampleTestSuite.cs b/addons/gdUnit4/test/core/ExampleTestSuite.cs new file mode 100644 index 0000000..17b591a --- /dev/null +++ b/addons/gdUnit4/test/core/ExampleTestSuite.cs @@ -0,0 +1,21 @@ +namespace GdUnit4.Tests.Resource +{ + using static Assertions; + + [TestSuite] + public partial class ExampleTestSuiteA + { + + [TestCase] + public void TestCase1() + { + AssertBool(true).IsEqual(true); + } + + [TestCase] + public void TestCase2() + { + AssertBool(false).IsEqual(false); + } + } +} diff --git a/addons/gdUnit4/test/core/GdArrayToolsTest.gd b/addons/gdUnit4/test/core/GdArrayToolsTest.gd new file mode 100644 index 0000000..350527f --- /dev/null +++ b/addons/gdUnit4/test/core/GdArrayToolsTest.gd @@ -0,0 +1,154 @@ +# GdUnit generated TestSuite +class_name GdArrayToolsTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdArrayTools.gd' + + +@warning_ignore('unused_parameter') +func test_as_string(_test :String, value, expected :String, test_parameters = [ + ['Array', Array([1, 2]), '[1, 2]'], + ['Array', Array([1.0, 2.212]), '[1.000000, 2.212000]'], + ['Array', Array([true, false]), '[true, false]'], + ['Array', Array(["1", "2"]), '["1", "2"]'], + ['Array', Array([Vector2.ZERO, Vector2.LEFT]), '[Vector2(), Vector2(-1, 0)]'], + ['Array', Array([Vector3.ZERO, Vector3.LEFT]), '[Vector3(), Vector3(-1, 0, 0)]'], + ['Array', Array([Color.RED, Color.GREEN]), '[Color(1, 0, 0, 1), Color(0, 1, 0, 1)]'], + ['ArrayInt', Array([1, 2]) as Array[int], '[1, 2]'], + ['ArrayFloat', Array([1.0, 2.212]) as Array[float], '[1.000000, 2.212000]'], + ['ArrayBool', Array([true, false]) as Array[bool], '[true, false]'], + ['ArrayString', Array(["1", "2"]) as Array[String], '["1", "2"]'], + ['ArrayVector2', Array([Vector2.ZERO, Vector2.LEFT]) as Array[Vector2], '[Vector2(), Vector2(-1, 0)]'], + ['ArrayVector2i', Array([Vector2i.ZERO, Vector2i.LEFT]) as Array[Vector2i], '[Vector2i(), Vector2i(-1, 0)]'], + ['ArrayVector3', Array([Vector3.ZERO, Vector3.LEFT]) as Array[Vector3], '[Vector3(), Vector3(-1, 0, 0)]'], + ['ArrayVector3i', Array([Vector3i.ZERO, Vector3i.LEFT]) as Array[Vector3i], '[Vector3i(), Vector3i(-1, 0, 0)]'], + ['ArrayVector4', Array([Vector4.ZERO, Vector4.ONE]) as Array[Vector4], '[Vector4(), Vector4(1, 1, 1, 1)]'], + ['ArrayVector4i', Array([Vector4i.ZERO, Vector4i.ONE]) as Array[Vector4i], '[Vector4i(), Vector4i(1, 1, 1, 1)]'], + ['ArrayColor', Array([Color.RED, Color.GREEN]) as Array[Color], '[Color(1, 0, 0, 1), Color(0, 1, 0, 1)]'], + ['PackedByteArray', PackedByteArray([1, 2]), 'PackedByteArray[1, 2]'], + ['PackedInt32Array', PackedInt32Array([1, 2]), 'PackedInt32Array[1, 2]'], + ['PackedInt64Array', PackedInt64Array([1, 2]), 'PackedInt64Array[1, 2]'], + ['PackedFloat32Array', PackedFloat32Array([1, 2.212]), 'PackedFloat32Array[1.000000, 2.212000]'], + ['PackedFloat64Array', PackedFloat64Array([1, 2.212]), 'PackedFloat64Array[1.000000, 2.212000]'], + ['PackedStringArray', PackedStringArray([1, 2]), 'PackedStringArray["1", "2"]'], + ['PackedVector2Array', PackedVector2Array([Vector2.ZERO, Vector2.LEFT]), 'PackedVector2Array[Vector2(), Vector2(-1, 0)]'], + ['PackedVector3Array', PackedVector3Array([Vector3.ZERO, Vector3.LEFT]), 'PackedVector3Array[Vector3(), Vector3(-1, 0, 0)]'], + ['PackedColorArray', PackedColorArray([Color.RED, Color.GREEN]), 'PackedColorArray[Color(1, 0, 0, 1), Color(0, 1, 0, 1)]'], +]) -> void: + + assert_that(GdArrayTools.as_string(value)).is_equal(expected) + + +func test_as_string_simple_format(): + var value := PackedStringArray(["a", "b"]) + prints(GdArrayTools.as_string(value, false)) + assert_that(GdArrayTools.as_string(value, false)).is_equal('[a, b]') + + +@warning_ignore("unused_parameter") +func test_is_array_type(_test :String, value, expected :bool, test_parameters = [ + ['bool', true, false], + ['int', 42, false], + ['float', 1.21, false], + ['String', "abc", false], + ['Dictionary', {}, false], + ['RefCounted', RefCounted.new(), false], + ['Array', Array([1, 2]), true], + ['Array', Array([1.0, 2.212]), true], + ['Array', Array([true, false]), true], + ['Array', Array(["1", "2"]), true], + ['Array', Array([Vector2.ZERO, Vector2.LEFT]), true], + ['Array', Array([Vector3.ZERO, Vector3.LEFT]), true], + ['Array', Array([Color.RED, Color.GREEN]), true], + ['ArrayInt', Array([1, 2]) as Array[int], true], + ['ArrayFloat', Array([1.0, 2.212]) as Array[float], true], + ['ArrayBool', Array([true, false]) as Array[bool], true], + ['ArrayString', Array(["1", "2"]) as Array[String], true], + ['ArrayVector2', Array([Vector2.ZERO, Vector2.LEFT]) as Array[Vector2], true], + ['ArrayVector2i', Array([Vector2i.ZERO, Vector2i.LEFT]) as Array[Vector2i], true], + ['ArrayVector3', Array([Vector3.ZERO, Vector3.LEFT]) as Array[Vector3], true], + ['ArrayVector3i', Array([Vector3i.ZERO, Vector3i.LEFT]) as Array[Vector3i], true], + ['ArrayVector4', Array([Vector4.ZERO, Vector4.ONE]) as Array[Vector4], true], + ['ArrayVector4i', Array([Vector4i.ZERO, Vector4i.ONE]) as Array[Vector4i], true], + ['ArrayColor', Array([Color.RED, Color.GREEN]) as Array[Color], true], + ['PackedByteArray', PackedByteArray([1, 2]), true], + ['PackedInt32Array', PackedInt32Array([1, 2]), true], + ['PackedInt64Array', PackedInt64Array([1, 2]), true], + ['PackedFloat32Array', PackedFloat32Array([1, 2.212]), true], + ['PackedFloat64Array', PackedFloat64Array([1, 2.212]), true], + ['PackedStringArray', PackedStringArray([1, 2]), true], + ['PackedVector2Array', PackedVector2Array([Vector2.ZERO, Vector2.LEFT]), true], + ['PackedVector3Array', PackedVector3Array([Vector3.ZERO, Vector3.LEFT]), true], + ['PackedColorArray', PackedColorArray([Color.RED, Color.GREEN]), true], +]) -> void: + + assert_that(GdArrayTools.is_array_type(value)).is_equal(expected) + + +func test_is_type_array() -> void: + for type in [TYPE_NIL, TYPE_MAX]: + if type in [TYPE_ARRAY, TYPE_PACKED_COLOR_ARRAY]: + assert_that(GdArrayTools.is_type_array(type)).is_true() + else: + assert_that(GdArrayTools.is_type_array(type)).is_false() + + +@warning_ignore("unused_parameter") +func test_filter_value(value, expected_type :int, test_parameters = [ + [[1, 2, 3, 1], TYPE_ARRAY], + [Array([1, 2, 3, 1]) as Array[int], TYPE_ARRAY], + [PackedByteArray([1, 2, 3, 1]), TYPE_PACKED_BYTE_ARRAY], + [PackedInt32Array([1, 2, 3, 1]), TYPE_PACKED_INT32_ARRAY], + [PackedInt64Array([1, 2, 3, 1]), TYPE_PACKED_INT64_ARRAY], + [PackedFloat32Array([1.0, 2, 1.1, 1.0]), TYPE_PACKED_FLOAT32_ARRAY], + [PackedFloat64Array([1.0, 2, 1.1, 1.0]), TYPE_PACKED_FLOAT64_ARRAY], + [PackedStringArray(["1", "2", "3", "1"]), TYPE_PACKED_STRING_ARRAY], + [PackedVector2Array([Vector2.ZERO, Vector2.ONE, Vector2.DOWN, Vector2.ZERO]), TYPE_PACKED_VECTOR2_ARRAY], + [PackedVector3Array([Vector3.ZERO, Vector3.ONE, Vector3.DOWN, Vector3.ZERO]), TYPE_PACKED_VECTOR3_ARRAY], + [PackedColorArray([Color.RED, Color.GREEN, Color.BLUE, Color.RED]), TYPE_PACKED_COLOR_ARRAY] + ]) -> void: + + var value_to_remove = value[0] + var result :Variant = GdArrayTools.filter_value(value, value_to_remove) + assert_array(result).not_contains([value_to_remove]).has_size(2) + assert_that(typeof(result)).is_equal(expected_type) + + +func test_filter_value_() -> void: + assert_array(GdArrayTools.filter_value([], null)).is_empty() + assert_array(GdArrayTools.filter_value([], "")).is_empty() + + var current :Array = [null, "a", "b", null, "c", null] + var filtered :Variant= GdArrayTools.filter_value(current, null) + assert_array(filtered).contains_exactly(["a", "b", "c"]) + # verify the source is not affected + assert_array(current).contains_exactly([null, "a", "b", null, "c", null]) + + current = [null, "a", "xxx", null, "xx", null] + filtered = GdArrayTools.filter_value(current, "xxx") + assert_array(filtered).contains_exactly([null, "a", null, "xx", null]) + # verify the source is not affected + assert_array(current).contains_exactly([null, "a", "xxx", null, "xx", null]) + + +func test_erase_value() -> void: + var current := [] + GdArrayTools.erase_value(current, null) + assert_array(current).is_empty() + + current = [null] + GdArrayTools.erase_value(current, null) + assert_array(current).is_empty() + + current = [null, "a", "b", null, "c", null] + GdArrayTools.erase_value(current, null) + # verify the source is affected + assert_array(current).contains_exactly(["a", "b", "c"]) + + +func test_scan_typed() -> void: + assert_that(GdArrayTools.scan_typed([1, 2, 3])).is_equal(TYPE_INT) + assert_that(GdArrayTools.scan_typed([1, 2.2, 3])).is_equal(GdObjects.TYPE_VARIANT) diff --git a/addons/gdUnit4/test/core/GdDiffToolTest.gd b/addons/gdUnit4/test/core/GdDiffToolTest.gd new file mode 100644 index 0000000..5b138fc --- /dev/null +++ b/addons/gdUnit4/test/core/GdDiffToolTest.gd @@ -0,0 +1,48 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdDiffToolTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdDiffTool.gd' + + +func test_string_diff_empty(): + var diffs := GdDiffTool.string_diff("", "") + assert_array(diffs).has_size(2) + assert_array(diffs[0]).is_empty() + assert_array(diffs[1]).is_empty() + + +func test_string_diff_equals(): + var diffs := GdDiffTool.string_diff("Abc", "Abc") + var expected_l_diff := "Abc".to_ascii_buffer() + var expected_r_diff := "Abc".to_ascii_buffer() + + assert_array(diffs).has_size(2) + assert_array(diffs[0]).contains_exactly(expected_l_diff) + assert_array(diffs[1]).contains_exactly(expected_r_diff) + + +func test_string_diff(): + # tests the result of string diff function like assert_str("Abc").is_equal("abc") + var diffs := GdDiffTool.string_diff("Abc", "abc") + var chars := "Aabc".to_ascii_buffer() + var ord_A := chars[0] + var ord_a := chars[1] + var ord_b := chars[2] + var ord_c := chars[3] + var expected_l_diff := PackedByteArray([GdDiffTool.DIV_SUB, ord_A, GdDiffTool.DIV_ADD, ord_a, ord_b, ord_c]) + var expected_r_diff := PackedByteArray([GdDiffTool.DIV_ADD, ord_A, GdDiffTool.DIV_SUB, ord_a, ord_b, ord_c]) + + assert_array(diffs).has_size(2) + assert_array(diffs[0]).contains_exactly(expected_l_diff) + assert_array(diffs[1]).contains_exactly(expected_r_diff) + + +@warning_ignore("unused_parameter") +func test_string_diff_large_value(fuzzer := Fuzzers.rand_str(1000, 4000), fuzzer_iterations = 10): + # test diff with large values not crashes the API GD-100 + var value :String = fuzzer.next_value() + GdDiffTool.string_diff(value, value) diff --git a/addons/gdUnit4/test/core/GdObjectsTest.gd b/addons/gdUnit4/test/core/GdObjectsTest.gd new file mode 100644 index 0000000..c93ef19 --- /dev/null +++ b/addons/gdUnit4/test/core/GdObjectsTest.gd @@ -0,0 +1,512 @@ +extends GdUnitTestSuite + + +func test_equals_string(): + var a := "" + var b := "" + var c := "abc" + var d := "abC" + + assert_bool(GdObjects.equals("", "")).is_true() + assert_bool(GdObjects.equals(a, "")).is_true() + assert_bool(GdObjects.equals("", a)).is_true() + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(c, c)).is_true() + assert_bool(GdObjects.equals(c, String(c))).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals("", c)).is_false() + assert_bool(GdObjects.equals(c, "")).is_false() + assert_bool(GdObjects.equals(c, d)).is_false() + assert_bool(GdObjects.equals(d, c)).is_false() + # against diverent type + assert_bool(GdObjects.equals(d, Array())).is_false() + assert_bool(GdObjects.equals(d, Dictionary())).is_false() + assert_bool(GdObjects.equals(d, Vector2.ONE)).is_false() + assert_bool(GdObjects.equals(d, Vector3.ONE)).is_false() + + +func test_equals_stringname(): + assert_bool(GdObjects.equals("", &"")).is_true() + assert_bool(GdObjects.equals("abc", &"abc")).is_true() + assert_bool(GdObjects.equals("abc", &"abC")).is_false() + + +func test_equals_array(): + var a := [] + var b := [] + var c := Array() + var d := [1,2,3,4,5] + var e := [1,2,3,4,5] + var x := [1,2,3,6,4,5] + + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(a, c)).is_true() + assert_bool(GdObjects.equals(c, b)).is_true() + assert_bool(GdObjects.equals(d, d)).is_true() + assert_bool(GdObjects.equals(d, e)).is_true() + assert_bool(GdObjects.equals(e, d)).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals(a, d)).is_false() + assert_bool(GdObjects.equals(d, a)).is_false() + assert_bool(GdObjects.equals(d, x)).is_false() + assert_bool(GdObjects.equals(x, d)).is_false() + # against diverent type + assert_bool(GdObjects.equals(a, "")).is_false() + assert_bool(GdObjects.equals(a, Dictionary())).is_false() + assert_bool(GdObjects.equals(a, Vector2.ONE)).is_false() + assert_bool(GdObjects.equals(a, Vector3.ONE)).is_false() + + +func test_equals_dictionary(): + var a := {} + var b := {} + var c := {"a":"foo"} + var d := {"a":"foo"} + var e1 := {"a":"foo", "b":"bar"} + var e2 := {"b":"bar", "a":"foo"} + + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(c, c)).is_true() + assert_bool(GdObjects.equals(c, d)).is_true() + assert_bool(GdObjects.equals(e1, e2)).is_true() + assert_bool(GdObjects.equals(e2, e1)).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals(a, c)).is_false() + assert_bool(GdObjects.equals(c, a)).is_false() + assert_bool(GdObjects.equals(a, e1)).is_false() + assert_bool(GdObjects.equals(e1, a)).is_false() + assert_bool(GdObjects.equals(c, e1)).is_false() + assert_bool(GdObjects.equals(e1, c)).is_false() + + +class TestClass extends Resource: + + enum { + A, + B + } + + var _a:int + var _b:String + var _c:Array + + func _init(a:int = 0,b:String = "",c:Array = []): + _a = a + _b = b + _c = c + + +func test_equals_class(): + var a := TestClass.new() + var b := TestClass.new() + var c := TestClass.new(1, "foo", ["bar", "xxx"]) + var d := TestClass.new(1, "foo", ["bar", "xxx"]) + var x := TestClass.new(1, "foo", ["bar", "xsxx"]) + + assert_bool(GdObjects.equals(a, a)).is_true() + assert_bool(GdObjects.equals(a, b)).is_true() + assert_bool(GdObjects.equals(b, a)).is_true() + assert_bool(GdObjects.equals(c, d)).is_true() + assert_bool(GdObjects.equals(d, c)).is_true() + + assert_bool(GdObjects.equals(a, null)).is_false() + assert_bool(GdObjects.equals(null, a)).is_false() + assert_bool(GdObjects.equals(a, c)).is_false() + assert_bool(GdObjects.equals(c, a)).is_false() + assert_bool(GdObjects.equals(d, x)).is_false() + assert_bool(GdObjects.equals(x, d)).is_false() + + +func test_equals_with_stack_deep(): + # more extended version + var x2 := TestClass.new(1, "foo", [TestClass.new(22, "foo"), TestClass.new(22, "foo")]) + var x3 := TestClass.new(1, "foo", [TestClass.new(22, "foo"), TestClass.new(23, "foo")]) + assert_bool(GdObjects.equals(x2, x3)).is_false() + + +func test_equals_Node_with_deep_check(): + var nodeA = auto_free(Node.new()) + var nodeB = auto_free(Node.new()) + + # compares by default with deep parameter ckeck + assert_bool(GdObjects.equals(nodeA, nodeA)).is_true() + assert_bool(GdObjects.equals(nodeB, nodeB)).is_true() + assert_bool(GdObjects.equals(nodeA, nodeB)).is_true() + assert_bool(GdObjects.equals(nodeB, nodeA)).is_true() + # compares by object reference + assert_bool(GdObjects.equals(nodeA, nodeA, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_true() + assert_bool(GdObjects.equals(nodeB, nodeB, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_true() + assert_bool(GdObjects.equals(nodeA, nodeB, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_false() + assert_bool(GdObjects.equals(nodeB, nodeA, false, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)).is_false() + + +func test_is_primitive_type(): + assert_bool(GdObjects.is_primitive_type(false)).is_true() + assert_bool(GdObjects.is_primitive_type(true)).is_true() + assert_bool(GdObjects.is_primitive_type(0)).is_true() + assert_bool(GdObjects.is_primitive_type(0.1)).is_true() + assert_bool(GdObjects.is_primitive_type("")).is_true() + assert_bool(GdObjects.is_primitive_type(Vector2.ONE)).is_false() + + +class TestClassForIsType: + var x + + +func test_is_type(): + # check build-in types + assert_bool(GdObjects.is_type(1)).is_false() + assert_bool(GdObjects.is_type(1.3)).is_false() + assert_bool(GdObjects.is_type(true)).is_false() + assert_bool(GdObjects.is_type(false)).is_false() + assert_bool(GdObjects.is_type([])).is_false() + assert_bool(GdObjects.is_type("abc")).is_false() + + assert_bool(GdObjects.is_type(null)).is_false() + # an object type + assert_bool(GdObjects.is_type(Node)).is_true() + # an reference type + assert_bool(GdObjects.is_type(AStar3D)).is_true() + # an script type + assert_bool(GdObjects.is_type(GDScript)).is_true() + # an custom type + assert_bool(GdObjects.is_type(TestClassForIsType)).is_true() + # checked inner class type + assert_bool(GdObjects.is_type(CustomClass.InnerClassA)).is_true() + assert_bool(GdObjects.is_type(CustomClass.InnerClassC)).is_true() + + # for instances must allways endup with false + assert_bool(GdObjects.is_type(auto_free(Node.new()))).is_false() + assert_bool(GdObjects.is_type(AStar3D.new())).is_false() + assert_bool(GdObjects.is_type(Dictionary())).is_false() + assert_bool(GdObjects.is_type(PackedColorArray())).is_false() + assert_bool(GdObjects.is_type(GDScript.new())).is_false() + assert_bool(GdObjects.is_type(TestClassForIsType.new())).is_false() + assert_bool(GdObjects.is_type(auto_free(CustomClass.InnerClassC.new()))).is_false() + + +func test_is_singleton() -> void: + for singleton_name in Engine.get_singleton_list(): + var singleton = Engine.get_singleton(singleton_name) + assert_bool(GdObjects.is_singleton(singleton)) \ + .override_failure_message("Expect to a singleton: '%s' Instance: %s, Class: %s" % [singleton_name, singleton, singleton.get_class()]) \ + .is_true() + # false tests + assert_bool(GdObjects.is_singleton(10)).is_false() + assert_bool(GdObjects.is_singleton(true)).is_false() + assert_bool(GdObjects.is_singleton(Node)).is_false() + assert_bool(GdObjects.is_singleton(auto_free(Node.new()))).is_false() + + +func _is_instance(value) -> bool: + return GdObjects.is_instance(auto_free(value)) + + +func test_is_instance_true(): + assert_bool(_is_instance(RefCounted.new())).is_true() + assert_bool(_is_instance(Node.new())).is_true() + assert_bool(_is_instance(AStar3D.new())).is_true() + assert_bool(_is_instance(PackedScene.new())).is_true() + assert_bool(_is_instance(GDScript.new())).is_true() + assert_bool(_is_instance(Person.new())).is_true() + assert_bool(_is_instance(CustomClass.new())).is_true() + assert_bool(_is_instance(CustomNodeTestClass.new())).is_true() + assert_bool(_is_instance(TestClassForIsType.new())).is_true() + assert_bool(_is_instance(CustomClass.InnerClassC.new())).is_true() + + +func test_is_instance_false(): + assert_bool(_is_instance(RefCounted)).is_false() + assert_bool(_is_instance(Node)).is_false() + assert_bool(_is_instance(AStar3D)).is_false() + assert_bool(_is_instance(PackedScene)).is_false() + assert_bool(_is_instance(GDScript)).is_false() + assert_bool(_is_instance(Dictionary())).is_false() + assert_bool(_is_instance(PackedColorArray())).is_false() + assert_bool(_is_instance(Person)).is_false() + assert_bool(_is_instance(CustomClass)).is_false() + assert_bool(_is_instance(CustomNodeTestClass)).is_false() + assert_bool(_is_instance(TestClassForIsType)).is_false() + assert_bool(_is_instance(CustomClass.InnerClassC)).is_false() + + +# shorter helper func to extract class name and using auto_free +func extract_class_name(value) -> GdUnitResult: + return GdObjects.extract_class_name(auto_free(value)) + + +func test_get_class_name_from_class_path(): + # extract class name by resoure path + assert_result(extract_class_name("res://addons/gdUnit4/test/resources/core/Person.gd"))\ + .is_success().is_value("Person") + assert_result(extract_class_name("res://addons/gdUnit4/test/resources/core/CustomClass.gd"))\ + .is_success().is_value("CustomClass") + assert_result(extract_class_name("res://addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd"))\ + .is_success().is_value("CustomNodeTestClass") + assert_result(extract_class_name("res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd"))\ + .is_success().is_value("CustomResourceTestClass") + assert_result(extract_class_name("res://addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd"))\ + .is_success().is_value("OverridenGetClassTestClass") + + +func test_get_class_name_from_snake_case_class_path(): + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd"))\ + .is_success().is_value("SnakeCaseWithClassName") + # without class_name + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd"))\ + .is_success().is_value("SnakeCaseWithoutClassName") + + +func test_get_class_name_from_pascal_case_class_path(): + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd"))\ + .is_success().is_value("PascalCaseWithClassName") + # without class_name + assert_result(extract_class_name("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd"))\ + .is_success().is_value("PascalCaseWithoutClassName") + + +func test_get_class_name_from_type(): + assert_result(extract_class_name(Animation)).is_success().is_value("Animation") + assert_result(extract_class_name(GDScript)).is_success().is_value("GDScript") + assert_result(extract_class_name(Camera3D)).is_success().is_value("Camera3D") + assert_result(extract_class_name(Node)).is_success().is_value("Node") + assert_result(extract_class_name(Tree)).is_success().is_value("Tree") + # extract class name from custom classes + assert_result(extract_class_name(Person)).is_success().is_value("Person") + assert_result(extract_class_name(CustomClass)).is_success().is_value("CustomClass") + assert_result(extract_class_name(CustomNodeTestClass)).is_success().is_value("CustomNodeTestClass") + assert_result(extract_class_name(CustomResourceTestClass)).is_success().is_value("CustomResourceTestClass") + assert_result(extract_class_name(OverridenGetClassTestClass)).is_success().is_value("OverridenGetClassTestClass") + assert_result(extract_class_name(AdvancedTestClass)).is_success().is_value("AdvancedTestClass") + + +func test_get_class_name_from_inner_class(): + assert_result(extract_class_name(CustomClass))\ + .is_success().is_value("CustomClass") + assert_result(extract_class_name(CustomClass.InnerClassA))\ + .is_success().is_value("CustomClass.InnerClassA") + assert_result(extract_class_name(CustomClass.InnerClassB))\ + .is_success().is_value("CustomClass.InnerClassB") + assert_result(extract_class_name(CustomClass.InnerClassC))\ + .is_success().is_value("CustomClass.InnerClassC") + assert_result(extract_class_name(CustomClass.InnerClassD))\ + .is_success().is_value("CustomClass.InnerClassD") + assert_result(extract_class_name(AdvancedTestClass.SoundData))\ + .is_success().is_value("AdvancedTestClass.SoundData") + assert_result(extract_class_name(AdvancedTestClass.AtmosphereData))\ + .is_success().is_value("AdvancedTestClass.AtmosphereData") + assert_result(extract_class_name(AdvancedTestClass.Area4D))\ + .is_success().is_value("AdvancedTestClass.Area4D") + + +func test_extract_class_name_from_instance(): + assert_result(extract_class_name(Camera3D.new())).is_equal("Camera3D") + assert_result(extract_class_name(GDScript.new())).is_equal("GDScript") + assert_result(extract_class_name(Node.new())).is_equal("Node") + + # extract class name from custom classes + assert_result(extract_class_name(Person.new())).is_equal("Person") + assert_result(extract_class_name(ClassWithNameA.new())).is_equal("ClassWithNameA") + assert_result(extract_class_name(ClassWithNameB.new())).is_equal("ClassWithNameB") + var classWithoutNameA = load("res://addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd") + assert_result(extract_class_name(classWithoutNameA.new())).is_equal("ClassWithoutNameA") + assert_result(extract_class_name(CustomNodeTestClass.new())).is_equal("CustomNodeTestClass") + assert_result(extract_class_name(CustomResourceTestClass.new())).is_equal("CustomResourceTestClass") + assert_result(extract_class_name(OverridenGetClassTestClass.new())).is_equal("OverridenGetClassTestClass") + assert_result(extract_class_name(AdvancedTestClass.new())).is_equal("AdvancedTestClass") + # extract inner class name + assert_result(extract_class_name(AdvancedTestClass.SoundData.new())).is_equal("AdvancedTestClass.SoundData") + assert_result(extract_class_name(AdvancedTestClass.AtmosphereData.new())).is_equal("AdvancedTestClass.AtmosphereData") + assert_result(extract_class_name(AdvancedTestClass.Area4D.new(0))).is_equal("AdvancedTestClass.Area4D") + assert_result(extract_class_name(CustomClass.InnerClassC.new())).is_equal("CustomClass.InnerClassC") + + +# verify enigne class names are not converted by configured naming convention +@warning_ignore("unused_parameter") +func test_extract_class_name_from_class_path(fuzzer=GodotClassNameFuzzer.new(true, true), fuzzer_iterations = 100) -> void: + var clazz_name :String = fuzzer.next_value() + assert_str(GdObjects.extract_class_name_from_class_path(PackedStringArray([clazz_name]))).is_equal(clazz_name) + + +@warning_ignore("unused_parameter") +func test_extract_class_name_godot_classes(fuzzer=GodotClassNameFuzzer.new(true, true), fuzzer_iterations = 100): + var extract_class_name_ := fuzzer.next_value() as String + var instance :Variant = ClassDB.instantiate(extract_class_name_) + assert_result(extract_class_name(instance)).is_equal(extract_class_name_) + + +func test_extract_class_path_by_clazz(): + # engine classes has no class path + assert_array(GdObjects.extract_class_path(Animation)).is_empty() + assert_array(GdObjects.extract_class_path(GDScript)).is_empty() + assert_array(GdObjects.extract_class_path(Camera3D)).is_empty() + assert_array(GdObjects.extract_class_path(Tree)).is_empty() + assert_array(GdObjects.extract_class_path(Node)).is_empty() + + # script classes + assert_array(GdObjects.extract_class_path(Person))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/Person.gd"]) + assert_array(GdObjects.extract_class_path(CustomClass))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd"]) + assert_array(GdObjects.extract_class_path(CustomNodeTestClass))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd"]) + assert_array(GdObjects.extract_class_path(CustomResourceTestClass))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd"]) + assert_array(GdObjects.extract_class_path(OverridenGetClassTestClass))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd"]) + + # script inner classes + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassA))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassA"]) + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassB))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassB"]) + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassC))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassC"]) + assert_array(GdObjects.extract_class_path(AdvancedTestClass.SoundData))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd", "SoundData"]) + assert_array(GdObjects.extract_class_path(AdvancedTestClass.AtmosphereData))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd", "AtmosphereData"]) + assert_array(GdObjects.extract_class_path(AdvancedTestClass.Area4D))\ + .contains_exactly(["res://addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd", "Area4D"]) + + # inner inner class + assert_array(GdObjects.extract_class_path(CustomClass.InnerClassD.InnerInnerClassA))\ + .contains_exactly(["res://addons/gdUnit4/test/resources/core/CustomClass.gd", "InnerClassD", "InnerInnerClassA"]) + + +#func __test_can_instantiate(): +# assert_bool(GdObjects.can_instantiate(GDScript)).is_true() +# assert_bool(GdObjects.can_instantiate(Node)).is_true() +# assert_bool(GdObjects.can_instantiate(Tree)).is_true() +# assert_bool(GdObjects.can_instantiate(Camera3D)).is_true() +# assert_bool(GdObjects.can_instantiate(Person)).is_true() +# assert_bool(GdObjects.can_instantiate(CustomClass.InnerClassA)).is_true() +# assert_bool(GdObjects.can_instantiate(TreeItem)).is_true() +# +# creates a test instance by given class name or resource path +# instances created with auto free +func create_instance(clazz): + var result := GdObjects.create_instance(clazz) + if result.is_success(): + return auto_free(result.value()) + return null + + +func test_create_instance_by_class_name(): + # instance of engine classes + assert_object(create_instance(Node))\ + .is_not_null()\ + .is_instanceof(Node) + assert_object(create_instance(Camera3D))\ + .is_not_null()\ + .is_instanceof(Camera3D) + # instance of custom classes + assert_object(create_instance(Person))\ + .is_not_null()\ + .is_instanceof(Person) + # instance of inner classes + assert_object(create_instance(CustomClass.InnerClassA))\ + .is_not_null()\ + .is_instanceof(CustomClass.InnerClassA) + + +func test_extract_class_name_on_null_value(): + # we can't extract class name from a null value + assert_result(GdObjects.extract_class_name(null))\ + .is_error()\ + .contains_message("Can't extract class name form a null value.") + + +func test_is_public_script_class() -> void: + # snake case format class names + assert_bool(GdObjects.is_public_script_class("ScriptWithClassName")).is_true() + assert_bool(GdObjects.is_public_script_class("script_without_class_name")).is_false() + assert_bool(GdObjects.is_public_script_class("CustomClass")).is_true() + # inner classes not listed as public classes + assert_bool(GdObjects.is_public_script_class("CustomClass.InnerClassA")).is_false() + + +func test_is_instance_scene() -> void: + # checked none scene objects + assert_bool(GdObjects.is_instance_scene(RefCounted.new())).is_false() + assert_bool(GdObjects.is_instance_scene(CustomClass.new())).is_false() + assert_bool(GdObjects.is_instance_scene(auto_free(Control.new()))).is_false() + + # now check checked a loaded scene + var resource = load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_bool(GdObjects.is_instance_scene(resource)).is_false() + # checked a instance of a scene + assert_bool(GdObjects.is_instance_scene(auto_free(resource.instantiate()))).is_true() + + +func test_is_scene_resource_path() -> void: + assert_bool(GdObjects.is_scene_resource_path(RefCounted.new())).is_false() + assert_bool(GdObjects.is_scene_resource_path(CustomClass.new())).is_false() + assert_bool(GdObjects.is_scene_resource_path(auto_free(Control.new()))).is_false() + + # check checked a loaded scene + var resource = load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_bool(GdObjects.is_scene_resource_path(resource)).is_false() + # checked resource path + assert_bool(GdObjects.is_scene_resource_path("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn")).is_true() + + +func test_extract_class_functions() -> void: + var functions := GdObjects.extract_class_functions("Resource", [""]) + for f in functions: + if f["name"] == "get_path": + assert_str(GdFunctionDescriptor.extract_from(f)._to_string()).is_equal("[Line:-1] func get_path() -> String:") + + functions = GdObjects.extract_class_functions("CustomResourceTestClass", ["res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd"]) + for f in functions: + if f["name"] == "get_path": + assert_str(GdFunctionDescriptor.extract_from(f)._to_string()).is_equal("[Line:-1] func get_path() -> String:") + + +func test_all_types() -> void: + var expected_types :Array[int] = [] + for type_index in TYPE_MAX: + expected_types.append(type_index) + expected_types.append(GdObjects.TYPE_VOID) + expected_types.append(GdObjects.TYPE_VARARG) + expected_types.append(GdObjects.TYPE_FUNC) + expected_types.append(GdObjects.TYPE_FUZZER) + expected_types.append(GdObjects.TYPE_VARIANT) + assert_array(GdObjects.all_types()).contains_exactly_in_any_order(expected_types) + + +func test_to_camel_case() -> void: + assert_str(GdObjects.to_camel_case("MyClassName")).is_equal("myClassName") + assert_str(GdObjects.to_camel_case("my_class_name")).is_equal("myClassName") + assert_str(GdObjects.to_camel_case("myClassName")).is_equal("myClassName") + + +func test_to_pascal_case() -> void: + assert_str(GdObjects.to_pascal_case("MyClassName")).is_equal("MyClassName") + assert_str(GdObjects.to_pascal_case("my_class_name")).is_equal("MyClassName") + assert_str(GdObjects.to_pascal_case("myClassName")).is_equal("MyClassName") + + +func test_to_snake_case() -> void: + assert_str(GdObjects.to_snake_case("MyClassName")).is_equal("my_class_name") + assert_str(GdObjects.to_snake_case("my_class_name")).is_equal("my_class_name") + assert_str(GdObjects.to_snake_case("myClassName")).is_equal("my_class_name") + + +func test_is_snake_case() -> void: + assert_bool(GdObjects.is_snake_case("my_class_name")).is_true() + assert_bool(GdObjects.is_snake_case("myclassname")).is_true() + assert_bool(GdObjects.is_snake_case("MyClassName")).is_false() + assert_bool(GdObjects.is_snake_case("my_class_nameTest")).is_false() diff --git a/addons/gdUnit4/test/core/GdUnit4VersionTest.gd b/addons/gdUnit4/test/core/GdUnit4VersionTest.gd new file mode 100644 index 0000000..94a9c12 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnit4VersionTest.gd @@ -0,0 +1,67 @@ +# GdUnit generated TestSuite +class_name GdUnit4VersionTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/.gd' + + +func test_parse() -> void: + var expected := GdUnit4Version.new(0, 9, 1) + assert_object(GdUnit4Version.parse("v0.9.1-rc")).is_equal(expected) + assert_object(GdUnit4Version.parse("v0.9.1RC")).is_equal(expected) + assert_object(GdUnit4Version.parse("0.9.1 rc")).is_equal(expected) + assert_object(GdUnit4Version.parse("0.9.1")).is_equal(expected) + + +func test_equals() -> void: + var version := GdUnit4Version.new(0, 9, 1) + assert_bool(version.equals(version)).is_true() + assert_bool(version.equals(GdUnit4Version.new(0, 9, 1))).is_true() + assert_bool(GdUnit4Version.new(0, 9, 1).equals(version)).is_true() + + assert_bool(GdUnit4Version.new(0, 9, 2).equals(version)).is_false() + assert_bool(GdUnit4Version.new(0, 8, 1).equals(version)).is_false() + assert_bool(GdUnit4Version.new(1, 9, 1).equals(version)).is_false() + + +func test_to_string() -> void: + var version := GdUnit4Version.new(0, 9, 1) + assert_str(str(version)).is_equal("v0.9.1") + assert_str("%s" % version).is_equal("v0.9.1") + + +@warning_ignore("unused_parameter") +func test_is_greater_major(fuzzer_major := Fuzzers.rangei(1, 20), fuzzer_minor := Fuzzers.rangei(0, 20), fuzzer_patch := Fuzzers.rangei(0, 20), fuzzer_iterations = 500) -> void: + var version := GdUnit4Version.new(0, 9, 1) + var current := GdUnit4Version.new(fuzzer_major.next_value(), fuzzer_minor.next_value(), fuzzer_patch.next_value()); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is greater then %s" % [current, version])\ + .is_true() + + +@warning_ignore("unused_parameter") +func test_is_not_greater_major(fuzzer_major := Fuzzers.rangei(1, 10), fuzzer_minor := Fuzzers.rangei(0, 20), fuzzer_patch := Fuzzers.rangei(0, 20), fuzzer_iterations = 500) -> void: + var version := GdUnit4Version.new(11, 0, 0) + var current := GdUnit4Version.new(fuzzer_major.next_value(), fuzzer_minor.next_value(), fuzzer_patch.next_value()); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is not greater then %s" % [current, version])\ + .is_false() + + +@warning_ignore("unused_parameter") +func test_is_greater_minor(fuzzer_minor := Fuzzers.rangei(3, 20), fuzzer_patch := Fuzzers.rangei(0, 20), fuzzer_iterations = 500) -> void: + var version := GdUnit4Version.new(0, 2, 1) + var current := GdUnit4Version.new(0, fuzzer_minor.next_value(), fuzzer_patch.next_value()); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is greater then %s" % [current, version])\ + .is_true() + + +@warning_ignore("unused_parameter") +func test_is_greater_patch(fuzzer_patch := Fuzzers.rangei(1, 20), fuzzer_iterations = 500) -> void: + var version := GdUnit4Version.new(0, 2, 0) + var current := GdUnit4Version.new(0, 2, fuzzer_patch.next_value()); + assert_bool(current.is_greater(version))\ + .override_failure_message("Expect %s is greater then %s" % [current, version])\ + .is_true() diff --git a/addons/gdUnit4/test/core/GdUnitClassDoublerTest.gd b/addons/gdUnit4/test/core/GdUnitClassDoublerTest.gd new file mode 100644 index 0000000..21c266f --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitClassDoublerTest.gd @@ -0,0 +1,13 @@ +# GdUnit generated TestSuite +class_name GdUnitClassDoublerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitClassDoubler.gd' + +# simple function doubler whitout any modifications +class TestFunctionDoubler extends GdFunctionDoubler: + func double(_func_descriptor :GdFunctionDescriptor) -> PackedStringArray: + return PackedStringArray([]) + + diff --git a/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd b/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd new file mode 100644 index 0000000..d160394 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitFileAccessTest.gd @@ -0,0 +1,231 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitFileAccess.gd' + + +var file_to_save :String + + +func after() -> void: + # verify tmp files are deleted automatically + assert_bool(FileAccess.file_exists(file_to_save)).is_false() + + +func _create_file(p_path :String, p_name :String) -> void: + var file := create_temp_file(p_path, p_name) + file.store_string("some content") + + +func test_copy_directory() -> void: + var temp_dir := create_temp_dir("test_copy_directory") + assert_bool(GdUnitFileAccess.copy_directory("res://addons/gdUnit4/test/core/resources/copy_test/folder_a/", temp_dir)).is_true() + assert_file("%s/file_a.txt" % temp_dir).exists() + assert_file("%s/file_b.txt" % temp_dir).exists() + + +func test_copy_directory_recursive() -> void: + var temp_dir := create_temp_dir("test_copy_directory_recursive") + assert_bool(GdUnitFileAccess.copy_directory("res://addons/gdUnit4/test/core/resources/copy_test/", temp_dir, true)).is_true() + assert_file("%s/folder_a/file_a.txt" % temp_dir).exists() + assert_file("%s/folder_a/file_b.txt" % temp_dir).exists() + assert_file("%s/folder_b/file_a.txt" % temp_dir).exists() + assert_file("%s/folder_b/file_b.txt" % temp_dir).exists() + assert_file("%s/folder_b/folder_ba/file_x.txt" % temp_dir).exists() + assert_file("%s/folder_c/file_z.txt" % temp_dir).exists() + + +func test_create_temp_dir() -> void: + var temp_dir := create_temp_dir("examples/game/save") + file_to_save = temp_dir + "/save_game.dat" + + var data := { + 'user': "Hoschi", + 'level': 42 + } + var file := FileAccess.open(file_to_save, FileAccess.WRITE) + file.store_line(JSON.stringify(data)) + assert_bool(FileAccess.file_exists(file_to_save)).is_true() + + +func test_create_temp_file() -> void: + # setup - stores a tmp file with "user://tmp/examples/game/game.sav" (auto closed) + var file := create_temp_file("examples/game", "game.sav") + assert_object(file).is_not_null() + # write some example data + file.store_line("some data") + file.close() + + # verify + var file_read := create_temp_file("examples/game", "game.sav", FileAccess.READ) + assert_object(file_read).is_not_null() + assert_str(file_read.get_as_text()).is_equal("some data\n") + # not needs to be manually close, will be auto closed after test suite execution + + +func test_make_qualified_path() -> void: + assert_str(GdUnitFileAccess.make_qualified_path("MyTest")).is_equal("MyTest") + assert_str(GdUnitFileAccess.make_qualified_path("/MyTest.gd")).is_equal("res://MyTest.gd") + assert_str(GdUnitFileAccess.make_qualified_path("/foo/bar/MyTest.gd")).is_equal("res://foo/bar/MyTest.gd") + assert_str(GdUnitFileAccess.make_qualified_path("res://MyTest.gd")).is_equal("res://MyTest.gd") + assert_str(GdUnitFileAccess.make_qualified_path("res://foo/bar/MyTest.gd")).is_equal("res://foo/bar/MyTest.gd") + + +func test_find_last_path_index() -> void: + # not existing directory + assert_int(GdUnitFileAccess.find_last_path_index("/foo", "report_")).is_equal(0) + # empty directory + var temp_dir := create_temp_dir("test_reports") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(0) + # create some report directories + create_temp_dir("test_reports/report_1") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(1) + create_temp_dir("test_reports/report_2") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(2) + create_temp_dir("test_reports/report_3") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(3) + create_temp_dir("test_reports/report_5") + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(5) + # create some more + for index in range(10, 42): + create_temp_dir("test_reports/report_%d" % index) + assert_int(GdUnitFileAccess.find_last_path_index(temp_dir, "report_")).is_equal(41) + + +func test_delete_path_index_lower_equals_than() -> void: + var temp_dir := create_temp_dir("test_reports_delete") + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).is_empty() + assert_int(GdUnitFileAccess.delete_path_index_lower_equals_than(temp_dir, "report_", 0)).is_equal(0) + + # create some directories + for index in range(10, 42): + create_temp_dir("test_reports_delete/report_%d" % index) + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).has_size(32) + + # try to delete directories with index lower than 0, shold delete nothing + assert_int(GdUnitFileAccess.delete_path_index_lower_equals_than(temp_dir, "report_", 0)).is_equal(0) + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).has_size(32) + + # try to delete directories with index lower_equals than 30 + # shold delet directories report_10 to report_30 = 21 + assert_int(GdUnitFileAccess.delete_path_index_lower_equals_than(temp_dir, "report_", 30)).is_equal(21) + # and 12 directories are left + assert_array(GdUnitFileAccess.scan_dir(temp_dir))\ + .has_size(11)\ + .contains([ + "report_31", + "report_32", + "report_33", + "report_34", + "report_35", + "report_36", + "report_37", + "report_38", + "report_39", + "report_40", + "report_41", + ]) + + +func test_scan_dir() -> void: + var temp_dir := create_temp_dir("test_scan_dir") + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).is_empty() + + create_temp_dir("test_scan_dir/report_2") + assert_array(GdUnitFileAccess.scan_dir(temp_dir)).contains_exactly(["report_2"]) + # create some more directories and files + create_temp_dir("test_scan_dir/report_4") + create_temp_dir("test_scan_dir/report_5") + create_temp_dir("test_scan_dir/report_6") + create_temp_file("test_scan_dir", "file_a") + create_temp_file("test_scan_dir", "file_b") + # this shoul not be counted it is a file in a subdirectory + create_temp_file("test_scan_dir/report_6", "file_b") + assert_array(GdUnitFileAccess.scan_dir(temp_dir))\ + .has_size(6)\ + .contains([ + "report_2", + "report_4", + "report_5", + "report_6", + "file_a", + "file_b"]) + + +func test_delete_directory() -> void: + var tmp_dir := create_temp_dir("test_delete_dir") + create_temp_dir("test_delete_dir/data1") + create_temp_dir("test_delete_dir/data2") + _create_file("test_delete_dir", "example_a.txt") + _create_file("test_delete_dir", "example_b.txt") + _create_file("test_delete_dir/data1", "example.txt") + _create_file("test_delete_dir/data2", "example2.txt") + + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).contains_exactly_in_any_order([ + "data1", + "data2", + "example_a.txt", + "example_b.txt" + ]) + + # Delete the entire directory and its contents + GdUnitFileAccess.delete_directory(tmp_dir) + assert_bool(DirAccess.dir_exists_absolute(tmp_dir)).is_false() + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).is_empty() + + +func test_delete_directory_content_only() -> void: + var tmp_dir := create_temp_dir("test_delete_dir") + create_temp_dir("test_delete_dir/data1") + create_temp_dir("test_delete_dir/data2") + _create_file("test_delete_dir", "example_a.txt") + _create_file("test_delete_dir", "example_b.txt") + _create_file("test_delete_dir/data1", "example.txt") + _create_file("test_delete_dir/data2", "example2.txt") + + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).contains_exactly_in_any_order([ + "data1", + "data2", + "example_a.txt", + "example_b.txt" + ]) + + # Delete the entire directory and its contents + GdUnitFileAccess.delete_directory(tmp_dir, true) + assert_bool(DirAccess.dir_exists_absolute(tmp_dir)).is_true() + assert_array(GdUnitFileAccess.scan_dir(tmp_dir)).is_empty() + + +func test_extract_package() -> void: + clean_temp_dir() + var tmp_path := GdUnitFileAccess.create_temp_dir("test_update") + var source := "res://addons/gdUnit4/test/update/resources/update.zip" + + # the temp should be inital empty + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).is_empty() + # now extract to temp + var result := GdUnitFileAccess.extract_zip(source, tmp_path) + assert_result(result).is_success() + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).contains_exactly_in_any_order([ + "addons", + "runtest.cmd", + "runtest.sh", + ]) + + +func test_extract_package_invalid_package() -> void: + clean_temp_dir() + var tmp_path := GdUnitFileAccess.create_temp_dir("test_update") + var source := "res://addons/gdUnit4/test/update/resources/update_invalid.zip" + + # the temp should be inital empty + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).is_empty() + # now extract to temp + var result := GdUnitFileAccess.extract_zip(source, tmp_path) + assert_result(result).is_error()\ + .contains_message("Extracting `%s` failed! Please collect the error log and report this. Error Code: 1" % source) + assert_array(GdUnitFileAccess.scan_dir(tmp_path)).is_empty() + diff --git a/addons/gdUnit4/test/core/GdUnitObjectInteractionsTemplateTest.gd b/addons/gdUnit4/test/core/GdUnitObjectInteractionsTemplateTest.gd new file mode 100644 index 0000000..55950fe --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitObjectInteractionsTemplateTest.gd @@ -0,0 +1,36 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd' + + +func test___filter_vargs(): + var template = load(__source).new() + + var varags :Array = [ + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE] + assert_array(template.__filter_vargs(varags)).is_empty() + + var object := RefCounted.new() + varags = [ + "foo", + "bar", + null, + true, + 1, + object, + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE] + assert_array(template.__filter_vargs(varags)).contains_exactly([ + "foo", + "bar", + null, + true, + 1, + object]) diff --git a/addons/gdUnit4/test/core/GdUnitResultTest.gd b/addons/gdUnit4/test/core/GdUnitResultTest.gd new file mode 100644 index 0000000..1094f9c --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitResultTest.gd @@ -0,0 +1,37 @@ +# GdUnit generated TestSuite +class_name GdUnitResultTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitResult.gd' + + +func test_serde(): + var value = { + "info" : "test", + "meta" : 42 + } + var source := GdUnitResult.success(value) + var serialized_result = GdUnitResult.serialize(source) + var deserialised_result := GdUnitResult.deserialize(serialized_result) + assert_object(deserialised_result)\ + .is_instanceof(GdUnitResult) \ + .is_equal(source) + + +func test_or_else_on_success(): + var result := GdUnitResult.success("some value") + assert_str(result.value()).is_equal("some value") + assert_str(result.or_else("other value")).is_equal("some value") + + +func test_or_else_on_warning(): + var result := GdUnitResult.warn("some warning message") + assert_object(result.value()).is_null() + assert_str(result.or_else("other value")).is_equal("other value") + + +func test_or_else_on_error(): + var result := GdUnitResult.error("some error message") + assert_object(result.value()).is_null() + assert_str(result.or_else("other value")).is_equal("other value") diff --git a/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd b/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd new file mode 100644 index 0000000..03b245f --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitRunnerConfigTest.gd @@ -0,0 +1,189 @@ +# GdUnit generated TestSuite +class_name GdUnitRunnerConfigTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitRunnerConfig.gd' + + +func test_initial_config(): + var config := GdUnitRunnerConfig.new() + assert_dict(config.to_execute()).is_empty() + assert_dict(config.skipped()).is_empty() + + +func test_clear_on_initial_config(): + var config := GdUnitRunnerConfig.new() + config.clear() + assert_dict(config.to_execute()).is_empty() + assert_dict(config.skipped()).is_empty() + + +func test_clear_on_filled_config(): + var config := GdUnitRunnerConfig.new() + config.add_test_suite("res://foo") + config.skip_test_suite("res://bar") + assert_dict(config.to_execute()).is_not_empty() + assert_dict(config.skipped()).is_not_empty() + + # clear it + config.clear() + assert_dict(config.to_execute()).is_empty() + assert_dict(config.skipped()).is_empty() + + +func test_set_server_port(): + var config := GdUnitRunnerConfig.new() + # intial value + assert_int(config.server_port()).is_equal(-1) + + config.set_server_port(1000) + assert_int(config.server_port()).is_equal(1000) + + +func test_self_test(): + var config := GdUnitRunnerConfig.new() + # initial is empty + assert_dict(config.to_execute()).is_empty() + + # configure self test + var to_execute := config.self_test().to_execute() + assert_dict(to_execute).contains_key_value("res://addons/gdUnit4/test/", PackedStringArray()) + + +func test_add_test_suite(): + var config := GdUnitRunnerConfig.new() + # skip should have no affect + config.skip_test_suite("res://bar") + + config.add_test_suite("res://foo") + assert_dict(config.to_execute()).contains_key_value("res://foo", PackedStringArray()) + # add two more + config.add_test_suite("res://foo2") + config.add_test_suite("res://bar/foo") + assert_dict(config.to_execute())\ + .contains_key_value("res://foo", PackedStringArray())\ + .contains_key_value("res://foo2", PackedStringArray())\ + .contains_key_value("res://bar/foo", PackedStringArray()) + + +func test_add_test_suites(): + var config := GdUnitRunnerConfig.new() + # skip should have no affect + config.skip_test_suite("res://bar") + + config.add_test_suites(PackedStringArray(["res://foo2", "res://bar/foo", "res://foo1"])) + + assert_dict(config.to_execute())\ + .contains_key_value("res://foo1", PackedStringArray())\ + .contains_key_value("res://foo2", PackedStringArray())\ + .contains_key_value("res://bar/foo", PackedStringArray()) + + +func test_add_test_case(): + var config := GdUnitRunnerConfig.new() + # skip should have no affect + config.skip_test_suite("res://bar") + + config.add_test_case("res://foo1", "testcaseA") + assert_dict(config.to_execute()).contains_key_value("res://foo1", PackedStringArray(["testcaseA"])) + # add two more + config.add_test_case("res://foo1", "testcaseB") + config.add_test_case("res://foo2", "testcaseX") + assert_dict(config.to_execute())\ + .contains_key_value("res://foo1", PackedStringArray(["testcaseA", "testcaseB"]))\ + .contains_key_value("res://foo2", PackedStringArray(["testcaseX"])) + + +func test_add_test_suites_and_test_cases_combi(): + var config := GdUnitRunnerConfig.new() + + config.add_test_suite("res://foo1") + config.add_test_suite("res://foo2") + config.add_test_suite("res://bar/foo") + config.add_test_case("res://foo1", "testcaseA") + config.add_test_case("res://foo1", "testcaseB") + config.add_test_suites(PackedStringArray(["res://foo3", "res://bar/foo3", "res://foo4"])) + + assert_dict(config.to_execute())\ + .has_size(6)\ + .contains_key_value("res://foo1", PackedStringArray(["testcaseA", "testcaseB"]))\ + .contains_key_value("res://foo2", PackedStringArray())\ + .contains_key_value("res://foo3", PackedStringArray())\ + .contains_key_value("res://foo4", PackedStringArray())\ + .contains_key_value("res://bar/foo3", PackedStringArray())\ + .contains_key_value("res://bar/foo", PackedStringArray()) + + +func test_skip_test_suite(): + var config := GdUnitRunnerConfig.new() + + config.skip_test_suite("res://foo1") + assert_dict(config.skipped()).contains_key_value("res://foo1", PackedStringArray()) + # add two more + config.skip_test_suite("res://foo2") + config.skip_test_suite("res://bar/foo1") + assert_dict(config.skipped())\ + .contains_key_value("res://foo1", PackedStringArray())\ + .contains_key_value("res://foo2", PackedStringArray())\ + .contains_key_value("res://bar/foo1", PackedStringArray()) + + +func test_skip_test_suite_and_test_case(): + var possible_paths :PackedStringArray = [ + "/foo/MyTest.gd", + "res://foo/MyTest.gd", + "/foo/MyTest.gd:test_x", + "res://foo/MyTest.gd:test_y", + "MyTest", + "MyTest:test", + "MyTestX", + ] + var config := GdUnitRunnerConfig.new() + for path in possible_paths: + config.skip_test_suite(path) + assert_dict(config.skipped())\ + .has_size(3)\ + .contains_key_value("res://foo/MyTest.gd", PackedStringArray(["test_x", "test_y"]))\ + .contains_key_value("MyTest", PackedStringArray(["test"]))\ + .contains_key_value("MyTestX", PackedStringArray()) + + +func test_skip_test_case(): + var config := GdUnitRunnerConfig.new() + + config.skip_test_case("res://foo1", "testcaseA") + assert_dict(config.skipped()).contains_key_value("res://foo1", PackedStringArray(["testcaseA"])) + # add two more + config.skip_test_case("res://foo1", "testcaseB") + config.skip_test_case("res://foo2", "testcaseX") + assert_dict(config.skipped())\ + .contains_key_value("res://foo1", PackedStringArray(["testcaseA", "testcaseB"]))\ + .contains_key_value("res://foo2", PackedStringArray(["testcaseX"])) + + +func test_load_fail(): + var config := GdUnitRunnerConfig.new() + + assert_result(config.load_config("invalid_path"))\ + .is_error()\ + .contains_message("Can't find test runner configuration 'invalid_path'! Please select a test to run.") + + +func test_save_load(): + var config := GdUnitRunnerConfig.new() + # add some dummy conf + config.set_server_port(1000) + config.skip_test_suite("res://bar") + config.add_test_suite("res://foo2") + config.add_test_case("res://foo1", "testcaseA") + + var config_file := create_temp_dir("test_save_load") + "/testconf.cfg" + + assert_result(config.save_config(config_file)).is_success() + assert_file(config_file).exists() + + var config2 := GdUnitRunnerConfig.new() + assert_result(config2.load_config(config_file)).is_success() + # verify the config has original enties + assert_object(config2).is_equal(config).is_not_same(config) diff --git a/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd b/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd new file mode 100644 index 0000000..926c176 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSceneRunnerInputEventTest.gd @@ -0,0 +1,527 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSceneRunner.gd' + + +var _runner :GdUnitSceneRunner +var _scene_spy :Node + + +func before_test(): + _scene_spy = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + _runner = scene_runner(_scene_spy) + assert_inital_mouse_state() + assert_inital_key_state() + + +# asserts to KeyList Enums +func assert_inital_key_state(): + # scacode 4194304-4194415 + for key in range(KEY_SPECIAL, KEY_LAUNCHF): + assert_that(Input.is_key_pressed(key)).is_false() + assert_that(Input.is_physical_key_pressed(key)).is_false() + # keycode 32-255 + for key in range(KEY_SPACE, KEY_SECTION): + assert_that(Input.is_key_pressed(key)).is_false() + assert_that(Input.is_physical_key_pressed(key)).is_false() + + +#asserts to Mouse ButtonList Enums +func assert_inital_mouse_state(): + for button in [ + MOUSE_BUTTON_LEFT, + MOUSE_BUTTON_MIDDLE, + MOUSE_BUTTON_RIGHT, + MOUSE_BUTTON_XBUTTON1, + MOUSE_BUTTON_XBUTTON2, + MOUSE_BUTTON_WHEEL_UP, + MOUSE_BUTTON_WHEEL_DOWN, + MOUSE_BUTTON_WHEEL_LEFT, + MOUSE_BUTTON_WHEEL_RIGHT, + ]: + assert_that(Input.is_mouse_button_pressed(button)).is_false() + assert_that(Input.get_mouse_button_mask()).is_equal(0) + + +func test_reset_to_inital_state_on_release(): + var runner = scene_runner("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + # simulate mouse buttons and key press but we never released it + runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + runner.simulate_mouse_button_press(MOUSE_BUTTON_MIDDLE) + runner.simulate_key_press(KEY_0) + runner.simulate_key_press(KEY_X) + await await_idle_frame() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE)).is_true() + assert_that(Input.is_key_pressed(KEY_0)).is_true() + assert_that(Input.is_key_pressed(KEY_X)).is_true() + # unreference the scene runner to enforce reset to initial Input state + runner._notification(NOTIFICATION_PREDELETE) + await await_idle_frame() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_false() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_false() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE)).is_false() + assert_that(Input.is_key_pressed(KEY_0)).is_false() + assert_that(Input.is_key_pressed(KEY_X)).is_false() + + +func test_simulate_key_press() -> void: + # iterate over some example keys + for key in [KEY_A, KEY_D, KEY_X, KEY_0]: + _runner.simulate_key_press(key) + await await_idle_frame() + + var event := InputEventKey.new() + event.keycode = key + event.physical_keycode = key + event.pressed = true + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_key_pressed(key)).is_true() + # verify all this keys are still handled as pressed + assert_that(Input.is_key_pressed(KEY_A)).is_true() + assert_that(Input.is_key_pressed(KEY_D)).is_true() + assert_that(Input.is_key_pressed(KEY_X)).is_true() + assert_that(Input.is_key_pressed(KEY_0)).is_true() + # other keys are not pressed + assert_that(Input.is_key_pressed(KEY_B)).is_false() + assert_that(Input.is_key_pressed(KEY_G)).is_false() + assert_that(Input.is_key_pressed(KEY_Z)).is_false() + assert_that(Input.is_key_pressed(KEY_1)).is_false() + + +func test_simulate_key_press_with_modifiers() -> void: + # press shift key + A + _runner.simulate_key_press(KEY_SHIFT) + _runner.simulate_key_press(KEY_A) + await await_idle_frame() + + # results in two events, first is the shift key is press + var event := InputEventKey.new() + event.keycode = KEY_SHIFT + event.physical_keycode = KEY_SHIFT + event.pressed = true + event.shift_pressed = true + verify(_scene_spy, 1)._input(event) + + # second is the comnbination of current press shift and key A + event = InputEventKey.new() + event.keycode = KEY_A + event.physical_keycode = KEY_A + event.pressed = true + event.shift_pressed = true + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_key_pressed(KEY_SHIFT)).is_true() + assert_that(Input.is_key_pressed(KEY_A)).is_true() + + +func test_simulate_many_keys_press() -> void: + # press and hold keys W and Z + _runner.simulate_key_press(KEY_W) + _runner.simulate_key_press(KEY_Z) + await await_idle_frame() + + assert_that(Input.is_key_pressed(KEY_W)).is_true() + assert_that(Input.is_physical_key_pressed(KEY_W)).is_true() + assert_that(Input.is_key_pressed(KEY_Z)).is_true() + assert_that(Input.is_physical_key_pressed(KEY_Z)).is_true() + + #now release key w + _runner.simulate_key_release(KEY_W) + await await_idle_frame() + + assert_that(Input.is_key_pressed(KEY_W)).is_false() + assert_that(Input.is_physical_key_pressed(KEY_W)).is_false() + assert_that(Input.is_key_pressed(KEY_Z)).is_true() + assert_that(Input.is_physical_key_pressed(KEY_Z)).is_true() + + +func test_simulate_keypressed_as_action() -> void: + # add custom action `player_jump` for key 'Space' is pressed + var event := InputEventKey.new() + event.keycode = KEY_SPACE + InputMap.add_action("player_jump") + InputMap.action_add_event("player_jump", event) + var runner := scene_runner("res://addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn") + + # precondition checks + var action_event = InputMap.action_get_events("player_jump") + assert_array(action_event).contains_exactly([event]) + assert_bool(Input.is_action_just_released("player_jump", true)).is_false() + assert_bool(Input.is_action_just_released("ui_accept", true)).is_false() + assert_bool(Input.is_action_just_released("ui_select", true)).is_false() + assert_bool(runner.scene()._player_jump_action_released).is_false() + + # test a key event is trigger action event + # simulate press space + runner.simulate_key_pressed(KEY_SPACE) + # it is important do not wait for next frame here, otherwise the input action cache is cleared and can't be use to verify + assert_bool(Input.is_action_just_released("player_jump", true)).is_true() + assert_bool(Input.is_action_just_released("ui_accept", true)).is_true() + assert_bool(Input.is_action_just_released("ui_select", true)).is_true() + assert_bool(runner.scene()._player_jump_action_released).is_true() + + # test a key event is not trigger the custom action event + # simulate press only space+ctrl + runner._reset_input_to_default() + runner.simulate_key_pressed(KEY_SPACE, false, true) + # it is important do not wait for next frame here, otherwise the input action cache is cleared and can't be use to verify + assert_bool(Input.is_action_just_released("player_jump", true)).is_false() + assert_bool(Input.is_action_just_released("ui_accept", true)).is_false() + assert_bool(Input.is_action_just_released("ui_select", true)).is_false() + assert_bool(runner.scene()._player_jump_action_released).is_false() + + # cleanup custom action + InputMap.erase_action("player_jump") + InputMap.action_erase_events("player_jump") + + +func test_simulate_set_mouse_pos(): + # save current global mouse pos + var gmp := _runner.get_global_mouse_position() + # set mouse to pos 100, 100 + _runner.set_mouse_pos(Vector2(100, 100)) + await await_idle_frame() + var event := InputEventMouseMotion.new() + event.position = Vector2(100, 100) + event.global_position = gmp + verify(_scene_spy, 1)._input(event) + + # set mouse to pos 800, 400 + gmp = _runner.get_global_mouse_position() + _runner.set_mouse_pos(Vector2(800, 400)) + await await_idle_frame() + event = InputEventMouseMotion.new() + event.position = Vector2(800, 400) + event.global_position = gmp + verify(_scene_spy, 1)._input(event) + + # and again back to 100,100 + gmp = _runner.get_global_mouse_position() + _runner.set_mouse_pos(Vector2(100, 100)) + await await_idle_frame() + event = InputEventMouseMotion.new() + event.position = Vector2(100, 100) + event.global_position = gmp + verify(_scene_spy, 1)._input(event) + + +func test_simulate_set_mouse_pos_with_modifiers(): + var is_alt := false + var is_control := false + var is_shift := false + + for modifier in [KEY_SHIFT, KEY_CTRL, KEY_ALT]: + is_alt = is_alt or KEY_ALT == modifier + is_control = is_control or KEY_CTRL == modifier + is_shift = is_shift or KEY_SHIFT == modifier + + for mouse_button in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + # simulate press shift, set mouse pos and final press mouse button + var gmp := _runner.get_global_mouse_position() + _runner.simulate_key_press(modifier) + _runner.set_mouse_pos(Vector2.ZERO) + _runner.simulate_mouse_button_press(mouse_button) + await await_idle_frame() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.alt_pressed = is_alt + event.ctrl_pressed = is_control + event.shift_pressed = is_shift + event.pressed = true + event.button_index = mouse_button + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(mouse_button)).is_true() + # finally release it + _runner.simulate_mouse_button_release(mouse_button) + await await_idle_frame() + + +func test_simulate_mouse_move(): + _runner.set_mouse_pos(Vector2(10, 10)) + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_move(Vector2(400, 100)) + await await_idle_frame() + + var event = InputEventMouseMotion.new() + event.position = Vector2(400, 100) + event.global_position = gmp + event.relative = Vector2(400, 100) - Vector2(10, 10) + verify(_scene_spy, 1)._input(event) + + # move mouse to next pos + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_move(Vector2(55, 42)) + await await_idle_frame() + + event = InputEventMouseMotion.new() + event.position = Vector2(55, 42) + event.global_position = gmp + event.relative = Vector2(55, 42) - Vector2(400, 100) + verify(_scene_spy, 1)._input(event) + + +func test_simulate_mouse_move_relative(): + #OS.window_minimized = false + _runner.set_mouse_pos(Vector2(10, 10)) + await await_idle_frame() + assert_that(_runner.get_mouse_position()).is_equal(Vector2(10, 10)) + + # move the mouse in time of 1 second + # the final position is current + relative = Vector2(10, 10) + (Vector2(900, 400) + await _runner.simulate_mouse_move_relative(Vector2(900, 400), 1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(910, 410), Vector2.ONE) + + # move the mouse back in time of 0.1 second + # Use the negative value of the previously moved action to move it back to the starting position + await _runner.simulate_mouse_move_relative(Vector2(-900, -400), 0.1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(10, 10), Vector2.ONE) + + +func test_simulate_mouse_move_absolute(): + #OS.window_minimized = false + _runner.set_mouse_pos(Vector2(10, 10)) + await await_idle_frame() + assert_that(_runner.get_mouse_position()).is_equal(Vector2(10, 10)) + + # move the mouse in time of 1 second + await _runner.simulate_mouse_move_absolute(Vector2(900, 400), 1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(900, 400), Vector2.ONE) + + # move the mouse back in time of 0.1 second + await _runner.simulate_mouse_move_absolute(Vector2(10, 10), 0.1) + assert_vector(_runner.get_mouse_position()).is_equal_approx(Vector2(10, 10), Vector2.ONE) + + +func test_simulate_mouse_button_press_left(): + # simulate mouse button press and hold + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + await await_idle_frame() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(MOUSE_BUTTON_LEFT) + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + + +func test_simulate_mouse_button_press_left_doubleclick(): + # simulate mouse button press double_click + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT, true) + await await_idle_frame() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.double_click = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(MOUSE_BUTTON_LEFT) + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + + +func test_simulate_mouse_button_press_right(): + # simulate mouse button press and hold + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + await await_idle_frame() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(MOUSE_BUTTON_RIGHT) + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + + +func test_simulate_mouse_button_press_left_and_right(): + # simulate mouse button press left+right + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + _runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + await await_idle_frame() + + # results in two events, first is left mouse button + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = MOUSE_BUTTON_MASK_LEFT + verify(_scene_spy, 1)._input(event) + + # second is left+right and combined mask + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + assert_that(Input.get_mouse_button_mask()).is_equal(MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT) + + +func test_simulate_mouse_button_press_left_and_right_and_release(): + # simulate mouse button press left+right + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + _runner.simulate_mouse_button_press(MOUSE_BUTTON_RIGHT) + await await_idle_frame() + + # will results into two events + # first for left mouse button + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = MOUSE_BUTTON_MASK_LEFT + verify(_scene_spy, 1)._input(event) + + # second is left+right and combined mask + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_true() + assert_that(Input.get_mouse_button_mask()).is_equal(MOUSE_BUTTON_MASK_LEFT|MOUSE_BUTTON_MASK_RIGHT) + + # now release the right button + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_RIGHT) + await await_idle_frame() + # will result in right button press false but stay with mask for left pressed + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = MOUSE_BUTTON_RIGHT + event.button_mask = MOUSE_BUTTON_MASK_LEFT + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_false() + assert_that(Input.get_mouse_button_mask()).is_equal(MOUSE_BUTTON_MASK_LEFT) + + # finally relase left button + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await await_idle_frame() + # will result in right button press false but stay with mask for left pressed + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = MOUSE_BUTTON_LEFT + event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_false() + assert_that(Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)).is_false() + assert_that(Input.get_mouse_button_mask()).is_equal(0) + + +func test_simulate_mouse_button_pressed(): + for mouse_button in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + # simulate mouse button press and release + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(mouse_button) + await await_idle_frame() + + # it genrates two events, first for press and second as released + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = mouse_button + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = mouse_button + event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(mouse_button)).is_false() + verify(_scene_spy, 2)._input(any_class(InputEventMouseButton)) + reset(_scene_spy) + +func test_simulate_mouse_button_pressed_doubleclick(): + for mouse_button in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + # simulate mouse button press and release by double_click + var gmp := _runner.get_global_mouse_position() + _runner.simulate_mouse_button_pressed(mouse_button, true) + await await_idle_frame() + + # it genrates two events, first for press and second as released + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.double_click = true + event.button_index = mouse_button + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.double_click = false + event.button_index = mouse_button + event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(mouse_button)).is_false() + verify(_scene_spy, 2)._input(any_class(InputEventMouseButton)) + reset(_scene_spy) + + +func test_simulate_mouse_button_press_and_release(): + for mouse_button in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT]: + var gmp := _runner.get_global_mouse_position() + # simulate mouse button press and release + _runner.simulate_mouse_button_press(mouse_button) + await await_idle_frame() + + var event := InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = true + event.button_index = mouse_button + event.button_mask = GdUnitSceneRunnerImpl.MAP_MOUSE_BUTTON_MASKS.get(mouse_button) + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(mouse_button)).is_true() + + # now simulate mouse button release + gmp = _runner.get_global_mouse_position() + _runner.simulate_mouse_button_release(mouse_button) + await await_idle_frame() + + event = InputEventMouseButton.new() + event.position = Vector2.ZERO + event.global_position = gmp + event.pressed = false + event.button_index = mouse_button + #event.button_mask = 0 + verify(_scene_spy, 1)._input(event) + assert_that(Input.is_mouse_button_pressed(mouse_button)).is_false() diff --git a/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd b/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd new file mode 100644 index 0000000..c5edd60 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSceneRunnerTest.gd @@ -0,0 +1,356 @@ +# GdUnit generated TestSuite +class_name GdUnitSceneRunnerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd' + + +# loads the test runner and register for auto freeing after test +func load_test_scene() -> Node: + return auto_free(load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn").instantiate()) + + +func before(): + # use a dedicated FPS because we calculate frames by time + Engine.set_max_fps(60) + + +func after(): + Engine.set_max_fps(0) + + +func test_get_property() -> void: + var runner := scene_runner(load_test_scene()) + + assert_that(runner.get_property("_box1")).is_instanceof(ColorRect) + assert_that(runner.get_property("_invalid")).is_equal("The property '_invalid' not exist checked loaded scene.") + assert_that(runner.get_property("_nullable")).is_null() + + +func test_set_property() -> void: + var runner := scene_runner(load_test_scene()) + + assert_that(runner.set_property("_invalid", 42)).is_equal(false) + + assert_that(runner.set_property("_nullable", RefCounted.new())).is_equal(true) + assert_that(runner.get_property("_nullable")).is_instanceof(RefCounted) + +func test_invoke_method() -> void: + var runner := scene_runner(load_test_scene()) + + assert_that(runner.invoke("add", 10, 12)).is_equal(22) + assert_that(runner.invoke("sub", 10, 12)).is_equal("The method 'sub' not exist checked loaded scene.") + + +@warning_ignore("unused_parameter") +func test_simulate_frames(timeout = 5000) -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # initial is white + assert_object(box1.color).is_equal(Color.WHITE) + + # start color cycle by invoke the function 'start_color_cycle' + runner.invoke("start_color_cycle") + + # we wait for 10 frames + await runner.simulate_frames(10) + # after 10 frame is still white + assert_object(box1.color).is_equal(Color.WHITE) + + # we wait 30 more frames + await runner.simulate_frames(30) + # after 40 frames the box one should be changed to red + assert_object(box1.color).is_equal(Color.RED) + + +@warning_ignore("unused_parameter") +func test_simulate_frames_withdelay(timeout = 4000) -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # initial is white + assert_object(box1.color).is_equal(Color.WHITE) + + # start color cycle by invoke the function 'start_color_cycle' + runner.invoke("start_color_cycle") + + # we wait for 10 frames each with a 50ms delay + await runner.simulate_frames(10, 50) + # after 10 frame and in sum 500ms is should be changed to red + assert_object(box1.color).is_equal(Color.RED) + + +@warning_ignore("unused_parameter") +func test_run_scene_colorcycle(timeout=2000) -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # verify inital color + assert_object(box1.color).is_equal(Color.WHITE) + + # start color cycle by invoke the function 'start_color_cycle' + runner.invoke("start_color_cycle") + + # await for each color cycle is emited + await runner.await_signal("panel_color_change", [box1, Color.RED]) + assert_object(box1.color).is_equal(Color.RED) + await runner.await_signal("panel_color_change", [box1, Color.BLUE]) + assert_object(box1.color).is_equal(Color.BLUE) + await runner.await_signal("panel_color_change", [box1, Color.GREEN]) + assert_object(box1.color).is_equal(Color.GREEN) + + +func test_simulate_scene_inteaction_by_press_enter(timeout=2000) -> void: + var runner := scene_runner(load_test_scene()) + + # inital no spell is fired + assert_object(runner.find_child("Spell")).is_null() + + # fire spell be pressing enter key + runner.simulate_key_pressed(KEY_ENTER) + # wait until next frame + await await_idle_frame() + + # verify a spell is created + assert_object(runner.find_child("Spell")).is_not_null() + + # wait until spell is explode after around 1s + var spell = runner.find_child("Spell") + if spell == null: + return + await await_signal_on(spell, "spell_explode", [spell], timeout) + + # verify spell is removed when is explode + assert_object(runner.find_child("Spell")).is_null() + + +# mock on a runner and spy on created spell +func test_simulate_scene_inteaction_in_combination_with_spy(): + var spy_ = spy(load_test_scene()) + # create a runner runner + var runner := scene_runner(spy_) + + # simulate a key event to fire a spell + runner.simulate_key_pressed(KEY_ENTER) + verify(spy_).create_spell() + + var spell = runner.find_child("Spell") + assert_that(spell).is_not_null() + assert_that(spell.is_connected("spell_explode", Callable(spy_, "_destroy_spell"))).is_true() + + +func test_simulate_scene_interact_with_buttons(): + var spyed_scene = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var runner := scene_runner(spyed_scene) + # test button 1 interaction + await await_millis(1000) + runner.set_mouse_pos(Vector2(60, 20)) + runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await await_idle_frame() + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box1, Color.RED) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box1, Color.GRAY) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box2, any_color()) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box3, any_color()) + + # test button 2 interaction + reset(spyed_scene) + await await_millis(1000) + runner.set_mouse_pos(Vector2(160, 20)) + runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await await_idle_frame() + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box1, any_color()) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box2, Color.RED) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box2, Color.GRAY) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box3, any_color()) + + # test button 3 interaction (is changed after 1s to gray) + reset(spyed_scene) + await await_millis(1000) + runner.set_mouse_pos(Vector2(260, 20)) + runner.simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT) + await await_idle_frame() + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box1, any_color()) + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box2, any_color()) + # is changed to red + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box3, Color.RED) + # no gray + verify(spyed_scene, 0)._on_panel_color_changed(spyed_scene._box3, Color.GRAY) + # after one second is changed to gray + await await_millis(1200) + verify(spyed_scene)._on_panel_color_changed(spyed_scene._box3, Color.GRAY) + + +func test_await_func_without_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + await runner.await_func("color_cycle").is_equal("black") + + +func test_await_func_with_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + + # set max time factor to minimize waiting time checked `runner.wait_func` + runner.set_time_factor(10) + await runner.await_func("color_cycle").wait_until(200).is_equal("black") + + +func test_await_signal_without_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + + runner.invoke("start_color_cycle") + await runner.await_signal("panel_color_change", [box1, Color.RED]) + await runner.await_signal("panel_color_change", [box1, Color.BLUE]) + await runner.await_signal("panel_color_change", [box1, Color.GREEN]) + ( + # should be interrupted is will never change to Color.KHAKI + await assert_failure_await(func x(): await runner.await_signal( "panel_color_change", [box1, Color.KHAKI], 300)) + ).has_message("await_signal_on(panel_color_change, [%s, %s]) timed out after 300ms" % [str(box1), str(Color.KHAKI)])\ + .has_line(205) + + +func test_await_signal_with_time_factor() -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + # set max time factor to minimize waiting time checked `runner.wait_func` + runner.set_time_factor(10) + runner.invoke("start_color_cycle") + + await runner.await_signal("panel_color_change", [box1, Color.RED], 100) + await runner.await_signal("panel_color_change", [box1, Color.BLUE], 100) + await runner.await_signal("panel_color_change", [box1, Color.GREEN], 100) + ( + # should be interrupted is will never change to Color.KHAKI + await assert_failure_await(func x(): await runner.await_signal("panel_color_change", [box1, Color.KHAKI], 30)) + ).has_message("await_signal_on(panel_color_change, [%s, %s]) timed out after 30ms" % [str(box1), str(Color.KHAKI)])\ + .has_line(222) + + +func test_simulate_until_signal() -> void: + var runner := scene_runner(load_test_scene()) + var box1 :ColorRect = runner.get_property("_box1") + + # set max time factor to minimize waiting time checked `runner.wait_func` + runner.invoke("start_color_cycle") + + await runner.simulate_until_signal("panel_color_change", box1, Color.RED) + await runner.simulate_until_signal("panel_color_change", box1, Color.BLUE) + await runner.simulate_until_signal("panel_color_change", box1, Color.GREEN) + + +@warning_ignore("unused_parameter") +func test_simulate_until_object_signal(timeout=2000) -> void: + var runner := scene_runner(load_test_scene()) + + # inital no spell is fired + assert_object(runner.find_child("Spell")).is_null() + + # fire spell be pressing enter key + runner.simulate_key_pressed(KEY_ENTER) + # wait until next frame + await await_idle_frame() + var spell = runner.find_child("Spell") + prints(spell) + + # simmulate scene until the spell is explode + await runner.simulate_until_object_signal(spell, "spell_explode", spell) + + # verify spell is removed when is explode + assert_object(runner.find_child("Spell")).is_null() + + +func test_runner_by_null_instance() -> void: + var runner := scene_runner(null) + assert_object(runner._current_scene).is_null() + + +func test_runner_by_invalid_resource_path() -> void: + # not existing scene + assert_object(scene_runner("res://test_scene.tscn")._current_scene).is_null() + # not a path to a scene + assert_object(scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.gd")._current_scene).is_null() + + +func test_runner_by_resource_path() -> void: + var runner = scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn") + assert_object(runner.scene()).is_instanceof(Node2D) + + # verify the scene is freed when the runner is freed + var scene = runner.scene() + assert_bool(is_instance_valid(scene)).is_true() + runner._notification(NOTIFICATION_PREDELETE) + # give engine time to free the resources + await await_idle_frame() + # verify runner and scene is freed + assert_bool(is_instance_valid(scene)).is_false() + + +func test_runner_by_invalid_scene_instance() -> void: + var scene = RefCounted.new() + var runner := scene_runner(scene) + assert_object(runner._current_scene).is_null() + + +func test_runner_by_scene_instance() -> void: + var scene = load("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn").instantiate() + var runner := scene_runner(scene) + assert_object(runner.scene()).is_instanceof(Node2D) + + # verify the scene is freed when the runner is freed + runner._notification(NOTIFICATION_PREDELETE) + # give engine time to free the resources + await await_idle_frame() + # scene runner using external scene do not free the scene at exit + assert_bool(is_instance_valid(scene)).is_true() + # needs to be manually freed + scene.free() + + +func test_mouse_drag_and_drop() -> void: + var spy_scene = spy("res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn") + var runner := scene_runner(spy_scene) + + var slot_left :TextureRect = $"/root/DragAndDropScene/left/TextureRect" + var slot_right :TextureRect = $"/root/DragAndDropScene/right/TextureRect" + + var save_mouse_pos := get_tree().root.get_mouse_position() + # set inital mouse pos over the left slot + var mouse_pos := slot_left.global_position + Vector2(10, 10) + runner.set_mouse_pos(mouse_pos) + await await_millis(1000) + + await await_idle_frame() + var event := InputEventMouseMotion.new() + event.position = mouse_pos + event.global_position = save_mouse_pos + verify(spy_scene, 1)._gui_input(event) + + runner.simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + await await_idle_frame() + assert_bool(Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)).is_true() + + # start drag&drop to left pannel + for i in 20: + runner.simulate_mouse_move(mouse_pos + Vector2(i*.5*i, 0)) + await await_millis(40) + + runner.simulate_mouse_button_release(MOUSE_BUTTON_LEFT) + await await_idle_frame() + assert_that(slot_right.texture).is_equal(slot_left.texture) + + +func test_runner_GD_356() -> void: + # to avoid reporting the expected push_error as test failure we disable it + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + var runner = scene_runner("res://addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn") + var player = runner.invoke("find_child", "Player", true, false) + assert_that(player).is_not_null() + await assert_func(player, "is_on_floor").wait_until(500).is_true() + assert_that(runner.scene()).is_not_null() + # run simulate_mouse_move_relative without await to reproduce https://github.com/MikeSchulze/gdUnit4/issues/356 + # this results into releasing the scene while `simulate_mouse_move_relative` is processing the mouse move + runner.simulate_mouse_move_relative(Vector2(100, 100), 1.0) + assert_that(runner.scene()).is_not_null() + + +# we override the scene runner function for test purposes to hide push_error notifications +func scene_runner(scene, verbose := false) -> GdUnitSceneRunner: + return auto_free(GdUnitSceneRunnerImpl.new(scene, verbose, true)) diff --git a/addons/gdUnit4/test/core/GdUnitSettingsTest.gd b/addons/gdUnit4/test/core/GdUnitSettingsTest.gd new file mode 100644 index 0000000..5e52a02 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSettingsTest.gd @@ -0,0 +1,155 @@ +# GdUnit generated TestSuite +class_name GdUnitSettingsTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSettings.gd' + +const MAIN_CATEGORY = "unit_test" +const CATEGORY_A = MAIN_CATEGORY + "/category_a" +const CATEGORY_B = MAIN_CATEGORY + "/category_b" +const TEST_PROPERTY_A = CATEGORY_A + "/a/prop_a" +const TEST_PROPERTY_B = CATEGORY_A + "/a/prop_b" +const TEST_PROPERTY_C = CATEGORY_A + "/a/prop_c" +const TEST_PROPERTY_D = CATEGORY_B + "/prop_d" +const TEST_PROPERTY_E = CATEGORY_B + "/c/prop_e" +const TEST_PROPERTY_F = CATEGORY_B + "/c/prop_f" +const TEST_PROPERTY_G = CATEGORY_B + "/a/prop_g" + + +func before() -> void: + GdUnitSettings.dump_to_tmp() + + +func after() -> void: + GdUnitSettings.restore_dump_from_tmp() + + +func before_test() -> void: + GdUnitSettings.create_property_if_need(TEST_PROPERTY_A, true, "helptext TEST_PROPERTY_A.") + GdUnitSettings.create_property_if_need(TEST_PROPERTY_B, false, "helptext TEST_PROPERTY_B.") + GdUnitSettings.create_property_if_need(TEST_PROPERTY_C, 100, "helptext TEST_PROPERTY_C.") + GdUnitSettings.create_property_if_need(TEST_PROPERTY_D, true, "helptext TEST_PROPERTY_D.") + GdUnitSettings.create_property_if_need(TEST_PROPERTY_E, false, "helptext TEST_PROPERTY_E.") + GdUnitSettings.create_property_if_need(TEST_PROPERTY_F, "abc", "helptext TEST_PROPERTY_F.") + GdUnitSettings.create_property_if_need(TEST_PROPERTY_G, 200, "helptext TEST_PROPERTY_G.") + + +func after_test() -> void: + ProjectSettings.clear(TEST_PROPERTY_A) + ProjectSettings.clear(TEST_PROPERTY_B) + ProjectSettings.clear(TEST_PROPERTY_C) + ProjectSettings.clear(TEST_PROPERTY_D) + ProjectSettings.clear(TEST_PROPERTY_E) + ProjectSettings.clear(TEST_PROPERTY_F) + ProjectSettings.clear(TEST_PROPERTY_G) + + +func test_list_settings() -> void: + var settingsA := GdUnitSettings.list_settings(CATEGORY_A) + assert_array(settingsA).extractv(extr("name"), extr("type"), extr("value"), extr("default"), extr("help"))\ + .contains_exactly_in_any_order([ + tuple(TEST_PROPERTY_A, TYPE_BOOL, true, true, "helptext TEST_PROPERTY_A."), + tuple(TEST_PROPERTY_B, TYPE_BOOL,false, false, "helptext TEST_PROPERTY_B."), + tuple(TEST_PROPERTY_C, TYPE_INT, 100, 100, "helptext TEST_PROPERTY_C.") + ]) + var settingsB := GdUnitSettings.list_settings(CATEGORY_B) + assert_array(settingsB).extractv(extr("name"), extr("type"), extr("value"), extr("default"), extr("help"))\ + .contains_exactly_in_any_order([ + tuple(TEST_PROPERTY_D, TYPE_BOOL, true, true, "helptext TEST_PROPERTY_D."), + tuple(TEST_PROPERTY_E, TYPE_BOOL, false, false, "helptext TEST_PROPERTY_E."), + tuple(TEST_PROPERTY_F, TYPE_STRING, "abc", "abc", "helptext TEST_PROPERTY_F."), + tuple(TEST_PROPERTY_G, TYPE_INT, 200, 200, "helptext TEST_PROPERTY_G.") + ]) + + +func test_enum_property() -> void: + var value_set :PackedStringArray = GdUnitSettings.NAMING_CONVENTIONS.keys() + GdUnitSettings.create_property_if_need("test/enum", GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT, "help", value_set) + + var property := GdUnitSettings.get_property("test/enum") + assert_that(property.default()).is_equal(GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + assert_that(property.value()).is_equal(GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + assert_that(property.type()).is_equal(TYPE_INT) + assert_that(property.help()).is_equal('help ["AUTO_DETECT", "SNAKE_CASE", "PASCAL_CASE"]') + assert_that(property.value_set()).is_equal(value_set) + + +func test_migrate_property_change_key() -> void: + # setup old property + var old_property_X = "/category_patch/group_old/name" + var new_property_X = "/category_patch/group_new/name" + GdUnitSettings.create_property_if_need(old_property_X, "foo") + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_equal("foo") + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_null() + var old_property := GdUnitSettings.get_property(old_property_X) + + # migrate + GdUnitSettings.migrate_property(old_property.name(),\ + new_property_X,\ + old_property.default(),\ + old_property.help()) + + var new_property := GdUnitSettings.get_property(new_property_X) + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_null() + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_equal("foo") + assert_object(new_property).is_not_equal(old_property) + assert_str(new_property.value()).is_equal(old_property.value()) + assert_array(new_property.value_set()).is_equal(old_property.value_set()) + assert_int(new_property.type()).is_equal(old_property.type()) + assert_str(new_property.default()).is_equal(old_property.default()) + assert_str(new_property.help()).is_equal(old_property.help()) + + # cleanup + ProjectSettings.clear(new_property_X) + + +func test_migrate_property_change_value() -> void: + # setup old property + var old_property_X = "/category_patch/group_old/name" + var new_property_X = "/category_patch/group_new/name" + GdUnitSettings.create_property_if_need(old_property_X, "foo", "help to foo") + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_equal("foo") + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_null() + var old_property := GdUnitSettings.get_property(old_property_X) + + # migrate property + GdUnitSettings.migrate_property(old_property.name(),\ + new_property_X,\ + old_property.default(),\ + old_property.help(),\ + func(_value): return "bar") + + var new_property := GdUnitSettings.get_property(new_property_X) + assert_str(GdUnitSettings.get_setting(old_property_X, null)).is_null() + assert_str(GdUnitSettings.get_setting(new_property_X, null)).is_equal("bar") + assert_object(new_property).is_not_equal(old_property) + assert_str(new_property.value()).is_equal("bar") + assert_array(new_property.value_set()).is_equal(old_property.value_set()) + assert_int(new_property.type()).is_equal(old_property.type()) + assert_str(new_property.default()).is_equal(old_property.default()) + assert_str(new_property.help()).is_equal(old_property.help()) + # cleanup + ProjectSettings.clear(new_property_X) + + +const TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder" +const HELP_TEST_ROOT_FOLDER := "Sets the root folder where test-suites located/generated." + +func test_migrate_properties_v215() -> void: + # rebuild original settings + GdUnitSettings.create_property_if_need(TEST_ROOT_FOLDER, "test", HELP_TEST_ROOT_FOLDER) + ProjectSettings.set_setting(TEST_ROOT_FOLDER, "xxx") + + # migrate + GdUnitSettings.migrate_properties() + + # verify + var property := GdUnitSettings.get_property(GdUnitSettings.TEST_LOOKUP_FOLDER) + assert_str(property.value()).is_equal("xxx") + assert_array(property.value_set()).is_empty() + assert_int(property.type()).is_equal(TYPE_STRING) + assert_str(property.default()).is_equal(GdUnitSettings.DEFAULT_TEST_LOOKUP_FOLDER) + assert_str(property.help()).is_equal(GdUnitSettings.HELP_TEST_LOOKUP_FOLDER) + assert_that(GdUnitSettings.get_property(TEST_ROOT_FOLDER)).is_null() + ProjectSettings.clear(GdUnitSettings.TEST_LOOKUP_FOLDER) diff --git a/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd b/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd new file mode 100644 index 0000000..f0d6a56 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSignalAwaiterTest.gd @@ -0,0 +1,46 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitSignalAwaiterTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd' + + +class Monster extends Node: + + signal move(value :float) + signal slide(value, x, z ) + + var _pos :float = 0.0 + + func _process(_delta): + _pos += 0.2 + emit_signal("move", _pos) + emit_signal("slide", _pos, 1 , 2) + + +func test_on_signal_with_single_arg() -> void: + var monster = auto_free(Monster.new()) + add_child(monster) + var signal_arg = await await_signal_on(monster, "move", [1.0]) + assert_float(signal_arg).is_equal(1.0) + remove_child(monster) + + +func test_on_signal_with_many_args() -> void: + var monster = auto_free(Monster.new()) + add_child(monster) + var signal_args = await await_signal_on(monster, "slide", [1.0, 1, 2]) + assert_array(signal_args).is_equal([1.0, 1, 2]) + remove_child(monster) + + +func test_on_signal_fail() -> void: + var monster = auto_free(Monster.new()) + add_child(monster) + ( + await assert_failure_await( func x(): await await_signal_on(monster, "move", [4.0])) + ).has_message("await_signal_on(move, [4]) timed out after 2000ms") + remove_child(monster) diff --git a/addons/gdUnit4/test/core/GdUnitSingletonTest.gd b/addons/gdUnit4/test/core/GdUnitSingletonTest.gd new file mode 100644 index 0000000..4e9935a --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitSingletonTest.gd @@ -0,0 +1,11 @@ +extends GdUnitTestSuite + + +func test_instance() -> void: + var n = GdUnitSingleton.instance("singelton_test", func(): return Node.new() ) + assert_object(n).is_instanceof(Node) + assert_bool(is_instance_valid(n)).is_true() + + # free the singleton + GdUnitSingleton.unregister("singelton_test") + assert_bool(is_instance_valid(n)).is_false() diff --git a/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd new file mode 100644 index 0000000..c9ba2eb --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitTestSuiteBuilderTest.gd @@ -0,0 +1,65 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitTestSuiteBuilderTest +extends GdUnitTestSuite + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd' + +var _example_source_gd :String + + +func before_test(): + var temp := create_temp_dir("examples") + var result := GdUnitFileAccess.copy_file("res://addons/gdUnit4/test/core/resources/sources/test_person.gd", temp) + assert_result(result).is_success() + _example_source_gd = result.value() as String + + +func after_test(): + clean_temp_dir() + + +func assert_tests(test_suite :Script) -> GdUnitArrayAssert: + # needs to be reload to get fresh method list + test_suite.reload() + var methods := test_suite.get_script_method_list() + var test_cases := Array() + for method in methods: + if method.name.begins_with("test_"): + test_cases.append(method.name) + return assert_array(test_cases) + + +func test_create_gd_success() -> void: + var source := load(_example_source_gd) + + # create initial test suite based checked function selected by line 9 + var result := GdUnitTestSuiteBuilder.create(source, 9) + + assert_result(result).is_success() + var info := result.value() as Dictionary + assert_str(info.get("path")).is_equal("user://tmp/test/examples/test_person_test.gd") + assert_int(info.get("line")).is_equal(11) + assert_tests(load(info.get("path"))).contains_exactly(["test_first_name"]) + + # create additional test checked existing suite based checked function selected by line 15 + result = GdUnitTestSuiteBuilder.create(source, 15) + + assert_result(result).is_success() + info = result.value() as Dictionary + assert_str(info.get("path")).is_equal("user://tmp/test/examples/test_person_test.gd") + assert_int(info.get("line")).is_equal(16) + assert_tests(load(info.get("path"))).contains_exactly_in_any_order(["test_first_name", "test_fully_name"]) + + +func test_create_gd_fail() -> void: + var source := load(_example_source_gd) + + # attempt to create an initial test suite based checked the function selected in line 8, which has no function definition + var result := GdUnitTestSuiteBuilder.create(source, 8) + assert_result(result).is_error().contains_message("No function found at line: 8.") diff --git a/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd b/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd new file mode 100644 index 0000000..6ee5a4f --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd @@ -0,0 +1,372 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name TestSuiteScannerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd' + +func before_test(): + ProjectSettings.set_setting(GdUnitSettings.TEST_SITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + clean_temp_dir() + + +func after(): + clean_temp_dir() + + +func resolve_path(source_file :String) -> String: + return GdUnitTestSuiteScanner.resolve_test_suite_path(source_file, "_test_") + + +func test_resolve_test_suite_path_project() -> void: + # if no `src` folder found use test folder as root + assert_str(resolve_path("res://foo.gd")).is_equal("res://_test_/foo_test.gd") + assert_str(resolve_path("res://project_name/module/foo.gd")).is_equal("res://_test_/project_name/module/foo_test.gd") + # otherwise build relative to 'src' + assert_str(resolve_path("res://src/foo.gd")).is_equal("res://_test_/foo_test.gd") + assert_str(resolve_path("res://project_name/src/foo.gd")).is_equal("res://project_name/_test_/foo_test.gd") + assert_str(resolve_path("res://project_name/src/module/foo.gd")).is_equal("res://project_name/_test_/module/foo_test.gd") + + +func test_resolve_test_suite_path_plugins() -> void: + assert_str(resolve_path("res://addons/plugin_a/foo.gd")).is_equal("res://addons/plugin_a/_test_/foo_test.gd") + assert_str(resolve_path("res://addons/plugin_a/src/foo.gd")).is_equal("res://addons/plugin_a/_test_/foo_test.gd") + + +func test_resolve_test_suite_path__no_test_root(): + # from a project path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/models/events/ModelChangedEvent.gd", ""))\ + .is_equal("res://project/src/models/events/ModelChangedEventTest.gd") + # from a plugin path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/src/models/events/ModelChangedEvent.gd", ""))\ + .is_equal("res://addons/MyPlugin/src/models/events/ModelChangedEventTest.gd") + # located in user path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/src/models/events/ModelChangedEvent.gd", ""))\ + .is_equal("user://project/src/models/events/ModelChangedEventTest.gd") + + +func test_resolve_test_suite_path__path_contains_src_folder(): + # from a project path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://project/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://project/custom_test/models/events/ModelChangedEventTest.gd") + # from a plugin path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/src/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://addons/MyPlugin/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/src/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://addons/MyPlugin/custom_test/models/events/ModelChangedEventTest.gd") + # located in user path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/src/models/events/ModelChangedEvent.gd"))\ + .is_equal("user://project/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/src/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("user://project/custom_test/models/events/ModelChangedEventTest.gd") + + +func test_resolve_test_suite_path__path_not_contains_src_folder(): + # from a project path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://test/project/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://custom_test/project/models/events/ModelChangedEventTest.gd") + # from a plugin path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/models/events/ModelChangedEvent.gd"))\ + .is_equal("res://addons/MyPlugin/test/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/MyPlugin/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("res://addons/MyPlugin/custom_test/models/events/ModelChangedEventTest.gd") + # located in user path + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/models/events/ModelChangedEvent.gd"))\ + .is_equal("user://test/project/models/events/ModelChangedEventTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("user://project/models/events/ModelChangedEvent.gd", "custom_test"))\ + .is_equal("user://custom_test/project/models/events/ModelChangedEventTest.gd") + + +func test_test_suite_exists(): + var path_exists := "res://addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd" + var path_not_exists := "res://addons/gdUnit4/test/resources/core/FamilyTest.gd" + assert_that(GdUnitTestSuiteScanner.test_suite_exists(path_exists)).is_true() + assert_that(GdUnitTestSuiteScanner.test_suite_exists(path_not_exists)).is_false() + + +func test_test_case_exists(): + var test_suite_path := "res://addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd" + assert_that(GdUnitTestSuiteScanner.test_case_exists(test_suite_path, "name")).is_true() + assert_that(GdUnitTestSuiteScanner.test_case_exists(test_suite_path, "last_name")).is_false() + + +func test_create_test_suite_pascal_case_path(): + var temp_dir := create_temp_dir("TestSuiteScannerTest") + # checked source with class_name is set + var source_path := "res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd" + var suite_path := temp_dir + "/test/MyClassTest1.gd" + var result := GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_that(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name PascalCaseWithClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source = '%s'" % source_path, + ""]) + # checked source with class_name is NOT set + source_path = "res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd" + suite_path = temp_dir + "/test/MyClassTest2.gd" + result = GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_that(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name PascalCaseWithoutClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source = '%s'" % source_path, + ""]) + + +func test_create_test_suite_snake_case_path(): + var temp_dir := create_temp_dir("TestSuiteScannerTest") + # checked source with class_name is set + var source_path :="res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd" + var suite_path := temp_dir + "/test/my_class_test1.gd" + var result := GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_that(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name SnakeCaseWithClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source = '%s'" % source_path, + ""]) + # checked source with class_name is NOT set + source_path ="res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd" + suite_path = temp_dir + "/test/my_class_test2.gd" + result = GdUnitTestSuiteScanner.create_test_suite(suite_path, source_path) + assert_that(result.is_success()).is_true() + assert_str(result.value()).is_equal(suite_path) + assert_file(result.value()).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name SnakeCaseWithoutClassNameTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source = '%s'" % source_path, + ""]) + + +func test_create_test_case(): + # store test class checked temp dir + var tmp_path := create_temp_dir("TestSuiteScannerTest") + var source_path := "res://addons/gdUnit4/test/resources/core/Person.gd" + # generate new test suite with test 'test_last_name()' + var test_suite_path = tmp_path + "/test/PersonTest.gd" + var result := GdUnitTestSuiteScanner.create_test_case(test_suite_path, "last_name", source_path) + assert_that(result.is_success()).is_true() + var info :Dictionary = result.value() + assert_int(info.get("line")).is_equal(11) + assert_file(info.get("path")).exists()\ + .is_file()\ + .is_script()\ + .contains_exactly([ + "# GdUnit generated TestSuite", + "class_name PersonTest", + "extends GdUnitTestSuite", + "@warning_ignore('unused_parameter')", + "@warning_ignore('return_value_discarded')", + "", + "# TestSuite generated from", + "const __source = '%s'" % source_path, + "", + "", + "func test_last_name() -> void:", + " # remove this line and complete your test", + " assert_not_yet_implemented()", + ""]) + # try to add again + result = GdUnitTestSuiteScanner.create_test_case(test_suite_path, "last_name", source_path) + assert_that(result.is_success()).is_true() + assert_that(result.value()).is_equal({"line" : 16, "path": test_suite_path}) + + +# https://github.com/MikeSchulze/gdUnit4/issues/25 +func test_build_test_suite_path() -> void: + # checked project root + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://new_script.gd")).is_equal("res://test/new_script_test.gd") + + # checked project without src folder + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://foo/bar/new_script.gd")).is_equal("res://test/foo/bar/new_script_test.gd") + + # project code structured by 'src' + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://src/new_script.gd")).is_equal("res://test/new_script_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://src/foo/bar/new_script.gd")).is_equal("res://test/foo/bar/new_script_test.gd") + # folder name contains 'src' in name + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://foo/srcare/new_script.gd")).is_equal("res://test/foo/srcare/new_script_test.gd") + + # checked plugins without src folder + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/plugin/foo/bar/new_script.gd")).is_equal("res://addons/plugin/test/foo/bar/new_script_test.gd") + # plugin code structured by 'src' + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://addons/plugin/src/foo/bar/new_script.gd")).is_equal("res://addons/plugin/test/foo/bar/new_script_test.gd") + + # checked user temp folder + var tmp_path := create_temp_dir("projectX/entity") + var source_path := tmp_path + "/Person.gd" + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path(source_path)).is_equal("user://tmp/test/projectX/entity/PersonTest.gd") + + +func test_parse_and_add_test_cases() -> void: + var default_time := GdUnitSettings.test_timeout() + var scanner :GdUnitTestSuiteScanner = auto_free(GdUnitTestSuiteScanner.new()) + # fake a test suite + var test_suite :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + test_suite.set_script( load("res://addons/gdUnit4/test/core/resources/test_script_with_arguments.gd")) + + var test_case_names := PackedStringArray([ + "test_no_args", + "test_with_timeout", + "test_with_fuzzer", + "test_with_fuzzer_iterations", + "test_with_multible_fuzzers", + "test_multiline_arguments_a", + "test_multiline_arguments_b", + "test_multiline_arguments_c"]) + scanner._parse_and_add_test_cases(test_suite, test_suite.get_script(), test_case_names) + assert_array(test_suite.get_children())\ + .extractv(extr("get_name"), extr("timeout"), extr("fuzzer_arguments"), extr("iterations"))\ + .contains_exactly([ + tuple("test_no_args", default_time, [], Fuzzer.ITERATION_DEFAULT_COUNT), + tuple("test_with_timeout", 2000, [], Fuzzer.ITERATION_DEFAULT_COUNT), + tuple("test_with_fuzzer", default_time, [GdFunctionArgument.new("fuzzer", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(-10, 22)")], Fuzzer.ITERATION_DEFAULT_COUNT), + tuple("test_with_fuzzer_iterations", default_time, [GdFunctionArgument.new("fuzzer", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(-10, 22)")], 10), + tuple("test_with_multible_fuzzers", default_time, [GdFunctionArgument.new("fuzzer_a", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(-10, 22)"), + GdFunctionArgument.new("fuzzer_b", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(23, 42)")], 10), + tuple("test_multiline_arguments_a", default_time, [GdFunctionArgument.new("fuzzer_a", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(-10, 22)"), + GdFunctionArgument.new("fuzzer_b", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(23, 42)")], 42), + tuple("test_multiline_arguments_b", default_time, [GdFunctionArgument.new("fuzzer_a", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(-10, 22)"), + GdFunctionArgument.new("fuzzer_b", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(23, 42)")], 23), + tuple("test_multiline_arguments_c", 2000, [GdFunctionArgument.new("fuzzer_a", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(-10, 22)"), + GdFunctionArgument.new("fuzzer_b", GdObjects.TYPE_FUZZER, "Fuzzers.rangei(23, 42)")], 33) + ]) + + +func test_scan_by_inheritance_class_name() -> void: + var scanner :GdUnitTestSuiteScanner = auto_free(GdUnitTestSuiteScanner.new()) + var test_suites := scanner.scan("res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/") + + assert_array(test_suites).has_size(3) + # sort by names + test_suites.sort_custom(func by_name(a, b): return a.get_name() <= b.get_name()) + assert_array(test_suites).extract("get_name")\ + .contains_exactly(["BaseTest", "ExtendedTest", "ExtendsExtendedTest"]) + assert_array(test_suites).extract("get_script.get_path")\ + .contains_exactly([ + "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd", + "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd", + "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd"]) + assert_array(test_suites[0].get_children()).extract("name")\ + .contains_same_exactly_in_any_order([&"test_foo1"]) + assert_array(test_suites[1].get_children()).extract("name")\ + .contains_same_exactly_in_any_order([&"test_foo2", &"test_foo1"]) + assert_array(test_suites[2].get_children()).extract("name")\ + .contains_same_exactly_in_any_order([&"test_foo3", &"test_foo2", &"test_foo1"]) + # finally free all scaned test suites + for ts in test_suites: + ts.free() + + +func test_scan_by_inheritance_class_path() -> void: + var scanner :GdUnitTestSuiteScanner = auto_free(GdUnitTestSuiteScanner.new()) + var test_suites := scanner.scan("res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/") + + assert_array(test_suites).extractv(extr("get_name"), extr("get_script.get_path"), extr("get_children.get_name"))\ + .contains_exactly_in_any_order([ + tuple("BaseTest", "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd", [&"test_foo1"]), + tuple("ExtendedTest","res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd", [&"test_foo2", &"test_foo1"]), + tuple("ExtendsExtendedTest", "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd", [&"test_foo3", &"test_foo2", &"test_foo1"]) + ]) + # finally free all scaned test suites + for ts in test_suites: + ts.free() + + +func test_get_test_case_line_number() -> void: + assert_int(GdUnitTestSuiteScanner.get_test_case_line_number("res://addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd", "get_test_case_line_number")).is_equal(317) + assert_int(GdUnitTestSuiteScanner.get_test_case_line_number("res://addons/gdUnit4/test/core/GdUnitTestSuiteScannerTest.gd", "unknown")).is_equal(-1) + + +func test__to_naming_convention() -> void: + ProjectSettings.set_setting(GdUnitSettings.TEST_SITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT) + assert_str(GdUnitTestSuiteScanner._to_naming_convention("MyClass")).is_equal("MyClassTest") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("my_class")).is_equal("my_class_test") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("myclass")).is_equal("myclass_test") + + ProjectSettings.set_setting(GdUnitSettings.TEST_SITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.SNAKE_CASE) + assert_str(GdUnitTestSuiteScanner._to_naming_convention("MyClass")).is_equal("my_class_test") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("my_class")).is_equal("my_class_test") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("myclass")).is_equal("myclass_test") + + ProjectSettings.set_setting(GdUnitSettings.TEST_SITE_NAMING_CONVENTION, GdUnitSettings.NAMING_CONVENTIONS.PASCAL_CASE) + assert_str(GdUnitTestSuiteScanner._to_naming_convention("MyClass")).is_equal("MyClassTest") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("my_class")).is_equal("MyClassTest") + assert_str(GdUnitTestSuiteScanner._to_naming_convention("myclass")).is_equal("MyclassTest") + + +func test_is_script_format_supported() -> void: + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.gd")).is_true() + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.gdns")).is_false() + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.vs")).is_false() + assert_bool(GdUnitTestSuiteScanner._is_script_format_supported("res://exampe.tres")).is_false() + + +func test_resolve_test_suite_path() -> void: + # forcing the use of a test folder next to the source folder + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/myclass.gd", "test")).is_equal("res://project/test/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/MyClass.gd", "test")).is_equal("res://project/test/folder/MyClassTest.gd") + # forcing to use source directory to create the test + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/myclass.gd", "")).is_equal("res://project/src/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/MyClass.gd", "")).is_equal("res://project/src/folder/MyClassTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/myclass.gd", "/")).is_equal("res://project/src/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/src/folder/MyClass.gd", "/")).is_equal("res://project/src/folder/MyClassTest.gd") + + +func test_resolve_test_suite_path_with_src_folders() -> void: + # forcing the use of a test folder next + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/myclass.gd", "test")).is_equal("res://test/project/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/MyClass.gd", "test")).is_equal("res://test/project/folder/MyClassTest.gd") + # forcing to use source directory to create the test + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/myclass.gd", "")).is_equal("res://project/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/MyClass.gd", "")).is_equal("res://project/folder/MyClassTest.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/myclass.gd", "/")).is_equal("res://project/folder/myclass_test.gd") + assert_str(GdUnitTestSuiteScanner.resolve_test_suite_path("res://project/folder/MyClass.gd", "/")).is_equal("res://project/folder/MyClassTest.gd") + + +func test_scan_test_suite_without_tests() -> void: + var scanner :GdUnitTestSuiteScanner = auto_free(GdUnitTestSuiteScanner.new()) + var test_suites := scanner.scan("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteWithoutTests.gd") + + assert_that(test_suites).is_empty() diff --git a/addons/gdUnit4/test/core/GdUnitToolsTest.gd b/addons/gdUnit4/test/core/GdUnitToolsTest.gd new file mode 100644 index 0000000..c17b5a7 --- /dev/null +++ b/addons/gdUnit4/test/core/GdUnitToolsTest.gd @@ -0,0 +1,49 @@ +# GdUnit generated TestSuite +class_name GdUnitToolsTest +extends GdUnitTestSuite + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/GdUnitTools.gd' + + +class InnerTestNodeClass extends Node: + pass + +class InnerTestRefCountedClass extends RefCounted: + pass + + +func test_free_instance() -> void: + # on valid instances + assert_that(await GdUnitTools.free_instance(RefCounted.new())).is_true() + assert_that(await GdUnitTools.free_instance(Node.new())).is_true() + assert_that(await GdUnitTools.free_instance(JavaClass.new())).is_true() + assert_that(await GdUnitTools.free_instance(InnerTestNodeClass.new())).is_true() + assert_that(await GdUnitTools.free_instance(InnerTestRefCountedClass.new())).is_true() + + # on invalid instances + assert_that(await GdUnitTools.free_instance(null)).is_false() + assert_that(await GdUnitTools.free_instance(RefCounted)).is_false() + + # on already freed instances + var node := Node.new() + node.free() + assert_that(await GdUnitTools.free_instance(node)).is_false() + + +func test_richtext_normalize() -> void: + assert_that(GdUnitTools.richtext_normalize("")).is_equal("") + assert_that(GdUnitTools.richtext_normalize("This is a Color Message")).is_equal("This is a Color Message") + + var message = """ + [color=green]line [/color][color=aqua]11:[/color] [color=#CD5C5C]Expecting:[/color] + must be empty but was + '[color=#1E90FF]after[/color]' + """ + assert_that(GdUnitTools.richtext_normalize(message)).is_equal(""" + line 11: Expecting: + must be empty but was + 'after' + """) diff --git a/addons/gdUnit4/test/core/LocalTimeTest.gd b/addons/gdUnit4/test/core/LocalTimeTest.gd new file mode 100644 index 0000000..458197b --- /dev/null +++ b/addons/gdUnit4/test/core/LocalTimeTest.gd @@ -0,0 +1,69 @@ +# GdUnit generated TestSuite +class_name LocalTimeTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/LocalTime.gd' + + +func test_time_constants(): + assert_int(LocalTime.MILLIS_PER_HOUR).is_equal(1000*60*60) + assert_int(LocalTime.MILLIS_PER_MINUTE).is_equal(1000*60) + assert_int(LocalTime.MILLIS_PER_SECOND).is_equal(1000) + assert_int(LocalTime.HOURS_PER_DAY).is_equal(24) + assert_int(LocalTime.MINUTES_PER_HOUR).is_equal(60) + assert_int(LocalTime.SECONDS_PER_MINUTE).is_equal(60) + + +func test_now(): + var current := Time.get_datetime_dict_from_system(true) + var local_time := LocalTime.now() + assert_int(local_time.hour()).is_equal(current.get("hour")) + assert_int(local_time.minute()).is_equal(current.get("minute")) + assert_int(local_time.second()).is_equal(current.get("second")) + # Time.get_datetime_dict_from_system() does not provide milliseconds + #assert_that(local_time.millis()).is_equal(0) + + +@warning_ignore("integer_division") +func test_of_unix_time(): + var time := LocalTime._get_system_time_msecs() + var local_time := LocalTime.of_unix_time(time) + assert_int(local_time.hour()).is_equal((time / LocalTime.MILLIS_PER_HOUR) % 24) + assert_int(local_time.minute()).is_equal((time / LocalTime.MILLIS_PER_MINUTE) % 60) + assert_int(local_time.second()).is_equal((time / LocalTime.MILLIS_PER_SECOND) % 60) + assert_int(local_time.millis()).is_equal(time % 1000) + + +func test_to_string(): + assert_str(LocalTime.local_time(10, 12, 22, 333)._to_string()).is_equal("10:12:22.333") + assert_str(LocalTime.local_time(23, 59, 59, 999)._to_string()).is_equal("23:59:59.999") + assert_str(LocalTime.local_time( 0, 0, 0, 000)._to_string()).is_equal("00:00:00.000") + assert_str(LocalTime.local_time( 2, 4, 3, 10)._to_string()).is_equal("02:04:03.010") + + +func test_plus_seconds(): + var time := LocalTime.local_time(10, 12, 22, 333) + assert_str(time.plus(LocalTime.TimeUnit.SECOND, 10)._to_string()).is_equal("10:12:32.333") + assert_str(time.plus(LocalTime.TimeUnit.SECOND, 27)._to_string()).is_equal("10:12:59.333") + assert_str(time.plus(LocalTime.TimeUnit.SECOND, 1)._to_string()).is_equal("10:13:00.333") + + # test overflow + var time2 := LocalTime.local_time(10, 59, 59, 333) + var start_time = time2._time + for iteration in 10000: + var t = LocalTime.of_unix_time(start_time) + var seconds:int = randi_range(0, 1000) + t.plus(LocalTime.TimeUnit.SECOND, seconds) + var expected := LocalTime.of_unix_time(start_time + (seconds * LocalTime.MILLIS_PER_SECOND)) + assert_str(t._to_string()).is_equal(expected._to_string()) + + +func test_elapsed(): + assert_str(LocalTime.elapsed(10)).is_equal("10ms") + assert_str(LocalTime.elapsed(201)).is_equal("201ms") + assert_str(LocalTime.elapsed(999)).is_equal("999ms") + assert_str(LocalTime.elapsed(1000)).is_equal("1s 0ms") + assert_str(LocalTime.elapsed(2000)).is_equal("2s 0ms") + assert_str(LocalTime.elapsed(3040)).is_equal("3s 40ms") + assert_str(LocalTime.elapsed(LocalTime.MILLIS_PER_MINUTE * 6 + 3040)).is_equal("6min 3s 40ms") diff --git a/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd b/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd new file mode 100644 index 0000000..04ad2a1 --- /dev/null +++ b/addons/gdUnit4/test/core/ParameterizedTestCaseTest.gd @@ -0,0 +1,296 @@ +#warning-ignore-all:unused_argument +class_name ParameterizedTestCaseTest +extends GdUnitTestSuite + +var _collected_tests = {} +var _expected_tests = { + "test_parameterized_bool_value" : [ + [0, false], + [1, true] + ], + "test_parameterized_int_values" : [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] + ], + "test_parameterized_float_values" : [ + [2.2, 2.2, 4.4], + [2.2, 2.3, 4.5], + [3.3, 2.2, 5.5] + ], + "test_parameterized_string_values" : [ + ["2.2", "2.2", "2.22.2"], + ["foo", "bar", "foobar"], + ["a", "b", "ab"] + ], + "test_parameterized_Vector2_values" : [ + [Vector2.ONE, Vector2.ONE, Vector2(2, 2)], + [Vector2.LEFT, Vector2.RIGHT, Vector2.ZERO], + [Vector2.ZERO, Vector2.LEFT, Vector2.LEFT] + ], + "test_parameterized_Vector3_values" : [ + [Vector3.ONE, Vector3.ONE, Vector3(2, 2, 2)], + [Vector3.LEFT, Vector3.RIGHT, Vector3.ZERO], + [Vector3.ZERO, Vector3.LEFT, Vector3.LEFT] + ], + "test_parameterized_obj_values" : [ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"] + ], + "test_parameterized_dict_values" : [ + [{"key_a":"value_a"}, '{"key_a":"value_a"}'], + [{"key_b":"value_b"}, '{"key_b":"value_b"}'] + ], + "test_with_dynamic_paramater_resolving" : [ + ["test_a"], + ["test_b"], + ["test_c"], + ["test_d"] + ], + "test_with_dynamic_paramater_resolving2" : [ + ["test_a"], + ["test_b"], + ["test_c"] + ], + "test_with_extern_parameter_set" : [ + ["test_a"], + ["test_b"], + ["test_c"] + ] +} + + +var _test_node_before :Node +var _test_node_before_test :Node + + +func before() -> void: + _test_node_before = auto_free(SubViewport.new()) + + +func before_test() -> void: + _test_node_before_test = auto_free(SubViewport.new()) + + +func after(): + for test_name in _expected_tests.keys(): + if _collected_tests.has(test_name): + var current_values = _collected_tests[test_name] + var expected_values = _expected_tests[test_name] + assert_that(current_values)\ + .override_failure_message("Expecting '%s' called with parameters:\n %s\n but was\n %s" % [test_name, expected_values, current_values])\ + .is_equal(expected_values) + else: + fail("Missing test '%s' executed!" % test_name) + + +func collect_test_call(test_name :String, values :Array) -> void: + if not _collected_tests.has(test_name): + _collected_tests[test_name] = Array() + _collected_tests[test_name].append(values) + + +@warning_ignore("unused_parameter") +func test_parameterized_bool_value(a: int, expected :bool, test_parameters := [ + [0, false], + [1, true]]): + collect_test_call("test_parameterized_bool_value", [a, expected]) + assert_that(bool(a)).is_equal(expected) + + +@warning_ignore("unused_parameter") +func test_parameterized_int_values(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] ]): + + collect_test_call("test_parameterized_int_values", [a, b, c, expected]) + assert_that(a+b+c).is_equal(expected) + + +@warning_ignore("unused_parameter") +func test_parameterized_float_values(a: float, b :float, expected :float, test_parameters := [ + [2.2, 2.2, 4.4], + [2.2, 2.3, 4.5], + [3.3, 2.2, 5.5] ]): + + collect_test_call("test_parameterized_float_values", [a, b, expected]) + assert_float(a+b).is_equal(expected) + + +@warning_ignore("unused_parameter") +func test_parameterized_string_values(a: String, b :String, expected :String, test_parameters := [ + ["2.2", "2.2", "2.22.2"], + ["foo", "bar", "foobar"], + ["a", "b", "ab"] ]): + + collect_test_call("test_parameterized_string_values", [a, b, expected]) + assert_that(a+b).is_equal(expected) + + +@warning_ignore("unused_parameter") +func test_parameterized_Vector2_values(a: Vector2, b :Vector2, expected :Vector2, test_parameters := [ + [Vector2.ONE, Vector2.ONE, Vector2(2, 2)], + [Vector2.LEFT, Vector2.RIGHT, Vector2.ZERO], + [Vector2.ZERO, Vector2.LEFT, Vector2.LEFT] ]): + + collect_test_call("test_parameterized_Vector2_values", [a, b, expected]) + assert_that(a+b).is_equal(expected) + + +@warning_ignore("unused_parameter") +func test_parameterized_Vector3_values(a: Vector3, b :Vector3, expected :Vector3, test_parameters := [ + [Vector3.ONE, Vector3.ONE, Vector3(2, 2, 2)], + [Vector3.LEFT, Vector3.RIGHT, Vector3.ZERO], + [Vector3.ZERO, Vector3.LEFT, Vector3.LEFT] ]): + + collect_test_call("test_parameterized_Vector3_values", [a, b, expected]) + assert_that(a+b).is_equal(expected) + + +class TestObj extends RefCounted: + var _value :String + + func _init(value :String): + _value = value + + func _to_string() -> String: + return _value + + +@warning_ignore("unused_parameter") +func test_parameterized_obj_values(a: Object, b :Object, expected :String, test_parameters := [ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"]]): + + collect_test_call("test_parameterized_obj_values", [a, b, expected]) + assert_that(a.to_string()+b.to_string()).is_equal(expected) + + +@warning_ignore("unused_parameter") +func test_parameterized_dict_values(data: Dictionary, expected :String, test_parameters := [ + [{"key_a" : "value_a"}, '{"key_a":"value_a"}'], + [{"key_b" : "value_b"}, '{"key_b":"value_b"}'] + ]): + collect_test_call("test_parameterized_dict_values", [data, expected]) + assert_that(str(data).replace(" ", "")).is_equal(expected) + + +@warning_ignore("unused_parameter") +func test_dictionary_div_number_types( + value : Dictionary, + expected : Dictionary, + test_parameters : Array = [ + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50, bottom = 50, left = 50, right = 50}], + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50, bottom = 50, left = 50, right = 50}], + ] +) -> void: + # allow to compare type unsave + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, false) + assert_that(value).is_equal(expected) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true) + + +@warning_ignore("unused_parameter") +func test_with_string_paramset( + values : Array, + expected : String, + test_parameters : Array = [ + [ ["a"], "a" ], + [ ["a", "very", "long", "argument"], "a very long argument" ], + ] +): + var current := " ".join(values) + assert_that(current.strip_edges()).is_equal(expected) + + +# https://github.com/MikeSchulze/gdUnit4/issues/213 +@warning_ignore("unused_parameter") +func test_with_string_contains_brackets( + test_index :int, + value :String, + test_parameters := [ + [1, "flowchart TD\nid>This is a flag shaped node]"], + [2, "flowchart TD\nid(((This is a double circle node)))"], + [3, "flowchart TD\nid((This is a circular node))"], + [4, "flowchart TD\nid>This is a flag shaped node]"], + [5, "flowchart TD\nid{'This is a rhombus node'}"], + [6, 'flowchart TD\nid((This is a circular node))'], + [7, 'flowchart TD\nid>This is a flag shaped node]'], + [8, 'flowchart TD\nid{"This is a rhombus node"}'], + [9, """ + flowchart TD + id{"This is a rhombus node"} + """], + ] +): + match test_index: + 1: assert_str(value).is_equal("flowchart TD\nid>This is a flag shaped node]") + 2: assert_str(value).is_equal("flowchart TD\nid(((This is a double circle node)))") + 3: assert_str(value).is_equal("flowchart TD\nid((This is a circular node))") + 4: assert_str(value).is_equal("flowchart TD\nid>" + "This is a flag shaped node]") + 5: assert_str(value).is_equal("flowchart TD\nid{'This is a rhombus node'}") + 6: assert_str(value).is_equal('flowchart TD\nid((This is a circular node))') + 7: assert_str(value).is_equal('flowchart TD\nid>This is a flag shaped node]') + 8: assert_str(value).is_equal('flowchart TD\nid{"This is a rhombus node"}') + 9: assert_str(value).is_equal(""" + flowchart TD + id{"This is a rhombus node"} + """) + + +func test_with_dynamic_parameter_resolving(name: String, value, expected, test_parameters := [ + ["test_a", auto_free(Node2D.new()), Node2D], + ["test_b", auto_free(Node3D.new()), Node3D], + ["test_c", _test_node_before, SubViewport], + ["test_d", _test_node_before_test, SubViewport], +]) -> void: + # all values must be resolved + assert_that(value).is_not_null().is_instanceof(expected) + if name == "test_c": + assert_that(value).is_same(_test_node_before) + if name == "test_d": + assert_that(value).is_same(_test_node_before_test) + # the argument 'test_parameters' must be replaced by set to avoid re-instantiate of test arguments + assert_that(test_parameters).is_empty() + collect_test_call("test_with_dynamic_paramater_resolving", [name]) + + +@warning_ignore("unused_parameter") +func test_with_dynamic_parameter_resolving2( + name: String, + type, + log_level, + expected_logs, + test_parameters = [ + ["test_a", null, "LOG", {}], + [ + "test_b", + Node2D, + null, + {Node2D: "ERROR"} + ], + [ + "test_c", + Node2D, + "LOG", + {Node2D: "LOG"} + ] + ] +): + # the argument 'test_parameters' must be replaced by set to avoid re-instantiate of test arguments + assert_that(test_parameters).is_empty() + collect_test_call("test_with_dynamic_paramater_resolving2", [name]) + + +var _test_set =[ + ["test_a"], + ["test_b"], + ["test_c"] +] + +@warning_ignore("unused_parameter") +func test_with_extern_parameter_set(value, test_parameters = _test_set): + assert_that(value).is_not_empty() + assert_that(test_parameters).is_empty() + collect_test_call("test_with_extern_parameter_set", [value]) diff --git a/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd b/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd new file mode 100644 index 0000000..231cba8 --- /dev/null +++ b/addons/gdUnit4/test/core/command/GdUnitCommandHandlerTest.gd @@ -0,0 +1,70 @@ + +# GdUnit generated TestSuite +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd' + +var _handler :GdUnitCommandHandler + + +func before(): + _handler = GdUnitCommandHandler.new() + + +func after(): + _handler._notification(NOTIFICATION_PREDELETE) + _handler = null + + +@warning_ignore('unused_parameter') +func test_create_shortcuts_defaults(shortcut :GdUnitShortcut.ShortCut, expected :String, test_parameters := [ + [GdUnitShortcut.ShortCut.RUN_TESTCASE, "GdUnitShortcutAction: RUN_TESTCASE (Ctrl+Alt+F5) -> Run TestCases"], + [GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG, "GdUnitShortcutAction: RUN_TESTCASE_DEBUG (Ctrl+Alt+F6) -> Run TestCases (Debug)"], + [GdUnitShortcut.ShortCut.RERUN_TESTS, "GdUnitShortcutAction: RERUN_TESTS (Ctrl+F5) -> ReRun Tests"], + [GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG, "GdUnitShortcutAction: RERUN_TESTS_DEBUG (Ctrl+F6) -> ReRun Tests (Debug)"], + [GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL, "GdUnitShortcutAction: RUN_TESTS_OVERALL (Ctrl+F7) -> Debug Overall TestSuites"], + [GdUnitShortcut.ShortCut.STOP_TEST_RUN, "GdUnitShortcutAction: STOP_TEST_RUN (Ctrl+F8) -> Stop Test Run"], + [GdUnitShortcut.ShortCut.CREATE_TEST, "GdUnitShortcutAction: CREATE_TEST (Ctrl+Alt+F10) -> Create TestCase"],]) -> void: + + if OS.get_name().to_lower() == "macos": + expected.replace("Ctrl", "Command") + + var action := _handler.get_shortcut_action(shortcut) + assert_that(str(action)).is_equal(expected) + + +## actually needs to comment out, it produces a lot of leaked instances +func _test__check_test_run_stopped_manually() -> void: + var inspector :GdUnitCommandHandler = mock(GdUnitCommandHandler, CALL_REAL_FUNC) + inspector._client_id = 1 + + # simulate no test is running + do_return(false).on(inspector).is_test_running_but_stop_pressed() + inspector.check_test_run_stopped_manually() + verify(inspector, 0).cmd_stop(any_int()) + + # simulate the test runner was manually stopped by the editor + do_return(true).on(inspector).is_test_running_but_stop_pressed() + inspector.check_test_run_stopped_manually() + verify(inspector, 1).cmd_stop(inspector._client_id) + + +func test_scan_test_directorys() -> void: + assert_array(GdUnitCommandHandler.scan_test_directorys("res://", "test", [])).contains_exactly([ + "res://addons/gdUnit4/test" + ]) + # for root folders + assert_array(GdUnitCommandHandler.scan_test_directorys("res://", "", [])).contains_exactly([ + "res://addons", "res://assets", "res://gdUnit3-examples" + ]) + assert_array(GdUnitCommandHandler.scan_test_directorys("res://", "/", [])).contains_exactly([ + "res://addons", "res://assets", "res://gdUnit3-examples" + ]) + assert_array(GdUnitCommandHandler.scan_test_directorys("res://", "res://", [])).contains_exactly([ + "res://addons", "res://assets", "res://gdUnit3-examples" + ]) + # a test folder not exists + assert_array(GdUnitCommandHandler.scan_test_directorys("res://", "notest", [])).is_empty() diff --git a/addons/gdUnit4/test/core/event/GdUnitEventTest.gd b/addons/gdUnit4/test/core/event/GdUnitEventTest.gd new file mode 100644 index 0000000..cbe8aa2 --- /dev/null +++ b/addons/gdUnit4/test/core/event/GdUnitEventTest.gd @@ -0,0 +1,22 @@ +# GdUnit generated TestSuite +class_name GdUnitEventTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/event/GdUnitEvent.gd' + + +func test_GdUnitEvent_defaults() -> void: + var event := GdUnitEvent.new() + + assert_bool(event.is_success()).is_true() + assert_bool(event.is_warning()).is_false() + assert_bool(event.is_failed()).is_false() + assert_bool(event.is_error()).is_false() + assert_bool(event.is_skipped()).is_false() + + assert_int(event.elapsed_time()).is_zero() + assert_int(event.orphan_nodes()).is_zero() + assert_int(event.total_count()).is_zero() + assert_int(event.failed_count()).is_zero() + assert_int(event.skipped_count()).is_zero() diff --git a/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd b/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd new file mode 100644 index 0000000..9f231e1 --- /dev/null +++ b/addons/gdUnit4/test/core/event/GdUnitTestEventSerdeTest.gd @@ -0,0 +1,51 @@ +# this test test for serialization and deserialization succcess +# of GdUnitEvent class +extends GdUnitTestSuite + + +func test_serde_suite_before(): + var event := GdUnitEvent.new().suite_before("path", "test_suite_a", 22) + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_instanceof(GdUnitEvent) + assert_that(deserialized).is_equal(event) + + +func test_serde_suite_after(): + var event := GdUnitEvent.new().suite_after("path","test_suite_a") + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + + +func test_serde_test_before(): + var event := GdUnitEvent.new().test_before("path", "test_suite_a", "test_foo") + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + + +func test_serde_test_after_no_report(): + var event := GdUnitEvent.new().test_after("path", "test_suite_a", "test_foo") + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + + +func test_serde_test_after_with_report(): + var reports :Array[GdUnitReport] = [\ + GdUnitReport.new().create(GdUnitReport.FAILURE, 24, "this is a error a"), \ + GdUnitReport.new().create(GdUnitReport.FAILURE, 26, "this is a error b")] + var event := GdUnitEvent.new().test_after("path", "test_suite_a", "test_foo", {}, reports) + + var serialized := event.serialize() + var deserialized := GdUnitEvent.new().deserialize(serialized) + assert_that(deserialized).is_equal(event) + assert_array(deserialized.reports()).contains_exactly(reports) + + +func test_serde_TestReport(): + var report := GdUnitReport.new().create(GdUnitReport.FAILURE, 24, "this is a error") + var serialized := report.serialize() + var deserialized := GdUnitReport.new().deserialize(serialized) + assert_that(deserialized).is_equal(report) diff --git a/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd b/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd new file mode 100644 index 0000000..429efc5 --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitExecutionContextTest.gd @@ -0,0 +1,222 @@ +# GdUnit generated TestSuite +class_name GdUnitExecutionContextTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd' + + +func add_report(ec :GdUnitExecutionContext, report :GdUnitReport) -> void: + ec._report_collector.on_reports(ec.get_instance_id(), report) + + +func assert_statistics(ec :GdUnitExecutionContext): + assert_that(ec.has_failures()).is_false() + assert_that(ec.has_errors()).is_false() + assert_that(ec.has_warnings()).is_false() + assert_that(ec.has_skipped()).is_false() + assert_that(ec.count_failures(true)).is_equal(0) + assert_that(ec.count_errors(true)).is_equal(0) + assert_that(ec.count_skipped(true)).is_equal(0) + assert_that(ec.count_orphans()).is_equal(0) + assert_dict(ec.build_report_statistics(0))\ + .contains_key_value(GdUnitEvent.FAILED, false)\ + .contains_key_value(GdUnitEvent.ERRORS, false)\ + .contains_key_value(GdUnitEvent.WARNINGS, false)\ + .contains_key_value(GdUnitEvent.SKIPPED, false)\ + .contains_key_value(GdUnitEvent.ORPHAN_NODES, 0)\ + .contains_key_value(GdUnitEvent.FAILED_COUNT, 0)\ + .contains_key_value(GdUnitEvent.ERROR_COUNT, 0)\ + .contains_key_value(GdUnitEvent.SKIPPED_COUNT, 0)\ + .contains_keys([GdUnitEvent.ELAPSED_TIME]) + + +func test_create_context_of_test_suite() -> void: + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var ec := GdUnitExecutionContext.of_test_suite(ts) + # verify the current context is not affected by this test itself + assert_object(__execution_context).is_not_same(ec) + + # verify the execution context is assigned to the test suite + assert_object(ts.__execution_context).is_same(ec) + + # verify execution context is fully initialized + assert_that(ec).is_not_null() + assert_object(ec.test_suite).is_same(ts) + assert_object(ec.test_case).is_null() + assert_array(ec._sub_context).is_empty() + assert_object(ec._orphan_monitor).is_not_null() + assert_object(ec._memory_observer).is_not_null() + assert_object(ec._report_collector).is_not_null() + assert_statistics(ec) + ec.dispose() + + +func test_create_context_of_test_case() -> void: + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(_TestCase.new().configure("test_case1", 0, "")) + ts.add_child(tc) + var ec1 := GdUnitExecutionContext.of_test_suite(ts) + var ec2 := GdUnitExecutionContext.of_test_case(ec1, "test_case1") + # verify the current context is not affected by this test itself + assert_object(__execution_context).is_not_same(ec1) + assert_object(__execution_context).is_not_same(ec2) + + # verify current execution contest is assigned to the test suite + assert_object(ts.__execution_context).is_same(ec2) + # verify execution context is fully initialized + assert_that(ec2).is_not_null() + assert_object(ec2.test_suite).is_same(ts) + assert_object(ec2.test_case).is_same(tc) + assert_array(ec2._sub_context).is_empty() + assert_object(ec2._orphan_monitor).is_not_null().is_not_same(ec1._orphan_monitor) + assert_object(ec2._memory_observer).is_not_null().is_not_same(ec1._memory_observer) + assert_object(ec2._report_collector).is_not_null().is_not_same(ec1._report_collector) + assert_statistics(ec2) + # check parent context ec1 is still valid + assert_that(ec1).is_not_null() + assert_object(ec1.test_suite).is_same(ts) + assert_object(ec1.test_case).is_null() + assert_array(ec1._sub_context).contains_exactly([ec2]) + assert_object(ec1._orphan_monitor).is_not_null() + assert_object(ec1._memory_observer).is_not_null() + assert_object(ec1._report_collector).is_not_null() + assert_statistics(ec1) + ec1.dispose() + + +func test_create_context_of_test() -> void: + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(_TestCase.new().configure("test_case1", 0, "")) + ts.add_child(tc) + var ec1 := GdUnitExecutionContext.of_test_suite(ts) + var ec2 := GdUnitExecutionContext.of_test_case(ec1, "test_case1") + var ec3 := GdUnitExecutionContext.of(ec2) + # verify the current context is not affected by this test itself + assert_object(__execution_context).is_not_same(ec1) + assert_object(__execution_context).is_not_same(ec2) + assert_object(__execution_context).is_not_same(ec3) + + # verify current execution contest is assigned to the test suite + assert_object(ts.__execution_context).is_same(ec3) + # verify execution context is fully initialized + assert_that(ec3).is_not_null() + assert_object(ec3.test_suite).is_same(ts) + assert_object(ec3.test_case).is_same(tc) + assert_array(ec3._sub_context).is_empty() + assert_object(ec3._orphan_monitor).is_not_null()\ + .is_not_same(ec1._orphan_monitor)\ + .is_not_same(ec2._orphan_monitor) + assert_object(ec3._memory_observer).is_not_null()\ + .is_not_same(ec1._memory_observer)\ + .is_not_same(ec2._memory_observer) + assert_object(ec3._report_collector).is_not_null()\ + .is_not_same(ec1._report_collector)\ + .is_not_same(ec2._report_collector) + assert_statistics(ec3) + # check parent context ec2 is still valid + assert_that(ec2).is_not_null() + assert_object(ec2.test_suite).is_same(ts) + assert_object(ec2.test_case).is_same(tc) + assert_array(ec2._sub_context).contains_exactly([ec3]) + assert_object(ec2._orphan_monitor).is_not_null()\ + .is_not_same(ec1._orphan_monitor) + assert_object(ec2._memory_observer).is_not_null()\ + .is_not_same(ec1._memory_observer) + assert_object(ec2._report_collector).is_not_null()\ + .is_not_same(ec1._report_collector) + assert_statistics(ec2) + # check parent context ec1 is still valid + assert_that(ec1).is_not_null() + assert_object(ec1.test_suite).is_same(ts) + assert_object(ec1.test_case).is_null() + assert_array(ec1._sub_context).contains_exactly([ec2]) + assert_object(ec1._orphan_monitor).is_not_null() + assert_object(ec1._memory_observer).is_not_null() + assert_object(ec1._report_collector).is_not_null() + assert_statistics(ec1) + ec1.dispose() + + +func test_report_collectors() -> void: + # setup + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(_TestCase.new().configure("test_case1", 0, "")) + ts.add_child(tc) + var ec1 := GdUnitExecutionContext.of_test_suite(ts) + var ec2 := GdUnitExecutionContext.of_test_case(ec1, "test_case1") + var ec3 := GdUnitExecutionContext.of(ec2) + + # add reports + var failure11 := GdUnitReport.new().create(GdUnitReport.FAILURE, 1, "error_ec11") + add_report(ec1, failure11) + var failure21 := GdUnitReport.new().create(GdUnitReport.FAILURE, 3, "error_ec21") + var failure22 := GdUnitReport.new().create(GdUnitReport.FAILURE, 3, "error_ec22") + add_report(ec2, failure21) + add_report(ec2, failure22) + var failure31 := GdUnitReport.new().create(GdUnitReport.FAILURE, 3, "error_ec31") + var failure32 := GdUnitReport.new().create(GdUnitReport.FAILURE, 3, "error_ec32") + var failure33 := GdUnitReport.new().create(GdUnitReport.FAILURE, 3, "error_ec33") + add_report(ec3, failure31) + add_report(ec3, failure32) + add_report(ec3, failure33) + # verify + assert_array(ec1.reports()).contains_exactly([failure11]) + assert_array(ec2.reports()).contains_exactly([failure21, failure22]) + assert_array(ec3.reports()).contains_exactly([failure31, failure32, failure33]) + ec1.dispose() + + +func test_has_and_count_failures() -> void: + # setup + var ts :GdUnitTestSuite = auto_free(GdUnitTestSuite.new()) + var tc :_TestCase = auto_free(_TestCase.new().configure("test_case1", 0, "")) + ts.add_child(tc) + var ec1 := GdUnitExecutionContext.of_test_suite(ts) + var ec2 := GdUnitExecutionContext.of_test_case(ec1, "test_case1") + var ec3 := GdUnitExecutionContext.of(ec2) + + # precheck + assert_that(ec1.has_failures()).is_false() + assert_that(ec1.count_failures(true)).is_equal(0) + assert_that(ec2.has_failures()).is_false() + assert_that(ec2.count_failures(true)).is_equal(0) + assert_that(ec3.has_failures()).is_false() + assert_that(ec3.count_failures(true)).is_equal(0) + + # add four failure report to test + add_report(ec3, GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "error_ec31")) + add_report(ec3, GdUnitReport.new().create(GdUnitReport.FAILURE, 43, "error_ec32")) + add_report(ec3, GdUnitReport.new().create(GdUnitReport.FAILURE, 44, "error_ec33")) + add_report(ec3, GdUnitReport.new().create(GdUnitReport.FAILURE, 45, "error_ec34")) + # verify + assert_that(ec1.has_failures()).is_true() + assert_that(ec1.count_failures(true)).is_equal(4) + assert_that(ec2.has_failures()).is_true() + assert_that(ec2.count_failures(true)).is_equal(4) + assert_that(ec3.has_failures()).is_true() + assert_that(ec3.count_failures(true)).is_equal(4) + + # add two failure report to test_case_stage + add_report(ec2, GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "error_ec21")) + add_report(ec2, GdUnitReport.new().create(GdUnitReport.FAILURE, 43, "error_ec22")) + # verify + assert_that(ec1.has_failures()).is_true() + assert_that(ec1.count_failures(true)).is_equal(6) + assert_that(ec2.has_failures()).is_true() + assert_that(ec2.count_failures(true)).is_equal(6) + assert_that(ec3.has_failures()).is_true() + assert_that(ec3.count_failures(true)).is_equal(4) + + # add one failure report to test_suite_stage + add_report(ec1, GdUnitReport.new().create(GdUnitReport.FAILURE, 42, "error_ec1")) + # verify + assert_that(ec1.has_failures()).is_true() + assert_that(ec1.count_failures(true)).is_equal(7) + assert_that(ec2.has_failures()).is_true() + assert_that(ec2.count_failures(true)).is_equal(6) + assert_that(ec3.has_failures()).is_true() + assert_that(ec3.count_failures(true)).is_equal(4) + ec1.dispose() diff --git a/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd b/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd new file mode 100644 index 0000000..71af96d --- /dev/null +++ b/addons/gdUnit4/test/core/execution/GdUnitTestSuiteExecutorTest.gd @@ -0,0 +1,708 @@ +# GdUnit generated TestSuite +class_name GdUnitTestSuiteExecutorTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd' +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +const SUCCEEDED = true +const FAILED = false +const SKIPPED = true +const NOT_SKIPPED = false + +var _collected_events :Array[GdUnitEvent] = [] + + +func before() -> void: + GdUnitSignals.instance().gdunit_event_debug.connect(_on_gdunit_event_debug) + + +func after() -> void: + GdUnitSignals.instance().gdunit_event_debug.disconnect(_on_gdunit_event_debug) + + +func after_test(): + _collected_events.clear() + + +func _on_gdunit_event_debug(event :GdUnitEvent) -> void: + _collected_events.append(event) + + +func _load(resource_path :String) -> GdUnitTestSuite: + return GdUnitTestResourceLoader.load_test_suite(resource_path) as GdUnitTestSuite + + +func flating_message(message :String) -> String: + return GdUnitTools.richtext_normalize(message) + + +func execute(test_suite :GdUnitTestSuite) -> Array[GdUnitEvent]: + await GdUnitThreadManager.run("test_executor", func(): + var executor := GdUnitTestSuiteExecutor.new(true) + await executor.execute(test_suite)) + return _collected_events + + +func assert_event_list(events :Array[GdUnitEvent], suite_name :String, test_case_names :Array[String]) -> void: + var expected_events := Array() + expected_events.append(tuple(GdUnitEvent.TESTSUITE_BEFORE, suite_name, "before", test_case_names.size())) + for test_case in test_case_names: + expected_events.append(tuple(GdUnitEvent.TESTCASE_BEFORE, suite_name, test_case, 0)) + expected_events.append(tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, test_case, 0)) + expected_events.append(tuple(GdUnitEvent.TESTSUITE_AFTER, suite_name, "after", 0)) + + var expected_event_count := 2 + test_case_names.size() * 2 + assert_array(events)\ + .override_failure_message("Expecting be %d events emitted, but counts %d." % [expected_event_count, events.size()])\ + .has_size(expected_event_count) + + assert_array(events)\ + .extractv(extr("type"), extr("suite_name"), extr("test_name"), extr("total_count"))\ + .contains_exactly(expected_events) + + +func assert_event_counters(events :Array[GdUnitEvent]) -> GdUnitArrayAssert: + return assert_array(events).extractv(extr("type"), extr("error_count"), extr("failed_count"), extr("orphan_nodes")) + + +func assert_event_states(events :Array[GdUnitEvent]) -> GdUnitArrayAssert: + return assert_array(events).extractv(extr("test_name"), extr("is_success"), extr("is_skipped"), extr("is_warning"), extr("is_failed"), extr("is_error")) + + +func assert_event_reports(events :Array[GdUnitEvent], expected_reports :Array) -> void: + for event_index in events.size(): + var current :Array = events[event_index].reports() + var expected = expected_reports[event_index] if expected_reports.size() > event_index else [] + if expected.is_empty(): + for m in current.size(): + assert_str(flating_message(current[m].message())).is_empty() + + for m in expected.size(): + if m < current.size(): + assert_str(flating_message(current[m].message())).is_equal(expected[m]) + else: + assert_str("").is_equal(expected[m]) + + +func test_execute_success() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource") + # simulate test suite execution + var events := await execute(test_suite) + assert_event_list(events,\ + "TestSuiteAllStagesSuccess",\ + ["test_case1", "test_case2"]) + # verify all counters are zero / no errors, failures, orphans + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("after", SUCCEEDED, NOT_SKIPPED, false, false, false), + ]) + # all success no reports expected + assert_event_reports(events, [ + [], [], [], [], [], [] + ]) + + +func test_execute_failure_on_stage_before() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailOnStageBefore",\ + ["test_case1", "test_case2"]) + # we expect the testsuite is failing on stage 'before()' and commits one failure + # reported finally at TESTSUITE_AFTER event + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + # report failure failed_count = 1 + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 1, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + # report suite is not success, is failed + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # one failure at before() + assert_event_reports(events, [ + [], + [], + [], + [], + [], + ["failed on before()"] + ]) + + +func test_execute_failure_on_stage_after() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailOnStageAfter",\ + ["test_case1", "test_case2"]) + # we expect the testsuite is failing on stage 'before()' and commits one failure + # reported finally at TESTSUITE_AFTER event + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + # report failure failed_count = 1 + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 1, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + # report suite is not success, is failed + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # one failure at after() + assert_event_reports(events, [ + [], + [], + [], + [], + [], + ["failed on after()"] + ]) + + +func test_execute_failure_on_stage_before_test() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailOnStageBeforeTest",\ + ["test_case1", "test_case2"]) + # we expect the testsuite is failing on stage 'before_test()' and commits one failure on each test case + # because is in scope of test execution + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # failure is count to the test + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # failure is count to the test + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", FAILED, NOT_SKIPPED, false, true, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", FAILED, NOT_SKIPPED, false, true, false), + # report suite is not success, is failed + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # before_test() failure report is append to each test + assert_event_reports(events, [ + [], + [], + # verify failure report is append to 'test_case1' + ["failed on before_test()"], + [], + # verify failure report is append to 'test_case2' + ["failed on before_test()"], + [] + ]) + + +func test_execute_failure_on_stage_after_test() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailOnStageAfterTest",\ + ["test_case1", "test_case2"]) + # we expect the testsuite is failing on stage 'after_test()' and commits one failure on each test case + # because is in scope of test execution + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # failure is count to the test + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # failure is count to the test + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", FAILED, NOT_SKIPPED, false, true, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", FAILED, NOT_SKIPPED, false, true, false), + # report suite is not success, is failed + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # 'after_test' failure report is append to each test + assert_event_reports(events, [ + [], + [], + # verify failure report is append to 'test_case1' + ["failed on after_test()"], + [], + # verify failure report is append to 'test_case2' + ["failed on after_test()"], + [] + ]) + + +func test_execute_failure_on_stage_test_case1() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailOnStageTestCase1",\ + ["test_case1", "test_case2"]) + # we expect the test case 'test_case1' is failing and commits one failure + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # test has one failure + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", FAILED, NOT_SKIPPED, false, true, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + # report suite is not success, is failed + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # only 'test_case1' reports a failure + assert_event_reports(events, [ + [], + [], + # verify failure report is append to 'test_case1' + ["failed on test_case1()"], + [], + [], + [] + ]) + + +func test_execute_failure_on_multiple_stages() -> void: + # this is a more complex failure state, we expect to find multipe failures on different stages + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailOnMultipeStages",\ + ["test_case1", "test_case2"]) + # we expect failing on multiple stages + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the first test has two failures plus one from 'before_test' + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 3, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the second test has no failures but one from 'before_test' + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + # and one failure is on stage 'after' found + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 1, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", FAILED, NOT_SKIPPED, false, true, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", FAILED, NOT_SKIPPED, false, true, false), + # report suite is not success, is failed + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # only 'test_case1' reports a 'real' failures plus test setup stage failures + assert_event_reports(events, [ + [], + [], + # verify failure reports to 'test_case1' + ["failed on before_test()", "failed 1 on test_case1()", "failed 2 on test_case1()"], + [], + # verify failure reports to 'test_case2' + ["failed on before_test()"], + # and one failure detected at stage 'after' + ["failed on after()"] + ]) + + +# GD-63 +func test_execute_failure_and_orphans() -> void: + # this is a more complex failure state, we expect to find multipe orphans on different stages + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAndOrpahnsDetected.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailAndOrpahnsDetected",\ + ["test_case1", "test_case2"]) + # we expect failing on multiple stages + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the first test ends with a warning and in summ 5 orphans detected + # 2 from stage 'before_test' + 3 from test itself + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 5), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # the second test ends with a one failure and in summ 6 orphans detected + # 2 from stage 'before_test' + 4 from test itself + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 6), + # and one orphan detected from stage 'before' + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 1), + ]) + # is_success, is_warning, is_failed, is_error + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + # test case has only warnings + tuple("test_case1", FAILED, NOT_SKIPPED, true, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + # test case has failures and warnings + tuple("test_case2", FAILED, NOT_SKIPPED, true, true, false), + # report suite is not success, has warnings and failures + tuple("after", FAILED, NOT_SKIPPED, true, true, false), + ]) + # only 'test_case1' reports a 'real' failures plus test setup stage failures + assert_event_reports(events, [ + [], + [], + # ends with warnings + ["WARNING:\n Detected <2> orphan nodes during test setup! Check before_test() and after_test()!", + "WARNING:\n Detected <3> orphan nodes during test execution!"], + [], + # ends with failure and warnings + ["WARNING:\n Detected <2> orphan nodes during test setup! Check before_test() and after_test()!", + "WARNING:\n Detected <4> orphan nodes during test execution!", + "faild on test_case2()"], + # and one failure detected at stage 'after' + ["WARNING:\n Detected <1> orphan nodes during test suite setup stage! Check before() and after()!"] + ]) + + +func test_execute_failure_and_orphans_report_orphan_disabled() -> void: + # this is a more complex failure state, we expect to find multipe orphans on different stages + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAndOrpahnsDetected.resource") + # simulate test suite execution whit disabled orphan detection + + ProjectSettings.set_setting(GdUnitSettings.REPORT_ORPHANS, false) + var events = await execute(test_suite) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ORPHANS, true) + + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailAndOrpahnsDetected",\ + ["test_case1", "test_case2"]) + # we expect failing on multiple stages, no orphans reported + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # one failure + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # is_success, is_warning, is_failed, is_error + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + # test case has success + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + # test case has a failure + tuple("test_case2", FAILED, NOT_SKIPPED, false, true, false), + # report suite is not success, has warnings and failures + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # only 'test_case1' reports a failure, orphans are not reported + assert_event_reports(events, [ + [], + [], + [], + [], + # ends with a failure + ["faild on test_case2()"], + [] + ]) + + +func test_execute_error_on_test_timeout() -> void: + # this tests a timeout on a test case reported as error + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteErrorOnTestTimeout",\ + ["test_case1", "test_case2"]) + # we expect test_case1 fails by a timeout + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + # the first test timed out after 2s + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 1, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + # testcase ends with a timeout error + tuple("test_case1", FAILED, NOT_SKIPPED, false, false, true), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + # report suite is not success, is error + tuple("after", FAILED, NOT_SKIPPED, false, false, true), + ]) + # 'test_case1' reports a error triggered by test timeout + assert_event_reports(events, [ + [], + [], + # verify error reports to 'test_case1' + ["Timeout !\n 'Test timed out after 2s 0ms'"], + [], + [], + [] + ]) + + +# This test checks if all test stages are called at each test iteration. +func test_execute_fuzzed_metrics() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.resource") + + var events = await execute(test_suite) + assert_event_states(events).contains([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("after", SUCCEEDED, NOT_SKIPPED, false, false, false), + ]) + assert_event_reports(events, [ + [], + [], + [], + [], + [], + [] + ]) + + +# This test checks if all test stages are called at each test iteration. +func test_execute_parameterized_metrics() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.resource") + + var events = await execute(test_suite) + assert_event_states(events).contains([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("after", SUCCEEDED, NOT_SKIPPED, false, false, false), + ]) + assert_event_reports(events, [ + [], + [], + [], + [], + [], + [] + ]) + + +func test_execute_failure_fuzzer_iteration() -> void: + # this tests a timeout on a test case reported as error + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource") + + # simulate test suite execution + var events = await execute(test_suite) + + # verify basis infos + assert_event_list(events, "GdUnitFuzzerTest", [ + "test_multi_yielding_with_fuzzer", + "test_multi_yielding_with_fuzzer_fail_after_3_iterations"]) + # we expect failing at 'test_multi_yielding_with_fuzzer_fail_after_3_iterations' after three iterations + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + # test failed after 3 iterations + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 1, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + # is_success, is_warning, is_failed, is_error + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_multi_yielding_with_fuzzer", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_multi_yielding_with_fuzzer", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_multi_yielding_with_fuzzer_fail_after_3_iterations", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_multi_yielding_with_fuzzer_fail_after_3_iterations", FAILED, NOT_SKIPPED, false, true, false), + tuple("after", FAILED, NOT_SKIPPED, false, true, false), + ]) + # 'test_case1' reports a error triggered by test timeout + assert_event_reports(events, [ + [], + [], + [], + [], + # must fail after three iterations + ["Found an error after '3' test iterations\n Expecting: 'false' but is 'true'"], + [] + ]) + + +func test_execute_add_child_on_before_GD_106() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource") + # simulate test suite execution + var events = await execute(test_suite) + # verify basis infos + assert_event_list(events,\ + "TestSuiteFailAddChildStageBefore",\ + ["test_case1", "test_case2"]) + # verify all counters are zero / no errors, failures, orphans + assert_event_counters(events).contains_exactly([ + tuple(GdUnitEvent.TESTSUITE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_BEFORE, 0, 0, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, 0, 0, 0), + tuple(GdUnitEvent.TESTSUITE_AFTER, 0, 0, 0), + ]) + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("after", SUCCEEDED, NOT_SKIPPED, false, false, false), + ]) + # all success no reports expected + assert_event_reports(events, [ + [], [], [], [], [], [] + ]) + + +func test_execute_parameterizied_tests() -> void: + # this is a more complex failure state, we expect to find multipe failures on different stages + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource") + # simulate test suite execution + # run the tests with to compare type save + var original_mode = ProjectSettings.get_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true) + var events = await execute(test_suite) + var suite_name = "TestSuiteParameterizedTests" + # the test is partial failing because of diverent type in the dictionary + assert_array(events).extractv( + extr("type"), extr("suite_name"), TestCaseNameExtractor.new(), extr("is_error"), extr("is_failed"), extr("orphan_nodes"))\ + .contains([ + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:0", false, true, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:1", false, false, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:2", false, true, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:3", false, false, 0) + ]) + + # rerun the same tests again with allow to compare type unsave + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, false) + # simulate test suite execution + test_suite = _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource") + events = await execute(test_suite) + ProjectSettings.set_setting(GdUnitSettings.REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, original_mode) + + # the test should now be successful + assert_array(events).extractv( + extr("type"), extr("suite_name"), TestCaseNameExtractor.new(), extr("is_error"), extr("is_failed"), extr("orphan_nodes"))\ + .contains([ + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:0", false, false, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:1", false, false, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:2", false, false, 0), + tuple(GdUnitEvent.TESTCASE_AFTER, suite_name, "test_dictionary_div_number_types:3", false, false, 0) + ]) + + +func test_execute_test_suite_is_skipped() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource") + # simulate test suite execution + var events = await execute(test_suite) + # the entire test-suite is skipped + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("after", FAILED, SKIPPED, false, false, false), + ]) + assert_event_reports(events, [ + [], + # must fail after three iterations + [""" + Entire test-suite is skipped! + Tests skipped: '2' + Reason: '"do not run this"' + """.dedent().trim_prefix("\n")] + ]) + + +func test_execute_test_case_is_skipped() -> void: + var test_suite := _load("res://addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource") + # simulate test suite execution + var events = await execute(test_suite) + # the test_case1 is skipped + assert_event_states(events).contains_exactly([ + tuple("before", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case1", FAILED, SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("test_case2", SUCCEEDED, NOT_SKIPPED, false, false, false), + tuple("after", SUCCEEDED, NOT_SKIPPED, false, false, false), + ]) + + assert_event_reports(events, [ + [], + [], + [""" + This test is skipped! + Reason: '"do not run this"' + """.dedent().trim_prefix("\n")], + [], + [], + [] + ]) + + +class TestCaseNameExtractor extends GdUnitValueExtractor: + var r := RegEx.create_from_string("^.*:\\d") + + func extract_value(value): + var m := r.search(value.test_name()) + return m.get_string(0) if m != null else value.test_name() diff --git a/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd b/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd new file mode 100644 index 0000000..ae64c8d --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdDefaultValueDecoderTest.gd @@ -0,0 +1,255 @@ +# GdUnit generated TestSuite +class_name GdDefaultValueDecoderTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd' + + +var _tested_types = {} + + +func after(): + # we verify we have covered all variant types + for type_id in TYPE_MAX: + if type_id == TYPE_OBJECT: + continue + assert_that(_tested_types.get(type_id))\ + .override_failure_message("Missing Variant type '%s'" % GdObjects.type_as_string(type_id))\ + .is_not_null() + + +@warning_ignore("unused_parameter") +func test_decode_Primitives(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_NIL, null, "null"], + [TYPE_BOOL, true, "true"], + [TYPE_BOOL, false, "false"], + [TYPE_INT, -100, "-100"], + [TYPE_INT, 0, "0"], + [TYPE_INT, 100, "100"], + [TYPE_FLOAT, -100.123, "-100.123000"], + [TYPE_FLOAT, 0.00, "0.000000"], + [TYPE_FLOAT, 100, "100.000000"], + [TYPE_FLOAT, 100.123, "100.123000"], + [TYPE_STRING, "hello", '"hello"'], + [TYPE_STRING, "", '""'], + [TYPE_STRING_NAME, StringName("hello"), 'StringName("hello")'], + [TYPE_STRING_NAME, StringName(""), 'StringName()'], + ]) -> void: + + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Vectors(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_VECTOR2, Vector2(), "Vector2()"], + [TYPE_VECTOR2, Vector2(1,2), "Vector2(1, 2)"], + [TYPE_VECTOR2I, Vector2i(), "Vector2i()"], + [TYPE_VECTOR2I, Vector2i(1,2), "Vector2i(1, 2)"], + [TYPE_VECTOR3, Vector3(), "Vector3()"], + [TYPE_VECTOR3, Vector3(1,2,3), "Vector3(1, 2, 3)"], + [TYPE_VECTOR3I, Vector3i(), "Vector3i()"], + [TYPE_VECTOR3I, Vector3i(1,2,3), "Vector3i(1, 2, 3)"], + [TYPE_VECTOR4, Vector4(), "Vector4()"], + [TYPE_VECTOR4, Vector4(1,2,3,4), "Vector4(1, 2, 3, 4)"], + [TYPE_VECTOR4I, Vector4i(), "Vector4i()"], + [TYPE_VECTOR4I, Vector4i(1,2,3,4), "Vector4i(1, 2, 3, 4)"], + ]) -> void: + + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Rect2(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_RECT2, Rect2(), "Rect2()"], + [TYPE_RECT2, Rect2(1,2, 10,20), "Rect2(Vector2(1, 2), Vector2(10, 20))"], + [TYPE_RECT2I, Rect2i(), "Rect2i()"], + [TYPE_RECT2I, Rect2i(1,2, 10,20), "Rect2i(Vector2i(1, 2), Vector2i(10, 20))"], + ]) -> void: + + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Transforms(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_TRANSFORM2D, Transform2D(), + "Transform2D()"], + [TYPE_TRANSFORM2D, Transform2D(2.0, Vector2(1,2)), + "Transform2D(Vector2(-0.416147, 0.909297), Vector2(-0.909297, -0.416147), Vector2(1, 2))"], + [TYPE_TRANSFORM2D, Transform2D(2.0, Vector2(1,2), 2.0, Vector2(3,4)), + "Transform2D(Vector2(-0.416147, 0.909297), Vector2(1.513605, -1.307287), Vector2(3, 4))"], + [TYPE_TRANSFORM2D, Transform2D(Vector2(1,2), Vector2(3,4), Vector2.ONE), + "Transform2D(Vector2(1, 2), Vector2(3, 4), Vector2(1, 1))"], + [TYPE_TRANSFORM3D, Transform3D(), + "Transform3D()"], + [TYPE_TRANSFORM3D, Transform3D(Basis.FLIP_X, Vector3.ONE), + "Transform3D(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, 1), Vector3(1, 1, 1))"], + [TYPE_TRANSFORM3D, Transform3D(Vector3(1,2,3), Vector3(4,5,6), Vector3(7,8,9), Vector3.ONE), + "Transform3D(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9), Vector3(1, 1, 1))"], + [TYPE_PROJECTION, Projection(), + "Projection(Vector4(1, 0, 0, 0), Vector4(0, 1, 0, 0), Vector4(0, 0, 1, 0), Vector4(0, 0, 0, 1))"], + [TYPE_PROJECTION, Projection(Vector4.ONE, Vector4.ONE*2, Vector4.ONE*3, Vector4.ZERO), + "Projection(Vector4(1, 1, 1, 1), Vector4(2, 2, 2, 2), Vector4(3, 3, 3, 3), Vector4(0, 0, 0, 0))"] + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Plane(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_PLANE, Plane(), "Plane()"], + [TYPE_PLANE, Plane(1,2,3,4), "Plane(1, 2, 3, 4)"], + [TYPE_PLANE, Plane(Vector3.ONE, Vector3.ZERO), "Plane(1, 1, 1, 0)"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Quaternion(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_QUATERNION, Quaternion(), "Quaternion()"], + [TYPE_QUATERNION, Quaternion(1,2,3,4), "Quaternion(1, 2, 3, 4)"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_AABB(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_AABB, AABB(), "AABB()"], + [TYPE_AABB, AABB(Vector3.ONE, Vector3(10,20,30)), "AABB(Vector3(1, 1, 1), Vector3(10, 20, 30))"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Basis(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_BASIS, Basis(), "Basis()"], + [TYPE_BASIS, Basis(Vector3(0.1,0.2,0.3).normalized(), .1), + "Basis(Vector3(0.995361, 0.080758, -0.052293), Vector3(-0.079331, 0.996432, 0.028823), Vector3(0.054434, -0.024541, 0.998216))"], + [TYPE_BASIS, Basis(Vector3.ONE, Vector3.ONE*2, Vector3.ONE*3), + "Basis(Vector3(1, 1, 1), Vector3(2, 2, 2), Vector3(3, 3, 3))"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Color(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_COLOR, Color(), "Color()"], + [TYPE_COLOR, Color.RED, "Color(1, 0, 0, 1)"], + [TYPE_COLOR, Color(1,.2,.5,.5), "Color(1, 0.2, 0.5, 0.5)"], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_NodePath(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_NODE_PATH, NodePath(), 'NodePath()'], + [TYPE_NODE_PATH, NodePath("/foo/bar"), 'NodePath("/foo/bar")'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_RID(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_RID, RID(), 'RID()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func _test_decode_Object(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_OBJECT, Node.new(), 'Node.new()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Callable(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_CALLABLE, Callable(), 'Callable()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Signal(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_SIGNAL, Signal(), 'Signal()'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Dictionary(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_DICTIONARY, {}, '{}'], + [TYPE_DICTIONARY, Dictionary(), '{}'], + [TYPE_DICTIONARY, {1:2, 2:3}, '{ 1: 2, 2: 3 }'], + [TYPE_DICTIONARY, {"aa":2, "bb":"cc"}, '{ "aa": 2, "bb": "cc" }'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_Array(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_ARRAY, [], '[]'], + [TYPE_ARRAY, Array(), '[]'], + [TYPE_ARRAY, [1,2,3], '[1, 2, 3]'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 + + +@warning_ignore("unused_parameter") +func test_decode_typedArrays(variant_type :int, value, expected :String, test_parameters := [ + [TYPE_PACKED_BYTE_ARRAY, PackedByteArray(), + 'PackedByteArray()'], + [TYPE_PACKED_BYTE_ARRAY, PackedByteArray([1, 2, 3]), + 'PackedByteArray([1, 2, 3])'], + [TYPE_PACKED_COLOR_ARRAY, PackedColorArray(), + 'PackedColorArray()'], + [TYPE_PACKED_COLOR_ARRAY, PackedColorArray([Color.RED, Color.BLUE]), + 'PackedColorArray([Color(1, 0, 0, 1), Color(0, 0, 1, 1)])'], + [TYPE_PACKED_FLOAT32_ARRAY, PackedFloat32Array(), + 'PackedFloat32Array()'], + [TYPE_PACKED_FLOAT32_ARRAY, PackedFloat32Array([1.2, 2.3]), + 'PackedFloat32Array([1.20000004768372, 2.29999995231628])'], + [TYPE_PACKED_FLOAT64_ARRAY, PackedFloat64Array(), + 'PackedFloat64Array()'], + [TYPE_PACKED_FLOAT64_ARRAY, PackedFloat64Array([1.2, 2.3]), + 'PackedFloat64Array([1.2, 2.3])'], + [TYPE_PACKED_INT32_ARRAY, PackedInt32Array(), + 'PackedInt32Array()'], + [TYPE_PACKED_INT32_ARRAY, PackedInt32Array([1, 2]), + 'PackedInt32Array([1, 2])'], + [TYPE_PACKED_INT64_ARRAY, PackedInt64Array(), + 'PackedInt64Array()'], + [TYPE_PACKED_INT64_ARRAY, PackedInt64Array([1, 2]), + 'PackedInt64Array([1, 2])'], + [TYPE_PACKED_STRING_ARRAY, PackedStringArray(), + 'PackedStringArray()'], + [TYPE_PACKED_STRING_ARRAY, PackedStringArray(["aa", "bb"]), + 'PackedStringArray(["aa", "bb"])'], + [TYPE_PACKED_VECTOR2_ARRAY, PackedVector2Array(), + 'PackedVector2Array()'], + [TYPE_PACKED_VECTOR2_ARRAY, PackedVector2Array([Vector2.ONE, Vector2.ONE*2]), + 'PackedVector2Array([Vector2(1, 1), Vector2(2, 2)])'], + [TYPE_PACKED_VECTOR3_ARRAY, PackedVector3Array(), + 'PackedVector3Array()'], + [TYPE_PACKED_VECTOR3_ARRAY, PackedVector3Array([Vector3.ONE, Vector3.ONE*2]), + 'PackedVector3Array([Vector3(1, 1, 1), Vector3(2, 2, 2)])'], + ]) -> void: + assert_that(GdDefaultValueDecoder.decode_typed(variant_type, value)).is_equal(expected) + _tested_types[variant_type] = 1 diff --git a/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd b/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd new file mode 100644 index 0000000..08f5b6e --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdFunctionArgumentTest.gd @@ -0,0 +1,76 @@ +# GdUnit generated TestSuite +class_name GdFunctionArgumentTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdFunctionArgument.gd' + + +func test__parse_argument_as_array_typ1() -> void: + var test_parameters := """[ + [1, "flowchart TD\nid>This is a flag shaped node]"], + [ + 2, + "flowchart TD\nid(((This is a\tdouble circle node)))" + ], + [3, + "flowchart TD\nid((This is a circular node))"], + [ + 4, "flowchart TD\nid>This is a flag shaped node]" + ], + [5, "flowchart TD\nid{'This is a rhombus node'}"], + [6, 'flowchart TD\nid((This is a circular node))'], + [7, 'flowchart TD\nid>This is a flag shaped node]'], [8, 'flowchart TD\nid{"This is a rhombus node"}'], + [9, \"\"\" + flowchart TD + id{"This is a rhombus node"} + \"\"\"] + ]""" + + var fa := GdFunctionArgument.new(GdFunctionArgument.ARG_PARAMETERIZED_TEST, TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).contains_exactly([ + """[1, "flowchart TDid>This is a flag shaped node]"]""", + """[2, "flowchart TDid(((This is a\tdouble circle node)))"]""", + """[3, "flowchart TDid((This is a circular node))"]""", + """[4, "flowchart TDid>This is a flag shaped node]"]""", + """[5, "flowchart TDid{'This is a rhombus node'}"]""", + """[6, 'flowchart TDid((This is a circular node))']""", + """[7, 'flowchart TDid>This is a flag shaped node]']""", + """[8, 'flowchart TDid{"This is a rhombus node"}']""", + """[9, \"\"\"flowchart TDid{"This is a rhombus node"}\"\"\"]""" + ] + ) + + +func test__parse_argument_as_array_typ2() -> void: + var test_parameters := """[ + ["test_a", null, "LOG", {}], + [ + "test_b", + Node2D, + null, + {Node2D: "ER,ROR"} + ], + [ + "test_c", + Node2D, + "LOG", + {Node2D: "LOG"} + ] + ]""" + var fa := GdFunctionArgument.new(GdFunctionArgument.ARG_PARAMETERIZED_TEST, TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).contains_exactly([ + """["test_a", null, "LOG", {}]""", + """["test_b", Node2D, null, {Node2D: "ER,ROR"}]""", + """["test_c", Node2D, "LOG", {Node2D: "LOG"}]""" + ] + ) + + +func test__parse_argument_as_reference() -> void: + var test_parameters := "_test_args()" + + var fa := GdFunctionArgument.new(GdFunctionArgument.ARG_PARAMETERIZED_TEST, TYPE_STRING, test_parameters) + assert_array(fa.parameter_sets()).is_empty() diff --git a/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd b/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd new file mode 100644 index 0000000..42a28f7 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdFunctionDescriptorTest.gd @@ -0,0 +1,146 @@ +# GdUnit generated TestSuite +class_name GdFunctionDescriptorTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd' + + +# helper to get method descriptor +func get_method_description(clazz_name :String, method_name :String) -> Dictionary: + var method_list :Array = ClassDB.class_get_method_list(clazz_name) + for method_descriptor in method_list: + if method_descriptor["name"] == method_name: + return method_descriptor + return Dictionary() + + +func test_extract_from_func_without_return_type(): + # void add_sibling(sibling: Node, force_readable_name: bool = false) + var method_descriptor := get_method_description("Node", "add_sibling") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("add_sibling") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(TYPE_NIL) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("sibling_", GdObjects.TYPE_NODE), + GdFunctionArgument.new("force_readable_name_", TYPE_BOOL, "false") + ]) + # void add_sibling(node: Node, child_node: Node, legible_unique_name: bool = false) + assert_str(fd.typeless()).is_equal("func add_sibling(sibling_, force_readable_name_=false) -> void:") + + +func test_extract_from_func_with_return_type(): + # Node find_child(pattern: String, recursive: bool = true, owned: bool = true) const + var method_descriptor := get_method_description("Node", "find_child") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("find_child") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(TYPE_OBJECT) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("pattern_", TYPE_STRING), + GdFunctionArgument.new("recursive_", TYPE_BOOL, "true"), + GdFunctionArgument.new("owned_", TYPE_BOOL, "true"), + ]) + # Node find_child(mask: String, recursive: bool = true, owned: bool = true) const + assert_str(fd.typeless()).is_equal("func find_child(pattern_, recursive_=true, owned_=true) -> Node:") + + +func test_extract_from_func_with_vararg(): + # Error emit_signal(signal: StringName, ...) vararg + var method_descriptor := get_method_description("Node", "emit_signal") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("emit_signal") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_true() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_ENUM) + assert_array(fd.args()).contains_exactly([GdFunctionArgument.new("signal_", TYPE_STRING_NAME)]) + assert_array(fd.varargs()).contains_exactly([ + GdFunctionArgument.new("vararg0_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg1_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg2_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg3_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg4_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg5_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg6_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg7_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg8_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE), + GdFunctionArgument.new("vararg9_", GdObjects.TYPE_VARARG, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) + ]) + assert_str(fd.typeless()).is_equal("func emit_signal(signal_, vararg0_=\"__null__\", vararg1_=\"__null__\", vararg2_=\"__null__\", vararg3_=\"__null__\", vararg4_=\"__null__\", vararg5_=\"__null__\", vararg6_=\"__null__\", vararg7_=\"__null__\", vararg8_=\"__null__\", vararg9_=\"__null__\") -> Error:") + + +func test_extract_from_descriptor_is_virtual_func(): + var method_descriptor := get_method_description("Node", "_enter_tree") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("_enter_tree") + assert_bool(fd.is_virtual()).is_true() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(TYPE_NIL) + assert_array(fd.args()).is_empty() + # void _enter_tree() virtual + assert_str(fd.typeless()).is_equal("func _enter_tree() -> void:") + + +func test_extract_from_descriptor_is_virtual_func_full_check(): + var methods := ClassDB.class_get_method_list("Node") + var expected_virtual_functions := [ + # Object virtuals + "_get", + "_get_property_list", + "_init", + "_notification", + "_property_can_revert", + "_property_get_revert", + "_set", + "_to_string", + # Note virtuals + "_enter_tree", + "_exit_tree", + "_get_configuration_warnings", + "_input", + "_physics_process", + "_process", + "_ready", + "_shortcut_input", + "_unhandled_input", + "_unhandled_key_input" + ] + # since Godot 4.2 there are more virtual functions + if Engine.get_version_info().hex >= 0x40200: + expected_virtual_functions.append("_validate_property") + + var _count := 0 + for method_descriptor in methods: + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + + if fd.is_virtual(): + _count += 1 + assert_array(expected_virtual_functions).contains([fd.name()]) + assert_int(_count).is_equal(expected_virtual_functions.size()) + + +func test_extract_from_func_with_return_type_variant(): + var method_descriptor := get_method_description("Node", "get") + var fd := GdFunctionDescriptor.extract_from(method_descriptor) + assert_str(fd.name()).is_equal("get") + assert_bool(fd.is_virtual()).is_false() + assert_bool(fd.is_static()).is_false() + assert_bool(fd.is_engine()).is_true() + assert_bool(fd.is_vararg()).is_false() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_VARIANT) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("property_", TYPE_STRING_NAME), + ]) + # Variant get(property: String) const + assert_str(fd.typeless()).is_equal("func get(property_) -> Variant:") diff --git a/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd b/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd new file mode 100644 index 0000000..cd2bd96 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdScriptParserTest.gd @@ -0,0 +1,632 @@ +extends GdUnitTestSuite + +var _parser: GdScriptParser + + +func before(): + _parser = GdScriptParser.new() + + +func test_parse_argument(): + # create example row whit different assignment types + var row = "func test_foo(arg1 = 41, arg2 := 42, arg3 : int = 43)" + assert_that(_parser.parse_argument(row, "arg1", 23)).is_equal(41) + assert_that(_parser.parse_argument(row, "arg2", 23)).is_equal(42) + assert_that(_parser.parse_argument(row, "arg3", 23)).is_equal(43) + + +func test_parse_argument_default_value(): + # arg4 not exists expect to return the default value + var row = "func test_foo(arg1 = 41, arg2 := 42, arg3 : int = 43)" + assert_that(_parser.parse_argument(row, "arg4", 23)).is_equal(23) + + +func test_parse_argument_has_no_arguments(): + assert_that(_parser.parse_argument("func test_foo()", "arg4", 23)).is_equal(23) + + +func test_parse_argument_with_bad_formatting(): + var row = "func test_foo( arg1 = 41, arg2 : = 42, arg3 : int = 43 )" + assert_that(_parser.parse_argument(row, "arg3", 23)).is_equal(43) + + +func test_parse_argument_with_same_func_name(): + var row = "func test_arg1(arg1 = 41)" + assert_that(_parser.parse_argument(row, "arg1", 23)).is_equal(41) + + +func test_parse_argument_timeout(): + var DEFAULT_TIMEOUT = 1000 + assert_that(_parser.parse_argument("func test_foo()", "timeout", DEFAULT_TIMEOUT)).is_equal(DEFAULT_TIMEOUT) + assert_that(_parser.parse_argument("func test_foo(timeout = 2000)", "timeout", DEFAULT_TIMEOUT)).is_equal(2000) + assert_that(_parser.parse_argument("func test_foo(timeout: = 2000)", "timeout", DEFAULT_TIMEOUT)).is_equal(2000) + assert_that(_parser.parse_argument("func test_foo(timeout:int = 2000)", "timeout", DEFAULT_TIMEOUT)).is_equal(2000) + assert_that(_parser.parse_argument("func test_foo(arg1 = false, timeout=2000)", "timeout", DEFAULT_TIMEOUT)).is_equal(2000) + + +func test_parse_arguments(): + assert_array(_parser.parse_arguments("func foo():")) \ + .has_size(0) + + assert_array(_parser.parse_arguments("func foo() -> String:\n")) \ + .has_size(0) + + assert_array(_parser.parse_arguments("func foo(arg1, arg2, name):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_NIL), + GdFunctionArgument.new("arg2", TYPE_NIL), + GdFunctionArgument.new("name", TYPE_NIL)]) + + assert_array(_parser.parse_arguments('func foo(arg1 :int, arg2 :bool, name :String = "abc"):')) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_BOOL), + GdFunctionArgument.new("name", TYPE_STRING, '"abc"')]) + + assert_array(_parser.parse_arguments('func bar(arg1 :int, arg2 :int = 23, name :String = "test") -> String:')) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_INT, "23"), + GdFunctionArgument.new("name", TYPE_STRING, '"test"')]) + + assert_array(_parser.parse_arguments("func foo(arg1, arg2=value(1,2,3), name:=foo()):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_NIL), + GdFunctionArgument.new("arg2", GdObjects.TYPE_FUNC, "value(1,2,3)"), + GdFunctionArgument.new("name", GdObjects.TYPE_FUNC, "foo()")]) + # enum as prefix in value name + assert_array(_parser.parse_arguments("func get_value( type := ENUM_A) -> int:"))\ + .contains_exactly([GdFunctionArgument.new("type", TYPE_STRING, "ENUM_A")]) + + assert_array(_parser.parse_arguments("func create_timer(timeout :float) -> Timer:")) \ + .contains_exactly([ + GdFunctionArgument.new("timeout", TYPE_FLOAT)]) + + # array argument + assert_array(_parser.parse_arguments("func foo(a :int, b :int, parameters = [[1, 2], [3, 4], [5, 6]]):")) \ + .contains_exactly([ + GdFunctionArgument.new("a", TYPE_INT), + GdFunctionArgument.new("b", TYPE_INT), + GdFunctionArgument.new("parameters", TYPE_ARRAY, "[[1, 2], [3, 4], [5, 6]]")]) + + assert_array(_parser.parse_arguments("func test_values(a:Vector2, b:Vector2, expected:Vector2, test_parameters:=[[Vector2.ONE,Vector2.ONE,Vector2(1,1)]]):"))\ + .contains_exactly([ + GdFunctionArgument.new("a", TYPE_VECTOR2), + GdFunctionArgument.new("b", TYPE_VECTOR2), + GdFunctionArgument.new("expected", TYPE_VECTOR2), + GdFunctionArgument.new("test_parameters", TYPE_ARRAY, "[[Vector2.ONE,Vector2.ONE,Vector2(1,1)]]"), + ]) + + +func test_parse_arguments_with_super_constructor(): + assert_array(_parser.parse_arguments('func foo().foo("abc"):')).is_empty() + assert_array(_parser.parse_arguments('func foo(arg1 = "arg").foo("abc", arg1):'))\ + .contains_exactly([GdFunctionArgument.new("arg1", TYPE_STRING, '"arg"')]) + + +func test_parse_arguments_default_build_in_type_String(): + assert_array(_parser.parse_arguments('func foo(arg1 :String, arg2="default"):')) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_STRING, '"default"')]) + + assert_array(_parser.parse_arguments('func foo(arg1 :String, arg2 :="default"):')) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_STRING, '"default"')]) + + assert_array(_parser.parse_arguments('func foo(arg1 :String, arg2 :String ="default"):')) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_STRING, '"default"')]) + + +func test_parse_arguments_default_build_in_type_Boolean(): + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2=false):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_BOOL, "false")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :=false):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_BOOL, "false")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :bool=false):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_BOOL, "false")]) + + +func test_parse_arguments_default_build_in_type_Real(): + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2=3.14):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_FLOAT, "3.14")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :=3.14):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_FLOAT, "3.14")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :float=3.14):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_FLOAT, "3.14")]) + + +func test_parse_arguments_default_build_in_type_Array(): + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :Array=[]):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_ARRAY, "[]")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :Array=Array()):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_ARRAY, "Array()")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :Array=[1, 2, 3]):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_ARRAY, "[1, 2, 3]")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :=[1, 2, 3]):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_ARRAY, "[1, 2, 3]")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2=[]):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_ARRAY, "[]")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :Array=[1, 2, 3], arg3 := false):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_ARRAY, "[1, 2, 3]"), + GdFunctionArgument.new("arg3", TYPE_BOOL, "false")]) + + +func test_parse_arguments_default_build_in_type_Color(): + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2=Color.RED):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_COLOR, "Color.RED")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :=Color.RED):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_COLOR, "Color.RED")]) + + assert_array(_parser.parse_arguments("func foo(arg1 :String, arg2 :Color=Color.RED):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_COLOR, "Color.RED")]) + + +func test_parse_arguments_default_build_in_type_Vector(): + assert_array(_parser.parse_arguments("func bar(arg1 :String, arg2 =Vector3.FORWARD):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_VECTOR3, "Vector3.FORWARD")]) + + assert_array(_parser.parse_arguments("func bar(arg1 :String, arg2 :=Vector3.FORWARD):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_VECTOR3, "Vector3.FORWARD")]) + + assert_array(_parser.parse_arguments("func bar(arg1 :String, arg2 :Vector3=Vector3.FORWARD):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_VECTOR3, "Vector3.FORWARD")]) + + +func test_parse_arguments_default_build_in_type_AABB(): + assert_array(_parser.parse_arguments("func bar(arg1 :String, arg2 := AABB()):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_AABB, "AABB()")]) + + assert_array(_parser.parse_arguments("func bar(arg1 :String, arg2 :AABB=AABB()):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_AABB, "AABB()")]) + + +func test_parse_arguments_default_build_in_types(): + assert_array(_parser.parse_arguments("func bar(arg1 :String, arg2 := Vector3.FORWARD, aabb := AABB()):")) \ + .contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_STRING), + GdFunctionArgument.new("arg2", TYPE_VECTOR3, "Vector3.FORWARD"), + GdFunctionArgument.new("aabb", TYPE_AABB, "AABB()")]) + + +func test_parse_arguments_fuzzers() -> void: + assert_array(_parser.parse_arguments("func test_foo(fuzzer_a = fuzz_a(), fuzzer_b := fuzz_b(), fuzzer_c :Fuzzer = fuzz_c(), fuzzer_iterations = 234, fuzzer_seed = 100):")) \ + .contains_exactly([ + GdFunctionArgument.new("fuzzer_a", GdObjects.TYPE_FUZZER, "fuzz_a()"), + GdFunctionArgument.new("fuzzer_b", GdObjects.TYPE_FUZZER, "fuzz_b()"), + GdFunctionArgument.new("fuzzer_c", GdObjects.TYPE_FUZZER, "fuzz_c()"), + GdFunctionArgument.new("fuzzer_iterations", TYPE_INT, "234"), + GdFunctionArgument.new("fuzzer_seed", TYPE_INT, "100"),]) + + +func test_parse_arguments_no_function(): + assert_array(_parser.parse_arguments("var x:=10")) \ + .has_size(0) + + +class TestObject: + var x + + +func test_parse_function_return_type(): + assert_that(_parser.parse_func_return_type("func foo():")).is_equal(TYPE_NIL) + assert_that(_parser.parse_func_return_type("func foo() -> void:")).is_equal(GdObjects.TYPE_VOID) + assert_that(_parser.parse_func_return_type("func foo() -> TestObject:")).is_equal(TYPE_OBJECT) + assert_that(_parser.parse_func_return_type("func foo() -> bool:")).is_equal(TYPE_BOOL) + assert_that(_parser.parse_func_return_type("func foo() -> String:")).is_equal(TYPE_STRING) + assert_that(_parser.parse_func_return_type("func foo() -> int:")).is_equal(TYPE_INT) + assert_that(_parser.parse_func_return_type("func foo() -> float:")).is_equal(TYPE_FLOAT) + assert_that(_parser.parse_func_return_type("func foo() -> Vector2:")).is_equal(TYPE_VECTOR2) + assert_that(_parser.parse_func_return_type("func foo() -> Rect2:")).is_equal(TYPE_RECT2) + assert_that(_parser.parse_func_return_type("func foo() -> Vector3:")).is_equal(TYPE_VECTOR3) + assert_that(_parser.parse_func_return_type("func foo() -> Transform2D:")).is_equal(TYPE_TRANSFORM2D) + assert_that(_parser.parse_func_return_type("func foo() -> Plane:")).is_equal(TYPE_PLANE) + assert_that(_parser.parse_func_return_type("func foo() -> Quaternion:")).is_equal(TYPE_QUATERNION) + assert_that(_parser.parse_func_return_type("func foo() -> AABB:")).is_equal(TYPE_AABB) + assert_that(_parser.parse_func_return_type("func foo() -> Basis:")).is_equal(TYPE_BASIS) + assert_that(_parser.parse_func_return_type("func foo() -> Transform3D:")).is_equal(TYPE_TRANSFORM3D) + assert_that(_parser.parse_func_return_type("func foo() -> Color:")).is_equal(TYPE_COLOR) + assert_that(_parser.parse_func_return_type("func foo() -> NodePath:")).is_equal(TYPE_NODE_PATH) + assert_that(_parser.parse_func_return_type("func foo() -> RID:")).is_equal(TYPE_RID) + assert_that(_parser.parse_func_return_type("func foo() -> Dictionary:")).is_equal(TYPE_DICTIONARY) + assert_that(_parser.parse_func_return_type("func foo() -> Array:")).is_equal(TYPE_ARRAY) + assert_that(_parser.parse_func_return_type("func foo() -> PackedByteArray:")).is_equal(TYPE_PACKED_BYTE_ARRAY) + assert_that(_parser.parse_func_return_type("func foo() -> PackedInt32Array:")).is_equal(TYPE_PACKED_INT32_ARRAY) + assert_that(_parser.parse_func_return_type("func foo() -> PackedFloat32Array:")).is_equal(TYPE_PACKED_FLOAT32_ARRAY) + assert_that(_parser.parse_func_return_type("func foo() -> PackedStringArray:")).is_equal(TYPE_PACKED_STRING_ARRAY) + assert_that(_parser.parse_func_return_type("func foo() -> PackedVector2Array:")).is_equal(TYPE_PACKED_VECTOR2_ARRAY) + assert_that(_parser.parse_func_return_type("func foo() -> PackedVector3Array:")).is_equal(TYPE_PACKED_VECTOR3_ARRAY) + assert_that(_parser.parse_func_return_type("func foo() -> PackedColorArray:")).is_equal(TYPE_PACKED_COLOR_ARRAY) + + +func test_parse_func_name(): + assert_str(_parser.parse_func_name("func foo():")).is_equal("foo") + assert_str(_parser.parse_func_name("static func foo():")).is_equal("foo") + assert_str(_parser.parse_func_name("func a() -> String:")).is_equal("a") + # function name contains tokens e.g func or class + assert_str(_parser.parse_func_name("func foo_func_class():")).is_equal("foo_func_class") + # should fail + assert_str(_parser.parse_func_name("#func foo():")).is_empty() + assert_str(_parser.parse_func_name("var x")).is_empty() + + +func test_extract_source_code(): + var path := GdObjects.extract_class_path(AdvancedTestClass) + var rows = _parser.extract_source_code(path) + + var file_content := resource_as_array(path[0]) + assert_array(rows).contains_exactly(file_content) + + +func test_extract_source_code_inner_class_AtmosphereData(): + var path := GdObjects.extract_class_path(AdvancedTestClass.AtmosphereData) + var rows = _parser.extract_source_code(path) + var file_content := resource_as_array("res://addons/gdUnit4/test/core/resources/AtmosphereData.txt") + assert_array(rows).contains_exactly(file_content) + + +func test_extract_source_code_inner_class_SoundData(): + var path := GdObjects.extract_class_path(AdvancedTestClass.SoundData) + var rows = _parser.extract_source_code(path) + var file_content := resource_as_array("res://addons/gdUnit4/test/core/resources/SoundData.txt") + assert_array(rows).contains_exactly(file_content) + + +func test_extract_source_code_inner_class_Area4D(): + var path := GdObjects.extract_class_path(AdvancedTestClass.Area4D) + var rows = _parser.extract_source_code(path) + var file_content := resource_as_array("res://addons/gdUnit4/test/core/resources/Area4D.txt") + assert_array(rows).contains_exactly(file_content) + + +func test_extract_function_signature() -> void: + var path := GdObjects.extract_class_path("res://addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd") + var rows = _parser.extract_source_code(path) + + assert_that(_parser.extract_func_signature(rows, 12))\ + .is_equal(""" + func a1(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false + ) -> void:""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 19))\ + .is_equal(""" + func a2(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false + ) -> void:""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 26))\ + .is_equal(""" + func a3(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false + ) :""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 33))\ + .is_equal(""" + func a4(set_name:String, + path:String="", + load_on_init:bool=false, + set_auto_save:bool=false, + set_network_sync:bool=false + ):""".dedent().trim_prefix("\n")) + assert_that(_parser.extract_func_signature(rows, 43))\ + .is_equal(""" + func a5( + value : Array, + expected : String, + test_parameters : Array = [ + [ ["a"], "a" ], + [ ["a", "very", "long", "argument"], "a very long argument" ], + ] + ):""".dedent().trim_prefix("\n")) + + +func test_strip_leading_spaces(): + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces("")).is_empty() + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces(" ")).is_empty() + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces(" ")).is_empty() + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces(" ")).is_equal(" ") + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces("var x=")).is_equal("var x=") + assert_str(GdScriptParser.TokenInnerClass._strip_leading_spaces("class foo")).is_equal("class foo") + + +func test_extract_clazz_name(): + assert_str(_parser.extract_clazz_name("classSoundData:\n")).is_equal("SoundData") + assert_str(_parser.extract_clazz_name("classSoundDataextendsNode:\n")).is_equal("SoundData") + + +func test_is_virtual_func() -> void: + # checked non virtual func + assert_bool(_parser.is_virtual_func("UnknownClass", [""], "")).is_false() + assert_bool(_parser.is_virtual_func("Node", [""], "")).is_false() + assert_bool(_parser.is_virtual_func("Node", [""], "func foo():")).is_false() + # checked virtual func + assert_bool(_parser.is_virtual_func("Node", [""], "_exit_tree")).is_true() + assert_bool(_parser.is_virtual_func("Node", [""], "_ready")).is_true() + assert_bool(_parser.is_virtual_func("Node", [""], "_init")).is_true() + + +func test_is_static_func(): + assert_bool(_parser.is_static_func("")).is_false() + assert_bool(_parser.is_static_func("var a=0")).is_false() + assert_bool(_parser.is_static_func("func foo():")).is_false() + assert_bool(_parser.is_static_func("func foo() -> void:")).is_false() + assert_bool(_parser.is_static_func("static func foo():")).is_true() + assert_bool(_parser.is_static_func("static func foo() -> void:")).is_true() + + +func test_parse_func_description(): + var fd := _parser.parse_func_description("func foo():", "clazz_name", [""], 10) + assert_str(fd.name()).is_equal("foo") + assert_bool(fd.is_static()).is_false() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_VARIANT) + assert_array(fd.args()).is_empty() + assert_str(fd.typeless()).is_equal("func foo() -> Variant:") + + # static function + fd = _parser.parse_func_description("static func foo(arg1 :int, arg2:=false) -> String:", "clazz_name", [""], 22) + assert_str(fd.name()).is_equal("foo") + assert_bool(fd.is_static()).is_true() + assert_int(fd.return_type()).is_equal(TYPE_STRING) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_BOOL, "false") + ]) + assert_str(fd.typeless()).is_equal("static func foo(arg1, arg2=false) -> String:") + + # static function without return type + fd = _parser.parse_func_description("static func foo(arg1 :int, arg2:=false):", "clazz_name", [""], 23) + assert_str(fd.name()).is_equal("foo") + assert_bool(fd.is_static()).is_true() + assert_int(fd.return_type()).is_equal(GdObjects.TYPE_VARIANT) + assert_array(fd.args()).contains_exactly([ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_BOOL, "false") + ]) + assert_str(fd.typeless()).is_equal("static func foo(arg1, arg2=false) -> Variant:") + + +func test_parse_func_description_return_type_enum(): + var result := _parser.parse("ClassWithEnumReturnTypes", ["res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd"]) + assert_result(result).is_success() + + var fd := _parser.parse_func_description("func get_enum() -> TEST_ENUM:", "ClassWithEnumReturnTypes", ["res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd"], 33) + assert_that(fd.name()).is_equal("get_enum") + assert_that(fd.is_static()).is_false() + assert_that(fd.return_type()).is_equal(GdObjects.TYPE_ENUM) + assert_that(fd._return_class).is_equal("ClassWithEnumReturnTypes.TEST_ENUM") + assert_that(fd.args()).is_empty() + + +func test_parse_func_description_return_type_internal_class_enum(): + var result := _parser.parse("ClassWithEnumReturnTypes", ["res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd"]) + assert_result(result).is_success() + + var fd := _parser.parse_func_description("func get_inner_class_enum() -> InnerClass.TEST_ENUM:", "ClassWithEnumReturnTypes", ["res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd"], 33) + assert_that(fd.name()).is_equal("get_inner_class_enum") + assert_that(fd.is_static()).is_false() + assert_that(fd.return_type()).is_equal(GdObjects.TYPE_ENUM) + assert_that(fd._return_class).is_equal("ClassWithEnumReturnTypes.InnerClass.TEST_ENUM") + assert_that(fd.args()).is_empty() + + +func test_parse_func_description_return_type_external_class_enum(): + var result := _parser.parse("ClassWithEnumReturnTypes", ["res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd"]) + assert_result(result).is_success() + + var fd := _parser.parse_func_description("func get_external_class_enum() -> CustomEnums.TEST_ENUM:", "ClassWithEnumReturnTypes", ["res://addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd"], 33) + assert_that(fd.name()).is_equal("get_external_class_enum") + assert_that(fd.is_static()).is_false() + assert_that(fd.return_type()).is_equal(GdObjects.TYPE_ENUM) + assert_that(fd._return_class).is_equal("CustomEnums.TEST_ENUM") + assert_that(fd.args()).is_empty() + + +func test_parse_class_inherits(): + var clazz_path := GdObjects.extract_class_path(CustomClassExtendsCustomClass) + var clazz_name := GdObjects.extract_class_name_from_class_path(clazz_path) + var result := _parser.parse(clazz_name, clazz_path) + assert_result(result).is_success() + + # verify class extraction + var clazz_desccriptor :GdClassDescriptor = result.value() + assert_object(clazz_desccriptor).is_not_null() + assert_str(clazz_desccriptor.name()).is_equal("CustomClassExtendsCustomClass") + assert_bool(clazz_desccriptor.is_inner_class()).is_false() + assert_array(clazz_desccriptor.functions()).contains_exactly([ + GdFunctionDescriptor.new("foo2", 5, false, false, false, GdObjects.TYPE_VARIANT, "", []), + GdFunctionDescriptor.new("bar2", 8, false, false, false, TYPE_STRING, "", []) + ]) + + # extends from CustomResourceTestClass + clazz_desccriptor = clazz_desccriptor.parent() + assert_object(clazz_desccriptor).is_not_null() + assert_str(clazz_desccriptor.name()).is_equal("CustomResourceTestClass") + assert_bool(clazz_desccriptor.is_inner_class()).is_false() + assert_array(clazz_desccriptor.functions()).contains_exactly([ + GdFunctionDescriptor.new("foo", 4, false, false, false, TYPE_STRING, "", []), + GdFunctionDescriptor.new("foo2", 7, false, false, false, GdObjects.TYPE_VARIANT, "", []), + GdFunctionDescriptor.new("foo_void", 10, false, false, false, GdObjects.TYPE_VOID, "", []), + GdFunctionDescriptor.new("bar", 13, false, false, false, TYPE_STRING, "", [ + GdFunctionArgument.new("arg1", TYPE_INT), + GdFunctionArgument.new("arg2", TYPE_INT, "23"), + GdFunctionArgument.new("name", TYPE_STRING, '"test"'), + ]), + GdFunctionDescriptor.new("foo5", 16, false, false, false, GdObjects.TYPE_VARIANT, "", []), + ]) + + # no other class extends + clazz_desccriptor = clazz_desccriptor.parent() + assert_object(clazz_desccriptor).is_null() + + +func test_get_class_name_pascal_case() -> void: + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd")))\ + .is_equal("PascalCaseWithClassName") + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd")))\ + .is_equal("PascalCaseWithoutClassName") + + +func test_get_class_name_snake_case() -> void: + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd")))\ + .is_equal("SnakeCaseWithClassName") + assert_str(_parser.get_class_name(load("res://addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd")))\ + .is_equal("SnakeCaseWithoutClassName") + + +func test_is_func_end() -> void: + assert_bool(_parser.is_func_end("")).is_false() + assert_bool(_parser.is_func_end("func test_a():")).is_true() + assert_bool(_parser.is_func_end("func test_a() -> void:")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1) :")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1 ): ")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1 ): ")).is_true() + assert_bool(_parser.is_func_end(" ):")).is_true() + assert_bool(_parser.is_func_end(" ):")).is_true() + assert_bool(_parser.is_func_end(" -> void:")).is_true() + assert_bool(_parser.is_func_end(" ) -> void :")).is_true() + assert_bool(_parser.is_func_end("func test_a(arg1, arg2 = {1:2} ):")).is_true() + + +func test_extract_func_signature_multiline() -> void: + var source_code = """ + + func test_parameterized(a: int, b :int, c :int, expected :int, parameters = [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]): + + assert_that(a+b+c).is_equal(expected) + """.dedent().split("\n") + + var fs = _parser.extract_func_signature(source_code, 0) + + assert_that(fs).is_equal(""" + func test_parameterized(a: int, b :int, c :int, expected :int, parameters = [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]):""" + .dedent() + .trim_prefix("\n") + ) + + +func test_parse_func_description_paramized_test(): + var fd = _parser.parse_func_description("functest_parameterized(a:int,b:int,c:int,expected:int,parameters=[[1,2,3,6],[3,4,5,11],[6,7,8,21]]):", "class", ["path"], 22) + + assert_that(fd).is_equal(GdFunctionDescriptor.new("test_parameterized", 22, false, false, false, GdObjects.TYPE_VARIANT, "", [ + GdFunctionArgument.new("a", TYPE_INT), + GdFunctionArgument.new("b", TYPE_INT), + GdFunctionArgument.new("c", TYPE_INT), + GdFunctionArgument.new("expected", TYPE_INT), + GdFunctionArgument.new("parameters", TYPE_ARRAY, "[[1,2,3,6],[3,4,5,11],[6,7,8,21]]"), + ])) + + +func test_parse_func_description_paramized_test_with_comments() -> void: + var source_code = """ + func test_parameterized(a: int, b :int, c :int, expected :int, parameters = [ + # before data set + [1, 2, 3, 6], # after data set + # between data sets + [3, 4, 5, 11], + [6, 7, 'string #ABCD', 21], # dataset with [comment] singn + [6, 7, "string #ABCD", 21] # dataset with "#comment" singn + #eof + ]): + pass + """.dedent().split("\n") + + var fs = _parser.extract_func_signature(source_code, 0) + + assert_that(fs).is_equal(""" + func test_parameterized(a: int, b :int, c :int, expected :int, parameters = [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 'string #ABCD', 21], + [6, 7, "string #ABCD", 21] + ]):""" + .dedent() + .trim_prefix("\n") + ) + + +func test_parse_func_descriptor_with_fuzzers(): + var source_code := """ + func test_foo(fuzzer_a = fuzz_a(), fuzzer_b := fuzz_b(), + fuzzer_c :Fuzzer = fuzz_c(), + fuzzer = Fuzzers.random_rangei(-23, 22), + fuzzer_iterations = 234, + fuzzer_seed = 100): + """.split("\n") + var fs = _parser.extract_func_signature(source_code, 0) + var fd = _parser.parse_func_description(fs, "class", ["path"], 22) + + assert_that(fd).is_equal(GdFunctionDescriptor.new("test_foo", 22, false, false, false, GdObjects.TYPE_VARIANT, "", [ + GdFunctionArgument.new("fuzzer_a", GdObjects.TYPE_FUZZER, "fuzz_a()"), + GdFunctionArgument.new("fuzzer_b", GdObjects.TYPE_FUZZER, "fuzz_b()"), + GdFunctionArgument.new("fuzzer_c", GdObjects.TYPE_FUZZER, "fuzz_c()"), + GdFunctionArgument.new("fuzzer", GdObjects.TYPE_FUZZER, "Fuzzers.random_rangei(-23, 22)"), + GdFunctionArgument.new("fuzzer_iterations", TYPE_INT, "234"), + GdFunctionArgument.new("fuzzer_seed", TYPE_INT, "100") + ])) + + +func test_is_class_enum_type() -> void: + var parser := GdScriptParser.new() + assert_that(parser.is_class_enum_type("ClassWithEnumReturnTypes.InnerClass.TEST_ENUM")).is_true() + assert_that(parser.is_class_enum_type("ClassWithEnumReturnTypes.InnerClass")).is_false() + assert_that(parser.is_class_enum_type("ClassWithEnumReturnTypes.TEST_ENUM")).is_true() + assert_that(parser.is_class_enum_type("CustomEnums.TEST_ENUM")).is_true() + assert_that(parser.is_class_enum_type("CustomEnums")).is_false() + assert_that(parser.is_class_enum_type("ClassWithEnumReturnTypes.NOT_AN_ENUM")).is_false() diff --git a/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd b/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd new file mode 100644 index 0000000..5c0ce73 --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdUnitExpressionRunnerTest.gd @@ -0,0 +1,65 @@ +# GdUnit generated TestSuite +class_name GdUnitExpressionsTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd' + +const TestFuzzers := preload("res://addons/gdUnit4/test/fuzzers/TestFuzzers.gd") + + +func test_create_fuzzer_argument_default(): + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(GDScript.new(), "Fuzzers.rangei(-10, 22)") + assert_that(fuzzer).is_not_null() + assert_that(fuzzer).is_instanceof(Fuzzer) + assert_int(fuzzer.next_value()).is_between(-10, 22) + + +func test_create_fuzzer_argument_with_constants(): + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "Fuzzers.rangei(-10, MAX_VALUE)") + assert_that(fuzzer).is_not_null() + assert_that(fuzzer).is_instanceof(Fuzzer) + assert_int(fuzzer.next_value()).is_between(-10, 22) + + +func test_create_fuzzer_argument_with_custom_function(): + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "get_fuzzer()") + assert_that(fuzzer).is_not_null() + assert_that(fuzzer).is_instanceof(Fuzzer) + assert_int(fuzzer.next_value()).is_between(TestFuzzers.MIN_VALUE, TestFuzzers.MAX_VALUE) + + +func test_create_fuzzer_do_fail(): + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "non_fuzzer()") + assert_that(fuzzer).is_null() + + +func test_create_nested_fuzzer_do_fail(): + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "NestedFuzzer.new()") + assert_that(fuzzer).is_not_null() + assert_that(fuzzer is Fuzzer).is_true() + assert_bool(fuzzer is TestFuzzers.NestedFuzzer).is_true() + + +func test_create_external_fuzzer(): + var fuzzer := GdUnitExpressionRunner.new().to_fuzzer(GDScript.new(), "TestExternalFuzzer.new()") + assert_that(fuzzer).is_not_null() + assert_that(fuzzer is Fuzzer).is_true() + assert_bool(fuzzer is TestExternalFuzzer).is_true() + + +func test_create_multipe_fuzzers(): + var fuzzer_a := GdUnitExpressionRunner.new().to_fuzzer(TestFuzzers, "Fuzzers.rangei(-10, MAX_VALUE)") + var fuzzer_b := GdUnitExpressionRunner.new().to_fuzzer(GDScript.new(), "Fuzzers.rangei(10, 20)") + assert_that(fuzzer_a).is_not_null() + assert_that(fuzzer_a).is_instanceof(IntFuzzer) + var a :IntFuzzer = fuzzer_a + assert_int(a._from).is_equal(-10) + assert_int(a._to).is_equal(TestFuzzers.MAX_VALUE) + assert_that(fuzzer_b).is_not_null() + assert_that(fuzzer_b).is_instanceof(IntFuzzer) + var b :IntFuzzer = fuzzer_b + assert_int(b._from).is_equal(10) + assert_int(b._to).is_equal(20) diff --git a/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd b/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd new file mode 100644 index 0000000..5372f2b --- /dev/null +++ b/addons/gdUnit4/test/core/parse/GdUnitTestParameterSetResolverTest.gd @@ -0,0 +1,222 @@ +# GdUnit generated TestSuite +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd' + + +var _test_param1 := 10 +var _test_param2 := 20 + + +func before(): + _test_param1 = 11 + + +func test_before(): + _test_param2 = 22 + + +@warning_ignore("unused_parameter") +func test_example_a(a: int, b: int, test_parameters=[[1, 2], [3,4]]) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_example_b(a: Vector2, b: Vector2, test_parameters=[ + [Vector2.ZERO, Vector2.ONE], [Vector2(1.1, 3.2), Vector2.DOWN]] ) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_example_c(a: Object, b: Object, test_parameters=[ + [Resource.new(), Resource.new()], + [Resource.new(), null] + ] ) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_resolve_parameters_static(a: int, b: int, test_parameters=[ + [1, 10], + [2, 20] + ]) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_resolve_parameters_at_runtime(a: int, b: int, test_parameters=[ + [1, _test_param1], + [2, _test_param2], + [3, 30] + ]) -> void: + pass + + +@warning_ignore("unused_parameter") +func test_parameterized_with_comments(a: int, b :int, c :String, expected :int, test_parameters = [ + # before data set + [1, 2, '3', 6], # after data set + # between data sets + [3, 4, '5', 11], + [6, 7, 'string #ABCD', 21], # dataset with [comment] singn + [6, 7, "string #ABCD", 21] # dataset with "comment" singn + #eof +]): + pass + + +func build_param(value: float) -> Vector3: + return Vector3(value, value, value) + + +@warning_ignore("unused_parameter") +func test_example_d(a: Vector3, b: Vector3, test_parameters=[ + [build_param(1), build_param(3)], + [Vector3.BACK, Vector3.UP] + ] ) -> void: + pass + + +class TestObj extends RefCounted: + var _value: String + + func _init(value: String): + _value = value + + func _to_string() -> String: + return _value + + +@warning_ignore("unused_parameter") +func test_example_e(a: Object, b: Object, expected: String, test_parameters:=[ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"]]): + pass + + +# verify the used 'test_parameters' is completly resolved +func test_load_parameter_sets() -> void: + var tc := get_test_case("test_example_a") + assert_array(tc.parameter_set_resolver().load_parameter_sets(tc)) \ + .is_equal([[1, 2], [3, 4]]) + + tc = get_test_case("test_example_b") + assert_array(tc.parameter_set_resolver().load_parameter_sets(tc)) \ + .is_equal([[Vector2.ZERO, Vector2.ONE], [Vector2(1.1, 3.2), Vector2.DOWN]]) + + tc = get_test_case("test_example_c") + assert_array(tc.parameter_set_resolver().load_parameter_sets(tc)) \ + .is_equal([[Resource.new(), Resource.new()], [Resource.new(), null]]) + + tc = get_test_case("test_example_d") + assert_array(tc.parameter_set_resolver().load_parameter_sets(tc)) \ + .is_equal([[Vector3(1, 1, 1), Vector3(3, 3, 3)], [Vector3.BACK, Vector3.UP]]) + + tc = get_test_case("test_example_e") + assert_array(tc.parameter_set_resolver().load_parameter_sets(tc)) \ + .is_equal([[TestObj.new("abc"), TestObj.new("def"), "abcdef"]]) + + +func test_load_parameter_sets_at_runtime() -> void: + var tc := get_test_case("test_resolve_parameters_at_runtime") + assert_that(tc).is_not_null() + # check the parameters resolved at runtime + assert_array(tc.parameter_set_resolver().load_parameter_sets(tc)) \ + .is_equal([ + # the value `_test_param1` is changed from 10 to 11 on `before` stage + [1, 11], + # the value `_test_param2` is changed from 20 to 2 on `test_before` stage + [2, 22], + # the value is static initial `30` + [3, 30]]) + + +func test_load_parameter_with_comments() -> void: + var tc := get_test_case("test_parameterized_with_comments") + assert_that(tc).is_not_null() + # check the parameters resolved at runtime + assert_array(tc.parameter_set_resolver().load_parameter_sets(tc)) \ + .is_equal([ + [1, 2, '3', 6], + [3, 4, '5', 11], + [6, 7, 'string #ABCD', 21], + [6, 7, "string #ABCD", 21]]) + + +func test_build_test_case_names_on_static_parameter_set() -> void: + var test_case := get_test_case("test_resolve_parameters_static") + var resolver := test_case.parameter_set_resolver() + + assert_array(resolver.build_test_case_names(test_case))\ + .contains_exactly([ + "test_resolve_parameters_static:0 [1, 10]", + "test_resolve_parameters_static:1 [2, 20]"]) + assert_that(resolver.is_parameter_sets_static()).is_true() + assert_that(resolver.is_parameter_set_static(0)).is_true() + assert_that(resolver.is_parameter_set_static(1)).is_true() + + +func test_build_test_case_names_on_runtime_parameter_set() -> void: + var test_case := get_test_case("test_resolve_parameters_at_runtime") + var resolver := test_case.parameter_set_resolver() + + assert_array(resolver.build_test_case_names(test_case))\ + .contains_exactly([ + "test_resolve_parameters_at_runtime:0 [1, _test_param1]", + "test_resolve_parameters_at_runtime:1 [2, _test_param2]", + "test_resolve_parameters_at_runtime:2 [3, 30]"]) + assert_that(resolver.is_parameter_sets_static()).is_false() + assert_that(resolver.is_parameter_set_static(0)).is_false() + assert_that(resolver.is_parameter_set_static(1)).is_false() + assert_that(resolver.is_parameter_set_static(2)).is_false() + + +func test_validate_test_parameter_set(): + var test_suite :GdUnitTestSuite = auto_free(GdUnitTestResourceLoader.load_test_suite("res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource")) + + assert_is_not_skipped(test_suite, "test_no_parameters") + assert_is_not_skipped(test_suite, "test_parameterized_success") + assert_is_not_skipped(test_suite, "test_parameterized_failed") + assert_is_skipped(test_suite, "test_parameterized_to_less_args")\ + .contains("The parameter set at index [0] does not match the expected input parameters!")\ + .contains("The test case requires [3] input parameters, but the set contains [4]") + assert_is_skipped(test_suite, "test_parameterized_to_many_args")\ + .contains("The parameter set at index [0] does not match the expected input parameters!")\ + .contains("The test case requires [5] input parameters, but the set contains [4]") + assert_is_skipped(test_suite, "test_parameterized_to_less_args_at_index_1")\ + .contains("The parameter set at index [1] does not match the expected input parameters!")\ + .contains("The test case requires [3] input parameters, but the set contains [4]") + assert_is_skipped(test_suite, "test_parameterized_invalid_struct")\ + .contains("The parameter set at index [1] does not match the expected input parameters!")\ + .contains("The test case requires [3] input parameters, but the set contains [1]") + assert_is_skipped(test_suite, "test_parameterized_invalid_args")\ + .contains("The parameter set at index [1] does not match the expected input parameters!")\ + .contains("The value '4' does not match the required input parameter .") + + +func assert_is_not_skipped(test_suite :GdUnitTestSuite, test_case :String) -> void: + # set actual execution context for this test suite + test_suite.__execution_context = GdUnitExecutionContext.new(test_suite.get_name()) + var test :_TestCase = test_suite.find_child(test_case, true, false) + if test.is_parameterized(): + # to load parameter set and force validate + var resolver := test.parameter_set_resolver() + resolver.build_test_case_names(test) + resolver.load_parameter_sets(test, true) + assert_that(test.is_skipped()).is_false() + + +func assert_is_skipped(test_suite :GdUnitTestSuite, test_case :String) -> GdUnitStringAssert: + # set actual execution context for this test suite + test_suite.__execution_context = GdUnitExecutionContext.new(test_suite.get_name()) + var test :_TestCase = test_suite.find_child(test_case, true, false) + if test.is_parameterized(): + # to load parameter set and force validate + var resolver := test.parameter_set_resolver() + resolver.build_test_case_names(test) + resolver.load_parameter_sets(test, true) + assert_that(test.is_skipped()).is_true() + return assert_str(test.skip_info()) + +func get_test_case(name: String) -> _TestCase: + return find_child(name, true, false) diff --git a/addons/gdUnit4/test/core/resources/Area4D.txt b/addons/gdUnit4/test/core/resources/Area4D.txt new file mode 100644 index 0000000..e3da456 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/Area4D.txt @@ -0,0 +1,17 @@ +class Area4D extends Resource: + +const SOUND := 1 +const ATMOSPHERE := 2 +var _meta := Dictionary() + +func _init(_x :int, atmospere :AtmosphereData = null): + _meta[ATMOSPHERE] = atmospere + +func get_sound() -> SoundData: + # sounds are optional + if _meta.has(SOUND): + return _meta[SOUND] as SoundData + return null + +func get_atmoshere() -> AtmosphereData: + return _meta[ATMOSPHERE] as AtmosphereData diff --git a/addons/gdUnit4/test/core/resources/AtmosphereData.txt b/addons/gdUnit4/test/core/resources/AtmosphereData.txt new file mode 100644 index 0000000..3a69974 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/AtmosphereData.txt @@ -0,0 +1,22 @@ +class AtmosphereData: +enum { + WATER, + AIR, + SMOKY, +} +var _toxigen :float +var _type :int + +func _init(type := AIR, toxigen := 0.0): + _type = type + _toxigen = toxigen +# some comment, and an row staring with an space to simmulate invalid formatting + + +# seter func with default values +func set_data(type := AIR, toxigen := 0.0) : + _type = type + _toxigen = toxigen + +static func to_atmosphere(_value :Dictionary) -> AtmosphereData: + return null diff --git a/addons/gdUnit4/test/core/resources/GdUnitRunner_old_format.cfg b/addons/gdUnit4/test/core/resources/GdUnitRunner_old_format.cfg new file mode 100644 index 0000000..cc6eece Binary files /dev/null and b/addons/gdUnit4/test/core/resources/GdUnitRunner_old_format.cfg differ diff --git a/addons/gdUnit4/test/core/resources/SoundData.txt b/addons/gdUnit4/test/core/resources/SoundData.txt new file mode 100644 index 0000000..91e62ba --- /dev/null +++ b/addons/gdUnit4/test/core/resources/SoundData.txt @@ -0,0 +1,5 @@ +class SoundData: +@warning_ignore("unused_private_class_variable") +var _sample :String +@warning_ignore("unused_private_class_variable") +var _randomnes :float diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_a.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_a.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_a.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_b.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_b.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_a/file_b.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_a.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_a.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_a.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_b.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_b.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_b/file_b.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_b/folder_ba/file_x.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_b/folder_ba/file_x.txt new file mode 100644 index 0000000..d6459e0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/copy_test/folder_b/folder_ba/file_x.txt @@ -0,0 +1 @@ +xxx diff --git a/addons/gdUnit4/test/core/resources/copy_test/folder_c/file_z.txt b/addons/gdUnit4/test/core/resources/copy_test/folder_c/file_z.txt new file mode 100644 index 0000000..e69de29 diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd new file mode 100644 index 0000000..32df9ea --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd @@ -0,0 +1,7 @@ +class_name PascalCaseWithClassName +extends Resource + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd new file mode 100644 index 0000000..7f377d4 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithoutClassName.gd @@ -0,0 +1,6 @@ +extends RefCounted + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd new file mode 100644 index 0000000..d3e93f0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_with_class_name.gd @@ -0,0 +1,7 @@ +class_name SnakeCaseWithClassName +extends Resource + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd new file mode 100644 index 0000000..7f377d4 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/naming_conventions/snake_case_without_class_name.gd @@ -0,0 +1,6 @@ +extends RefCounted + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd new file mode 100644 index 0000000..d974340 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/BaseTest.gd @@ -0,0 +1,22 @@ +class_name BaseTest +extends GdUnitTestSuite + + +func before(): + pass + + +func after(): + pass + + +func before_test(): + pass + + +func after_test(): + pass + + +func test_foo1() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd new file mode 100644 index 0000000..068bba0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendedTest.gd @@ -0,0 +1,14 @@ +class_name ExtendedTest +extends BaseTest + + +func before_test(): + pass + + +func after_test(): + pass + + +func test_foo2() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd new file mode 100644 index 0000000..299a52d --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_name/ExtendsExtendedTest.gd @@ -0,0 +1,4 @@ +extends ExtendedTest + +func test_foo3() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd new file mode 100644 index 0000000..17cfc19 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd @@ -0,0 +1,4 @@ +extends GdUnitTestSuite + +func test_foo1() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd new file mode 100644 index 0000000..eea2d4f --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd @@ -0,0 +1,4 @@ +extends "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/BaseTest.gd" + +func test_foo2() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd new file mode 100644 index 0000000..a14c8d3 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendsExtendedTest.gd @@ -0,0 +1,4 @@ +extends "res://addons/gdUnit4/test/core/resources/scan_testsuite_inheritance/by_class_path/ExtendedTest.gd" + +func test_foo3() -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd new file mode 100644 index 0000000..6bf00a9 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd @@ -0,0 +1,41 @@ +extends PanelContainer + + +# Godot calls this method to get data that can be dragged and dropped onto controls that expect drop data. +# Returns null if there is no data to drag. +# Controls that want to receive drop data should implement can_drop_data() and drop_data(). +# position is local to this control. Drag may be forced with force_drag(). +func _get_drag_data(_position: Vector2) -> Variant: + var x :TextureRect = $TextureRect + var data: = {texture = x.texture} + var drag_texture := x.duplicate() + drag_texture.size = x.size + drag_texture.position = x.global_position * -0.2 + + # set drag preview + var control := Panel.new() + control.add_child(drag_texture) + # center texture relative to mouse pos + set_drag_preview(control) + return data + + +# Godot calls this method to test if data from a control's get_drag_data() can be dropped at position. position is local to this control. +func _can_drop_data(_position: Vector2, data :Variant) -> bool: + return typeof(data) == TYPE_DICTIONARY and data.has("texture") + + +# Godot calls this method to pass you the data from a control's get_drag_data() result. +# Godot first calls can_drop_data() to test if data is allowed to drop at position where position is local to this control. +func _drop_data(_position: Vector2, data :Variant) -> void: + var drag_texture :Texture = data["texture"] + if drag_texture != null: + $TextureRect.texture = drag_texture + + +# Virtual method to be implemented by the user. Use this method to process and accept inputs on UI elements. See accept_event(). +func _gui_input(_event): + #prints("Panel _gui_input", _event.as_text()) + #if _event is InputEventMouseButton: + # prints("Panel _gui_input", _event.as_text()) + pass diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn new file mode 100644 index 0000000..e4738cf --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=2 format=3 uid="uid://ca2rr3dan4vvw"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.gd" id="1"] + +[node name="Panel" type="PanelContainer"] +offset_left = 245.0 +offset_top = 232.0 +offset_right = 350.0 +offset_bottom = 337.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="TextureRect" type="TextureRect" parent="."] +layout_mode = 2 +expand_mode = 1 diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd new file mode 100644 index 0000000..eedd57a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd @@ -0,0 +1,18 @@ +extends Control + +@onready var texture = preload("res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png") + +func _ready(): + # set initial drag texture + $left/TextureRect.texture = texture + + +# Virtual method to be implemented by the user. Use this method to process and accept inputs on UI elements. See accept_event(). +func _gui_input(_event): + #prints("Game _gui_input", _event.as_text()) + pass + + +func _on_Button_button_down(): + # print("BUTTON DOWN") + pass diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn new file mode 100644 index 0000000..aa61b7c --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.tscn @@ -0,0 +1,43 @@ +[gd_scene load_steps=3 format=3 uid="uid://skueh3d5qn46"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropTestScene.gd" id="1"] +[ext_resource type="PackedScene" uid="uid://ca2rr3dan4vvw" path="res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/DragAndDropControl.tscn" id="2_u5ccv"] + +[node name="DragAndDropScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="left" parent="." instance=ExtResource("2_u5ccv")] +layout_mode = 0 +offset_left = 250.0 +offset_top = 240.0 +offset_right = 355.0 +offset_bottom = 345.0 +auto_translate = false +localize_numeral_system = false +metadata/_edit_use_anchors_ = true + +[node name="right" parent="." instance=ExtResource("2_u5ccv")] +layout_mode = 0 +offset_left = 370.0 +offset_top = 240.0 +offset_right = 475.0 +offset_bottom = 345.0 + +[node name="Button" type="Button" parent="."] +layout_mode = 0 +offset_left = 243.0 +offset_top = 40.0 +offset_right = 479.0 +offset_bottom = 200.0 +text = "BUTTON" +metadata/_edit_use_anchors_ = true + +[connection signal="button_down" from="Button" to="." method="_on_Button_button_down"] diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png new file mode 100644 index 0000000..eeac292 Binary files /dev/null and b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png differ diff --git a/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png.import b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png.import new file mode 100644 index 0000000..b5a8487 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://di6ovw8bnk7wg" +path="res://.godot/imported/icon.png-50a1939c45a5f06328a9e414b58963b1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/test/core/resources/scenes/drag_and_drop/icon.png" +dest_files=["res://.godot/imported/icon.png-50a1939c45a5f06328a9e414b58963b1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd new file mode 100644 index 0000000..2dccb32 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd @@ -0,0 +1,15 @@ +extends Control + +var _player_jump_action_released := false + +# enable for manual testing +func __init(): + var event := InputEventKey.new() + event.keycode = KEY_SPACE + InputMap.add_action("player_jump") + InputMap.action_add_event("player_jump", event) + + +func _input(event): + _player_jump_action_released = Input.is_action_just_released("player_jump", true) + #prints("_input2:player_jump", Input.is_action_just_released("player_jump"), Input.is_action_just_released("player_jump", true)) diff --git a/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn new file mode 100644 index 0000000..6ee1fc9 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://cvklb72mxqh1h"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/input_actions/InputEventTestScene.gd" id="1_wslmn"] + +[node name="InputEventTestScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_wslmn") diff --git a/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd b/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd new file mode 100644 index 0000000..4c25c4f --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/simple_scene.gd @@ -0,0 +1,15 @@ +extends Node2D + +class Player extends Node: + var position :Vector3 = Vector3.ZERO + + + func _init(): + set_name("Player") + + func is_on_floor() -> bool: + return true + + +func _ready(): + add_child(Player.new(), true) diff --git a/addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn b/addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn new file mode 100644 index 0000000..a7648ad --- /dev/null +++ b/addons/gdUnit4/test/core/resources/scenes/simple_scene.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=3 uid="uid://cn8ucy2rheu0f"] + +[ext_resource type="Texture2D" uid="uid://t80a6k3vyrrd" path="res://icon.png" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/test/core/resources/scenes/simple_scene.gd" id="2"] + +[node name="Node2D" type="Node2D"] +script = ExtResource("2") + +[node name="Sprite2D" type="Sprite2D" parent="."] +position = Vector2(504, 252) +texture = ExtResource("1") diff --git a/addons/gdUnit4/test/core/resources/script_with_class_name.gd b/addons/gdUnit4/test/core/resources/script_with_class_name.gd new file mode 100644 index 0000000..c2e0fbb --- /dev/null +++ b/addons/gdUnit4/test/core/resources/script_with_class_name.gd @@ -0,0 +1,6 @@ +class_name ScriptWithClassName +extends Resource + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/script_without_class_name.gd b/addons/gdUnit4/test/core/resources/script_without_class_name.gd new file mode 100644 index 0000000..2a74876 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/script_without_class_name.gd @@ -0,0 +1,5 @@ +extends RefCounted + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. diff --git a/addons/gdUnit4/test/core/resources/sources/test_person.gd b/addons/gdUnit4/test/core/resources/sources/test_person.gd new file mode 100644 index 0000000..c4a86ca --- /dev/null +++ b/addons/gdUnit4/test/core/resources/sources/test_person.gd @@ -0,0 +1,17 @@ +extends RefCounted + +var _first_name :String +var _last_name :String + +func _init(first_name_ :String,last_name_ :String): + _first_name = first_name_ + _last_name = last_name_ + +func first_name() -> String: + return _first_name + +func last_name() -> String: + return _last_name + +func fully_name() -> String: + return _first_name + " " + _last_name diff --git a/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd b/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd new file mode 100644 index 0000000..b9f5455 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/test_script_with_arguments.gd @@ -0,0 +1,49 @@ +extends Node + + +func test_no_args(): + pass + +@warning_ignore("unused_parameter") +func test_with_timeout(timeout=2000): + pass + + +@warning_ignore("unused_parameter") +func test_with_fuzzer(fuzzer := Fuzzers.rangei(-10, 22)): + pass + + +@warning_ignore("unused_parameter") +func test_with_fuzzer_iterations(fuzzer := Fuzzers.rangei(-10, 22), fuzzer_iterations = 10): + pass + + +@warning_ignore("unused_parameter") +func test_with_multible_fuzzers(fuzzer_a := Fuzzers.rangei(-10, 22), fuzzer_b := Fuzzers.rangei(23, 42), fuzzer_iterations = 10): + pass + + +@warning_ignore("unused_parameter") +func test_multiline_arguments_a(fuzzer_a := Fuzzers.rangei(-10, 22), fuzzer_b := Fuzzers.rangei(23, 42), + fuzzer_iterations = 42): + pass + + +@warning_ignore("unused_parameter") +func test_multiline_arguments_b( + fuzzer_a := Fuzzers.rangei(-10, 22), + fuzzer_b := Fuzzers.rangei(23, 42), + fuzzer_iterations = 23 + ): + pass + + +@warning_ignore("unused_parameter") +func test_multiline_arguments_c( + timeout=2000, + fuzzer_a := Fuzzers.rangei(-10, 22), + fuzzer_b := Fuzzers.rangei(23, 42), + fuzzer_iterations = 33 + ) -> void: + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource b/addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource new file mode 100644 index 0000000..4ffc7b2 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/GdUnitFuzzerTest.resource @@ -0,0 +1,48 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite +@warning_ignore('unused_parameter') + +var _stack : Array + +func before(): + # init the stack + _stack = [] + +func before_test(): + # clean the stack before every test run + _stack.clear() + + +@warning_ignore('unused_parameter') +func test_multi_yielding_with_fuzzer(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations = 10): + # verify the used stack is cleaned by 'before_test' + assert_array(_stack).is_empty() + _stack.push_back(1) + + prints("test iteration %d" % fuzzer.iteration_index()) + prints("4") + await get_tree().process_frame + prints("3") + await get_tree().process_frame + prints("2") + await get_tree().process_frame + prints("1") + await get_tree().process_frame + prints("Go") + +@warning_ignore('unused_parameter') +func test_multi_yielding_with_fuzzer_fail_after_3_iterations(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations = 10): + prints("test iteration %d" % fuzzer.iteration_index()) + # should never be greater than 3 because we interuppted after three iterations + assert_int(fuzzer.iteration_index()).is_less_equal(3) + prints("4") + await get_tree().process_frame + prints("3") + await get_tree().process_frame + prints("2") + await get_tree().process_frame + prints("1") + await get_tree().process_frame + prints("Go") + if fuzzer.iteration_index() >= 3: + assert_bool(true).is_false() diff --git a/addons/gdUnit4/test/core/resources/testsuites/NotATestSuite.cs b/addons/gdUnit4/test/core/resources/testsuites/NotATestSuite.cs new file mode 100644 index 0000000..a7e4323 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/NotATestSuite.cs @@ -0,0 +1,14 @@ +namespace GdUnit4.Tests.Resources +{ + using static Assertions; + + // will be ignored because of missing `[TestSuite]` anotation + public partial class NotATestSuite + { + [TestCase] + public void TestFoo() + { + AssertBool(true).IsEqual(false); + } + } +} diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource b/addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource new file mode 100644 index 0000000..8482bbd --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestCaseSkipped.resource @@ -0,0 +1,11 @@ +extends GdUnitTestSuite + + +@warning_ignore('unused_parameter') +func test_case1(timeout = 1000, do_skip=1==1, skip_reason="do not run this"): + pass + + +@warning_ignore('unused_parameter') +func test_case2(skip_reason="ignored"): + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource new file mode 100644 index 0000000..31ae79c --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteAllStagesSuccess.resource @@ -0,0 +1,20 @@ +# this test suite ends with success, no failures or errors +extends GdUnitTestSuite + +func before(): + assert_str("suite before").is_equal("suite before") + +func after(): + assert_str("suite after").is_equal("suite after") + +func before_test(): + assert_str("test before").is_equal("test before") + +func after_test(): + assert_str("test after").is_equal("test after") + +func test_case1(): + assert_str("test_case1").is_equal("test_case1") + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource new file mode 100644 index 0000000..b91b3ee --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteErrorOnTestTimeout.resource @@ -0,0 +1,24 @@ +# this test suite ends with error on testcase1 by a timeout +extends GdUnitTestSuite + +func before(): + assert_str("suite before").is_equal("suite before") + +func after(): + assert_str("suite after").is_equal("suite after") + +func before_test(): + assert_str("test before").is_equal("test before") + +func after_test(): + assert_str("test after").is_equal("test after") + +# configure test with timeout of 2s +@warning_ignore('unused_parameter') +func test_case1(timeout=2000): + assert_str("test_case1").is_equal("test_case1") + # wait 3s to let the test fail by timeout + await await_millis(3000) + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource new file mode 100644 index 0000000..265aca0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAddChildStageBefore.resource @@ -0,0 +1,11 @@ +# this test suite fails if (https://github.com/MikeSchulze/gdUnit4/issues/106) not fixed on iterating over testcases +extends GdUnitTestSuite + +func before(): + add_child(auto_free(Node.new())) + +func test_case1(): + assert_str("test_case1").is_equal("test_case1") + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAndOrpahnsDetected.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAndOrpahnsDetected.resource new file mode 100644 index 0000000..6d14009 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAndOrpahnsDetected.resource @@ -0,0 +1,37 @@ +# this test suite fails on multiple stages and detects orphans +extends GdUnitTestSuite + +var _orphans := Array() + +func before(): + # create a node where never freed (orphan) + _orphans.append(Node.new()) + +func before_test(): + # create two node where never freed (orphan) + _orphans.append(Node.new()) + _orphans.append(Node.new()) + + +# ends with warning and 3 orphan detected +func test_case1(): + # create three node where never freed (orphan) + _orphans.append(Node.new()) + _orphans.append(Node.new()) + _orphans.append(Node.new()) + +# ends with error and 4 orphan detected +func test_case2(): + # create four node where never freed (orphan) + _orphans.append(Node.new()) + _orphans.append(Node.new()) + _orphans.append(Node.new()) + _orphans.append(Node.new()) + assert_str("test_case2").override_failure_message("faild on test_case2()").is_empty() + +# we manually freeing the orphans from the simulated testsuite to prevent memory leaks here +func _notification(what): + if what == NOTIFICATION_PREDELETE: + for orphan in _orphans: + orphan.free() + _orphans.clear() diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource new file mode 100644 index 0000000..7d22794 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnMultipeStages.resource @@ -0,0 +1,21 @@ +# this test suite fails on multiple stages +extends GdUnitTestSuite + +func before(): + assert_str("suite before").is_equal("suite before") + +func after(): + assert_str("suite after").override_failure_message("failed on after()").is_empty() + +func before_test(): + assert_str("test before").override_failure_message("failed on before_test()").is_empty() + +func after_test(): + assert_str("test after").is_equal("test after") + +func test_case1(): + assert_str("test_case1").override_failure_message("failed 1 on test_case1()").is_empty() + assert_str("test_case1").override_failure_message("failed 2 on test_case1()").is_empty() + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource new file mode 100644 index 0000000..a84e88b --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfter.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage after() +extends GdUnitTestSuite + +func before(): + assert_str("suite before").is_equal("suite before") + +func after(): + assert_str("suite after").override_failure_message("failed on after()").is_empty() + +func before_test(): + assert_str("test before").is_equal("test before") + +func after_test(): + assert_str("test after").is_equal("test after") + +func test_case1(): + assert_str("test_case1").is_equal("test_case1") + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource new file mode 100644 index 0000000..3bd86b2 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageAfterTest.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage after_test() +extends GdUnitTestSuite + +func before(): + assert_str("suite before").is_equal("suite before") + +func after(): + assert_str("suite after").is_equal("suite after") + +func before_test(): + assert_str("test before").is_equal("test before") + +func after_test(): + assert_str("test after").override_failure_message("failed on after_test()").is_empty() + +func test_case1(): + assert_str("test_case1").is_equal("test_case1") + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource new file mode 100644 index 0000000..a30bafa --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBefore.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage before() +extends GdUnitTestSuite + +func before(): + assert_str("suite before").override_failure_message("failed on before()").is_empty() + +func after(): + assert_str("suite after").is_equal("suite after") + +func before_test(): + assert_str("test before").is_equal("test before") + +func after_test(): + assert_str("test after").is_equal("test after") + +func test_case1(): + assert_str("test_case1").is_equal("test_case1") + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource new file mode 100644 index 0000000..b0b900d --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageBeforeTest.resource @@ -0,0 +1,20 @@ +# this test suite fails on stage before_test() +extends GdUnitTestSuite + +func before(): + assert_str("suite before").is_equal("suite before") + +func after(): + assert_str("suite after").is_equal("suite after") + +func before_test(): + assert_str("test before").override_failure_message("failed on before_test()").is_empty() + +func after_test(): + assert_str("test after").is_equal("test after") + +func test_case1(): + assert_str("test_case1").is_equal("test_case1") + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource new file mode 100644 index 0000000..40a9a7a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailOnStageTestCase1.resource @@ -0,0 +1,20 @@ +# this test suite fails on a single test case +extends GdUnitTestSuite + +func before(): + assert_str("suite before").is_equal("suite before") + +func after(): + assert_str("suite after").is_equal("suite after") + +func before_test(): + assert_str("test before").is_equal("test before") + +func after_test(): + assert_str("test after").is_equal("test after") + +func test_case1(): + assert_str("test_case1").override_failure_message("failed on test_case1()").is_empty() + +func test_case2(): + assert_str("test_case2").is_equal("test_case2") diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.resource new file mode 100644 index 0000000..ff0ccce --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteFuzzedMetricsTest.resource @@ -0,0 +1,82 @@ +# this test suite simulates long running test cases +extends GdUnitTestSuite +@warning_ignore('unused_parameter') + + +class TestCaseStatistics: + var _testcase_before_called := 0 + var _testcase_after_called := 0 + var _test_called := 0 + var _expected_calls :int + + func _init(expected_calls :int): + _expected_calls = expected_calls + + func count_test_before_test(): + _testcase_before_called +=1 + + func count_test_after_test(): + _testcase_after_called +=1 + + func count_test(): + _test_called += 1 + + +var _metrics = { + "test_execute_3times" : TestCaseStatistics.new(3), + "test_execute_5times" : TestCaseStatistics.new(5) +} + +var _stack : Array +var _before_called := 0 +var _after_called := 0 + + +func before(): + _before_called += 1 + # init the stack + _stack = [] + + +func after(): + _after_called += 1 + assert_that(_before_called)\ + .override_failure_message("Expecting 'before' is called only one times")\ + .is_equal(1) + assert_that(_after_called)\ + .override_failure_message("Expecting 'after' is called only one times")\ + .is_equal(1) + + for test_case in _metrics.keys(): + var statistics := _metrics[test_case] as TestCaseStatistics + assert_int(statistics._testcase_before_called)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_before_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._test_called)\ + .override_failure_message("Expect test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._test_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._testcase_after_called)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_after_called, test_case])\ + .is_equal(statistics._expected_calls) + + +func before_test(): + _metrics[__active_test_case].count_test_before_test() + # clean the stack before every test run + _stack.clear() + + +func after_test(): + _metrics[__active_test_case].count_test_after_test() + + +@warning_ignore('unused_parameter') +func test_execute_3times(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations = 3): + _metrics[__active_test_case].count_test() + pass + + +@warning_ignore('unused_parameter') +func test_execute_5times(fuzzer := Fuzzers.rangei(0, 1000), fuzzer_iterations = 5): + _metrics[__active_test_case].count_test() + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource new file mode 100644 index 0000000..bdf871a --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteInvalidParameterizedTests.resource @@ -0,0 +1,56 @@ +class_name TestSuiteInvalidParameterizedTests +extends GdUnitTestSuite + +func test_no_parameters(): + assert_that(true).is_equal(true) + +@warning_ignore('unused_parameter') +func test_parameterized_success(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] ]): + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_failed(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]): + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_to_less_args(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]): + pass + +@warning_ignore('unused_parameter') +func test_parameterized_to_many_args(a: int, b :int, c :int, d :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 21] ]): + pass + +@warning_ignore('unused_parameter') +func test_parameterized_to_less_args_at_index_1(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 6], + [3, 4, 5, 11], + [6, 7, 21] ]): + pass + +@warning_ignore('unused_parameter') +func test_parameterized_invalid_struct(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 6], + ["foo"], + [6, 7, 21] ]): + pass + +@warning_ignore('unused_parameter') +func test_parameterized_invalid_args(a: int, b :int, expected :int, test_parameters := [ + [1, 2, 6], + [3, "4", 11], + [6, 7, 21] ]): + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.resource new file mode 100644 index 0000000..08c2d42 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedMetricsTest.resource @@ -0,0 +1,85 @@ +extends GdUnitTestSuite + +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + + +class TestCaseStatistics: + var _testcase_before_called := 0 + var _testcase_after_called := 0 + var _test_called := 0 + var _expected_calls :int + + func _init(expected_calls :int): + _expected_calls = expected_calls + + func count_test_before_test(): + _testcase_before_called +=1 + + func count_test_after_test(): + _testcase_after_called +=1 + + func count_test(): + _test_called += 1 + + +var _metrics = { + "test_parameterized_2times" : TestCaseStatistics.new(2), + "test_parameterized_5times" : TestCaseStatistics.new(5) +} + +var _before_called := 0 +var _after_called := 0 + + +func before(): + _before_called += 1 + + +func after(): + _after_called += 1 + assert_that(_before_called)\ + .override_failure_message("Expecting 'before' is called only one times")\ + .is_equal(1) + assert_that(_after_called)\ + .override_failure_message("Expecting 'after' is called only one times")\ + .is_equal(1) + + for test_case in _metrics.keys(): + var statistics := _metrics[test_case] as TestCaseStatistics + assert_int(statistics._testcase_before_called)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_before_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._test_called)\ + .override_failure_message("Expect test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._test_called, test_case])\ + .is_equal(statistics._expected_calls) + assert_int(statistics._testcase_after_called)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [statistics._expected_calls, statistics._testcase_after_called, test_case])\ + .is_equal(statistics._expected_calls) + + +func before_test(): + _metrics[__active_test_case].count_test_before_test() + + +func after_test(): + _metrics[__active_test_case].count_test_after_test() + + +@warning_ignore('unused_parameter') +func test_parameterized_2times(a: int, expected :bool, test_parameters := [ + [0, false], + [1, true]]): + + _metrics[__active_test_case].count_test() + + +@warning_ignore('unused_parameter') +func test_parameterized_5times(a: int, expected :bool, test_parameters := [ + [0, false], + [1, true], + [0, false], + [1, true], + [1, true]]): + + _metrics[__active_test_case].count_test() diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource new file mode 100644 index 0000000..3bb5409 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteParameterizedTests.resource @@ -0,0 +1,148 @@ +extends GdUnitTestSuite + +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + + +class TestCaseStatistics: + var _testcase_before_called := 0 + var _testcase_after_called := 0 + var _expected_testcase_before :int + var _expected_testcase_after :int + + func _init(testcase_before_calls := 0, testcase_after_calls := 0): + _expected_testcase_before = testcase_before_calls + _expected_testcase_after = testcase_after_calls + + func count_test_before_test(): + _testcase_before_called +=1 + + func count_test_after_test(): + _testcase_after_called +=1 + + +var _metrics = { + "test_parameterized_bool_value" : TestCaseStatistics.new(2, 2), + "test_parameterized_int_values" : TestCaseStatistics.new(3, 3) +} + +var _before_called := 0 +var _after_called := 0 + + +func before(): + _before_called += 1 + + +func after(): + _after_called += 1 + assert_that(_before_called)\ + .override_failure_message("Expecting 'before' is called only one times")\ + .is_equal(1) + assert_that(_after_called)\ + .override_failure_message("Expecting 'after' is called only one times")\ + .is_equal(1) + + for test_case in _metrics.keys(): + var statistics := _metrics[test_case] as TestCaseStatistics + assert_int(statistics._testcase_before_called)\ + .override_failure_message("Expect before_test called %s times but is %s for test case %s" % [statistics._expected_testcase_before, statistics._testcase_before_called, test_case])\ + .is_equal(statistics._expected_testcase_before) + assert_int(statistics._testcase_after_called)\ + .override_failure_message("Expect after_test called %s times but is %s for test case %s" % [statistics._expected_testcase_after, statistics._testcase_after_called, test_case])\ + .is_equal(statistics._expected_testcase_after) + + +func before_test(): + if _metrics.has(__active_test_case): + _metrics[__active_test_case].count_test_before_test() + + +func after_test(): + if _metrics.has(__active_test_case): + _metrics[__active_test_case].count_test_after_test() + + +@warning_ignore('unused_parameter') +func test_parameterized_bool_value(a: int, expected :bool, test_parameters := [ + [0, false], + [1, true]]): + + assert_that(bool(a)).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_int_values(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 12], + [6, 7, 8, 21] ]): + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_int_values_fail(a: int, b :int, c :int, expected :int, test_parameters := [ + [1, 2, 3, 6], + [3, 4, 5, 11], + [6, 7, 8, 22] ]): + + assert_that(a+b+c).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_float_values(a: float, b :float, expected :float, test_parameters := [ + [2.2, 2.2, 4.4], + [2.2, 2.3, 4.5], + [3.3, 2.2, 5.5] ]): + + assert_float(a+b).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_string_values(a: String, b :String, expected :String, test_parameters := [ + ["2.2", "2.2", "2.22.2"], + ["foo", "bar", "foobar"], + ["a", "b", "ab"] ]): + + assert_that(a+b).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_Vector2_values(a: Vector2, b :Vector2, expected :Vector2, test_parameters := [ + [Vector2.ONE, Vector2.ONE, Vector2(2, 2)], + [Vector2.LEFT, Vector2.RIGHT, Vector2.ZERO], + [Vector2.ZERO, Vector2.LEFT, Vector2.LEFT] ]): + + assert_that(a+b).is_equal(expected) + +@warning_ignore('unused_parameter') +func test_parameterized_Vector3_values(a: Vector3, b :Vector3, expected :Vector3, test_parameters := [ + [Vector3.ONE, Vector3.ONE, Vector3(2, 2, 2)], + [Vector3.LEFT, Vector3.RIGHT, Vector3.ZERO], + [Vector3.ZERO, Vector3.LEFT, Vector3.LEFT] ]): + + assert_that(a+b).is_equal(expected) + +class TestObj extends Resource: + var _value :String + + func _init(value :String): + _value = value + + func _to_string() -> String: + return _value + +@warning_ignore('unused_parameter') +func test_parameterized_obj_values(a: Object, b :Object, expected :String, test_parameters := [ + [TestObj.new("abc"), TestObj.new("def"), "abcdef"]]): + + assert_that(a.to_string()+b.to_string()).is_equal(expected) + + +@warning_ignore('unused_parameter') +func test_dictionary_div_number_types( + value : Dictionary, + expected : Dictionary, + test_parameters : Array = [ + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50, bottom = 50, left = 50, right = 50}], + [{ top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50.0, bottom = 50.0, left = 50.0, right = 50.0}], + [{ top = 50, bottom = 50, left = 50, right = 50}, { top = 50, bottom = 50, left = 50, right = 50}], + ] + ) -> void: + assert_that(value).is_equal(expected) diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource new file mode 100644 index 0000000..994bca0 --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteSkipped.resource @@ -0,0 +1,20 @@ +extends GdUnitTestSuite + + +func is_skipped() -> bool: + return true + + +@warning_ignore('unused_parameter') +func before(do_skip=is_skipped(), skip_reason="do not run this"): + pass + + +@warning_ignore('unused_parameter') +func test_case1(timeout = 1000, do_skip=1==1, skip_reason="do not run this"): + pass + + +@warning_ignore('unused_parameter') +func test_case2(skip_reason="ignored"): + pass diff --git a/addons/gdUnit4/test/core/resources/testsuites/TestSuiteWithoutTests.gd b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteWithoutTests.gd new file mode 100644 index 0000000..9d1564e --- /dev/null +++ b/addons/gdUnit4/test/core/resources/testsuites/TestSuiteWithoutTests.gd @@ -0,0 +1,9 @@ +extends GdUnitTestSuite + + +func before(): + pass + + +func foo(): + pass diff --git a/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd b/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd new file mode 100644 index 0000000..50fc582 --- /dev/null +++ b/addons/gdUnit4/test/core/templates/test_suite/GdUnitTestSuiteTemplateTest.gd @@ -0,0 +1,97 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnitTestSuiteTemplateTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd' + +const CUSTOM_TEMPLATE = """ + # GdUnit generated TestSuite + class_name ${suite_class_name} + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var ${source_var}_1 := ${source_class}.new() + var ${source_var}_2 = load("${source_resource_path}") +""" + + +func after() -> void: + GdUnitTestSuiteTemplate.reset_to_default(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + + +func test_default_template() -> void: + assert_str(GdUnitTestSuiteTemplate.default_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD)).is_equal(GdUnitTestSuiteTemplate.default_GD_template()) + + +func test_build_template_default() -> void: + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/script_with_class_name.gd") + var expected := """ + # GdUnit generated TestSuite + class_name ScriptWithClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + # TestSuite generated from + const __source = 'res://addons/gdUnit4/test/core/resources/script_with_class_name.gd' + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) + + +# checked source with class_name definition +func test_build_template_custom1() -> void: + GdUnitTestSuiteTemplate.save_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD, CUSTOM_TEMPLATE) + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/script_with_class_name.gd") + var expected := """ + # GdUnit generated TestSuite + class_name ScriptWithClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var script_with_class_name_1 := ScriptWithClassName.new() + var script_with_class_name_2 = load("res://addons/gdUnit4/test/core/resources/script_with_class_name.gd") + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) + + +# checked source without class_name definition +func test_build_template_custom2() -> void: + GdUnitTestSuiteTemplate.save_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD, CUSTOM_TEMPLATE) + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/script_without_class_name.gd") + var expected := """ + # GdUnit generated TestSuite + class_name ScriptWithoutClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var script_without_class_name_1 := ScriptWithoutClassName.new() + var script_without_class_name_2 = load("res://addons/gdUnit4/test/core/resources/script_without_class_name.gd") + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) + + +# checked source with class_name definition pascal_case +func test_build_template_custom3() -> void: + GdUnitTestSuiteTemplate.save_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD, CUSTOM_TEMPLATE) + var template := GdUnitTestSuiteTemplate.build_template("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd") + var expected := """ + # GdUnit generated TestSuite + class_name PascalCaseWithClassNameTest + extends GdUnitTestSuite + @warning_ignore('unused_parameter') + @warning_ignore('return_value_discarded') + + func before() -> void: + var pascal_case_with_class_name_1 := PascalCaseWithClassName.new() + var pascal_case_with_class_name_2 = load("res://addons/gdUnit4/test/core/resources/naming_conventions/PascalCaseWithClassName.gd") + """.dedent().trim_prefix("\n") + assert_str(template).is_equal(expected) diff --git a/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd b/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd new file mode 100644 index 0000000..e79b0fc --- /dev/null +++ b/addons/gdUnit4/test/extractors/GdUnitFuncValueExtractorTest.gd @@ -0,0 +1,67 @@ +# GdUnit generated TestSuite +class_name GdUnitFuncValueExtractorTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd' +const GdUnitFuncValueExtractor = preload("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd") + + +class TestNode extends Resource: + var _parent = null + var _children := Array() + + func _init(name :String,parent = null): + set_name(name) + _parent = parent + if _parent: + _parent._children.append(self) + + + func _notification(what): + if what == NOTIFICATION_PREDELETE: + _parent = null + _children.clear() + + + func get_parent() -> TestNode: + return _parent + + + func get_children() -> Array: + return _children + + + +func test_extract_value_success() -> void: + var node = auto_free(TestNode.new("node_a")) + + assert_str(GdUnitFuncValueExtractor.new("get_name", []).extract_value(node)).is_equal("node_a") + + +func test_extract_value_func_not_exists() -> void: + var node = TestNode.new("node_a") + + assert_str(GdUnitFuncValueExtractor.new("get_foo", []).extract_value(node)).is_equal("n.a.") + + +func test_extract_value_on_null_value() -> void: + assert_str(GdUnitFuncValueExtractor.new("get_foo", []).extract_value(null)).is_null() + + +func test_extract_value_chanined() -> void: + var parent = TestNode.new("parent") + var node = auto_free(TestNode.new("node_a", parent)) + + assert_str(GdUnitFuncValueExtractor.new("get_name", []).extract_value(node)).is_equal("node_a") + assert_str(GdUnitFuncValueExtractor.new("get_parent.get_name", []).extract_value(node)).is_equal("parent") + + +func test_extract_value_chanined_array_values() -> void: + var parent = TestNode.new("parent") + auto_free(TestNode.new("node_a", parent)) + auto_free(TestNode.new("node_b", parent)) + auto_free(TestNode.new("node_c", parent)) + + assert_array(GdUnitFuncValueExtractor.new("get_children.get_name", []).extract_value(parent))\ + .contains_exactly(["node_a", "node_b", "node_c"]) diff --git a/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd b/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd new file mode 100644 index 0000000..f4dc7ba --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/GdUnitFuzzerValueInjectionTest.gd @@ -0,0 +1,161 @@ +extends GdUnitTestSuite + +var _current_iterations : Dictionary +var _expected_iterations: Dictionary + +# a simple test fuzzer where provided a hard coded value set +class TestFuzzer extends Fuzzer: + var _data := [0, 1, 2, 3, 4, 5, 6, 23, 8, 9] + + + func next_value(): + return _data.pop_front() + + +func max_value() -> int: + return 10 + + +func min_value() -> int: + return 1 + + +func fuzzer() -> Fuzzer: + return Fuzzers.rangei(min_value(), max_value()) + + +func before(): + # define expected iteration count + _expected_iterations = { + "test_fuzzer_has_same_instance_peer_iteration" : 10, + "test_multiple_fuzzers_inject_value_with_seed" : 10, + "test_fuzzer_iterations_default" : Fuzzer.ITERATION_DEFAULT_COUNT, + "test_fuzzer_iterations_custom_value" : 234, + "test_fuzzer_inject_value" : 100, + "test_multiline_fuzzer_args": 23, + } + # inital values + _current_iterations = { + "test_fuzzer_has_same_instance_peer_iteration" : 0, + "test_multiple_fuzzers_inject_value_with_seed" : 0, + "test_fuzzer_iterations_default" : 0, + "test_fuzzer_iterations_custom_value" : 0, + "test_fuzzer_inject_value" : 0, + "test_multiline_fuzzer_args": 0, + } + + +func after(): + for test_case in _expected_iterations.keys(): + var current = _current_iterations[test_case] + var expected = _expected_iterations[test_case] + + assert_int(current).override_failure_message("Expecting %s itertions but is %s checked test case %s" % [expected, current, test_case]).is_equal(expected) + +var _fuzzer_instance_before : Fuzzer = null + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_has_same_instance_peer_iteration(fuzzer=TestFuzzer.new(), fuzzer_iterations = 10): + _current_iterations["test_fuzzer_has_same_instance_peer_iteration"] += 1 + assert_object(fuzzer).is_not_null() + if _fuzzer_instance_before != null: + assert_that(fuzzer).is_same(_fuzzer_instance_before) + _fuzzer_instance_before = fuzzer + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_iterations_default(fuzzer := Fuzzers.rangei(-23, 22)): + _current_iterations["test_fuzzer_iterations_default"] += 1 + assert_object(fuzzer).is_not_null() + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_iterations_custom_value(fuzzer := Fuzzers.rangei(-23, 22), fuzzer_iterations = 234, fuzzer_seed = 100): + _current_iterations["test_fuzzer_iterations_custom_value"] += 1 + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_inject_value(fuzzer := Fuzzers.rangei(-23, 22), fuzzer_iterations = 100): + _current_iterations["test_fuzzer_inject_value"] += 1 + assert_object(fuzzer).is_not_null() + assert_int(fuzzer.next_value()).is_between(-23, 22) + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_with_timeout(fuzzer := Fuzzers.rangei(-23, 22), fuzzer_iterations = 20, timeout = 100): + discard_error_interupted_by_timeout() + assert_int(fuzzer.next_value()).is_between(-23, 22) + + if fuzzer.iteration_index() == 10: + await await_millis(100) + # we not expect more than 10 iterations it should be interuptead by a timeout + assert_int(fuzzer.iteration_index()).is_less_equal(10) + +var expected_value := [22, 3, -14, -16, 21, 20, 4, -23, -19, -5] + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_inject_value_with_seed(fuzzer := Fuzzers.rangei(-23, 22), fuzzer_iterations = 10, fuzzer_seed = 187772): + assert_object(fuzzer).is_not_null() + var iteration_index = fuzzer.iteration_index()-1 + var current = fuzzer.next_value() + var expected = expected_value[iteration_index] + assert_int(iteration_index).is_between(0, 9).is_less(10) + assert_int(current)\ + .override_failure_message("Expect value %s checked test iteration %s\n but was %s" % [expected, iteration_index, current])\ + .is_equal(expected) + +var expected_value_a := [22, -14, 21, 4, -19, -11, 5, 21, -6, -9] +var expected_value_b := [35, 38, 34, 39, 35, 41, 37, 35, 34, 39] + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_multiple_fuzzers_inject_value_with_seed(fuzzer_a := Fuzzers.rangei(-23, 22), fuzzer_b := Fuzzers.rangei(33, 44), fuzzer_iterations = 10, fuzzer_seed = 187772): + _current_iterations["test_multiple_fuzzers_inject_value_with_seed"] += 1 + assert_object(fuzzer_a).is_not_null() + assert_object(fuzzer_b).is_not_null() + var iteration_index_a = fuzzer_a.iteration_index()-1 + var current_a = fuzzer_a.next_value() + var expected_a = expected_value_a[iteration_index_a] + assert_int(iteration_index_a).is_between(0, 9).is_less(10) + assert_int(current_a).is_between(-23, 22) + assert_int(current_a)\ + .override_failure_message("Expect value %s checked test iteration %s\n but was %s" % [expected_a, iteration_index_a, current_a])\ + .is_equal(expected_a) + var iteration_index_b = fuzzer_b.iteration_index()-1 + var current_b = fuzzer_b.next_value() + var expected_b = expected_value_b[iteration_index_b] + assert_int(iteration_index_b).is_between(0, 9).is_less(10) + assert_int(current_b).is_between(33, 44) + assert_int(current_b)\ + .override_failure_message("Expect value %s checked test iteration %s\n but was %s" % [expected_b, iteration_index_b, current_b])\ + .is_equal(expected_b) + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_error_after_eight_iterations(fuzzer=TestFuzzer.new(), fuzzer_iterations = 10): + assert_object(fuzzer).is_not_null() + # should fail after 8 iterations + if fuzzer.iteration_index() == 8: + assert_failure(func(): assert_int(fuzzer.next_value()).is_between(0, 9)) \ + .is_failed() \ + .has_message("Expecting:\n '23'\n in range between\n '0' <> '9'") + else: + assert_int(fuzzer.next_value()).is_between(0, 9) + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_fuzzer_custom_func(fuzzer=fuzzer()): + assert_object(fuzzer).is_not_null() + assert_int(fuzzer.next_value()).is_between(1, 10) + + +@warning_ignore("shadowed_variable", "unused_parameter") +func test_multiline_fuzzer_args( + fuzzer_a := Fuzzers.rangev2(Vector2(-47, -47), Vector2(47, 47)), + fuzzer_b := Fuzzers.rangei(0, 9), + fuzzer_iterations = 23): + assert_object(fuzzer_a).is_not_null() + assert_object(fuzzer_b).is_not_null() + _current_iterations["test_multiline_fuzzer_args"] += 1 diff --git a/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd b/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd new file mode 100644 index 0000000..388dc97 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/StringFuzzerTest.gd @@ -0,0 +1,34 @@ +# GdUnit generated TestSuite +class_name StringFuzzerTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/fuzzers/StringFuzzer.gd' + + +func test_extract_charset() -> void: + assert_str(StringFuzzer.extract_charset("abc").get_string_from_ascii()).is_equal("abc") + assert_str(StringFuzzer.extract_charset("abcDXG").get_string_from_ascii()).is_equal("abcDXG") + assert_str(StringFuzzer.extract_charset("a-c").get_string_from_ascii()).is_equal("abc") + assert_str(StringFuzzer.extract_charset("a-z").get_string_from_ascii()).is_equal("abcdefghijklmnopqrstuvwxyz") + assert_str(StringFuzzer.extract_charset("A-Z").get_string_from_ascii()).is_equal("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + + # range token at start + assert_str(StringFuzzer.extract_charset("-a-dA-D2-8+_").get_string_from_ascii()).is_equal("-abcdABCD2345678+_") + # range token at end + assert_str(StringFuzzer.extract_charset("a-dA-D2-8+_-").get_string_from_ascii()).is_equal("abcdABCD2345678+_-") + # range token in the middle + assert_str(StringFuzzer.extract_charset("a-d-A-D2-8+_").get_string_from_ascii()).is_equal("abcd-ABCD2345678+_") + + +func test_next_value() -> void: + var pattern := "a-cD-X+2-5" + var fuzzer := StringFuzzer.new(4, 128, pattern) + var r := RegEx.new() + r.compile("[%s]+" % pattern) + for i in 100: + var value :String = fuzzer.next_value() + # verify the generated value has a length in the configured min/max range + assert_int(value.length()).is_between(4, 128) + # using regex to remove_at all expected chars to verify the value only containing expected chars by is empty + assert_str(r.sub(value, "")).is_empty() diff --git a/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd b/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd new file mode 100644 index 0000000..c88917f --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/TestExternalFuzzer.gd @@ -0,0 +1,10 @@ +class_name TestExternalFuzzer +extends Fuzzer + + +func _init(): + pass + + +func next_value() -> Variant: + return {} diff --git a/addons/gdUnit4/test/fuzzers/TestFuzzers.gd b/addons/gdUnit4/test/fuzzers/TestFuzzers.gd new file mode 100644 index 0000000..3700678 --- /dev/null +++ b/addons/gdUnit4/test/fuzzers/TestFuzzers.gd @@ -0,0 +1,27 @@ +extends RefCounted + +const MIN_VALUE := -10 +const MAX_VALUE := 22 + +class NestedFuzzer extends Fuzzer: + + func _init(): + pass + + func next_value() -> Variant: + return {} + + static func _s_max_value() -> int: + return MAX_VALUE + + +func min_value() -> int: + return MIN_VALUE + + +func get_fuzzer() -> Fuzzer: + return Fuzzers.rangei(min_value(), NestedFuzzer._s_max_value()) + + +func non_fuzzer() -> Resource: + return Image.new() diff --git a/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd new file mode 100644 index 0000000..7707986 --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyArgumentMatcherTest.gd @@ -0,0 +1,29 @@ +# GdUnit generated TestSuite +class_name AnyArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd' + + +func test_is_match(): + var matcher := AnyArgumentMatcher.new() + + assert_bool(matcher.is_match(null)).is_true() + assert_bool(matcher.is_match("")).is_true() + assert_bool(matcher.is_match("abc")).is_true() + assert_bool(matcher.is_match(true)).is_true() + assert_bool(matcher.is_match(false)).is_true() + assert_bool(matcher.is_match(0)).is_true() + assert_bool(matcher.is_match(100010)).is_true() + assert_bool(matcher.is_match(1.2)).is_true() + assert_bool(matcher.is_match(RefCounted.new())).is_true() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_true() + + +func test_any(): + assert_object(any()).is_instanceof(AnyArgumentMatcher) + + +func test_to_string() -> void: + assert_str(str(any())).is_equal("any()") diff --git a/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd new file mode 100644 index 0000000..a5448f5 --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyBuildInTypeArgumentMatcherTest.gd @@ -0,0 +1,228 @@ +# GdUnit generated TestSuite +class_name AnyBuildInTypeArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd' + + +func test_is_match_bool() -> void: + assert_object(any_bool()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_bool() + assert_bool(matcher.is_match(true)).is_true() + assert_bool(matcher.is_match(false)).is_true() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(0.2)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_int() -> void: + assert_object(any_int()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_int() + assert_bool(matcher.is_match(0)).is_true() + assert_bool(matcher.is_match(1000)).is_true() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match([])).is_false() + assert_bool(matcher.is_match(0.2)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_float() -> void: + assert_object(any_float()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_float() + assert_bool(matcher.is_match(.0)).is_true() + assert_bool(matcher.is_match(0.0)).is_true() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match([])).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_string() -> void: + assert_object(any_string()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_string() + assert_bool(matcher.is_match("")).is_true() + assert_bool(matcher.is_match("abc")).is_true() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match([])).is_false() + assert_bool(matcher.is_match(0.2)).is_false() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + + +func test_is_match_color() -> void: + assert_object(any_color()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_color() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Color.ALICE_BLUE)).is_true() + assert_bool(matcher.is_match(Color.RED)).is_true() + + +func test_is_match_vector() -> void: + assert_object(any_vector()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector() + assert_bool(matcher.is_match(Vector2.ONE)).is_true() + assert_bool(matcher.is_match(Vector2i.ONE)).is_true() + assert_bool(matcher.is_match(Vector3.ONE)).is_true() + assert_bool(matcher.is_match(Vector3i.ONE)).is_true() + assert_bool(matcher.is_match(Vector4.ONE)).is_true() + assert_bool(matcher.is_match(Vector4i.ONE)).is_true() + + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + + +func test_is_match_vector2() -> void: + assert_object(any_vector2()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector2() + assert_bool(matcher.is_match(Vector2.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector2i() -> void: + assert_object(any_vector2i()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector2i() + assert_bool(matcher.is_match(Vector2i.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector3() -> void: + assert_object(any_vector3()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector3() + assert_bool(matcher.is_match(Vector3.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector3i() -> void: + assert_object(any_vector3i()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector3i() + assert_bool(matcher.is_match(Vector3i.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector4() -> void: + assert_object(any_vector4()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector4() + assert_bool(matcher.is_match(Vector4.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4i.ONE)).is_false() + + +func test_is_match_vector4i() -> void: + assert_object(any_vector4i()).is_instanceof(AnyBuildInTypeArgumentMatcher) + + var matcher := any_vector4i() + assert_bool(matcher.is_match(Vector4i.ONE)).is_true() + assert_bool(matcher.is_match("")).is_false() + assert_bool(matcher.is_match("abc")).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(1000)).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(Vector2.ONE)).is_false() + assert_bool(matcher.is_match(Vector2i.ONE)).is_false() + assert_bool(matcher.is_match(Vector3.ONE)).is_false() + assert_bool(matcher.is_match(Vector3i.ONE)).is_false() + assert_bool(matcher.is_match(Vector4.ONE)).is_false() + + +func test_to_string() -> void: + assert_str(str(any_bool())).is_equal("any_bool()") + assert_str(str(any_int())).is_equal("any_int()") + assert_str(str(any_float())).is_equal("any_float()") + assert_str(str(any_string())).is_equal("any_string()") + assert_str(str(any_color())).is_equal("any_color()") + assert_str(str(any_vector())).is_equal("any_vector()") + assert_str(str(any_vector2())).is_equal("any_vector2()") + assert_str(str(any_vector2i())).is_equal("any_vector2i()") + assert_str(str(any_vector3())).is_equal("any_vector3()") + assert_str(str(any_vector3i())).is_equal("any_vector3i()") + assert_str(str(any_vector4())).is_equal("any_vector4()") + assert_str(str(any_vector4i())).is_equal("any_vector4i()") + assert_str(str(any_rect2())).is_equal("any_rect2()") + assert_str(str(any_plane())).is_equal("any_plane()") + assert_str(str(any_quat())).is_equal("any_quat()") + assert_str(str(any_quat())).is_equal("any_quat()") + assert_str(str(any_basis())).is_equal("any_basis()") + assert_str(str(any_transform_2d())).is_equal("any_transform_2d()") + assert_str(str(any_transform_3d())).is_equal("any_transform_3d()") + assert_str(str(any_node_path())).is_equal("any_node_path()") + assert_str(str(any_rid())).is_equal("any_rid()") + assert_str(str(any_object())).is_equal("any_object()") + assert_str(str(any_dictionary())).is_equal("any_dictionary()") + assert_str(str(any_array())).is_equal("any_array()") + assert_str(str(any_packed_byte_array())).is_equal("any_packed_byte_array()") + assert_str(str(any_packed_int32_array())).is_equal("any_packed_int32_array()") + assert_str(str(any_packed_int64_array())).is_equal("any_packed_int64_array()") + assert_str(str(any_packed_float32_array())).is_equal("any_packed_float32_array()") + assert_str(str(any_packed_float64_array())).is_equal("any_packed_float64_array()") + assert_str(str(any_packed_string_array())).is_equal("any_packed_string_array()") + assert_str(str(any_packed_vector2_array())).is_equal("any_packed_vector2_array()") + assert_str(str(any_packed_vector3_array())).is_equal("any_packed_vector3_array()") + assert_str(str(any_packed_color_array())).is_equal("any_packed_color_array()") diff --git a/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd new file mode 100644 index 0000000..3670c05 --- /dev/null +++ b/addons/gdUnit4/test/matchers/AnyClazzArgumentMatcherTest.gd @@ -0,0 +1,43 @@ +# GdUnit generated TestSuite +class_name AnyClazzArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd' + + +func test_is_match_reference(): + var matcher := AnyClazzArgumentMatcher.new(RefCounted) + + assert_bool(matcher.is_match(Resource.new())).is_true() + assert_bool(matcher.is_match(RefCounted.new())).is_true() + assert_bool(matcher.is_match(auto_free(Node.new()))).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(false)).is_false() + assert_bool(matcher.is_match(true)).is_false() + + +func test_is_match_node(): + var matcher := AnyClazzArgumentMatcher.new(Node) + + assert_bool(matcher.is_match(auto_free(Node.new()))).is_true() + assert_bool(matcher.is_match(auto_free(AnimationPlayer.new()))).is_true() + assert_bool(matcher.is_match(auto_free(Timer.new()))).is_true() + assert_bool(matcher.is_match(Resource.new())).is_false() + assert_bool(matcher.is_match(RefCounted.new())).is_false() + assert_bool(matcher.is_match(null)).is_false() + assert_bool(matcher.is_match(0)).is_false() + assert_bool(matcher.is_match(false)).is_false() + assert_bool(matcher.is_match(true)).is_false() + + +func test_any_class(): + assert_object(any_class(Node)).is_instanceof(AnyClazzArgumentMatcher) + + +func test_to_string() -> void: + assert_str(str(any_class(Node))).is_equal("any_class()") + assert_str(str(any_class(Object))).is_equal("any_class()") + assert_str(str(any_class(RefCounted))).is_equal("any_class()") + assert_str(str(any_class(GdObjects))).is_equal("any_class()") diff --git a/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd new file mode 100644 index 0000000..547a4cb --- /dev/null +++ b/addons/gdUnit4/test/matchers/ChainedArgumentMatcherTest.gd @@ -0,0 +1,36 @@ +# GdUnit generated TestSuite +class_name ChainedArgumentMatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd' + + +func test_is_match_one_arg(): + var matchers = [ + EqualsArgumentMatcher.new("foo") + ] + var matcher := ChainedArgumentMatcher.new(matchers) + + assert_bool(matcher.is_match(["foo"])).is_true() + assert_bool(matcher.is_match(["bar"])).is_false() + + +func test_is_match_two_arg(): + var matchers = [ + EqualsArgumentMatcher.new("foo"), + EqualsArgumentMatcher.new("value1") + ] + var matcher := ChainedArgumentMatcher.new(matchers) + + assert_bool(matcher.is_match(["foo", "value1"])).is_true() + assert_bool(matcher.is_match(["foo", "value2"])).is_false() + assert_bool(matcher.is_match(["bar", "value1"])).is_false() + + +func test_is_match_different_arg_and_matcher(): + var matchers = [ + EqualsArgumentMatcher.new("foo") + ] + var matcher := ChainedArgumentMatcher.new(matchers) + assert_bool(matcher.is_match(["foo", "value"])).is_false() diff --git a/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd b/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd new file mode 100644 index 0000000..5aa945a --- /dev/null +++ b/addons/gdUnit4/test/matchers/CustomArgumentMatcherTest.gd @@ -0,0 +1,26 @@ +extends GdUnitTestSuite + + +class CustomArgumentMatcher extends GdUnitArgumentMatcher: + var _peek :int + + func _init(peek :int): + _peek = peek + + func is_match(value) -> bool: + return value > _peek + + +func test_custom_matcher(): + var mocked_test_class : CustomArgumentMatcherTestClass = mock(CustomArgumentMatcherTestClass) + + mocked_test_class.set_value(1000) + mocked_test_class.set_value(1001) + mocked_test_class.set_value(1002) + mocked_test_class.set_value(2002) + + # counts 1001, 1002, 2002 = 3 times + verify(mocked_test_class, 3).set_value(CustomArgumentMatcher.new(1000)) + # counts 2002 = 1 times + verify(mocked_test_class, 1).set_value(CustomArgumentMatcher.new(2000)) + diff --git a/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd b/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd new file mode 100644 index 0000000..7115972 --- /dev/null +++ b/addons/gdUnit4/test/matchers/GdUnitArgumentMatchersTest.gd @@ -0,0 +1,16 @@ +# GdUnit generated TestSuite +class_name GdUnitArgumentMatchersTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd' + + +func test_arguments_to_chained_matcher(): + var matcher := GdUnitArgumentMatchers.to_matcher(["foo", false, 1]) + + assert_object(matcher).is_instanceof(ChainedArgumentMatcher) + assert_bool(matcher.is_match(["foo", false, 1])).is_true() + assert_bool(matcher.is_match(["foo", false, 2])).is_false() + assert_bool(matcher.is_match(["foo", true, 1])).is_false() + assert_bool(matcher.is_match(["bar", false, 1])).is_false() diff --git a/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd b/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd new file mode 100644 index 0000000..30d06d9 --- /dev/null +++ b/addons/gdUnit4/test/matchers/resources/CustomArgumentMatcherTestClass.gd @@ -0,0 +1,8 @@ +class_name CustomArgumentMatcherTestClass +extends RefCounted + +var _value :int + + +func set_value(value :int): + _value = value diff --git a/addons/gdUnit4/test/mocker/CustomEnums.gd b/addons/gdUnit4/test/mocker/CustomEnums.gd new file mode 100644 index 0000000..0ed0f91 --- /dev/null +++ b/addons/gdUnit4/test/mocker/CustomEnums.gd @@ -0,0 +1,8 @@ +class_name CustomEnums +extends RefCounted + + +enum TEST_ENUM { + FOO = 11, + BAR = 22 +} diff --git a/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd b/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd new file mode 100644 index 0000000..a7f1c8d --- /dev/null +++ b/addons/gdUnit4/test/mocker/GdUnitMockBuilderTest.gd @@ -0,0 +1,282 @@ +# GdUnit generated TestSuite +class_name GdUnitMockBuilderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd' + + +# helper to get function descriptor +func get_function_description(clazz_name :String, method_name :String) -> GdFunctionDescriptor: + var method_list :Array = ClassDB.class_get_method_list(clazz_name) + for method_descriptor in method_list: + if method_descriptor["name"] == method_name: + return GdFunctionDescriptor.extract_from(method_descriptor) + return null + + +func test_double_return_typed_function_without_arg() -> void: + var doubler := GdUnitMockFunctionDoubler.new(false) + # String get_class() const + var fd := get_function_description("Object", "get_class") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func get_class() -> String:', + ' var args :Array = ["get_class", ]', + '', + ' if __is_prepare_return_value():', + ' __save_function_return_value(args)', + ' return ""', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return ""', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("get_class", args):', + ' return super()', + ' return __get_mocked_return_value_or_default(args, "")', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_typed_function_with_args() -> void: + var doubler := GdUnitMockFunctionDoubler.new(false) + # bool is_connected(signal: String, callable_: Callable)) const + var fd := get_function_description("Object", "is_connected") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func is_connected(signal_, callable_) -> bool:', + ' var args :Array = ["is_connected", signal_, callable_]', + '', + ' if __is_prepare_return_value():', + ' __save_function_return_value(args)', + ' return false', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return false', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("is_connected", args):', + ' return super(signal_, callable_)', + ' return __get_mocked_return_value_or_default(args, false)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_untyped_function_with_args() -> void: + var doubler := GdUnitMockFunctionDoubler.new(false) + + # void disconnect(signal: StringName, callable: Callable) + var fd := get_function_description("Object", "disconnect") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func disconnect(signal_, callable_) -> void:', + ' var args :Array = ["disconnect", signal_, callable_]', + '', + ' if __is_prepare_return_value():', + ' if false:', + ' push_error("Mocking a void function \'disconnect() -> void:\' is not allowed.")', + ' return', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("disconnect"):', + ' super(signal_, callable_)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_int_function_with_varargs() -> void: + var doubler := GdUnitMockFunctionDoubler.new(false) + # Error emit_signal(signal: StringName, ...) vararg + var fd := get_function_description("Object", "emit_signal") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("int_as_enum_without_match")', + '@warning_ignore("int_as_enum_without_cast")', + '@warning_ignore("shadowed_variable")', + 'func emit_signal(signal_, vararg0_="__null__", vararg1_="__null__", vararg2_="__null__", vararg3_="__null__", vararg4_="__null__", vararg5_="__null__", vararg6_="__null__", vararg7_="__null__", vararg8_="__null__", vararg9_="__null__") -> Error:', + ' var varargs :Array = __filter_vargs([vararg0_, vararg1_, vararg2_, vararg3_, vararg4_, vararg5_, vararg6_, vararg7_, vararg8_, vararg9_])', + ' var args :Array = ["emit_signal", signal_] + varargs', + '', + ' if __is_prepare_return_value():', + ' if false:', + ' push_error("Mocking a void function \'emit_signal() -> void:\' is not allowed.")', + ' __save_function_return_value(args)', + ' return OK', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return OK', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("emit_signal", args):', + ' match varargs.size():', + ' 0: return super(signal_)', + ' 1: return super(signal_, varargs[0])', + ' 2: return super(signal_, varargs[0], varargs[1])', + ' 3: return super(signal_, varargs[0], varargs[1], varargs[2])', + ' 4: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3])', + ' 5: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4])', + ' 6: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5])', + ' 7: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6])', + ' 8: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7])', + ' 9: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8])', + ' 10: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9])', + ' return __get_mocked_return_value_or_default(args, OK)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_untyped_function_with_varargs() -> void: + var doubler := GdUnitMockFunctionDoubler.new(false) + + # void emit_custom(signal_name, args ...) vararg const + var fd := GdFunctionDescriptor.new("emit_custom", 10, false, false, false, TYPE_NIL, "", + [GdFunctionArgument.new("signal_", TYPE_SIGNAL)], + GdFunctionDescriptor._build_varargs(true)) + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("shadowed_variable")', + 'func emit_custom(signal_, vararg0_="__null__", vararg1_="__null__", vararg2_="__null__", vararg3_="__null__", vararg4_="__null__", vararg5_="__null__", vararg6_="__null__", vararg7_="__null__", vararg8_="__null__", vararg9_="__null__") -> void:', + ' var varargs :Array = __filter_vargs([vararg0_, vararg1_, vararg2_, vararg3_, vararg4_, vararg5_, vararg6_, vararg7_, vararg8_, vararg9_])', + ' var args :Array = ["emit_custom", signal_] + varargs', + '', + ' if __is_prepare_return_value():', + ' if false:', + ' push_error("Mocking a void function \'emit_custom() -> void:\' is not allowed.")', + ' __save_function_return_value(args)', + ' return null', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return null', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("emit_custom", args):', + ' match varargs.size():', + ' 0: return super(signal_)', + ' 1: return super(signal_, varargs[0])', + ' 2: return super(signal_, varargs[0], varargs[1])', + ' 3: return super(signal_, varargs[0], varargs[1], varargs[2])', + ' 4: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3])', + ' 5: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4])', + ' 6: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5])', + ' 7: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6])', + ' 8: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7])', + ' 9: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8])', + ' 10: return super(signal_, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9])', + ' return __get_mocked_return_value_or_default(args, null)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_virtual_script_function_without_arg() -> void: + var doubler := GdUnitMockFunctionDoubler.new(false) + + # void _ready() virtual + var fd := get_function_description("Node", "_ready") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func _ready() -> void:', + ' var args :Array = ["_ready", ]', + '', + ' if __is_prepare_return_value():', + ' if false:', + ' push_error("Mocking a void function \'_ready() -> void:\' is not allowed.")', + ' return', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("_ready"):', + ' super()', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_virtual_script_function_with_arg() -> void: + var doubler := GdUnitMockFunctionDoubler.new(false) + + # void _input(event: InputEvent) virtual + var fd := get_function_description("Node", "_input") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func _input(event_) -> void:', + ' var args :Array = ["_input", event_]', + '', + ' if __is_prepare_return_value():', + ' if false:', + ' push_error("Mocking a void function \'_input() -> void:\' is not allowed.")', + ' return', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("_input"):', + ' super(event_)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_mock_on_script_path_without_class_name() -> void: + var instance = load("res://addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd").new() + var script := GdUnitMockBuilder.mock_on_script(instance, "res://addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd", [], true); + assert_that(script.resource_name).is_equal("MockClassWithoutNameA.gd") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) + + +func test_mock_on_script_path_with_custom_class_name() -> void: + # the class contains a class_name definition + var instance = load("res://addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd").new() + var script := GdUnitMockBuilder.mock_on_script(instance, "res://addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd", [], false); + assert_that(script.resource_name).is_equal("MockGdUnitTestCustomClassName.gd") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) + + +func test_mock_on_class_with_class_name() -> void: + var script := GdUnitMockBuilder.mock_on_script(ClassWithNameA.new(), ClassWithNameA, [], false); + assert_that(script.resource_name).is_equal("MockClassWithNameA.gd") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) + + +func test_mock_on_class_with_custom_class_name() -> void: + # the class contains a class_name definition + var script := GdUnitMockBuilder.mock_on_script(GdUnit_Test_CustomClassName.new(), GdUnit_Test_CustomClassName, [], false); + assert_that(script.resource_name).is_equal("MockGdUnitTestCustomClassName.gd") + assert_that(script.get_instance_base_type()).is_equal("Resource") + # finally check the mocked script is valid + assert_int(script.reload()).is_equal(OK) diff --git a/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd b/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd new file mode 100644 index 0000000..b579d25 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GdUnitMockerTest.gd @@ -0,0 +1,1148 @@ +class_name GdUnitMockerTest +extends GdUnitTestSuite + + +var resource_path := "res://addons/gdUnit4/test/mocker/resources/" + +var _saved_report_error_settings + + +func before(): + # disable error pushing for testing + _saved_report_error_settings = ProjectSettings.get_setting(GdUnitSettings.REPORT_PUSH_ERRORS) + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + + +func after(): + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, _saved_report_error_settings) + + +func test_mock_instance_id_is_unique(): + var m1 = mock(RefCounted) + var m2 = mock(RefCounted) + # test the internal instance id is unique + assert_that(m1.__instance_id()).is_not_equal(m2.__instance_id()) + assert_object(m1).is_not_same(m2) + + +func test_is_mockable_godot_classes(): + # verify enigne classes + for clazz_name in ClassDB.get_class_list(): + # mocking is not allowed for: + # singleton classes + # unregistered classes in ClassDB + # protected classes (name starts with underscore) + var is_mockable :bool = not Engine.has_singleton(clazz_name) and ClassDB.can_instantiate(clazz_name) and clazz_name.find("_") != 0 + assert_that(GdUnitMockBuilder.is_mockable(clazz_name)) \ + .override_failure_message("Class '%s' expect mockable %s" % [clazz_name, is_mockable]) \ + .is_equal(is_mockable) + + +func test_is_mockable_by_class_type(): + assert_that(GdUnitMockBuilder.is_mockable(Node)).is_true() + assert_that(GdUnitMockBuilder.is_mockable(CSGBox3D)).is_true() + + +func test_is_mockable_custom_class_type(): + assert_that(GdUnitMockBuilder.is_mockable(CustomResourceTestClass)).is_true() + assert_that(GdUnitMockBuilder.is_mockable(CustomNodeTestClass)).is_true() + + +func test_is_mockable_by_script_path(): + assert_that(GdUnitMockBuilder.is_mockable(resource_path + "CustomResourceTestClass.gd")).is_true() + assert_that(GdUnitMockBuilder.is_mockable(resource_path + "CustomNodeTestClass.gd")).is_true() + # verify for non scripts + assert_that(GdUnitMockBuilder.is_mockable(resource_path + "capsuleshape2d.tres")).is_false() + + +func test_is_mockable__overriden_func_get_class(): + # test with class type + assert_that(GdUnitMockBuilder.is_mockable(OverridenGetClassTestClass))\ + .override_failure_message("The class 'CustomResourceTestClass' should be mockable when 'func get_class()' is overriden")\ + .is_true() + # test with resource path + assert_that(GdUnitMockBuilder.is_mockable(resource_path + "OverridenGetClassTestClass.gd"))\ + .override_failure_message("The class 'CustomResourceTestClass' should be mockable when 'func get_class()' is overriden")\ + .is_true() + + +@warning_ignore("unused_parameter") +func test_mock_godot_class_fullcheck(fuzzer=GodotClassNameFuzzer.new(), fuzzer_iterations=200): + var clazz_name = fuzzer.next_value() + # try to create a mock + if GdUnitMockBuilder.is_mockable(clazz_name): + var m = mock(clazz_name, CALL_REAL_FUNC) + assert_that(m)\ + .override_failure_message("The class %s should be mockable" % clazz_name)\ + .is_not_null() + + +func test_mock_by_script_path(): + assert_that(mock(resource_path + "CustomResourceTestClass.gd")).is_not_null() + assert_that(mock(resource_path + "CustomNodeTestClass.gd")).is_not_null() + + +func test_mock_class__overriden_func_get_class(): + assert_that(mock(OverridenGetClassTestClass)).is_not_null() + assert_that(mock(resource_path + "OverridenGetClassTestClass.gd")).is_not_null() + + +func test_mock_fail(): + # not godot class + assert_that(mock("CustomResourceTestClass")).is_null() + # invalid path to script + assert_that(mock("invalid/CustomResourceTestClass.gd")).is_null() + # try to mocking an existing instance is not allowed + assert_that(mock(CustomResourceTestClass.new())).is_null() + + +func test_mock_special_classes(): + var m = mock("JavaClass") as JavaClass + assert_that(m).is_not_null() + + +func test_mock_Node(): + var mocked_node = mock(Node) + assert_that(mocked_node).is_not_null() + + # test we have initial no interactions checked this mock + verify_no_interactions(mocked_node) + + # verify we have never called 'get_child_count()' + verify(mocked_node, 0).get_child_count() + + # call 'get_child_count()' once + mocked_node.get_child_count() + # verify we have called at once + verify(mocked_node).get_child_count() + + # call function 'get_child_count' a second time + mocked_node.get_child_count() + # verify we have called at twice + verify(mocked_node, 2).get_child_count() + + # test mocked function returns default typed value + assert_that(mocked_node.get_child_count()).is_equal(0) + # now mock return value for function 'foo' to 'overwriten value' + do_return(24).on(mocked_node).get_child_count() + # verify the return value is overwritten + assert_that(mocked_node.get_child_count()).is_equal(24) + + +func test_mock_source_with_class_name_by_resource_path() -> void: + var resource_path_ := 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd' + var m = mock(resource_path_) + var head :String = m.get_script().source_code.substr(0, 200) + assert_str(head)\ + .contains("class_name DoubledMunderwoodPathingWorld")\ + .contains("extends '%s'" % resource_path_) + + +func test_mock_source_with_class_name_by_class() -> void: + var resource_path_ := 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd' + var m = mock(Munderwood_Pathing_World) + var head :String = m.get_script().source_code.substr(0, 200) + assert_str(head)\ + .contains("class_name DoubledMunderwoodPathingWorld")\ + .contains("extends '%s'" % resource_path_) + + +func test_mock_extends_godot_class() -> void: + var m = mock(World3D) + var head :String = m.get_script().source_code.substr(0, 200) + assert_str(head)\ + .contains("class_name DoubledWorld")\ + .contains("extends World3D") + + +var _test_signal_args := Array() +func _emit_ready(a, b, c = null): + _test_signal_args = [a, b, c] + + +func test_mock_Node_func_vararg(): + # setup + var mocked_node = mock(Node) + + # mock return value + do_return(ERR_CANT_CONNECT).on(mocked_node).rpc("test", "arg1", "arg2", "invalid") + do_return(ERR_CANT_OPEN).on(mocked_node).rpc("test", "arg1", "argX", any_string()) + do_return(ERR_CANT_CREATE).on(mocked_node).rpc("test", "arg1", "argX", any_int()) + do_return(OK).on(mocked_node).rpc("test", "arg1", "argX", "arg3") + # verify + assert_that(mocked_node.rpc("test", "arg1", "arg2", "arg3")).is_equal(OK) + assert_that(mocked_node.rpc("test", "arg1", "arg2", "invalid")).is_equal(ERR_CANT_CONNECT) + assert_that(mocked_node.rpc("test", "arg1", "argX", "arg3")).is_equal(OK) + assert_that(mocked_node.rpc("test", "arg1", "argX", "other")).is_equal(ERR_CANT_OPEN) + assert_that(mocked_node.rpc("test", "arg1", "argX", 42)).is_equal(ERR_CANT_CREATE) + + +func test_mock_Node_func_vararg_call_real_func(): + # setup + var mocked_node = mock(Node, CALL_REAL_FUNC) + assert_that(mocked_node).is_not_null() + assert_that(_test_signal_args).is_empty() + mocked_node.connect("ready", _emit_ready) + + # test emit it + mocked_node.emit_signal("ready", "aa", "bb", "cc") + + # verify is emitted + verify(mocked_node).emit_signal("ready", "aa", "bb", "cc") + await get_tree().process_frame + assert_that(_test_signal_args).is_equal(["aa", "bb", "cc"]) + + # test emit it + mocked_node.emit_signal("ready", "aa", "xxx") + + # verify is emitted + verify(mocked_node).emit_signal("ready", "aa", "xxx") + await get_tree().process_frame + assert_that(_test_signal_args).is_equal(["aa", "xxx", null]) + + +class ClassWithSignal: + signal test_signal_a + signal test_signal_b + + func foo(arg :int) -> void: + if arg == 0: + emit_signal("test_signal_a", "aa") + else: + emit_signal("test_signal_b", "bb", true) + + func bar(arg :int) -> bool: + if arg == 0: + emit_signal("test_signal_a", "aa") + else: + emit_signal("test_signal_b", "bb", true) + return true + + +func _test_mock_verify_emit_signal(): + var mocked_node = mock(ClassWithSignal, CALL_REAL_FUNC) + assert_that(mocked_node).is_not_null() + + mocked_node.foo(0) + verify(mocked_node, 1).emit_signal("test_signal_a", "aa") + verify(mocked_node, 0).emit_signal("test_signal_b", "bb", true) + reset(mocked_node) + + mocked_node.foo(1) + verify(mocked_node, 0).emit_signal("test_signal_a", "aa") + verify(mocked_node, 1).emit_signal("test_signal_b", "bb", true) + reset(mocked_node) + + mocked_node.bar(0) + verify(mocked_node, 1).emit_signal("test_signal_a", "aa") + verify(mocked_node, 0).emit_signal("test_signal_b", "bb", true) + reset(mocked_node) + + mocked_node.bar(1) + verify(mocked_node, 0).emit_signal("test_signal_a", "aa") + verify(mocked_node, 1).emit_signal("test_signal_b", "bb", true) + + +func test_mock_custom_class_by_class_name(): + var m = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + + # test we have initial no interactions checked this mock + verify_no_interactions(m) + # test mocked function returns default typed value + assert_that(m.foo()).is_equal("") + + # now mock return value for function 'foo' to 'overwriten value' + do_return("overriden value").on(m).foo() + # verify the return value is overwritten + assert_that(m.foo()).is_equal("overriden value") + + # now mock return values by custom arguments + do_return("arg_1").on(m).bar(1) + do_return("arg_2").on(m).bar(2) + + assert_that(m.bar(1)).is_equal("arg_1") + assert_that(m.bar(2)).is_equal("arg_2") + + +func test_mock_custom_class_by_resource_path(): + var m = mock("res://addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd") + assert_that(m).is_not_null() + + # test we have initial no interactions checked this mock + verify_no_interactions(m) + # test mocked function returns default typed value + assert_that(m.foo()).is_equal("") + + # now mock return value for function 'foo' to 'overwriten value' + do_return("overriden value").on(m).foo() + # verify the return value is overwritten + assert_that(m.foo()).is_equal("overriden value") + + # now mock return values by custom arguments + do_return("arg_1").on(m).bar(1) + do_return("arg_2").on(m).bar(2) + + assert_that(m.bar(1)).is_equal("arg_1") + assert_that(m.bar(2)).is_equal("arg_2") + + +func test_mock_custom_class_func_foo_use_real_func(): + var m = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # test mocked function returns value from real function + assert_that(m.foo()).is_equal("foo") + # now mock return value for function 'foo' to 'overwriten value' + do_return("overridden value").on(m).foo() + # verify the return value is overwritten + assert_that(m.foo()).is_equal("overridden value") + + +func test_mock_custom_class_void_func(): + var m = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + # test mocked void function returns null by default + assert_that(m.foo_void()).is_null() + # try now mock return value for a void function. results into an error + do_return("overridden value").on(m).foo_void() + # verify it has no affect for void func + assert_that(m.foo_void()).is_null() + + +func test_mock_custom_class_void_func_real_func(): + var m = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # test mocked void function returns null by default + assert_that(m.foo_void()).is_null() + # try now mock return value for a void function. results into an error + do_return("overridden value").on(m).foo_void() + # verify it has no affect for void func + assert_that(m.foo_void()).is_null() + + +func test_mock_custom_class_func_foo_call_times(): + var m = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + verify(m, 0).foo() + m.foo() + verify(m, 1).foo() + m.foo() + verify(m, 2).foo() + m.foo() + m.foo() + verify(m, 4).foo() + + +func test_mock_custom_class_func_foo_call_times_real_func(): + var m = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + verify(m, 0).foo() + m.foo() + verify(m, 1).foo() + m.foo() + verify(m, 2).foo() + m.foo() + m.foo() + verify(m, 4).foo() + + +func test_mock_custom_class_func_foo_full_test(): + var m = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + verify(m, 0).foo() + assert_that(m.foo()).is_equal("") + verify(m, 1).foo() + do_return("new value").on(m).foo() + verify(m, 1).foo() + assert_that(m.foo()).is_equal("new value") + verify(m, 2).foo() + + +func test_mock_custom_class_func_foo_full_test_real_func(): + var m = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + verify(m, 0).foo() + assert_that(m.foo()).is_equal("foo") + verify(m, 1).foo() + do_return("new value").on(m).foo() + verify(m, 1).foo() + assert_that(m.foo()).is_equal("new value") + verify(m, 2).foo() + + +func test_mock_custom_class_func_bar(): + var m = mock(CustomResourceTestClass) + assert_that(m).is_not_null() + assert_that(m.bar(10)).is_equal("") + # verify 'bar' with args [10] is called one time at this point + verify(m, 1).bar(10) + # verify 'bar' with args [10, 20] is never called at this point + verify(m, 0).bar(10, 29) + # verify 'bar' with args [23] is never called at this point + verify(m, 0).bar(23) + + # now mock return value for function 'bar' with args [10] to 'overwriten value' + do_return("overridden value").on(m).bar(10) + # verify the return value is overwritten + assert_that(m.bar(10)).is_equal("overridden value") + # finally verify function call times + verify(m, 2).bar(10) + verify(m, 0).bar(10, 29) + verify(m, 0).bar(23) + + +func test_mock_custom_class_func_bar_real_func(): + var m = mock(CustomResourceTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + assert_that(m.bar(10)).is_equal("test_33") + # verify 'bar' with args [10] is called one time at this point + verify(m, 1).bar(10) + # verify 'bar' with args [10, 20] is never called at this point + verify(m, 0).bar(10, 29) + # verify 'bar' with args [23] is never called at this point + verify(m, 0).bar(23) + + # now mock return value for function 'bar' with args [10] to 'overwriten value' + do_return("overridden value").on(m).bar(10) + # verify the return value is overwritten + assert_that(m.bar(10)).is_equal("overridden value") + # verify the real implementation is used + assert_that(m.bar(10, 29)).is_equal("test_39") + assert_that(m.bar(10, 20, "other")).is_equal("other_30") + # finally verify function call times + verify(m, 2).bar(10) + verify(m, 1).bar(10, 29) + verify(m, 0).bar(10, 20) + verify(m, 1).bar(10, 20, "other") + + +func test_mock_custom_class_func_return_type_enum(): + var m = mock(ClassWithEnumReturnTypes) + assert_that(m).is_not_null() + verify(m, 0).get_enum() + + # verify enum return default ClassWithEnumReturnTypes.TEST_ENUM.FOO + assert_that(m.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.TEST_ENUM.BAR).on(m).get_enum() + assert_that(m.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.BAR) + verify(m, 2).get_enum() + + # with call real functions + var m2 = mock(ClassWithEnumReturnTypes, CALL_REAL_FUNC) + assert_that(m2).is_not_null() + + # verify enum return type + assert_that(m2.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.TEST_ENUM.BAR).on(m2).get_enum() + assert_that(m2.get_enum()).is_equal(ClassWithEnumReturnTypes.TEST_ENUM.BAR) + + +func test_mock_custom_class_func_return_type_internal_class_enum(): + var m = mock(ClassWithEnumReturnTypes) + assert_that(m).is_not_null() + verify(m, 0).get_inner_class_enum() + + # verify enum return default + assert_that(m.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR).on(m).get_inner_class_enum() + assert_that(m.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR) + verify(m, 2).get_inner_class_enum() + + # with call real functions + var m2= mock(ClassWithEnumReturnTypes, CALL_REAL_FUNC) + assert_that(m2).is_not_null() + + # verify enum return type + assert_that(m2.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.FOO) + do_return(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR).on(m2).get_inner_class_enum() + assert_that(m2.get_inner_class_enum()).is_equal(ClassWithEnumReturnTypes.InnerClass.TEST_ENUM.BAR) + + +func test_mock_custom_class_func_return_type_external_class_enum(): + var m = mock(ClassWithEnumReturnTypes) + assert_that(m).is_not_null() + verify(m, 0).get_external_class_enum() + + # verify enum return default + assert_that(m.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.FOO) + do_return(CustomEnums.TEST_ENUM.BAR).on(m).get_external_class_enum() + assert_that(m.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.BAR) + verify(m, 2).get_external_class_enum() + + # with call real functions + var m2 = mock(ClassWithEnumReturnTypes, CALL_REAL_FUNC) + assert_that(m2).is_not_null() + + # verify enum return type + assert_that(m2.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.FOO) + do_return(CustomEnums.TEST_ENUM.BAR).on(m2).get_external_class_enum() + assert_that(m2.get_external_class_enum()).is_equal(CustomEnums.TEST_ENUM.BAR) + + +func test_mock_custom_class_extends_Node(): + var m = mock(CustomNodeTestClass) + assert_that(m).is_not_null() + + # test mocked function returns null as default + assert_that(m.get_child_count()).is_equal(0) + assert_that(m.get_children()).contains_exactly([]) + # test seters has no affect + var node = auto_free(Node.new()) + m.add_child(node) + assert_that(m.get_child_count()).is_equal(0) + assert_that(m.get_children()).contains_exactly([]) + verify(m, 1).add_child(node) + verify(m, 2).get_child_count() + verify(m, 2).get_children() + + +func test_mock_custom_class_extends_Node_real_func(): + var m = mock(CustomNodeTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # test mocked function returns default mock value + assert_that(m.get_child_count()).is_equal(0) + assert_that(m.get_children()).is_equal([]) + # test real seters used + var nodeA = auto_free(Node.new()) + var nodeB = auto_free(Node.new()) + var nodeC = auto_free(Node.new()) + m.add_child(nodeA) + m.add_child(nodeB) + assert_that(m.get_child_count()).is_equal(2) + assert_that(m.get_children()).contains_exactly([nodeA, nodeB]) + verify(m, 1).add_child(nodeA) + verify(m, 1).add_child(nodeB) + verify(m, 0).add_child(nodeC) + verify(m, 2).get_child_count() + verify(m, 2).get_children() + + +func test_mock_custom_class_extends_other_custom_class(): + var m = mock(CustomClassExtendsCustomClass) + assert_that(mock).is_not_null() + + # foo() form parent class + verify(m, 0).foo() + # foo2() overriden + verify(m, 0).foo2() + # bar2() from class + verify(m, 0).bar2() + + assert_that(m.foo()).is_empty() + assert_that(m.foo2()).is_null() + assert_that(m.bar2()).is_empty() + + verify(m, 1).foo() + verify(m, 1).foo2() + verify(m, 1).bar2() + + # override returns + do_return("abc1").on(m).foo() + do_return("abc2").on(m).foo2() + do_return("abc3").on(m).bar2() + + assert_that(m.foo()).is_equal("abc1") + assert_that(m.foo2()).is_equal("abc2") + assert_that(m.bar2()).is_equal("abc3") + + +func test_mock_custom_class_extends_other_custom_class_call_real_func(): + var m = mock(CustomClassExtendsCustomClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + + # foo() form parent class + verify(m, 0).foo() + # foo2() overriden + verify(m, 0).foo2() + # bar2() from class + verify(m, 0).bar2() + + assert_that(m.foo()).is_equal("foo") + assert_that(m.foo2()).is_equal("foo2 overriden") + assert_that(m.bar2()).is_equal("test_65") + + verify(m, 1).foo() + verify(m, 1).foo2() + verify(m, 1).bar2() + + # override returns + do_return("abc1").on(m).foo() + do_return("abc2").on(m).foo2() + do_return("abc3").on(m).bar2() + + assert_that(m.foo()).is_equal("abc1") + assert_that(m.foo2()).is_equal("abc2") + assert_that(m.bar2()).is_equal("abc3") + + +func test_mock_static_func(): + var m = mock(CustomNodeTestClass) + assert_that(m).is_not_null() + # initial not called + verify(m, 0).static_test() + verify(m, 0).static_test_void() + + assert_that(m.static_test()).is_equal("") + assert_that(m.static_test_void()).is_null() + + verify(m, 1).static_test() + verify(m, 1).static_test_void() + m.static_test() + m.static_test_void() + m.static_test_void() + verify(m, 2).static_test() + verify(m, 3).static_test_void() + + +func test_mock_static_func_real_func(): + var m = mock(CustomNodeTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + # initial not called + verify(m, 0).static_test() + verify(m, 0).static_test_void() + + assert_that(m.static_test()).is_equal(CustomNodeTestClass.STATIC_FUNC_RETURN_VALUE) + assert_that(m.static_test_void()).is_null() + + verify(m, 1).static_test() + verify(m, 1).static_test_void() + m.static_test() + m.static_test_void() + m.static_test_void() + verify(m, 2).static_test() + verify(m, 3).static_test_void() + + +func test_mock_custom_class_assert_has_no_side_affect(): + var m = mock(CustomNodeTestClass) + assert_that(m).is_not_null() + var node = Node.new() + # verify the assertions has no side affect checked mocked object + verify(m, 0).add_child(node) + # expect no change checked childrens + assert_that(m.get_children()).contains_exactly([]) + + m.add_child(node) + # try thre times 'assert_called' to see it has no affect to the mock + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + assert_that(m.get_children()).contains_exactly([]) + # needs to be manually freed + node.free() + + +func test_mock_custom_class_assert_has_no_side_affect_real_func(): + var m = mock(CustomNodeTestClass, CALL_REAL_FUNC) + assert_that(m).is_not_null() + var node = Node.new() + # verify the assertions has no side affect checked mocked object + verify(m, 0).add_child(node) + # expect no change checked childrens + assert_that(m.get_children()).contains_exactly([]) + + m.add_child(node) + # try thre times 'assert_called' to see it has no affect to the mock + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + verify(m, 1).add_child(node) + assert_that(m.get_children()).contains_exactly([node]) + + +# This test verifies a function is calling other internally functions +# to collect the access times and the override return value is working as expected +func test_mock_advanced_func_path(): + var m = mock(AdvancedTestClass, CALL_REAL_FUNC) + # initial nothing is called + verify(m, 0).select(AdvancedTestClass.A) + verify(m, 0).select(AdvancedTestClass.B) + verify(m, 0).select(AdvancedTestClass.C) + verify(m, 0).a() + verify(m, 0).b() + verify(m, 0).c() + + # the function select() swiches based checked input argument to function a(), b() or c() + # call select where called internally func a() and returned "a" + assert_that(m.select(AdvancedTestClass.A)).is_equal("a") + # verify when call select() is also calling original func a() + verify(m, 1).select(AdvancedTestClass.A) + verify(m, 1).a() + + # call select again wiht overriden return value for func a() + do_return("overridden a func").on(m).a() + assert_that(m.select(AdvancedTestClass.A)).is_equal("overridden a func") + + # verify at this time select() and a() is called two times + verify(m, 2).select(AdvancedTestClass.A) + verify(m, 0).select(AdvancedTestClass.B) + verify(m, 0).select(AdvancedTestClass.C) + verify(m, 2).a() + verify(m, 0).b() + verify(m, 0).c() + + # finally use select to switch to internally func c() + assert_that(m.select(AdvancedTestClass.C)).is_equal("c") + verify(m, 2).select(AdvancedTestClass.A) + verify(m, 0).select(AdvancedTestClass.B) + verify(m, 1).select(AdvancedTestClass.C) + verify(m, 2).a() + verify(m, 0).b() + verify(m, 1).c() + + +func _test_mock_godot_class_calls_sub_function(): + var m = mock(MeshInstance3D, CALL_REAL_FUNC) + verify(m, 0)._mesh_changed() + m.set_mesh(QuadMesh.new()) + verify(m, 1).set_mesh(any_class(Mesh)) + verify(m, 1)._mesh_changed() + + +func test_mock_class_with_inner_classs(): + var mock_advanced = mock(AdvancedTestClass) + assert_that(mock_advanced).is_not_null() + + var mock_a := mock(AdvancedTestClass.SoundData) as AdvancedTestClass.SoundData + assert_object(mock_a).is_not_null() + + var mock_b := mock(AdvancedTestClass.AtmosphereData) as AdvancedTestClass.AtmosphereData + assert_object(mock_b).is_not_null() + + var mock_c := mock(AdvancedTestClass.Area4D) as AdvancedTestClass.Area4D + assert_object(mock_c).is_not_null() + + +func test_do_return(): + var mocked_node = mock(Node) + + # is return 0 by default + mocked_node.get_child_count() + # configure to return 10 when 'get_child_count()' is called + do_return(10).on(mocked_node).get_child_count() + # will now return 10 + assert_int(mocked_node.get_child_count()).is_equal(10) + + # is return 'null' by default + var node = mocked_node.get_child(0) + assert_object(node).is_null() + + # configure to return a mocked 'Camera3D' for child 0 + do_return(mock(Camera3D)).on(mocked_node).get_child(0) + # configure to return a mocked 'Area3D' for child 1 + do_return(mock(Area3D)).on(mocked_node).get_child(1) + + # will now return the Camera3D node + var node0 = mocked_node.get_child(0) + assert_object(node0).is_instanceof(Camera3D) + # will now return the Area3D node + var node1 = mocked_node.get_child(1) + assert_object(node1).is_instanceof(Area3D) + + +func test_matching_is_sorted(): + var mocked_node = mock(Node) + do_return(null).on(mocked_node).get_child(any(), false) + do_return(null).on(mocked_node).get_child(1, false) + do_return(null).on(mocked_node).get_child(10, false) + do_return(null).on(mocked_node).get_child(any(), true) + do_return(null).on(mocked_node).get_child(3, true) + + # get the sorted mocked args as array + var mocked_args :Array = mocked_node.__mocked_return_values.get("get_child").keys() + assert_array(mocked_args).has_size(5) + + # we expect all argument matchers are sorted to the end + var first_arguments = mocked_args.map(func (v): return v[0]) + assert_int(first_arguments[0]).is_equal(3) + assert_int(first_arguments[1]).is_equal(10) + assert_int(first_arguments[2]).is_equal(1) + assert_object(first_arguments[3]).is_instanceof(GdUnitArgumentMatcher) + assert_object(first_arguments[4]).is_instanceof(GdUnitArgumentMatcher) + + +func test_do_return_with_matchers(): + var mocked_node = mock(Node) + var childN :Node = auto_free(Node2D.new()) + var child1 :Node = auto_free(Node2D.new()) + var child10 :Node = auto_free(Node2D.new()) + + # for any index return childN by using any() matcher + do_return(childN).on(mocked_node).get_child(any(), false) + # for index 1 and 10 do return 'child1' and 'child10' + do_return(child1).on(mocked_node).get_child(1, false) + do_return(child10).on(mocked_node).get_child(10, false) + # for any index and flag true, we return null by using the 'any_int' matcher + do_return(null).on(mocked_node).get_child(any_int(), true) + + assert_that(mocked_node.get_child(0, true)).is_null() + assert_that(mocked_node.get_child(1, true)).is_null() + assert_that(mocked_node.get_child(2, true)).is_null() + assert_that(mocked_node.get_child(10, true)).is_null() + assert_that(mocked_node.get_child(0)).is_same(childN) + assert_that(mocked_node.get_child(1)).is_same(child1) + assert_that(mocked_node.get_child(2)).is_same(childN) + assert_that(mocked_node.get_child(3)).is_same(childN) + assert_that(mocked_node.get_child(4)).is_same(childN) + assert_that(mocked_node.get_child(5)).is_same(childN) + assert_that(mocked_node.get_child(6)).is_same(childN) + assert_that(mocked_node.get_child(7)).is_same(childN) + assert_that(mocked_node.get_child(8)).is_same(childN) + assert_that(mocked_node.get_child(9)).is_same(childN) + assert_that(mocked_node.get_child(10)).is_same(child10) + + +func test_example_verify(): + var mocked_node = mock(Node) + + # verify we have no interactions currently checked this instance + verify_no_interactions(mocked_node) + + # call with different arguments + mocked_node.set_process(false) # 1 times + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + # verify how often we called the function with different argument + verify(mocked_node, 2).set_process(true) # in sum two times with true + verify(mocked_node, 1).set_process(false)# in sum one time with false + + # verify total sum by using an argument matcher + verify(mocked_node, 3).set_process(any_bool()) + + +func test_verify_fail(): + var mocked_node = mock(Node) + + # interact two time + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + # verify we interacts two times + verify(mocked_node, 2).set_process(true) + + # verify should fail because we interacts two times and not one + var expected_error := """ + Expecting interaction on: + 'set_process(true :bool)' 1 time's + But found interactions on: + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n").replace("\r", "") + assert_failure(func(): verify(mocked_node, 1).set_process(true)) \ + .is_failed() \ + .has_message(expected_error) + + +func test_verify_func_interaction_wiht_PoolStringArray(): + var mocked = mock(ClassWithPoolStringArrayFunc) + + mocked.set_values(PackedStringArray()) + + verify(mocked).set_values(PackedStringArray()) + verify_no_more_interactions(mocked) + + +func test_verify_func_interaction_wiht_PoolStringArray_fail(): + var mocked = mock(ClassWithPoolStringArrayFunc) + + mocked.set_values(PackedStringArray()) + + # try to verify with default array type instead of PackedStringArray type + var expected_error := """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's""" \ + .dedent().trim_prefix("\n").replace("\r", "") + assert_failure(func(): verify(mocked, 1).set_values([])) \ + .is_failed() \ + .has_message(expected_error) + + reset(mocked) + # try again with called two times and different args + mocked.set_values(PackedStringArray()) + mocked.set_values(PackedStringArray(["a", "b"])) + mocked.set_values([1, 2]) + expected_error = """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's + 'set_values(["a", "b"] :PackedStringArray)' 1 time's + 'set_values([1, 2] :Array)' 1 time's""" \ + .dedent().trim_prefix("\n").replace("\r", "") + assert_failure(func(): verify(mocked, 1).set_values([])) \ + .is_failed() \ + .has_message(expected_error) + + +func test_reset(): + var mocked_node = mock(Node) + + # call with different arguments + mocked_node.set_process(false) # 1 times + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + verify(mocked_node, 2).set_process(true) + verify(mocked_node, 1).set_process(false) + + # now reset the mock + reset(mocked_node) + # verify all counters have been reset + verify_no_interactions(mocked_node) + + +func test_verify_no_interactions(): + var mocked_node = mock(Node) + + # verify we have no interactions checked this mock + verify_no_interactions(mocked_node) + + +func test_verify_no_interactions_fails(): + var mocked_node = mock(Node) + + # interact + mocked_node.set_process(false) # 1 times + mocked_node.set_process(true) # 1 times + mocked_node.set_process(true) # 2 times + + var expected_error =""" + Expecting no more interactions! + But found interactions on: + 'set_process(false :bool)' 1 time's + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n") + # it should fail because we have interactions + assert_failure(func(): verify_no_interactions(mocked_node)) \ + .is_failed() \ + .has_message(expected_error) + + +func test_verify_no_more_interactions(): + var mocked_node = mock(Node) + + mocked_node.is_ancestor_of(null) + mocked_node.set_process(false) + mocked_node.set_process(true) + mocked_node.set_process(true) + + # verify for called functions + verify(mocked_node, 1).is_ancestor_of(null) + verify(mocked_node, 2).set_process(true) + verify(mocked_node, 1).set_process(false) + + # There should be no more interactions checked this mock + verify_no_more_interactions(mocked_node) + + +func test_verify_no_more_interactions_but_has(): + var mocked_node = mock(Node) + + mocked_node.is_ancestor_of(null) + mocked_node.set_process(false) + mocked_node.set_process(true) + mocked_node.set_process(true) + + # now we simulate extra calls that we are not explicit verify + mocked_node.is_inside_tree() + mocked_node.is_inside_tree() + # a function with default agrs + mocked_node.find_child("mask") + # same function again with custom agrs + mocked_node.find_child("mask", false, false) + + # verify 'all' exclusive the 'extra calls' functions + verify(mocked_node, 1).is_ancestor_of(null) + verify(mocked_node, 2).set_process(true) + verify(mocked_node, 1).set_process(false) + + # now use 'verify_no_more_interactions' to check we have no more interactions checked this mock + # but should fail with a collecion of all not validated interactions + var expected_error =""" + Expecting no more interactions! + But found interactions on: + 'is_inside_tree()' 2 time's + 'find_child(mask :String, true :bool, true :bool)' 1 time's + 'find_child(mask :String, false :bool, false :bool)' 1 time's""" \ + .dedent().trim_prefix("\n") + assert_failure(func(): verify_no_more_interactions(mocked_node)) \ + .is_failed() \ + .has_message(expected_error) + + +func test_mock_snake_case_named_class_by_resource_path(): + var mock_a = mock("res://addons/gdUnit4/test/mocker/resources/snake_case.gd") + assert_object(mock_a).is_not_null() + + mock_a.custom_func() + verify(mock_a).custom_func() + verify_no_more_interactions(mock_a) + + var mock_b = mock("res://addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd") + assert_object(mock_b).is_not_null() + + mock_b.custom_func() + verify(mock_b).custom_func() + verify_no_more_interactions(mock_b) + + +func test_mock_snake_case_named_godot_class_by_name(): + # try checked Godot class + var mocked_tcp_server = mock("TCPServer") + assert_object(mocked_tcp_server).is_not_null() + + mocked_tcp_server.is_listening() + mocked_tcp_server.is_connection_available() + verify(mocked_tcp_server).is_listening() + verify(mocked_tcp_server).is_connection_available() + verify_no_more_interactions(mocked_tcp_server) + + +func test_mock_snake_case_named_class_by_class(): + var m = mock(snake_case_class_name) + assert_object(m).is_not_null() + + m.custom_func() + verify(m).custom_func() + verify_no_more_interactions(m) + + # try checked Godot class + var mocked_tcp_server = mock(TCPServer) + assert_object(mocked_tcp_server).is_not_null() + + mocked_tcp_server.is_listening() + mocked_tcp_server.is_connection_available() + verify(mocked_tcp_server).is_listening() + verify(mocked_tcp_server).is_connection_available() + verify_no_more_interactions(mocked_tcp_server) + + +func test_mock_func_with_default_build_in_type(): + var m = mock(ClassWithDefaultBuildIntTypes) + assert_object(m).is_not_null() + # call with default arg + m.foo("abc") + m.bar("def") + verify(m).foo("abc", Color.RED) + verify(m).bar("def", Vector3.FORWARD, AABB()) + verify_no_more_interactions(m) + + # call with custom color arg + m.foo("abc", Color.BLUE) + m.bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + verify(m).foo("abc", Color.BLUE) + verify(m).bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + verify_no_more_interactions(m) + + +func test_mock_virtual_function_is_not_called_twice() -> void: + # this test verifies the special handling of virtual functions by Godot + # virtual functions are handeld in a special way + # node.cpp + # case NOTIFICATION_READY: { + # + # if (get_script_instance()) { + # + # Variant::CallError err; + # get_script_instance()->call_multilevel_reversed(SceneStringNames::get_singleton()->_ready,NULL,0); + # } + + var m = mock(ClassWithOverridenVirtuals, CALL_REAL_FUNC) + assert_object(m).is_not_null() + + # inital constructor + assert_that(m._x).is_equal("_init") + + # add_child calls internally by "default" _ready() where is a virtual function + add_child(m) + + # verify _ready func is only once called + assert_that(m._x).is_equal("_ready") + + # now simulate an input event calls '_input' + var action = InputEventKey.new() + action.pressed = false + action.keycode = KEY_ENTER + get_tree().root.push_input(action) + assert_that(m._x).is_equal("ui_accept") + + +func test_mock_scene_by_path(): + var mocked_scene = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(mocked_scene).is_not_null() + assert_object(mocked_scene.get_script()).is_not_null() + assert_str(mocked_scene.get_script().resource_name).is_equal("MockTestScene.gd") + # check is mocked scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(mocked_scene)).is_true() + + +func test_mock_scene_by_resource(): + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var mocked_scene = mock(resource) + assert_object(mocked_scene).is_not_null() + assert_object(mocked_scene.get_script()).is_not_null() + assert_str(mocked_scene.get_script().resource_name).is_equal("MockTestScene.gd") + # check is mocked scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(mocked_scene)).is_true() + + +func test_mock_scene_by_instance(): + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var instance :Control = auto_free(resource.instantiate()) + var mocked_scene = mock(instance) + # must fail mock an instance is not allowed + assert_object(mocked_scene).is_null() + + +func test_mock_scene_by_path_fail_has_no_script_attached(): + var mocked_scene = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn") + assert_object(mocked_scene).is_null() + + +func test_mock_scene_variables_is_set(): + var mocked_scene = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(mocked_scene).is_not_null() + + # Add as child to a node to trigger _ready to initalize all variables + add_child(mocked_scene) + assert_object(mocked_scene._box1).is_not_null() + assert_object(mocked_scene._box2).is_not_null() + assert_object(mocked_scene._box3).is_not_null() + + # check signals are connected + assert_bool(mocked_scene.is_connected("panel_color_change", Callable(mocked_scene, "_on_panel_color_changed"))) + + # check exports + assert_str(mocked_scene._initial_color.to_html()).is_equal(Color.RED.to_html()) + + +func test_mock_scene_execute_func_yielded() -> void: + var mocked_scene = mock("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(mocked_scene).is_not_null() + add_child(mocked_scene) + # execute the 'color_cycle' func where emits three signals + # using yield to wait for function is completed + var result = await mocked_scene.color_cycle() + # verify the return value of 'color_cycle' + assert_str(result).is_equal("black") + + verify(mocked_scene)._on_panel_color_changed(mocked_scene._box1, Color.RED) + verify(mocked_scene)._on_panel_color_changed(mocked_scene._box1, Color.BLUE) + verify(mocked_scene)._on_panel_color_changed(mocked_scene._box1, Color.GREEN) + + +class Base: + func _init(_value :String): + pass + + +class Foo extends Base: + func _init(): + super("test") + pass + + +func test_mock_with_inheritance_method() -> void: + var foo = mock(Foo) + assert_object(foo).is_not_null() diff --git a/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd b/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd new file mode 100644 index 0000000..a48f428 --- /dev/null +++ b/addons/gdUnit4/test/mocker/GodotClassNameFuzzer.gd @@ -0,0 +1,43 @@ +# fuzzer to get available godot class names +class_name GodotClassNameFuzzer +extends Fuzzer + +var class_names := [] +const EXCLUDED_CLASSES = [ + "JavaClass", + "_ClassDB", + "MainLoop", + "JNISingleton", + "SceneTree", + "WebRTC", + "WebRTCPeerConnection", + "Tween", + "TextServerAdvanced", + "InputEventShortcut", + # GD-110 - missing enum `Vector3.Axis` + "Sprite3D", "AnimatedSprite3D", +] + + +func _init(no_singleton :bool = false,only_instancialbe :bool = false): + #class_names = ClassDB.get_class_list() + for clazz_name in ClassDB.get_class_list(): + #https://github.com/godotengine/godot/issues/67643 + if clazz_name.contains("Extension"): + continue + if no_singleton and Engine.has_singleton(clazz_name): + continue + if only_instancialbe and not ClassDB.can_instantiate(clazz_name): + continue + # exclude special classes + if EXCLUDED_CLASSES.has(clazz_name): + continue + # exlude Godot 3.5 *Tweener classes where produces and error + # `ERROR: Can't create empty IntervalTweener. Use get_tree().tween_property() or tween_property() instead.` + if clazz_name.find("Tweener") != -1: + continue + class_names.push_back(clazz_name) + + +func next_value(): + return class_names[randi() % class_names.size()] diff --git a/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd b/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd new file mode 100644 index 0000000..3d039c0 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/AdvancedTestClass.gd @@ -0,0 +1,84 @@ +# this class used to mock testing with inner classes and default arguments in functions +class_name AdvancedTestClass +extends Resource + +class SoundData: + @warning_ignore("unused_private_class_variable") + var _sample :String + @warning_ignore("unused_private_class_variable") + var _randomnes :float + +class AtmosphereData: + enum { + WATER, + AIR, + SMOKY, + } + var _toxigen :float + var _type :int + + func _init(type := AIR, toxigen := 0.0): + _type = type + _toxigen = toxigen +# some comment, and an row staring with an space to simmulate invalid formatting + + + # seter func with default values + func set_data(type := AIR, toxigen := 0.0) : + _type = type + _toxigen = toxigen + + static func to_atmosphere(_value :Dictionary) -> AtmosphereData: + return null + +class Area4D extends Resource: + + const SOUND := 1 + const ATMOSPHERE := 2 + var _meta := Dictionary() + + func _init(_x :int, atmospere :AtmosphereData = null): + _meta[ATMOSPHERE] = atmospere + + func get_sound() -> SoundData: + # sounds are optional + if _meta.has(SOUND): + return _meta[SOUND] as SoundData + return null + + func get_atmoshere() -> AtmosphereData: + return _meta[ATMOSPHERE] as AtmosphereData + +var _areas : = {} + +func _init(): + # add default atmoshere + _areas["default"] = Area4D.new(1, AtmosphereData.new()) + +func get_area(name :String, default :Area4D = null) -> Area4D: + return _areas.get(name, default) + + +# test spy is called sub functions select() -> a(), b(), c() +enum { + A, B, C +} + +func a() -> String: + return "a" + +func b() -> String: + return "b" + +func c() -> String: + return "c" + +func select( type :int) -> String: + match type: + A: return a() + B: return b() + C: return c() + _: return "" + +static func to_foo() -> String: + return "foo" diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd b/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd new file mode 100644 index 0000000..a51a32c --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithCustomClassName.gd @@ -0,0 +1,3 @@ +class_name GdUnit_Test_CustomClassName +extends Resource + diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd b/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd new file mode 100644 index 0000000..c0b53c5 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd @@ -0,0 +1,52 @@ +extends Object + +var _message + +@warning_ignore("unused_parameter") +func _init(message:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) -> void: + _message = message + + +@warning_ignore("unused_parameter") +func a1(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) -> void: + pass + + +@warning_ignore("unused_parameter") +func a2(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) -> void: + pass + + +@warning_ignore("unused_parameter") +func a3(set_name:String, path:String="", load_on_init:bool=false, + set_auto_save:bool=false, set_network_sync:bool=false +) : + pass + + +@warning_ignore("unused_parameter") +func a4(set_name:String, + path:String="", + load_on_init:bool=false, + set_auto_save:bool=false, + set_network_sync:bool=false +): + pass + + +@warning_ignore("unused_parameter") +func a5( + value : Array, + expected : String, + test_parameters : Array = [ + [ ["a"], "a" ], + [ ["a", "very", "long", "argument"], "a very long argument" ], + ] +): + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd b/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd new file mode 100644 index 0000000..f5c1e25 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithDefaultBuildIntTypes.gd @@ -0,0 +1,8 @@ +class_name ClassWithDefaultBuildIntTypes +extends RefCounted + +func foo(_value :String, _color := Color.RED): + pass + +func bar(_value :String, _direction := Vector3.FORWARD, _aabb := AABB()): + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd b/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd new file mode 100644 index 0000000..3d1cc73 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithEnumReturnTypes.gd @@ -0,0 +1,26 @@ +class_name ClassWithEnumReturnTypes +extends Resource + + +class InnerClass: + enum TEST_ENUM { FOO = 111, BAR = 222 } + +enum TEST_ENUM { FOO = 1, BAR = 2 } + +const NOT_AN_ENUM := 1 + +const X := { FOO=1, BAR=2 } + + +func get_enum() -> TEST_ENUM: + return TEST_ENUM.FOO + + +# function signature with an external enum reference +func get_external_class_enum() -> CustomEnums.TEST_ENUM: + return CustomEnums.TEST_ENUM.FOO + + +# function signature with an inner class enum reference +func get_inner_class_enum() -> InnerClass.TEST_ENUM: + return InnerClass.TEST_ENUM.FOO diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd b/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd new file mode 100644 index 0000000..dbdde7b --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd @@ -0,0 +1,5 @@ +class_name ClassWithNameA +extends Resource + +class InnerClass extends Resource: + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd b/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd new file mode 100644 index 0000000..fb50778 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithNameB.gd @@ -0,0 +1,8 @@ +# some comments and empty lines + +# similate bad formated class +# + +class_name ClassWithNameB +extends Resource + diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd b/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd new file mode 100644 index 0000000..2b6dd4e --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithOverridenVirtuals.gd @@ -0,0 +1,21 @@ +class_name ClassWithOverridenVirtuals +extends Node + +var _x := "default" + + +func _init(): + _x = "_init" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + if _x == "_init": + _x = "" + _x += "_ready" + + +func _input(event): + _x = "_input" + if event.is_action_released("ui_accept"): + _x = "ui_accept" diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd b/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd new file mode 100644 index 0000000..869dabb --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithPoolStringArrayFunc.gd @@ -0,0 +1,7 @@ +class_name ClassWithPoolStringArrayFunc +extends RefCounted + +var _values :PackedStringArray + +func set_values(values :PackedStringArray): + _values = values diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd b/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd new file mode 100644 index 0000000..bcc058b --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithVariables.gd @@ -0,0 +1,40 @@ +@tool +class_name ClassWithVariables +extends Node + +enum{ + A, + B, + C +} + +enum ETYPE { + EA, + EB + } + +# Declare member variables here. Examples: +var a = 2 +var b = "text" + +# Declare some const variables +const T1 = 1 + +const T2 = 2 + +signal source_changed( text ) + +@onready var name_label = load("res://addons/gdUnit4/test/mocker/resources/ClassWithNameA.gd") + +@export var path: NodePath = ".." + +class ClassA: + var x = 1 + # some comment + func foo()->String: + return "" + + +func foo(_value :int = T1): + var _c = a + b + pass diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd new file mode 100644 index 0000000..2156f64 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameA.gd @@ -0,0 +1,4 @@ +# some comment + +extends Resource + diff --git a/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd new file mode 100644 index 0000000..4b0a4e5 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/ClassWithoutNameAndNotExtends.gd @@ -0,0 +1,5 @@ +# some comment + +func foo(): + pass + diff --git a/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd b/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd new file mode 100644 index 0000000..a3e16cc --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomClassExtendsCustomClass.gd @@ -0,0 +1,9 @@ +class_name CustomClassExtendsCustomClass +extends CustomResourceTestClass + +# override +func foo2(): + return "foo2 overriden" + +func bar2() -> String: + return bar(23, 42) diff --git a/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd b/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd new file mode 100644 index 0000000..4aec1e5 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomNodeTestClass.gd @@ -0,0 +1,30 @@ +class_name CustomNodeTestClass +extends Node + +const STATIC_FUNC_RETURN_VALUE = "i'm a static function" + +enum { + ENUM_A, + ENUM_B +} + +#func get_path() -> NodePath: +# return NodePath() + +#func duplicate(flags_=15) -> Node: +# return self + +# added a custom static func for mock testing +static func static_test() -> String: + return STATIC_FUNC_RETURN_VALUE + +static func static_test_void() -> void: + pass + +func get_value( type := ENUM_A) -> int: + match type: + ENUM_A: + return 0 + ENUM_B: + return 1 + return -1 diff --git a/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd b/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd new file mode 100644 index 0000000..84f7541 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/CustomResourceTestClass.gd @@ -0,0 +1,17 @@ +class_name CustomResourceTestClass +extends Resource + +func foo() -> String: + return "foo" + +func foo2(): + return "foo2" + +func foo_void() -> void: + pass + +func bar(arg1 :int, arg2 :int = 23, name :String = "test") -> String: + return "%s_%d" % [name, arg1+arg2] + +func foo5(): + pass diff --git a/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd b/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd new file mode 100644 index 0000000..ae272cb --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/DeepStubTestClass.gd @@ -0,0 +1,16 @@ +class_name DeepStubTestClass + +class XShape: + var _shape : Shape3D = BoxShape3D.new() + + func get_shape() -> Shape3D: + return _shape + + +var _shape :XShape + +func add(shape :XShape): + _shape = shape + +func validate() -> bool: + return _shape.get_shape().get_margin() == 0.0 diff --git a/addons/gdUnit4/test/mocker/resources/GD-256/world.gd b/addons/gdUnit4/test/mocker/resources/GD-256/world.gd new file mode 100644 index 0000000..e1a1fd8 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/GD-256/world.gd @@ -0,0 +1,5 @@ +class_name Munderwood_Pathing_World +extends Node + +func foo() -> String: + return "test" diff --git a/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd b/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd new file mode 100644 index 0000000..5ae8a1a --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/OverridenGetClassTestClass.gd @@ -0,0 +1,11 @@ +class_name OverridenGetClassTestClass +extends Resource + + +@warning_ignore("native_method_override") +func get_class() -> String: + return "OverridenGetClassTestClass" + +func foo() -> String: + prints("foo") + return "foo" diff --git a/addons/gdUnit4/test/mocker/resources/TestPersion.gd b/addons/gdUnit4/test/mocker/resources/TestPersion.gd new file mode 100644 index 0000000..70b3ffb --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/TestPersion.gd @@ -0,0 +1,22 @@ +extends Object + +var _name +var _value +var _address :Address + +class Address: + var _street :String + var _code :int + + func _init(street :String, code :int): + _street = street + _code = code + + +func _init(name_ :String, street :String, code :int): + _name = name_ + _value = 1024 + _address = Address.new(street, code) + +func name() -> String: + return _name diff --git a/addons/gdUnit4/test/mocker/resources/capsuleshape2d.tres b/addons/gdUnit4/test/mocker/resources/capsuleshape2d.tres new file mode 100644 index 0000000..de1187d --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/capsuleshape2d.tres @@ -0,0 +1,3 @@ +[gd_resource type="CapsuleShape2D" format=2] + +[resource] diff --git a/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd b/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd new file mode 100644 index 0000000..ce21871 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/Spell.gd @@ -0,0 +1,39 @@ +class_name Spell +extends Node + +signal spell_explode + +const SPELL_LIVE_TIME = 1000 + +@warning_ignore("unused_private_class_variable") +var _spell_fired :bool = false +var _spell_live_time :float = 0 +var _spell_pos :Vector3 = Vector3.ZERO + +# helper counter for testing simulate_frames +@warning_ignore("unused_private_class_variable") +var _debug_process_counted := 0 + +func _ready(): + set_name("Spell") + +# only comment in for debugging reasons +#func _notification(what): +# prints("Spell", GdObjects.notification_as_string(what)) + +func _process(delta :float): + # added pseudo yield to check `simulate_frames` works wih custom yielding + await get_tree().process_frame + _spell_live_time += delta * 1000 + if _spell_live_time < SPELL_LIVE_TIME: + move(delta) + else: + explode() + +func move(delta :float) -> void: + #await get_tree().create_timer(0.1).timeout + _spell_pos.x += delta + +func explode() -> void: + emit_signal("spell_explode", self) + diff --git a/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd new file mode 100644 index 0000000..a182fa2 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd @@ -0,0 +1,104 @@ +extends Control + +signal panel_color_change(box, color) + +const COLOR_CYCLE := [Color.ROYAL_BLUE, Color.CHARTREUSE, Color.YELLOW_GREEN] + +@onready var _box1 = $VBoxContainer/PanelContainer/HBoxContainer/Panel1 +@onready var _box2 = $VBoxContainer/PanelContainer/HBoxContainer/Panel2 +@onready var _box3 = $VBoxContainer/PanelContainer/HBoxContainer/Panel3 + +@warning_ignore("unused_private_class_variable") +@export var _initial_color := Color.RED + +@warning_ignore("unused_private_class_variable") +var _nullable :Object + +func _ready(): + connect("panel_color_change", _on_panel_color_changed) + # we call this function to verify the _ready is only once called + # this is need to verify `add_child` is calling the original implementation only once + only_one_time_call() + + +func only_one_time_call() -> void: + pass + + +#func _notification(what): +# prints("TestScene", GdObjects.notification_as_string(what)) + + +func _on_test_pressed(button_id :int): + var box :ColorRect + match button_id: + 1: box = _box1 + 2: box = _box2 + 3: box = _box3 + emit_signal("panel_color_change", box, Color.RED) + # special case for button 3 we wait 1s to change to gray + if button_id == 3: + await get_tree().create_timer(1).timeout + emit_signal("panel_color_change", box, Color.GRAY) + + +func _on_panel_color_changed(box :ColorRect, color :Color): + box.color = color + + +func create_timer(timeout :float) -> Timer: + var timer :Timer = Timer.new() + add_child(timer) + timer.connect("timeout",Callable(self,"_on_timeout").bind(timer)) + timer.set_one_shot(true) + timer.start(timeout) + return timer + + +func _on_timeout(timer :Timer): + remove_child(timer) + timer.queue_free() + + +func color_cycle() -> String: + prints("color_cycle") + await create_timer(0.500).timeout + emit_signal("panel_color_change", _box1, Color.RED) + prints("timer1") + await create_timer(0.500).timeout + emit_signal("panel_color_change", _box1, Color.BLUE) + prints("timer2") + await create_timer(0.500).timeout + emit_signal("panel_color_change", _box1, Color.GREEN) + prints("cycle end") + return "black" + + +func start_color_cycle(): + color_cycle() + + +# used for manuall spy checked created spy +func _create_spell() -> Spell: + return Spell.new() + + +func create_spell() -> Spell: + var spell := _create_spell() + spell.connect("spell_explode", Callable(self, "_destroy_spell")) + return spell + + +func _destroy_spell(spell :Spell) -> void: + #prints("_destroy_spell", spell) + remove_child(spell) + spell.queue_free() + + +func _input(event): + if event.is_action_released("ui_accept"): + add_child(create_spell()) + + +func add(a: int, b :int) -> int: + return a + b diff --git a/addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn new file mode 100644 index 0000000..8e668a2 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn @@ -0,0 +1,104 @@ +[gd_scene load_steps=2 format=3 uid="uid://bf24pr1xj60o6"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.gd" id="1"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 + +[node name="test1" type="Button" parent="VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Test 1" + +[node name="test2" type="Button" parent="VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Test 2" + +[node name="test3" type="Button" parent="VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Test 3" + +[node name="PanelContainer" type="TabContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"] +layout_mode = 2 + +[node name="Panel1" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel1"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 14.0 +grow_horizontal = 2 +text = "Panel 1" + +[node name="Panel2" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel2"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 14.0 +grow_horizontal = 2 +text = "Panel 2" + +[node name="Panel3" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel3"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 14.0 +grow_horizontal = 2 +text = "Panel 3" + +[node name="Line2D" type="Line2D" parent="VBoxContainer"] +points = PackedVector2Array(0, 0, 20, 0) +width = 30.0 +default_color = Color(1, 0.0509804, 0.192157, 1) + +[node name="Line2D2" type="Line2D" parent="VBoxContainer"] +points = PackedVector2Array(20, 0, 40, 0) +width = 30.0 +default_color = Color(0.0392157, 1, 0.278431, 1) + +[node name="Line2D3" type="Line2D" parent="VBoxContainer"] +points = PackedVector2Array(40, 0, 60, 0) +width = 30.0 +default_color = Color(1, 0.0392157, 0.247059, 1) + +[connection signal="pressed" from="VBoxContainer/HBoxContainer/test1" to="." method="_on_test_pressed" binds= [1]] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/test2" to="." method="_on_test_pressed" binds= [2]] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/test3" to="." method="_on_test_pressed" binds= [3]] diff --git a/addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn b/addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn new file mode 100644 index 0000000..8bd646d --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn @@ -0,0 +1,67 @@ +[gd_scene format=3 uid="uid://bvp8uaof31fhm"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="test1" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Test 1" + +[node name="test2" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Test 2" + +[node name="test3" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Test 3" + +[node name="PanelContainer" type="TabContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer"] +layout_mode = 2 + +[node name="Panel1" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +color = Color(0.694118, 0.207843, 0.207843, 1) + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel1"] +layout_mode = 0 +anchor_right = 1.0 +offset_bottom = 14.0 +text = "Panel 1" + +[node name="Panel2" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +color = Color(0.219608, 0.662745, 0.380392, 1) + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel2"] +layout_mode = 0 +anchor_right = 1.0 +offset_bottom = 14.0 +text = "Panel 2" + +[node name="Panel3" type="ColorRect" parent="VBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +color = Color(0.12549, 0.286275, 0.776471, 1) + +[node name="Label" type="Label" parent="VBoxContainer/PanelContainer/HBoxContainer/Panel3"] +layout_mode = 0 +anchor_right = 1.0 +offset_bottom = 14.0 +text = "Panel 3" diff --git a/addons/gdUnit4/test/mocker/resources/snake_case.gd b/addons/gdUnit4/test/mocker/resources/snake_case.gd new file mode 100644 index 0000000..37c1eae --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/snake_case.gd @@ -0,0 +1,4 @@ +extends RefCounted + +func custom_func(): + pass diff --git a/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd b/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd new file mode 100644 index 0000000..8eccbe6 --- /dev/null +++ b/addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd @@ -0,0 +1,6 @@ +class_name snake_case_class_name +extends RefCounted + + +func custom_func(): + pass diff --git a/addons/gdUnit4/test/monitor/ErrorLogEntryTest.gd b/addons/gdUnit4/test/monitor/ErrorLogEntryTest.gd new file mode 100644 index 0000000..22cad49 --- /dev/null +++ b/addons/gdUnit4/test/monitor/ErrorLogEntryTest.gd @@ -0,0 +1,27 @@ +# GdUnit generated TestSuite +class_name ErrorLogEntryTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/monitor/ErrorLogEntry.gd' + +const error_report = """ + USER ERROR: this is an error + at: push_error (core/variant/variant_utility.cpp:880) + """ +const script_error = """ + USER SCRIPT ERROR: Trying to call a function on a previously freed instance. + at: GdUnitScriptTypeTest.test_xx (res://addons/gdUnit4/test/GdUnitScriptTypeTest.gd:22) +""" + + +func test_parse_script_error_line_number() -> void: + var line := ErrorLogEntry._parse_error_line_number(script_error.dedent()) + assert_int(line).is_equal(22) + + +func test_parse_push_error_line_number() -> void: + var line := ErrorLogEntry._parse_error_line_number(error_report.dedent()) + assert_int(line).is_equal(-1) diff --git a/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd b/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd new file mode 100644 index 0000000..48e7fec --- /dev/null +++ b/addons/gdUnit4/test/monitor/GodotGdErrorMonitorTest.gd @@ -0,0 +1,125 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GodotGdErrorMonitorTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd' + + +const error_report = """ + USER ERROR: this is an error + at: push_error (core/variant/variant_utility.cpp:880) + """ +const script_error = """ + USER SCRIPT ERROR: Trying to call a function on a previously freed instance. + at: GdUnitScriptTypeTest.test_xx (res://addons/gdUnit4/test/GdUnitScriptTypeTest.gd:22) +""" + + +var _save_is_report_push_errors :bool +var _save_is_report_script_errors :bool + + +func before(): + _save_is_report_push_errors = GdUnitSettings.is_report_push_errors() + _save_is_report_script_errors = GdUnitSettings.is_report_script_errors() + # disable default error reporting for testing + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, false) + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, false) + + +func after(): + ProjectSettings.set_setting(GdUnitSettings.REPORT_PUSH_ERRORS, _save_is_report_push_errors) + ProjectSettings.set_setting(GdUnitSettings.REPORT_SCRIPT_ERRORS, _save_is_report_script_errors) + + +func write_log(content :String) -> String: + var log_file := create_temp_file("/test_logs/", "test.log") + log_file.store_string(content) + log_file.flush() + return log_file.get_path_absolute() + + +func test_scan_for_push_errors() -> void: + var monitor := mock(GodotGdErrorMonitor, CALL_REAL_FUNC) as GodotGdErrorMonitor + monitor._godot_log_file = write_log(error_report) + monitor._report_enabled = true + + # with disabled push_error reporting + do_return(false).on(monitor)._is_report_push_errors() + await monitor.scan() + assert_array(monitor.to_reports()).is_empty() + + # with enabled push_error reporting + do_return(true).on(monitor)._is_report_push_errors() + + var entry := ErrorLogEntry.new(ErrorLogEntry.TYPE.PUSH_ERROR, -1, + "this is an error", + "at: push_error (core/variant/variant_utility.cpp:880)") + var expected_report := GodotGdErrorMonitor._to_report(entry) + monitor._eof = 0 + await monitor.scan() + assert_array(monitor.to_reports()).contains_exactly([expected_report]) + + +func test_scan_for_script_errors() -> void: + var log_file := write_log(script_error) + var monitor := mock(GodotGdErrorMonitor, CALL_REAL_FUNC) as GodotGdErrorMonitor + monitor._godot_log_file = log_file + monitor._report_enabled = true + + # with disabled push_error reporting + do_return(false).on(monitor)._is_report_script_errors() + await monitor.scan() + assert_array(monitor.to_reports()).is_empty() + + # with enabled push_error reporting + do_return(true).on(monitor)._is_report_script_errors() + + var entry := ErrorLogEntry.new(ErrorLogEntry.TYPE.PUSH_ERROR, 22, + "Trying to call a function on a previously freed instance.", + "at: GdUnitScriptTypeTest.test_xx (res://addons/gdUnit4/test/GdUnitScriptTypeTest.gd:22)") + var expected_report := GodotGdErrorMonitor._to_report(entry) + monitor._eof = 0 + await monitor.scan() + assert_array(monitor.to_reports()).contains_exactly([expected_report]) + + +func test_custom_log_path() -> void: + # save original log_path + var log_path :String = ProjectSettings.get_setting("debug/file_logging/log_path") + # set custom log path + var custom_log_path := "user://logs/test-run.log" + FileAccess.open(custom_log_path, FileAccess.WRITE).store_line("test-log") + ProjectSettings.set_setting("debug/file_logging/log_path", custom_log_path) + var monitor := GodotGdErrorMonitor.new() + + assert_that(monitor._godot_log_file).is_equal(custom_log_path) + # restore orignal log_path + ProjectSettings.set_setting("debug/file_logging/log_path", log_path) + + +func test_integration_test() -> void: + var monitor := GodotGdErrorMonitor.new() + monitor._report_enabled = true + # no errors reported + monitor.start() + monitor.stop() + await monitor.scan(true) + assert_array(monitor.to_reports()).is_empty() + + # push error + monitor.start() + push_error("Test GodotGdErrorMonitor 'push_error' reporting") + push_warning("Test GodotGdErrorMonitor 'push_warning' reporting") + monitor.stop() + await monitor.scan(true) + var reports := monitor.to_reports() + assert_array(reports).has_size(2) + if not reports.is_empty(): + assert_str(reports[0].message()).contains("Test GodotGdErrorMonitor 'push_error' reporting") + assert_str(reports[1].message()).contains("Test GodotGdErrorMonitor 'push_warning' reporting") + else: + fail("Expect reporting runtime errors") diff --git a/addons/gdUnit4/test/mono/ExampleTestSuite.cs b/addons/gdUnit4/test/mono/ExampleTestSuite.cs new file mode 100644 index 0000000..cc4c603 --- /dev/null +++ b/addons/gdUnit4/test/mono/ExampleTestSuite.cs @@ -0,0 +1,38 @@ +// GdUnit generated TestSuite +using Godot; +using GdUnit4; +using System; + +namespace GdUnit4 +{ + using static Assertions; + using static Utils; + + [TestSuite] + public partial class ExampleTestSuite + { + [TestCase] + public void IsFoo() + { + AssertThat("Foo").IsEqual("Foo"); + } + + [TestCase('A', Variant.Type.Int)] + [TestCase(SByte.MaxValue, Variant.Type.Int)] + [TestCase(Byte.MaxValue, Variant.Type.Int)] + [TestCase(Int16.MaxValue, Variant.Type.Int)] + [TestCase(UInt16.MaxValue, Variant.Type.Int)] + [TestCase(Int32.MaxValue, Variant.Type.Int)] + [TestCase(UInt32.MaxValue, Variant.Type.Int)] + [TestCase(Int64.MaxValue, Variant.Type.Int)] + [TestCase(UInt64.MaxValue, Variant.Type.Int)] + [TestCase(Single.MaxValue, Variant.Type.Float)] + [TestCase(Double.MaxValue, Variant.Type.Float)] + [TestCase("HalloWorld", Variant.Type.String)] + [TestCase(true, Variant.Type.Bool)] + public void ParameterizedTest(dynamic? value, Variant.Type type) { + Godot.Variant v = value == null ? new Variant() : Godot.Variant.CreateFrom(value); + AssertObject(v.VariantType).IsEqual(type); + } + } +} diff --git a/addons/gdUnit4/test/mono/GdUnit4CSharpApiLoaderTest.gd b/addons/gdUnit4/test/mono/GdUnit4CSharpApiLoaderTest.gd new file mode 100644 index 0000000..86134cf --- /dev/null +++ b/addons/gdUnit4/test/mono/GdUnit4CSharpApiLoaderTest.gd @@ -0,0 +1,71 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name GdUnit4CSharpApiLoaderTest +extends GdUnitTestSuite + +# TestSuite generated from +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const __source = 'res://addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd' + + +@warning_ignore("unused_parameter") +func before(do_skip = not GdUnit4CSharpApiLoader.is_mono_supported(), skip_reason = "Do run only for Godot Mono version"): + pass + + +@warning_ignore("unused_parameter") +func test_is_engine_version_supported(version :int, expected :bool, test_parameters := [ + [0x40000, false], + [0x40001, false], + [0x40002, false], + [0x40100, false], + [0x40101, false], + [0x40102, false], + [0x40100, false], + [0x40200, true], + [0x40201, true]]) -> void: + + assert_that(GdUnit4CSharpApiLoader.is_engine_version_supported(version)).is_equal(expected) + + +func test_api_version() -> void: + assert_str(GdUnit4CSharpApiLoader.version()).starts_with("4.2") + + +func test_create_test_suite() -> void: + var temp := create_temp_dir("examples") + var result := GdUnitFileAccess.copy_file("res://addons/gdUnit4/test/resources/core/sources/TestPerson.cs", temp) + assert_result(result).is_success() + + var example_source_cs = result.value() as String + var source := load(example_source_cs) + var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, "test") + result = GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, 18, test_suite_path) + + assert_result(result).is_success() + var info := result.value() as Dictionary + assert_str(info.get("path")).is_equal("user://tmp/test/examples/TestPersonTest.cs") + assert_int(info.get("line")).is_equal(16) + + +func test_parse_test_suite() -> void: + var test_suite := GdUnit4CSharpApiLoader.parse_test_suite("res://addons/gdUnit4/test/mono/GdUnit4CSharpApiTest.cs") + assert_that(test_suite).is_not_null() + assert_that(test_suite.get("IsCsTestSuite")).is_true() + test_suite.free() + + +class TestRunListener extends Node: + pass + + +func test_executor() -> void: + var listener :TestRunListener = auto_free(TestRunListener.new()) + var executor = GdUnit4CSharpApiLoader.create_executor(listener) + assert_that(executor).is_not_null() + + var test_suite := GdUnit4CSharpApiLoader.parse_test_suite("res://addons/gdUnit4/test/mono/GdUnit4CSharpApiTest.cs") + assert_that(executor.IsExecutable(test_suite)).is_true() + + test_suite.free() diff --git a/addons/gdUnit4/test/mono/GdUnit4CSharpApiTest.cs b/addons/gdUnit4/test/mono/GdUnit4CSharpApiTest.cs new file mode 100644 index 0000000..584503e --- /dev/null +++ b/addons/gdUnit4/test/mono/GdUnit4CSharpApiTest.cs @@ -0,0 +1,22 @@ +namespace GdUnit4 +{ + using static Assertions; + + [TestSuite] + public partial class GdUnit4CSharpApiTest + { + + [TestCase] + public void IsTestSuite() + { + AssertThat(GdUnit4CSharpApi.IsTestSuite("res://addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs")).IsFalse(); + AssertThat(GdUnit4CSharpApi.IsTestSuite("res://addons/gdUnit4/test/mono/ExampleTestSuite.cs")).IsTrue(); + } + + [TestCase] + public void GetVersion() + { + AssertThat(GdUnit4CSharpApi.Version()).IsEqual("4.2.2.0"); + } + } +} diff --git a/addons/gdUnit4/test/network/GdUnitTcpServerTest.gd b/addons/gdUnit4/test/network/GdUnitTcpServerTest.gd new file mode 100644 index 0000000..c00d522 --- /dev/null +++ b/addons/gdUnit4/test/network/GdUnitTcpServerTest.gd @@ -0,0 +1,89 @@ +# GdUnit generated TestSuite +class_name GdUnitTcpServerTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/network/GdUnitTcpServer.gd' + +const DLM := GdUnitServerConstants.JSON_RESPONSE_DELIMITER + + +func test_read_next_data_packages() -> void: + var server = mock(TCPServer) + var stream = mock(StreamPeerTCP) + + do_return(stream).on(server).take_connection() + + var connection :GdUnitTcpServer.TcpConnection = auto_free(GdUnitTcpServer.TcpConnection.new(server)) + + # single package + var data = DLM + "aaaa" + DLM + var data_packages := connection._read_next_data_packages(data.to_utf8_buffer()) + assert_array(data_packages).contains_exactly(["aaaa"]) + + # many package + data = DLM + "aaaa" + DLM + "bbbb" + DLM + "cccc" + DLM + "dddd" + DLM + "eeee" + DLM + data_packages = connection._read_next_data_packages(data.to_utf8_buffer()) + assert_array(data_packages).contains_exactly(["aaaa", "bbbb", "cccc", "dddd", "eeee"]) + + # with splitted package + data_packages.clear() + var data1 := DLM + "aaaa" + DLM + "bbbb" + DLM + "cc" + var data2 := "cc" + DLM + "dd" + var data3 := "dd" + DLM + "eeee" + DLM + data_packages.append_array(connection._read_next_data_packages(data1.to_utf8_buffer())) + data_packages.append_array(connection._read_next_data_packages(data2.to_utf8_buffer())) + data_packages.append_array(connection._read_next_data_packages(data3.to_utf8_buffer())) + assert_array(data_packages).contains_exactly(["aaaa", "bbbb", "cccc", "dddd", "eeee"]) + + +func test_receive_packages() -> void: + var server = mock(TCPServer) + var stream = mock(StreamPeerTCP) + + do_return(stream).on(server).take_connection() + + var connection :GdUnitTcpServer.TcpConnection = auto_free(GdUnitTcpServer.TcpConnection.new(server)) + var test_server :GdUnitTcpServer = auto_free(GdUnitTcpServer.new()) + test_server.add_child(connection) + # create a signal collector to catch all signals emitted on the test server during `receive_packages()` + var signal_collector_ := signal_collector(test_server) + + # mock send RPCMessage + var data := DLM + RPCMessage.of("Test Message").serialize() + DLM + var package_data = [0, data.to_ascii_buffer()] + do_return(data.length()).on(stream).get_available_bytes() + do_return(package_data).on(stream).get_partial_data(data.length()) + + # do receive next packages + connection.receive_packages() + + # expect the RPCMessage is received and emitted + assert_that(signal_collector_.is_emitted("rpc_data", [RPCMessage.of("Test Message")])).is_true() + + +# TODO refactor out and provide as public interface to can be reuse on other tests +class TestGdUnitSignalCollector: + var _signalCollector :GdUnitSignalCollector + var _emitter :Variant + + + func _init(emitter :Variant): + _emitter = emitter + _signalCollector = GdUnitSignalCollector.new() + _signalCollector.register_emitter(emitter) + + + func is_emitted(signal_name :String, expected_args :Array) -> bool: + return _signalCollector.match(_emitter, signal_name, expected_args) + + + func _notification(what): + if what == NOTIFICATION_PREDELETE: + _signalCollector.unregister_emitter(_emitter) + + +func signal_collector(instance :Variant) -> TestGdUnitSignalCollector: + return TestGdUnitSignalCollector.new(instance) diff --git a/addons/gdUnit4/test/report/JUnitXmlReportTest.gd b/addons/gdUnit4/test/report/JUnitXmlReportTest.gd new file mode 100644 index 0000000..17ab5ac --- /dev/null +++ b/addons/gdUnit4/test/report/JUnitXmlReportTest.gd @@ -0,0 +1,28 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name JUnitXmlReportTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/report/JUnitXmlReport.gd' + + +func test_to_time() -> void: + assert_str(JUnitXmlReport.to_time(0)).is_equal("0.000") + assert_str(JUnitXmlReport.to_time(1)).is_equal("0.001") + assert_str(JUnitXmlReport.to_time(10)).is_equal("0.010") + assert_str(JUnitXmlReport.to_time(100)).is_equal("0.100") + assert_str(JUnitXmlReport.to_time(1000)).is_equal("1.000") + assert_str(JUnitXmlReport.to_time(10123)).is_equal("10.123") + + +func test_to_type() -> void: + assert_str(JUnitXmlReport.to_type(GdUnitReport.SUCCESS)).is_equal("SUCCESS") + assert_str(JUnitXmlReport.to_type(GdUnitReport.WARN)).is_equal("WARN") + assert_str(JUnitXmlReport.to_type(GdUnitReport.FAILURE)).is_equal("FAILURE") + assert_str(JUnitXmlReport.to_type(GdUnitReport.ORPHAN)).is_equal("ORPHAN") + assert_str(JUnitXmlReport.to_type(GdUnitReport.TERMINATED)).is_equal("TERMINATED") + assert_str(JUnitXmlReport.to_type(GdUnitReport.INTERUPTED)).is_equal("INTERUPTED") + assert_str(JUnitXmlReport.to_type(GdUnitReport.ABORT)).is_equal("ABORT") + assert_str(JUnitXmlReport.to_type(1000)).is_equal("UNKNOWN") diff --git a/addons/gdUnit4/test/report/XmlElementTest.gd b/addons/gdUnit4/test/report/XmlElementTest.gd new file mode 100644 index 0000000..a8c9cc0 --- /dev/null +++ b/addons/gdUnit4/test/report/XmlElementTest.gd @@ -0,0 +1,212 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name XmlElementTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/report/XmlElement.gd' + + +func test_attribute() -> void: + var element := XmlElement.new("testsuites")\ + .attribute(JUnitXmlReport.ATTR_ID, "1")\ + .attribute(JUnitXmlReport.ATTR_NAME, "foo") + var expected = \ +""" + +""".replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + +func test_empty() -> void: + var element := XmlElement.new("testsuites") + var expected = \ +""" + +""".replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_add_child() -> void: + var child := XmlElement.new("foo")\ + .attribute(JUnitXmlReport.ATTR_ID, "1")\ + .attribute(JUnitXmlReport.ATTR_NAME, "foo") + var element := XmlElement.new("bar")\ + .attribute(JUnitXmlReport.ATTR_ID, "1")\ + .attribute(JUnitXmlReport.ATTR_NAME, "bar")\ + .add_child(child) + var expected = \ +""" + + + +""".replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_add_childs() -> void: + var child_a := XmlElement.new("foo_a")\ + .attribute(JUnitXmlReport.ATTR_ID, 1)\ + .attribute(JUnitXmlReport.ATTR_NAME, "foo_a") + var child_b := XmlElement.new("foo_b")\ + .attribute(JUnitXmlReport.ATTR_ID, 2)\ + .attribute(JUnitXmlReport.ATTR_NAME, "foo_b") + var element := XmlElement.new("bar")\ + .attribute(JUnitXmlReport.ATTR_ID, "1")\ + .attribute(JUnitXmlReport.ATTR_NAME, "bar")\ + .add_childs([child_a, child_b]) + var expected = \ +""" + + + + + +""".replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_add_text() -> void: + var element := XmlElement.new("testsuites")\ + .text("This is a message") + var expected = \ +""" + + +""".replace("\r", "") + assert_str(element.to_xml()).is_equal(expected) + element.dispose() + + +func test_complex_example() -> void: + var testsuite1 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReport.ATTR_ID, "1")\ + .attribute(JUnitXmlReport.ATTR_NAME, "bar") + for test_case in [1,2,3,4,5]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReport.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReport.ATTR_NAME, "test_case_%d" % test_case) + testsuite1.add_child(test) + var testsuite2 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReport.ATTR_ID, "2")\ + .attribute(JUnitXmlReport.ATTR_NAME, "bar2") + for test_case in [1,2,3]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReport.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReport.ATTR_NAME, "test_case_%d" % test_case) + if test_case == 2: + var failure := XmlElement.new("failure")\ + .attribute(JUnitXmlReport.ATTR_MESSAGE, "test_case.gd:12")\ + .attribute(JUnitXmlReport.ATTR_TYPE, "FAILURE")\ + .text("This is a failure\nExpecting true but was false\n") + test.add_child(failure) + testsuite2.add_child(test) + var root := XmlElement.new("testsuites")\ + .attribute(JUnitXmlReport.ATTR_ID, "ID-XXX")\ + .attribute(JUnitXmlReport.ATTR_NAME, "report_foo")\ + .attribute(JUnitXmlReport.ATTR_TESTS, 42)\ + .attribute(JUnitXmlReport.ATTR_FAILURES, 1)\ + .attribute(JUnitXmlReport.ATTR_TIME, "1.22")\ + .add_childs([testsuite1, testsuite2]) + var expected = \ +""" + + + + + + + + + + + + + + + + + + + + + + + + +""".replace("\r", "") + assert_str(root.to_xml()).is_equal(expected) + root.dispose() + + +func test_dispose() -> void: + var testsuite1 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReport.ATTR_ID, "1")\ + .attribute(JUnitXmlReport.ATTR_NAME, "bar") + var testsuite1_expected_tests := Array() + for test_case in [1,2,3,4,5]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReport.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReport.ATTR_NAME, "test_case_%d" % test_case) + testsuite1.add_child(test) + testsuite1_expected_tests.append(test) + var testsuite2 := XmlElement.new("testsuite")\ + .attribute(JUnitXmlReport.ATTR_ID, "2")\ + .attribute(JUnitXmlReport.ATTR_NAME, "bar2") + var testsuite2_expected_tests := Array() + for test_case in [1,2,3]: + var test := XmlElement.new("testcase")\ + .attribute(JUnitXmlReport.ATTR_ID, str(test_case))\ + .attribute(JUnitXmlReport.ATTR_NAME, "test_case_%d" % test_case) + testsuite2_expected_tests.append(test) + if test_case == 2: + var failure := XmlElement.new("failure")\ + .attribute(JUnitXmlReport.ATTR_MESSAGE, "test_case.gd:12")\ + .attribute(JUnitXmlReport.ATTR_TYPE, "FAILURE")\ + .text("This is a failure\nExpecting true but was false\n") + test.add_child(failure) + testsuite2.add_child(test) + var root := XmlElement.new("testsuites")\ + .attribute(JUnitXmlReport.ATTR_ID, "ID-XXX")\ + .attribute(JUnitXmlReport.ATTR_NAME, "report_foo")\ + .attribute(JUnitXmlReport.ATTR_TESTS, 42)\ + .attribute(JUnitXmlReport.ATTR_FAILURES, 1)\ + .attribute(JUnitXmlReport.ATTR_TIME, "1.22")\ + .add_childs([testsuite1, testsuite2]) + + assert_that(root._parent).is_null() + assert_array(root._childs).contains_exactly([testsuite1, testsuite2]) + assert_dict(root._attributes).has_size(5) + + assert_that(testsuite1._parent).is_equal(root) + assert_array(testsuite1._childs).contains_exactly(testsuite1_expected_tests) + assert_dict(testsuite1._attributes).has_size(2) + testsuite1_expected_tests.clear() + + assert_that(testsuite2._parent).is_equal(root) + assert_array(testsuite2._childs).contains_exactly(testsuite2_expected_tests) + assert_dict(testsuite2._attributes).has_size(2) + testsuite2_expected_tests.clear() + + # free all references + root.dispose() + assert_that(root._parent).is_null() + assert_array(root._childs).is_empty() + assert_dict(root._attributes).is_empty() + + assert_that(testsuite1._parent).is_null() + assert_array(testsuite1._childs).is_empty() + assert_dict(testsuite1._attributes).is_empty() + + assert_that(testsuite2._parent).is_null() + assert_array(testsuite2._childs).is_empty() + assert_dict(testsuite2._attributes).is_empty() diff --git a/addons/gdUnit4/test/resources/core/City.gd b/addons/gdUnit4/test/resources/core/City.gd new file mode 100644 index 0000000..a93e5d2 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/City.gd @@ -0,0 +1,8 @@ +class_name City +extends Node + +func name() -> String: + return "" + +func location() -> String: + return "" diff --git a/addons/gdUnit4/test/resources/core/CustomClass.gd b/addons/gdUnit4/test/resources/core/CustomClass.gd new file mode 100644 index 0000000..3b0a172 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/CustomClass.gd @@ -0,0 +1,22 @@ +# example class with inner classes +class_name CustomClass +extends RefCounted + + +# an inner class +class InnerClassA extends Node: + var x + +# an inner class inherits form another inner class +class InnerClassB extends InnerClassA: + var y + +# an inner class +class InnerClassC: + + func foo() -> String: + return "foo" + +class InnerClassD: + class InnerInnerClassA: + var x diff --git a/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd b/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd new file mode 100644 index 0000000..8aca0da --- /dev/null +++ b/addons/gdUnit4/test/resources/core/GeneratedPersonTest.gd @@ -0,0 +1,8 @@ +class_name GeneratedPersonTest +extends GdUnitTestSuite + +# TestSuite generated from", +const __source = "res://addons/gdUnit4/test/resources/core/Person.gd" + +func test_name(): + assert_that(Person.new().name()).is_equal("Hoschi") diff --git a/addons/gdUnit4/test/resources/core/Person.gd b/addons/gdUnit4/test/resources/core/Person.gd new file mode 100644 index 0000000..a05ea93 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/Person.gd @@ -0,0 +1,15 @@ +class_name Person +extends Resource + + +func name() -> String: + return "Hoschi" + +func last_name() -> String: + return "Horst" + +func age() -> int: + return 42 + +func street() -> String: + return "Route 66" diff --git a/addons/gdUnit4/test/resources/core/Udo.gd b/addons/gdUnit4/test/resources/core/Udo.gd new file mode 100644 index 0000000..4432427 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/Udo.gd @@ -0,0 +1,6 @@ +class_name Udo +extends Person + + +func _ready(): + pass # Replace with function body. diff --git a/addons/gdUnit4/test/resources/core/sources/TestPerson.cs b/addons/gdUnit4/test/resources/core/sources/TestPerson.cs new file mode 100644 index 0000000..20bf4f0 --- /dev/null +++ b/addons/gdUnit4/test/resources/core/sources/TestPerson.cs @@ -0,0 +1,28 @@ +using Godot; + +namespace Example.Test.Resources +{ + public partial class TestPerson + { + + public TestPerson(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + + public string FirstName { get; } + + public string LastName { get; } + + public string FullName => FirstName + " " + LastName; + + public string FullName2() => FirstName + " " + LastName; + + public string FullName3() + { + return FirstName + " " + LastName; + } + + } +} diff --git a/addons/gdUnit4/test/resources/issues/gd-166/issue.gd b/addons/gdUnit4/test/resources/issues/gd-166/issue.gd new file mode 100644 index 0000000..1123f6b --- /dev/null +++ b/addons/gdUnit4/test/resources/issues/gd-166/issue.gd @@ -0,0 +1,21 @@ +extends Object + +const Type = preload("types.gd") + +var type = -1 : + get: + return type + set(value): + type = value + _set_type_name(value) + +var type_name + + +func _set_type(t:int): + type = t + + +func _set_type_name(type_ :int): + type_name = Type.to_str(type_) + print("type was set to %s" % type_name) diff --git a/addons/gdUnit4/test/resources/issues/gd-166/types.gd b/addons/gdUnit4/test/resources/issues/gd-166/types.gd new file mode 100644 index 0000000..df685ea --- /dev/null +++ b/addons/gdUnit4/test/resources/issues/gd-166/types.gd @@ -0,0 +1,9 @@ +extends Object + +enum { FOO, BAR, BAZ } + +static func to_str(type:int): + match type: + FOO: return "FOO" + BAR: return "BAR" + BAZ: return "BAZ" diff --git a/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd b/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd new file mode 100644 index 0000000..7b2ab21 --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyBuilderTest.gd @@ -0,0 +1,332 @@ +# GdUnit generated TestSuite +class_name GdUnitSpyBuilderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd' + + +# helper to get function descriptor +func get_function_description(clazz_name :String, method_name :String) -> GdFunctionDescriptor: + var method_list :Array = ClassDB.class_get_method_list(clazz_name) + for method_descriptor in method_list: + if method_descriptor["name"] == method_name: + return GdFunctionDescriptor.extract_from(method_descriptor) + return null + + +func test_double__init() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # void _init() virtual + var fd := get_function_description("Object", "_init") + var expected := [ + 'func _init() -> void:', + ' super()', + ' pass', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_typed_function_without_arg() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # String get_class() const + var fd := get_function_description("Object", "get_class") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func get_class() -> String:', + ' var args :Array = ["get_class", ]', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return ""', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("get_class"):', + ' return super()', + ' return ""', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_typed_function_with_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # bool is_connected(signal: String,Callable(target: Object,method: String)) const + var fd := get_function_description("Object", "is_connected") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func is_connected(signal_, callable_) -> bool:', + ' var args :Array = ["is_connected", signal_, callable_]', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return false', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("is_connected"):', + ' return super(signal_, callable_)', + ' return false', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_void_function_with_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # void disconnect(signal: StringName, callable: Callable) + var fd := get_function_description("Object", "disconnect") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func disconnect(signal_, callable_) -> void:', + ' var args :Array = ["disconnect", signal_, callable_]', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("disconnect"):', + ' super(signal_, callable_)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_void_function_without_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # void free() + var fd := get_function_description("Object", "free") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func free() -> void:', + ' var args :Array = ["free", ]', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("free"):', + ' super()', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_typed_function_with_args_and_varargs() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # Error emit_signal(signal: StringName, ...) vararg + var fd := get_function_description("Object", "emit_signal") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("int_as_enum_without_match")', + '@warning_ignore("int_as_enum_without_cast")', + '@warning_ignore("shadowed_variable")', + 'func emit_signal(signal_, vararg0_="__null__", vararg1_="__null__", vararg2_="__null__", vararg3_="__null__", vararg4_="__null__", vararg5_="__null__", vararg6_="__null__", vararg7_="__null__", vararg8_="__null__", vararg9_="__null__") -> Error:', + ' var varargs :Array = __filter_vargs([vararg0_, vararg1_, vararg2_, vararg3_, vararg4_, vararg5_, vararg6_, vararg7_, vararg8_, vararg9_])', + ' var args :Array = ["emit_signal", signal_] + varargs', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return OK', + ' else:', + ' __save_function_interaction(args)', + '', + ' return __call_func("emit_signal", [signal_] + varargs)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_void_function_only_varargs() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # void bar(s...) vararg + var fd := GdFunctionDescriptor.new( "bar", 23, false, false, false, TYPE_NIL, "void", [], GdFunctionDescriptor._build_varargs(true)) + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("shadowed_variable")', + 'func bar(vararg0_="__null__", vararg1_="__null__", vararg2_="__null__", vararg3_="__null__", vararg4_="__null__", vararg5_="__null__", vararg6_="__null__", vararg7_="__null__", vararg8_="__null__", vararg9_="__null__") -> void:', + ' var varargs :Array = __filter_vargs([vararg0_, vararg1_, vararg2_, vararg3_, vararg4_, vararg5_, vararg6_, vararg7_, vararg8_, vararg9_])', + ' var args :Array = ["bar", ] + varargs', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' __call_func("bar", [] + varargs)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_return_typed_function_only_varargs() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # String bar(s...) vararg + var fd := GdFunctionDescriptor.new( "bar", 23, false, false, false, TYPE_STRING, "String", [], GdFunctionDescriptor._build_varargs(true)) + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("shadowed_variable")', + 'func bar(vararg0_="__null__", vararg1_="__null__", vararg2_="__null__", vararg3_="__null__", vararg4_="__null__", vararg5_="__null__", vararg6_="__null__", vararg7_="__null__", vararg8_="__null__", vararg9_="__null__") -> String:', + ' var varargs :Array = __filter_vargs([vararg0_, vararg1_, vararg2_, vararg3_, vararg4_, vararg5_, vararg6_, vararg7_, vararg8_, vararg9_])', + ' var args :Array = ["bar", ] + varargs', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return ""', + ' else:', + ' __save_function_interaction(args)', + '', + ' return __call_func("bar", [] + varargs)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_static_return_void_function_without_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # void foo() + var fd := GdFunctionDescriptor.new( "foo", 23, false, true, false, TYPE_NIL, "", []) + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("shadowed_variable")', + 'static func foo() -> void:', + ' var args :Array = ["foo", ]', + '', + ' if __instance().__is_verify_interactions():', + ' __instance().__verify_interactions(args)', + ' return', + ' else:', + ' __instance().__save_function_interaction(args)', + '', + ' if __instance().__do_call_real_func("foo"):', + ' super()', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_static_return_void_function_with_args() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + var fd := GdFunctionDescriptor.new( "foo", 23, false, true, false, TYPE_NIL, "", [ + GdFunctionArgument.new("arg1", TYPE_BOOL), + GdFunctionArgument.new("arg2", TYPE_STRING, '"default"') + ]) + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("shadowed_variable")', + 'static func foo(arg1, arg2="default") -> void:', + ' var args :Array = ["foo", arg1, arg2]', + '', + ' if __instance().__is_verify_interactions():', + ' __instance().__verify_interactions(args)', + ' return', + ' else:', + ' __instance().__save_function_interaction(args)', + '', + ' if __instance().__do_call_real_func("foo"):', + ' super(arg1, arg2)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_static_script_function_with_args_return_bool() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + + var fd := GdFunctionDescriptor.new( "foo", 23, false, true, false, TYPE_BOOL, "", [ + GdFunctionArgument.new("arg1", TYPE_BOOL), + GdFunctionArgument.new("arg2", TYPE_STRING, '"default"') + ]) + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("shadowed_variable")', + 'static func foo(arg1, arg2="default") -> bool:', + ' var args :Array = ["foo", arg1, arg2]', + '', + ' if __instance().__is_verify_interactions():', + ' __instance().__verify_interactions(args)', + ' return false', + ' else:', + ' __instance().__save_function_interaction(args)', + '', + ' if __instance().__do_call_real_func("foo"):', + ' return super(arg1, arg2)', + ' return false', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_virtual_return_void_function_with_arg() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # void _input(event: InputEvent) virtual + var fd := get_function_description("Node", "_input") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func _input(event_) -> void:', + ' var args :Array = ["_input", event_]', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("_input"):', + ' super(event_)', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +func test_double_virtual_return_void_function_without_arg() -> void: + var doubler := GdUnitSpyFunctionDoubler.new(false) + # void _ready() virtual + var fd := get_function_description("Node", "_ready") + var expected := [ + '@warning_ignore("untyped_declaration")' if Engine.get_version_info().hex >= 0x40200 else '', + '@warning_ignore("native_method_override")', + '@warning_ignore("shadowed_variable")', + 'func _ready() -> void:', + ' var args :Array = ["_ready", ]', + '', + ' if __is_verify_interactions():', + ' __verify_interactions(args)', + ' return', + ' else:', + ' __save_function_interaction(args)', + '', + ' if __do_call_real_func("_ready"):', + ' super()', + '', + ''] + assert_array(doubler.double(fd)).contains_exactly(expected) + + +class NodeWithOutVirtualFunc extends Node: + func _ready(): + pass + + #func _input(event :InputEvent) -> void: + +func test_spy_on_script_respect_virtual_functions(): + var do_spy = auto_free(GdUnitSpyBuilder.spy_on_script(auto_free(NodeWithOutVirtualFunc.new()), [], true).new()) + assert_that(do_spy.has_method("_ready")).is_true() + assert_that(do_spy.has_method("_input")).is_false() diff --git a/addons/gdUnit4/test/spy/GdUnitSpyTest.gd b/addons/gdUnit4/test/spy/GdUnitSpyTest.gd new file mode 100644 index 0000000..d52485c --- /dev/null +++ b/addons/gdUnit4/test/spy/GdUnitSpyTest.gd @@ -0,0 +1,621 @@ +class_name GdUnitSpyTest +extends GdUnitTestSuite + + +func test_spy_instance_id_is_unique(): + var m1 = spy(RefCounted.new()) + var m2 = spy(RefCounted.new()) + # test the internal instance id is unique + assert_that(m1.__instance_id()).is_not_equal(m2.__instance_id()) + assert_object(m1).is_not_same(m2) + + +func test_cant_spy_is_not_a_instance(): + # returns null because spy needs an 'real' instance to by spy checked + var spy_node = spy(Node) + assert_object(spy_node).is_null() + + +func test_spy_on_Node(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + # verify we have no interactions currently checked this instance + verify_no_interactions(spy_node) + + assert_object(spy_node)\ + .is_not_null()\ + .is_instanceof(Node)\ + .is_not_same(instance) + + # call first time + spy_node.set_process(false) + + # verify is called one times + verify(spy_node).set_process(false) + # just double check that verify has no affect to the counter + verify(spy_node).set_process(false) + + # call a scond time + spy_node.set_process(false) + # verify is called two times + verify(spy_node, 2).set_process(false) + verify(spy_node, 2).set_process(false) + + +func test_spy_source_with_class_name_by_resource_path() -> void: + var instance = auto_free(load('res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd').new()) + var m = spy(instance) + var head :String = m.get_script().source_code.substr(0, 200) + assert_str(head)\ + .contains("class_name DoubledMunderwoodPathingWorld")\ + .contains("extends 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd'") + + +func test_spy_source_with_class_name_by_class() -> void: + var m = spy(auto_free(Munderwood_Pathing_World.new())) + var head :String = m.get_script().source_code.substr(0, 200) + assert_str(head)\ + .contains("class_name DoubledMunderwoodPathingWorld")\ + .contains("extends 'res://addons/gdUnit4/test/mocker/resources/GD-256/world.gd'") + + +func test_spy_extends_godot_class() -> void: + var m = spy(auto_free(World3D.new())) + var head :String = m.get_script().source_code.substr(0, 200) + assert_str(head)\ + .contains("class_name DoubledWorld")\ + .contains("extends World3D") + + +func test_spy_on_custom_class(): + var instance :AdvancedTestClass = auto_free(AdvancedTestClass.new()) + var spy_instance = spy(instance) + + # verify we have currently no interactions + verify_no_interactions(spy_instance) + + assert_object(spy_instance)\ + .is_not_null()\ + .is_instanceof(AdvancedTestClass)\ + .is_not_same(instance) + + spy_instance.setup_local_to_scene() + verify(spy_instance, 1).setup_local_to_scene() + + # call first time script func with different arguments + spy_instance.get_area("test_a") + spy_instance.get_area("test_b") + spy_instance.get_area("test_c") + + # verify is each called only one time for different arguments + verify(spy_instance, 1).get_area("test_a") + verify(spy_instance, 1).get_area("test_b") + verify(spy_instance, 1).get_area("test_c") + # an second call with arg "test_c" + spy_instance.get_area("test_c") + verify(spy_instance, 1).get_area("test_a") + verify(spy_instance, 1).get_area("test_b") + verify(spy_instance, 2).get_area("test_c") + + # verify if a not used argument not counted + verify(spy_instance, 0).get_area("test_no") + + +# GD-291 https://github.com/MikeSchulze/gdUnit4/issues/291 +func test_spy_class_with_custom_formattings() -> void: + var resource = load("res://addons/gdUnit4/test/mocker/resources/ClassWithCustomFormattings.gd") + var do_spy = spy(auto_free(resource.new("test"))) + do_spy.a1("set_name", "", true) + verify(do_spy, 1).a1("set_name", "", true) + verify_no_more_interactions(do_spy) + assert_failure(func(): verify_no_interactions(do_spy))\ + .is_failed() \ + .has_line(112) + + +func test_spy_copied_class_members(): + var instance = auto_free(load("res://addons/gdUnit4/test/mocker/resources/TestPersion.gd").new("user-x", "street", 56616)) + assert_that(instance._name).is_equal("user-x") + assert_that(instance._value).is_equal(1024) + assert_that(instance._address._street).is_equal("street") + assert_that(instance._address._code).is_equal(56616) + + # spy it + var spy_instance = spy(instance) + reset(spy_instance) + + # verify members are inital copied + assert_that(spy_instance._name).is_equal("user-x") + assert_that(spy_instance._value).is_equal(1024) + assert_that(spy_instance._address._street).is_equal("street") + assert_that(spy_instance._address._code).is_equal(56616) + + spy_instance._value = 2048 + assert_that(instance._value).is_equal(1024) + assert_that(spy_instance._value).is_equal(2048) + + +func test_spy_copied_class_members_on_node(): + var node :Node = auto_free(Node.new()) + # checked a fresh node the name is empty and results into a error when copied at spy + # E 0:00:01.518 set_name: Condition "name == """ is true. + # C++ Source> scene/main/node.cpp:934 @ set_name() + # we set a placeholder instead + assert_that(spy(node).name).is_equal("") + + node.set_name("foo") + assert_that(spy(node).name).is_equal("foo") + + +func test_spy_on_inner_class(): + var instance :AdvancedTestClass.AtmosphereData = auto_free(AdvancedTestClass.AtmosphereData.new()) + var spy_instance :AdvancedTestClass.AtmosphereData = spy(instance) + + # verify we have currently no interactions + verify_no_interactions(spy_instance) + + assert_object(spy_instance)\ + .is_not_null()\ + .is_instanceof(AdvancedTestClass.AtmosphereData)\ + .is_not_same(instance) + + spy_instance.set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.2) + spy_instance.set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.3) + verify(spy_instance, 1).set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.2) + verify(spy_instance, 1).set_data(AdvancedTestClass.AtmosphereData.SMOKY, 1.3) + + +func test_spy_on_singleton(): + await assert_error(func () -> void: + var spy_node_ = spy(Input) + assert_object(spy_node_).is_null() + await await_idle_frame()).is_push_error("Spy on a Singleton is not allowed! 'Input'") + + +func test_example_verify(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + # verify we have no interactions currently checked this instance + verify_no_interactions(spy_node) + + # call with different arguments + spy_node.set_process(false) # 1 times + spy_node.set_process(true) # 1 times + spy_node.set_process(true) # 2 times + + # verify how often we called the function with different argument + verify(spy_node, 2).set_process(true) # in sum two times with true + verify(spy_node, 1).set_process(false)# in sum one time with false + + # verify total sum by using an argument matcher + verify(spy_node, 3).set_process(any_bool()) + + +func test_verify_fail(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + # interact two time + spy_node.set_process(true) # 1 times + spy_node.set_process(true) # 2 times + + # verify we interacts two times + verify(spy_node, 2).set_process(true) + + # verify should fail because we interacts two times and not one + var expected_error := """ + Expecting interaction on: + 'set_process(true :bool)' 1 time's + But found interactions on: + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n") + assert_failure(func(): verify(spy_node, 1).set_process(true)) \ + .is_failed() \ + .has_line(214) \ + .has_message(expected_error) + + +func test_verify_func_interaction_wiht_PoolStringArray(): + var spy_instance :ClassWithPoolStringArrayFunc = spy(ClassWithPoolStringArrayFunc.new()) + + spy_instance.set_values(PackedStringArray()) + + verify(spy_instance).set_values(PackedStringArray()) + verify_no_more_interactions(spy_instance) + + +func test_verify_func_interaction_wiht_PackedStringArray_fail(): + var spy_instance :ClassWithPoolStringArrayFunc = spy(ClassWithPoolStringArrayFunc.new()) + + spy_instance.set_values(PackedStringArray()) + + # try to verify with default array type instead of PackedStringArray type + var expected_error := """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's""" \ + .dedent().trim_prefix("\n") + assert_failure(func(): verify(spy_instance, 1).set_values([])) \ + .is_failed() \ + .has_line(241) \ + .has_message(expected_error) + + reset(spy_instance) + # try again with called two times and different args + spy_instance.set_values(PackedStringArray()) + spy_instance.set_values(PackedStringArray(["a", "b"])) + spy_instance.set_values([1, 2]) + expected_error = """ + Expecting interaction on: + 'set_values([] :Array)' 1 time's + But found interactions on: + 'set_values([] :PackedStringArray)' 1 time's + 'set_values(["a", "b"] :PackedStringArray)' 1 time's + 'set_values([1, 2] :Array)' 1 time's""" \ + .dedent().trim_prefix("\n") + assert_failure(func(): verify(spy_instance, 1).set_values([])) \ + .is_failed() \ + .has_line(259) \ + .has_message(expected_error) + + +func test_reset(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + # call with different arguments + spy_node.set_process(false) # 1 times + spy_node.set_process(true) # 1 times + spy_node.set_process(true) # 2 times + + verify(spy_node, 2).set_process(true) + verify(spy_node, 1).set_process(false) + + # now reset the spy + reset(spy_node) + # verify all counters have been reset + verify_no_interactions(spy_node) + + +func test_verify_no_interactions(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + # verify we have no interactions checked this mock + verify_no_interactions(spy_node) + + +func test_verify_no_interactions_fails(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + # interact + spy_node.set_process(false) # 1 times + spy_node.set_process(true) # 1 times + spy_node.set_process(true) # 2 times + + var expected_error =""" + Expecting no more interactions! + But found interactions on: + 'set_process(false :bool)' 1 time's + 'set_process(true :bool)' 2 time's""" \ + .dedent().trim_prefix("\n") + # it should fail because we have interactions + assert_failure(func(): verify_no_interactions(spy_node)) \ + .is_failed() \ + .has_line(307) \ + .has_message(expected_error) + + +func test_verify_no_more_interactions(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + spy_node.is_ancestor_of(instance) + spy_node.set_process(false) + spy_node.set_process(true) + spy_node.set_process(true) + + # verify for called functions + verify(spy_node, 1).is_ancestor_of(instance) + verify(spy_node, 2).set_process(true) + verify(spy_node, 1).set_process(false) + + # There should be no more interactions checked this mock + verify_no_more_interactions(spy_node) + + +func test_verify_no_more_interactions_but_has(): + var instance :Node = auto_free(Node.new()) + var spy_node = spy(instance) + + spy_node.is_ancestor_of(instance) + spy_node.set_process(false) + spy_node.set_process(true) + spy_node.set_process(true) + + # now we simulate extra calls that we are not explicit verify + spy_node.is_inside_tree() + spy_node.is_inside_tree() + # a function with default agrs + spy_node.find_child("mask") + # same function again with custom agrs + spy_node.find_child("mask", false, false) + + # verify 'all' exclusive the 'extra calls' functions + verify(spy_node, 1).is_ancestor_of(instance) + verify(spy_node, 2).set_process(true) + verify(spy_node, 1).set_process(false) + + # now use 'verify_no_more_interactions' to check we have no more interactions checked this mock + # but should fail with a collecion of all not validated interactions + var expected_error =""" + Expecting no more interactions! + But found interactions on: + 'is_inside_tree()' 2 time's + 'find_child(mask :String, true :bool, true :bool)' 1 time's + 'find_child(mask :String, false :bool, false :bool)' 1 time's""" \ + .dedent().trim_prefix("\n") + assert_failure(func(): verify_no_more_interactions(spy_node)) \ + .is_failed() \ + .has_line(362) \ + .has_message(expected_error) + + +class ClassWithStaticFunctions: + + static func foo() -> void: + pass + + static func bar(): + pass + + +func test_create_spy_static_func_untyped(): + var instance = spy(ClassWithStaticFunctions.new()) + assert_object(instance).is_not_null() + + +func test_spy_snake_case_named_class_by_resource_path(): + var instance_a = load("res://addons/gdUnit4/test/mocker/resources/snake_case.gd").new() + var spy_a = spy(instance_a) + assert_object(spy_a).is_not_null() + + spy_a.custom_func() + verify(spy_a).custom_func() + verify_no_more_interactions(spy_a) + + var instance_b = load("res://addons/gdUnit4/test/mocker/resources/snake_case_class_name.gd").new() + var spy_b = spy(instance_b) + assert_object(spy_b).is_not_null() + + spy_b.custom_func() + verify(spy_b).custom_func() + verify_no_more_interactions(spy_b) + + +func test_spy_snake_case_named_class_by_class(): + var do_spy = spy(snake_case_class_name.new()) + assert_object(do_spy).is_not_null() + + do_spy.custom_func() + verify(do_spy).custom_func() + verify_no_more_interactions(do_spy) + + # try checked Godot class + var spy_tcp_server = spy(TCPServer.new()) + assert_object(spy_tcp_server).is_not_null() + + spy_tcp_server.is_listening() + spy_tcp_server.is_connection_available() + verify(spy_tcp_server).is_listening() + verify(spy_tcp_server).is_connection_available() + verify_no_more_interactions(spy_tcp_server) + + +const Issue = preload("res://addons/gdUnit4/test/resources/issues/gd-166/issue.gd") +const Type = preload("res://addons/gdUnit4/test/resources/issues/gd-166/types.gd") + + +func test_spy_preload_class_GD_166() -> void: + var instance = auto_free(Issue.new()) + var spy_instance = spy(instance) + + spy_instance.type = Type.FOO + verify(spy_instance, 1)._set_type_name(Type.FOO) + assert_int(spy_instance.type).is_equal(Type.FOO) + assert_str(spy_instance.type_name).is_equal("FOO") + + +var _test_signal_args := Array() +func _emit_ready(a, b, c = null): + _test_signal_args = [a, b, c] + + +# https://github.com/MikeSchulze/gdUnit4/issues/38 +func test_spy_Node_use_real_func_vararg(): + # setup + var instance := Node.new() + instance.connect("ready", _emit_ready) + assert_that(_test_signal_args).is_empty() + var spy_node = spy(auto_free(instance)) + assert_that(spy_node).is_not_null() + + # test emit it + spy_node.emit_signal("ready", "aa", "bb", "cc") + # verify is emitted + verify(spy_node).emit_signal("ready", "aa", "bb", "cc") + await get_tree().process_frame + assert_that(_test_signal_args).is_equal(["aa", "bb", "cc"]) + + # test emit it + spy_node.emit_signal("ready", "aa", "xxx") + # verify is emitted + verify(spy_node).emit_signal("ready", "aa", "xxx") + await get_tree().process_frame + assert_that(_test_signal_args).is_equal(["aa", "xxx", null]) + + +class ClassWithSignal: + signal test_signal_a + signal test_signal_b + + func foo(arg :int) -> void: + if arg == 0: + emit_signal("test_signal_a", "aa") + else: + emit_signal("test_signal_b", "bb", true) + + func bar(arg :int) -> bool: + if arg == 0: + emit_signal("test_signal_a", "aa") + else: + emit_signal("test_signal_b", "bb", true) + return true + + +# https://github.com/MikeSchulze/gdUnit4/issues/14 +func _test_spy_verify_emit_signal(): + var spy_instance = spy(ClassWithSignal.new()) + assert_that(spy_instance).is_not_null() + + spy_instance.foo(0) + verify(spy_instance, 1).emit_signal("test_signal_a", "aa") + verify(spy_instance, 0).emit_signal("test_signal_b", "bb", true) + reset(spy_instance) + + spy_instance.foo(1) + verify(spy_instance, 0).emit_signal("test_signal_a", "aa") + verify(spy_instance, 1).emit_signal("test_signal_b", "bb", true) + reset(spy_instance) + + spy_instance.bar(0) + verify(spy_instance, 1).emit_signal("test_signal_a", "aa") + verify(spy_instance, 0).emit_signal("test_signal_b", "bb", true) + reset(spy_instance) + + spy_instance.bar(1) + verify(spy_instance, 0).emit_signal("test_signal_a", "aa") + verify(spy_instance, 1).emit_signal("test_signal_b", "bb", true) + + +func test_spy_func_with_default_build_in_type(): + var spy_instance :ClassWithDefaultBuildIntTypes = spy(ClassWithDefaultBuildIntTypes.new()) + assert_object(spy_instance).is_not_null() + # call with default arg + spy_instance.foo("abc") + spy_instance.bar("def") + verify(spy_instance).foo("abc", Color.RED) + verify(spy_instance).bar("def", Vector3.FORWARD, AABB()) + verify_no_more_interactions(spy_instance) + + # call with custom args + spy_instance.foo("abc", Color.BLUE) + spy_instance.bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + verify(spy_instance).foo("abc", Color.BLUE) + verify(spy_instance).bar("def", Vector3.DOWN, AABB(Vector3.ONE, Vector3.ZERO)) + verify_no_more_interactions(spy_instance) + + +func test_spy_scene_by_resource_path(): + var spy_scene = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(spy_scene)\ + .is_not_null()\ + .is_not_instanceof(PackedScene)\ + .is_instanceof(Control) + assert_str(spy_scene.get_script().resource_name).is_equal("SpyTestScene.gd") + # check is spyed scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(spy_scene)).is_true() + + +func test_spy_on_PackedScene(): + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var original_script = resource.get_script() + assert_object(resource).is_instanceof(PackedScene) + + var spy_scene = spy(resource) + + assert_object(spy_scene)\ + .is_not_null()\ + .is_not_instanceof(PackedScene)\ + .is_not_same(resource) + assert_object(spy_scene.get_script())\ + .is_not_null()\ + .is_instanceof(GDScript)\ + .is_not_same(original_script) + assert_str(spy_scene.get_script().resource_name).is_equal("SpyTestScene.gd") + # check is spyed scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(spy_scene)).is_true() + + +func test_spy_scene_by_instance(): + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + var instance :Control = resource.instantiate() + var original_script = instance.get_script() + var spy_scene = spy(instance) + + assert_object(spy_scene)\ + .is_not_null()\ + .is_same(instance)\ + .is_instanceof(Control) + assert_object(spy_scene.get_script())\ + .is_not_null()\ + .is_instanceof(GDScript)\ + .is_not_same(original_script) + assert_str(spy_scene.get_script().resource_name).is_equal("SpyTestScene.gd") + # check is mocked scene registered for auto freeing + assert_bool(GdUnitMemoryObserver.is_marked_auto_free(spy_scene)).is_true() + + +func test_spy_scene_by_path_fail_has_no_script_attached(): + var resource := load("res://addons/gdUnit4/test/mocker/resources/scenes/TestSceneWithoutScript.tscn") + var instance :Control = auto_free(resource.instantiate()) + + # has to fail and return null + var spy_scene = spy(instance) + assert_object(spy_scene).is_null() + + +func test_spy_scene_initalize(): + var spy_scene = spy("res://addons/gdUnit4/test/mocker/resources/scenes/TestScene.tscn") + assert_object(spy_scene).is_not_null() + + # Add as child to a scene tree to trigger _ready to initalize all variables + add_child(spy_scene) + # ensure _ready is recoreded and onyl once called + verify(spy_scene, 1)._ready() + verify(spy_scene, 1).only_one_time_call() + assert_object(spy_scene._box1).is_not_null() + assert_object(spy_scene._box2).is_not_null() + assert_object(spy_scene._box3).is_not_null() + + # check signals are connected + assert_bool(spy_scene.is_connected("panel_color_change",Callable(spy_scene,"_on_panel_color_changed"))) + + # check exports + assert_str(spy_scene._initial_color.to_html()).is_equal(Color.RED.to_html()) + + +class CustomNode extends Node: + + func _ready(): + # we call this function to verify the _ready is only once called + # this is need to verify `add_child` is calling the original implementation only once + only_one_time_call() + + func only_one_time_call() -> void: + pass + + +func test_spy_ready_called_once(): + var spy_node = spy(auto_free(CustomNode.new())) + + # Add as child to a scene tree to trigger _ready to initalize all variables + add_child(spy_node) + + # ensure _ready is recoreded and onyl once called + verify(spy_node, 1)._ready() + verify(spy_node, 1).only_one_time_call() diff --git a/addons/gdUnit4/test/ui/GdUnitFontsTest.gd b/addons/gdUnit4/test/ui/GdUnitFontsTest.gd new file mode 100644 index 0000000..96220de --- /dev/null +++ b/addons/gdUnit4/test/ui/GdUnitFontsTest.gd @@ -0,0 +1,17 @@ +# GdUnit generated TestSuite +class_name GdUnitFontsTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/GdUnitFonts.gd' + + +func test_load_and_resize_font() -> void: + var font8 = GdUnitFonts.load_and_resize_font(GdUnitFonts.FONT_MONO, 8) + var font16 = GdUnitFonts.load_and_resize_font(GdUnitFonts.FONT_MONO, 16) + + assert_object(font8).is_not_null().is_not_same(font16) + assert_that(font8.fixed_size).is_equal(8) + assert_that(font16.fixed_size).is_equal(16) diff --git a/addons/gdUnit4/test/ui/parts/InspectorProgressBarTest.gd b/addons/gdUnit4/test/ui/parts/InspectorProgressBarTest.gd new file mode 100644 index 0000000..530e91f --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/InspectorProgressBarTest.gd @@ -0,0 +1,84 @@ +# GdUnit generated TestSuite +class_name InspectorProgressBarTest +extends GdUnitTestSuite +@warning_ignore('unused_parameter') +@warning_ignore('return_value_discarded') + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd' + +var _runner :GdUnitSceneRunner +var _progress :ProgressBar +var _status :Label +var _style :StyleBoxFlat + + +func before_test(): + _runner = scene_runner('res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn') + _progress = _runner.get_property("bar") + _status = _runner.get_property("status") + _style = _runner.get_property("style") + # inital state + assert_that(_status.text).is_equal("0:0") + assert_that(_progress.value).is_equal(0) + assert_that(_progress.max_value).is_equal(0) + _runner.invoke("_on_gdunit_event", GdUnitInit.new(10, 42)) + + +func test_progress_init() -> void: + _runner.invoke("_on_gdunit_event", GdUnitInit.new(10, 230)) + assert_that(_progress.value).is_equal(0) + assert_that(_progress.max_value).is_equal(230) + assert_that(_status.text).is_equal("0:230") + assert_that(_style.bg_color).is_equal(Color.DARK_GREEN) + + +func test_progress_success() -> void: + _runner.invoke("_on_gdunit_event", GdUnitInit.new(10, 42)) + var expected_progess_index := 0 + # simulate execution of 20 success test runs + for index in 20: + _runner.invoke("_on_gdunit_event", GdUnitEvent.new().test_after("res://test/testA.gd", "TestSuiteA", "test_a%d" % index, {})) + expected_progess_index += 1 + assert_that(_progress.value).is_equal(expected_progess_index) + assert_that(_status.text).is_equal("%d:42" % expected_progess_index) + assert_that(_style.bg_color).is_equal(Color.DARK_GREEN) + + # simulate execution of parameterized test with 10 iterations + for index in 10: + _runner.invoke("_on_gdunit_event", GdUnitEvent.new().test_after("res://test/testA.gd", "TestSuiteA", "test_parameterized:%d (params)" % index, {})) + assert_that(_progress.value).is_equal(expected_progess_index) + # final test end event + _runner.invoke("_on_gdunit_event", GdUnitEvent.new().test_after("res://test/testA.gd", "TestSuiteA", "test_parameterized", {})) + # we expect only one progress step after a parameterized test has been executed, regardless of the iterations + expected_progess_index += 1 + assert_that(_progress.value).is_equal(expected_progess_index) + assert_that(_status.text).is_equal("%d:42" % expected_progess_index) + assert_that(_style.bg_color).is_equal(Color.DARK_GREEN) + + # verify the max progress state is not affected + assert_that(_progress.max_value).is_equal(42) + + +@warning_ignore("unused_parameter") +func test_progress_failed(test_name :String, is_failed :bool, is_error :bool, expected_color :Color, test_parameters = [ + ["test_a", false, false, Color.DARK_GREEN], + ["test_b", false, false, Color.DARK_GREEN], + ["test_c", false, false, Color.DARK_GREEN], + ["test_d", true, false, Color.DARK_RED], + ["test_e", true, false, Color.DARK_RED], +]) -> void: + var statistics = { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 100, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: is_error, + GdUnitEvent.ERROR_COUNT: 1 if is_error else 0, + GdUnitEvent.FAILED: is_failed, + GdUnitEvent.FAILED_COUNT: 1 if is_failed else 0, + GdUnitEvent.SKIPPED: false, + GdUnitEvent.SKIPPED_COUNT: 0, + } + + _runner.invoke("_on_gdunit_event", GdUnitEvent.new().test_after("res://test/testA.gd", "TestSuiteA", test_name, statistics)) + assert_that(_style.bg_color).is_equal(expected_color) diff --git a/addons/gdUnit4/test/ui/parts/InspectorTreeMainPanelPerformanceTest.gd b/addons/gdUnit4/test/ui/parts/InspectorTreeMainPanelPerformanceTest.gd new file mode 100644 index 0000000..3bbdfb6 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/InspectorTreeMainPanelPerformanceTest.gd @@ -0,0 +1,168 @@ +# GdUnit generated TestSuite +class_name InspectorTreeMainPanelPerformanceTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd' + +# this test-suite contains only empty test to run as performance indicator + + +func test_01() -> void: + pass + + +func test_02() -> void: + pass + + +func test_03() -> void: + pass + + +func test_04() -> void: + pass + + +func test_05() -> void: + pass + + +func test_06() -> void: + pass + + +func test_07() -> void: + pass + + +func test_08() -> void: + pass + + +func test_09() -> void: + pass + + +func test_10() -> void: + pass + + +func test_11() -> void: + pass + + +func test_12() -> void: + pass + + +func test_13() -> void: + pass + + +func test_14() -> void: + pass + + +func test_15() -> void: + pass + + +func test_16() -> void: + pass + + +func test_17() -> void: + pass + + +func test_18() -> void: + pass + + +func test_19() -> void: + pass + + +func test_20() -> void: + pass + + +func test_21() -> void: + pass + + +func test_22() -> void: + pass + + +func test_23() -> void: + pass + + +func test_24() -> void: + pass + + +func test_25() -> void: + pass + + +func test_26() -> void: + pass + + +func test_27() -> void: + pass + + +func test_28() -> void: + pass + + +func test_29() -> void: + pass + + +func test_30() -> void: + pass + + +func test_31() -> void: + pass + + +func test_32() -> void: + pass + + +func test_33() -> void: + pass + + +func test_34() -> void: + pass + + +func test_35() -> void: + pass + + +func test_36() -> void: + pass + + +func test_37() -> void: + pass + + +func test_38() -> void: + pass + + +func test_39() -> void: + pass + + +func test_40() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/InspectorTreeMainPanelTest.gd b/addons/gdUnit4/test/ui/parts/InspectorTreeMainPanelTest.gd new file mode 100644 index 0000000..b5a5bf9 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/InspectorTreeMainPanelTest.gd @@ -0,0 +1,303 @@ +# GdUnit generated TestSuite +class_name InspectorTreeMainPanelTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd' + +var TEST_SUITE_A :String +var TEST_SUITE_B :String +var TEST_SUITE_C :String + +var _inspector + + +func before_test(): + _inspector = load("res://addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn").instantiate() + add_child(_inspector) + _inspector.init_tree() + + # load a testsuite + for test_suite in setup_test_env(): + _inspector.add_test_suite(toDto(test_suite)) + # verify no failures are exists + assert_array(_inspector.collect_failures_and_errors()).is_empty() + + +func after_test(): + _inspector.cleanup_tree() + remove_child(_inspector) + _inspector.free() + + +func toDto(test_suite :Node) -> GdUnitTestSuiteDto: + var dto := GdUnitTestSuiteDto.new() + return dto.deserialize(dto.serialize(test_suite)) as GdUnitTestSuiteDto + + +func setup_test_env() -> Array: + var test_suite_a := GdUnitTestResourceLoader.load_test_suite("res://addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource") + var test_suite_b := GdUnitTestResourceLoader.load_test_suite("res://addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource") + var test_suite_c := GdUnitTestResourceLoader.load_test_suite("res://addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource") + TEST_SUITE_A = test_suite_a.get_script().resource_path + TEST_SUITE_B = test_suite_b.get_script().resource_path + TEST_SUITE_C = test_suite_c.get_script().resource_path + return Array([auto_free(test_suite_a), auto_free(test_suite_b), auto_free(test_suite_c)]) + + +func mark_as_failure(inspector, test_cases :Array) -> void: + var tree_root :TreeItem = inspector._tree_root + assert_object(tree_root).is_not_null() + # mark all test as failed + for parent in tree_root.get_children(): + inspector.set_state_succeded(parent) + for item in parent.get_children(): + if test_cases.has(item.get_text(0)): + inspector.set_state_failed(parent) + inspector.set_state_failed(item) + else: + inspector.set_state_succeded(item) + item = item.get_next() + parent = parent.get_next() + +func get_item_state(parent :TreeItem, item_name :String) -> int: + var item = _inspector._find_by_name(parent, item_name) + return item.get_meta(_inspector.META_GDUNIT_STATE) + +func test_collect_failures_and_errors() -> void: + # mark some test as failed + mark_as_failure(_inspector, ["test_aa", "test_ad", "test_cb", "test_cc", "test_ce"]) + + assert_array(_inspector.collect_failures_and_errors())\ + .extract("get_text", [0])\ + .contains_exactly(["test_aa", "test_ad", "test_cb", "test_cc", "test_ce"]) + +func test_select_first_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # we have no failures or errors + _inspector.collect_failures_and_errors() + _inspector.select_first_failure() + assert_object(_inspector._tree.get_selected()).is_null() + + # add failures + mark_as_failure(_inspector, ["test_aa", "test_ad", "test_cb", "test_cc", "test_ce"]) + _inspector.collect_failures_and_errors() + # select first failure + _inspector.select_first_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + +func test_select_last_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # we have no failures or errors + _inspector.collect_failures_and_errors() + _inspector.select_last_failure() + assert_object(_inspector._tree.get_selected()).is_null() + + # add failures + mark_as_failure(_inspector, ["test_aa", "test_ad", "test_cb", "test_cc", "test_ce"]) + _inspector.collect_failures_and_errors() + # select last failure + _inspector.select_last_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + + +func test_clear_failures() -> void: + assert_array(_inspector._current_failures).is_empty() + + mark_as_failure(_inspector, ["test_aa", "test_ad", "test_cb", "test_cc", "test_ce"]) + _inspector.collect_failures_and_errors() + assert_array(_inspector._current_failures).is_not_empty() + + # clear it + _inspector.clear_failures() + assert_array(_inspector._current_failures).is_empty() + +func test_select_next_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # first time select next but no failure exists + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected()).is_null() + + # add failures + mark_as_failure(_inspector, ["test_aa", "test_ad", "test_cb", "test_cc", "test_ce"]) + _inspector.collect_failures_and_errors() + + # first time select next than select first failure + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ad") + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cb") + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + # if current last failure selected than select first as next + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + _inspector.select_next_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ad") + +func test_select_previous_failure() -> void: + # test initial nothing is selected + assert_object(_inspector._tree.get_selected()).is_null() + + # first time select previous but no failure exists + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected()).is_null() + + # add failures + mark_as_failure(_inspector, ["test_aa", "test_ad", "test_cb", "test_cc", "test_ce"]) + _inspector.collect_failures_and_errors() + + # first time select previous than select last failure + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cb") + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ad") + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_aa") + # if current first failure selected than select last as next + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_ce") + _inspector.select_previous_failure() + assert_str(_inspector._tree.get_selected().get_text(0)).is_equal("test_cc") + +func test_find_item_for_test_suites() -> void: + var suite_a: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_A) + assert_str(suite_a.get_meta(_inspector.META_GDUNIT_NAME)).is_equal("ExampleTestSuiteA") + + var suite_b: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_B) + assert_str(suite_b.get_meta(_inspector.META_GDUNIT_NAME)).is_equal("ExampleTestSuiteB") + +func test_find_item_for_test_cases() -> void: + var case_aa: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_A, "test_aa") + assert_str(case_aa.get_meta(_inspector.META_GDUNIT_NAME)).is_equal("test_aa") + + var case_ce: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_C, "test_ce") + assert_str(case_ce.get_meta(_inspector.META_GDUNIT_NAME)).is_equal("test_ce") + +func test_suite_text_shows_amount_of_cases() -> void: + var suite_a: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_A) + assert_str(suite_a.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + + var suite_b: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_B) + assert_str(suite_b.get_text(0)).is_equal("(0/3) ExampleTestSuiteB") + +func test_suite_text_responds_to_test_case_events() -> void: + var suite_a: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_A) + + var success_aa := GdUnitEvent.new().test_after(TEST_SUITE_A, "ExampleTestSuiteA", "test_aa") + _inspector._on_gdunit_event(success_aa) + assert_str(suite_a.get_text(0)).is_equal("(1/5) ExampleTestSuiteA") + + var error_ad := GdUnitEvent.new().test_after(TEST_SUITE_A, "ExampleTestSuiteA", "test_ad", {GdUnitEvent.ERRORS: true}) + _inspector._on_gdunit_event(error_ad) + assert_str(suite_a.get_text(0)).is_equal("(1/5) ExampleTestSuiteA") + + var failure_ab := GdUnitEvent.new().test_after(TEST_SUITE_A, "ExampleTestSuiteA", "test_ab", {GdUnitEvent.FAILED: true}) + _inspector._on_gdunit_event(failure_ab) + assert_str(suite_a.get_text(0)).is_equal("(1/5) ExampleTestSuiteA") + + var skipped_ac := GdUnitEvent.new().test_after(TEST_SUITE_A, "ExampleTestSuiteA", "test_ac", {GdUnitEvent.SKIPPED: true}) + _inspector._on_gdunit_event(skipped_ac) + assert_str(suite_a.get_text(0)).is_equal("(1/5) ExampleTestSuiteA") + + var success_ae := GdUnitEvent.new().test_after(TEST_SUITE_A, "ExampleTestSuiteA", "test_ae") + _inspector._on_gdunit_event(success_ae) + assert_str(suite_a.get_text(0)).is_equal("(2/5) ExampleTestSuiteA") + +# test coverage for issue GD-117 +func test_update_test_case_on_multiple_test_suite_with_same_name() -> void: + # add a second test suite where has same name as TEST_SUITE_A + var test_suite = auto_free(GdUnitTestResourceLoader.load_test_suite("res://addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource")) + var test_suite_aa_path = test_suite.get_script().resource_path + _inspector.add_test_suite(toDto(test_suite)) + + # verify the items exists checked the tree + assert_str(TEST_SUITE_A).is_not_equal(test_suite_aa_path) + var suite_a: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_A) + var suite_aa: TreeItem = _inspector._find_item(_inspector._tree_root, test_suite_aa_path) + assert_object(suite_a).is_not_same(suite_aa) + assert_str(suite_a.get_meta(_inspector.META_RESOURCE_PATH)).is_equal(TEST_SUITE_A) + assert_str(suite_aa.get_meta(_inspector.META_RESOURCE_PATH)).is_equal(test_suite_aa_path) + + # verify inital state + assert_str(suite_a.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_a, "test_aa")).is_equal(_inspector.STATE.INITIAL) + assert_str(suite_aa.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + + # set test starting checked TEST_SUITE_A + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(TEST_SUITE_A, "ExampleTestSuiteA", "test_aa")) + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(TEST_SUITE_A, "ExampleTestSuiteA", "test_ab")) + assert_str(suite_a.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_a, "test_aa")).is_equal(_inspector.STATE.RUNNING) + assert_int(get_item_state(suite_a, "test_ab")).is_equal(_inspector.STATE.RUNNING) + # test test_suite_aa_path is not affected + assert_str(suite_aa.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_aa, "test_aa")).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite_aa, "test_ab")).is_equal(_inspector.STATE.INITIAL) + + # finish the tests with success + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(TEST_SUITE_A, "ExampleTestSuiteA", "test_aa")) + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(TEST_SUITE_A, "ExampleTestSuiteA", "test_ab")) + + # verify updated state checked TEST_SUITE_A + assert_str(suite_a.get_text(0)).is_equal("(2/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_a, "test_aa")).is_equal(_inspector.STATE.SUCCESS) + assert_int(get_item_state(suite_a, "test_ab")).is_equal(_inspector.STATE.SUCCESS) + # test test_suite_aa_path is not affected + assert_str(suite_aa.get_text(0)).is_equal("(0/5) ExampleTestSuiteA") + assert_int(get_item_state(suite_aa, "test_aa")).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite_aa, "test_ab")).is_equal(_inspector.STATE.INITIAL) + + +# Test coverage for issue GD-278: GdUnit Inspector: Test marks as passed if both warning and error +func test_update_icon_state() -> void: + var TEST_SUITE_PATH = "res://addons/gdUnit4/test/core/resources/testsuites/TestSuiteFailAndOrpahnsDetected.resource" + var TEST_SUITE_NAME = "TestSuiteFailAndOrpahnsDetected" + var test_suite = auto_free(GdUnitTestResourceLoader.load_test_suite(TEST_SUITE_PATH)) + _inspector.add_test_suite(toDto(test_suite)) + + var suite: TreeItem = _inspector._find_item(_inspector._tree_root, TEST_SUITE_PATH) + + # Verify the inital state + assert_str(suite.get_text(0)).is_equal("(0/2) "+ TEST_SUITE_NAME) + assert_str(suite.get_meta(_inspector.META_RESOURCE_PATH)).is_equal(TEST_SUITE_PATH) + assert_int(get_item_state(_inspector._tree_root, TEST_SUITE_NAME)).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite, "test_case1")).is_equal(_inspector.STATE.INITIAL) + assert_int(get_item_state(suite, "test_case2")).is_equal(_inspector.STATE.INITIAL) + + # Set tests to running + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(TEST_SUITE_PATH, TEST_SUITE_NAME, "test_case1")) + _inspector._on_gdunit_event(GdUnitEvent.new().test_before(TEST_SUITE_PATH, TEST_SUITE_NAME, "test_case2")) + # Verify all items on state running. + assert_str(suite.get_text(0)).is_equal("(0/2) " + TEST_SUITE_NAME) + assert_int(get_item_state(suite, "test_case1")).is_equal(_inspector.STATE.RUNNING) + assert_int(get_item_state(suite, "test_case2")).is_equal(_inspector.STATE.RUNNING) + + # Simulate test processed. + # test_case1 succeeded + _inspector._on_gdunit_event(GdUnitEvent.new().test_after(TEST_SUITE_PATH, TEST_SUITE_NAME, "test_case1")) + # test_case2 is failing by an orphan warning and an failure + _inspector._on_gdunit_event(GdUnitEvent.new()\ + .test_after(TEST_SUITE_PATH, TEST_SUITE_NAME, "test_case2", {GdUnitEvent.FAILED: true})) + # We check whether a test event with a warning does not overwrite a higher object status, e.g. an error. + _inspector._on_gdunit_event(GdUnitEvent.new()\ + .test_after(TEST_SUITE_PATH, TEST_SUITE_NAME, "test_case2", {GdUnitEvent.WARNINGS: true})) + + # Verify the final state + assert_str(suite.get_text(0)).is_equal("(1/2) " + TEST_SUITE_NAME) + assert_int(get_item_state(suite, "test_case1")).is_equal(_inspector.STATE.SUCCESS) + assert_int(get_item_state(suite, "test_case2")).is_equal(_inspector.STATE.FAILED) diff --git a/addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource b/addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource new file mode 100644 index 0000000..f192f7c --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/bar/ExampleTestSuiteA.resource @@ -0,0 +1,17 @@ +extends GdUnitTestSuite + + +func test_aa() -> void: + pass + +func test_ab() -> void: + pass + +func test_ac() -> void: + pass + +func test_ad() -> void: + pass + +func test_ae() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource new file mode 100644 index 0000000..2019b4b --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteA.resource @@ -0,0 +1,18 @@ +class_name ExampleTestSuiteA +extends GdUnitTestSuite + + +func test_aa() -> void: + pass + +func test_ab() -> void: + pass + +func test_ac() -> void: + pass + +func test_ad() -> void: + pass + +func test_ae() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource new file mode 100644 index 0000000..c0bf271 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteB.resource @@ -0,0 +1,12 @@ +class_name ExampleTestSuiteB +extends GdUnitTestSuite + + +func test_ba() -> void: + pass + +func test_bb() -> void: + pass + +func test_bc() -> void: + pass diff --git a/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource new file mode 100644 index 0000000..e96bc44 --- /dev/null +++ b/addons/gdUnit4/test/ui/parts/resources/foo/ExampleTestSuiteC.resource @@ -0,0 +1,18 @@ +class_name ExampleTestSuiteC +extends GdUnitTestSuite + + +func test_ca() -> void: + pass + +func test_cb() -> void: + pass + +func test_cc() -> void: + pass + +func test_cd() -> void: + pass + +func test_ce() -> void: + pass diff --git a/addons/gdUnit4/test/ui/templates/TestSuiteTemplateTest.gd b/addons/gdUnit4/test/ui/templates/TestSuiteTemplateTest.gd new file mode 100644 index 0000000..26e77a7 --- /dev/null +++ b/addons/gdUnit4/test/ui/templates/TestSuiteTemplateTest.gd @@ -0,0 +1,36 @@ +# GdUnit generated TestSuite +#warning-ignore-all:unused_argument +#warning-ignore-all:return_value_discarded +class_name TestSuiteTemplateTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd' + + +func test_show() -> void: + var template = spy("res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn") + scene_runner(template) + + # verify the followup functions are called by _ready() + verify(template)._ready() + verify(template).setup_editor_colors() + verify(template).setup_supported_types() + verify(template).load_template(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + verify(template).setup_tags_help() + + +func test_load_template_gd() -> void: + var runner := scene_runner("res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn") + runner.invoke("load_template", GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + + assert_int(runner.get_property("_selected_template")).is_equal(GdUnitTestSuiteTemplate.TEMPLATE_ID_GD) + assert_str(runner.get_property("_template_editor").text).is_equal(GdUnitTestSuiteTemplate.default_GD_template().replace("\r", "")) + + +func test_load_template_cs() -> void: + var runner := scene_runner("res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn") + runner.invoke("load_template", GdUnitTestSuiteTemplate.TEMPLATE_ID_CS) + + assert_int(runner.get_property("_selected_template")).is_equal(GdUnitTestSuiteTemplate.TEMPLATE_ID_CS) + assert_str(runner.get_property("_template_editor").text).is_equal(GdUnitTestSuiteTemplate.default_CS_template().replace("\r", "")) diff --git a/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd b/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd new file mode 100644 index 0000000..c68b87c --- /dev/null +++ b/addons/gdUnit4/test/update/GdMarkDownReaderTest.gd @@ -0,0 +1,122 @@ +# GdUnit generated TestSuite +class_name GdMarkDownReaderTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/update/GdMarkDownReader.gd' +const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +var _reader = preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd").new() +var _client :GdUnitUpdateClient + + +func before(): + _client = GdUnitUpdateClient.new() + add_child(_client) + _reader.set_http_client(_client) + + +func after(): + _client.queue_free() + + +func test_tobbcode() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/markdown_example.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_example.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_table() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/markdown_table.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_table.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_html_headers() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/html_header.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_header.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_md_headers() -> void: + var source := resource_as_string("res://addons/gdUnit4/test/update/resources/md_header.txt") + var expected := resource_as_string("res://addons/gdUnit4/test/update/resources/bbcode_md_header.txt") + assert_str(await _reader.to_bbcode(source)).is_equal(expected) + + +func test_tobbcode_list() -> void: + assert_str(await _reader.to_bbcode("- item")).is_equal("[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] item\n") + assert_str(await _reader.to_bbcode(" - item")).is_equal(" [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] item\n") + assert_str(await _reader.to_bbcode(" - item")).is_equal(" [img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] item\n") + assert_str(await _reader.to_bbcode(" - item")).is_equal(" [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] item\n") + + +func test_to_bbcode_embeded_text() -> void: + assert_str(await _reader.to_bbcode("> some text")).is_equal("[img=50x14]res://addons/gdUnit4/src/update/assets/embedded.png[/img][i] some text[/i]\n") + + +func test_process_image() -> void: + #regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)") + var reg_ex :RegEx = _reader.md_replace_patterns[24][0] + + # without tooltip + assert_str(await _reader.process_image(reg_ex, "![alt text](res://addons/gdUnit4/test/update/resources/icon48.png)"))\ + .is_equal("[img]res://addons/gdUnit4/test/update/resources/icon48.png[/img]") + # with tooltip + assert_str(await _reader.process_image(reg_ex, "![alt text](res://addons/gdUnit4/test/update/resources/icon48.png \"Logo Title Text 1\")"))\ + .is_equal("[img]res://addons/gdUnit4/test/update/resources/icon48.png[/img]") + # multiy lines + var input := """ + ![alt text](res://addons/gdUnit4/test/update/resources/icon48.png) + + ![alt text](res://addons/gdUnit4/test/update/resources/icon23.png \"Logo Title Text 1\") + + """.dedent() + var expected := """ + [img]res://addons/gdUnit4/test/update/resources/icon48.png[/img] + + [img]res://addons/gdUnit4/test/update/resources/icon23.png[/img] + + """.dedent() + assert_str(await _reader.process_image(reg_ex, input))\ + .is_equal(expected) + + +func test_process_image_by_reference() -> void: + #regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)") + var reg_ex :RegEx = _reader.md_replace_patterns[23][0] + var input := """ + ![alt text1][logo-1] + + [logo-1]:https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2" + + ![alt text2][logo-1] + + """.dedent() + + var expected := """ + ![alt text1](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png) + + + ![alt text2](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png) + + """.replace("\r", "").dedent() + + # without tooltip + assert_str(_reader.process_image_references(reg_ex, input))\ + .is_equal(expected) + + +func test_process_external_image_save_as_png() -> void: + var input := """ + [img]https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png[/img] + [img]https://github.com/MikeSchulze/gdUnit4/assets/347037/3205c9f1-1746-4716-aa6d-e3a1808b761d[/img] + """.dedent() + + var output := await _reader._process_external_image_resources(input) + assert_str(output).is_equal(""" + [img]res://addons/gdUnit4/tmp-update/icon48.png[/img] + [img]res://addons/gdUnit4/tmp-update/3205c9f1-1746-4716-aa6d-e3a1808b761d.png[/img] + """.dedent()) + assert_file("res://addons/gdUnit4/tmp-update/icon48.png").exists() + assert_file("res://addons/gdUnit4/tmp-update/3205c9f1-1746-4716-aa6d-e3a1808b761d.png").exists() diff --git a/addons/gdUnit4/test/update/GdUnitPatcherTest.gd b/addons/gdUnit4/test/update/GdUnitPatcherTest.gd new file mode 100644 index 0000000..f4ac204 --- /dev/null +++ b/addons/gdUnit4/test/update/GdUnitPatcherTest.gd @@ -0,0 +1,114 @@ +# GdUnit generated TestSuite +class_name GdUnitPatcherTest +extends GdUnitTestSuite + +# TestSuite generated from +const __source = 'res://addons/gdUnit4/src/update/GdUnitPatcher.gd' + +const _patches := "res://addons/gdUnit4/test/update/resources/patches/" + +var _patcher :GdUnitPatcher + + +func before(): + _patcher = auto_free(GdUnitPatcher.new()) + + +func before_test(): + Engine.set_meta(GdUnitPatch.PATCH_VERSION, []) + _patcher._patches.clear() + + +func test__collect_patch_versions_no_patches() -> void: + # using higher version than patches exists in patch folder + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(3,0,0))).is_empty() + + +func test__collect_patch_versions_current_eq_latest_version() -> void: + # using equal version than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(1,1,4))).is_empty() + + +func test__collect_patch_versions_current_lower_latest_version() -> void: + # using one version lower than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(0,9,9)))\ + .contains_exactly(["res://addons/gdUnit4/test/update/resources/patches/v1.1.4"]) + + # using two versions lower than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(0,9,8)))\ + .contains_exactly([ + "res://addons/gdUnit4/test/update/resources/patches/v0.9.9", + "res://addons/gdUnit4/test/update/resources/patches/v1.1.4"]) + + # using three versions lower than highst available patch + assert_array(_patcher._collect_patch_versions(_patches, GdUnit4Version.new(0,9,5)))\ + .contains_exactly([ + "res://addons/gdUnit4/test/update/resources/patches/v0.9.6", + "res://addons/gdUnit4/test/update/resources/patches/v0.9.9", + "res://addons/gdUnit4/test/update/resources/patches/v1.1.4"]) + + +func test_scan_patches() -> void: + _patcher._scan(_patches, GdUnit4Version.new(0,9,6)) + assert_dict(_patcher._patches)\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v0.9.9", PackedStringArray(["patch_a.gd", "patch_b.gd"]))\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v1.1.4", PackedStringArray(["patch_a.gd"])) + assert_int(_patcher.patch_count()).is_equal(3) + + _patcher._patches.clear() + _patcher._scan(_patches, GdUnit4Version.new(0,9,5)) + assert_dict(_patcher._patches)\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v0.9.6", PackedStringArray(["patch_x.gd"]))\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v0.9.9", PackedStringArray(["patch_a.gd", "patch_b.gd"]))\ + .contains_key_value("res://addons/gdUnit4/test/update/resources/patches/v1.1.4", PackedStringArray(["patch_a.gd"])) + assert_int(_patcher.patch_count()).is_equal(4) + + +func test_execute_no_patches() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + + +func test_execute_v_095() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.parse("v0.9.5")) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_equal([ + GdUnit4Version.parse("v0.9.6"), + GdUnit4Version.parse("v0.9.9-a"), + GdUnit4Version.parse("v0.9.9-b"), + GdUnit4Version.parse("v1.1.4"), + ]) + + +func test_execute_v_096() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.parse("v0.9.6")) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_equal([ + GdUnit4Version.parse("v0.9.9-a"), + GdUnit4Version.parse("v0.9.9-b"), + GdUnit4Version.parse("v1.1.4"), + ]) + + +func test_execute_v_099() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.new(0,9,9)) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_equal([ + GdUnit4Version.parse("v1.1.4"), + ]) + + +func test_execute_v_150() -> void: + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() + _patcher._scan(_patches, GdUnit4Version.parse("v1.5.0")) + + _patcher.execute() + assert_array(Engine.get_meta(GdUnitPatch.PATCH_VERSION)).is_empty() diff --git a/addons/gdUnit4/test/update/GdUnitUpdateTest.gd b/addons/gdUnit4/test/update/GdUnitUpdateTest.gd new file mode 100644 index 0000000..4dfff50 --- /dev/null +++ b/addons/gdUnit4/test/update/GdUnitUpdateTest.gd @@ -0,0 +1,18 @@ +# GdUnit generated TestSuite +class_name GdUnitUpdateTest +extends GdUnitTestSuite + +# TestSuite generated from +const GdUnitUpdate = preload('res://addons/gdUnit4/src/update/GdUnitUpdate.gd') + + +func after_test(): + clean_temp_dir() + + +func test__prepare_update_deletes_old_content() -> void: + var _update :GdUnitUpdate = auto_free(GdUnitUpdate.new()) + + + + diff --git a/addons/gdUnit4/test/update/bbcodeView.gd b/addons/gdUnit4/test/update/bbcodeView.gd new file mode 100644 index 0000000..12934f1 --- /dev/null +++ b/addons/gdUnit4/test/update/bbcodeView.gd @@ -0,0 +1,48 @@ +extends Control + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdMarkDownReader := preload("res://addons/gdUnit4/src/update/GdMarkDownReader.gd") +const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + +@onready var _input :TextEdit = $HSplitContainer/TextEdit +@onready var _text :RichTextLabel = $HSplitContainer/RichTextLabel + +@onready var _update_client :GdUnitUpdateClient = $GdUnitUpdateClient + +var _md_reader := GdMarkDownReader.new() + + +func _ready(): + _md_reader.set_http_client(_update_client) + var source := GdUnitFileAccess.resource_as_string("res://addons/gdUnit4/test/update/resources/markdown_example.txt") + _input.text = source + await set_bbcode(source) + + +func set_bbcode(text :String) : + var bbcode = await _md_reader.to_bbcode(text) + _text.clear() + _text.append_text(bbcode) + _text.queue_redraw() + + +func _on_TextEdit_text_changed(): + await set_bbcode(_input.get_text()) + + +func _on_RichTextLabel_meta_clicked(meta :String): + var properties = str_to_var(meta) + prints("meta_clicked", properties) + if properties.has("url"): + OS.shell_open(properties.get("url")) + + +func _on_RichTextLabel_meta_hover_started(meta :String): + var properties = str_to_var(meta) + prints("hover_started", properties) + if properties.has("tool_tip"): + _text.set_tooltip(properties.get("tool_tip")) + + +func _on_RichTextLabel_meta_hover_ended(_meta :String): + _text.set_tooltip("") diff --git a/addons/gdUnit4/test/update/bbcodeView.tscn b/addons/gdUnit4/test/update/bbcodeView.tscn new file mode 100644 index 0000000..e096c51 --- /dev/null +++ b/addons/gdUnit4/test/update/bbcodeView.tscn @@ -0,0 +1,39 @@ +[gd_scene load_steps=3 format=3 uid="uid://c1rwx6anh3u3m"] + +[ext_resource type="Script" path="res://addons/gdUnit4/test/update/bbcodeView.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="6"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") + +[node name="HSplitContainer" type="HSplitContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +split_offset = 600 + +[node name="TextEdit" type="TextEdit" parent="HSplitContainer"] +layout_mode = 2 + +[node name="RichTextLabel" type="RichTextLabel" parent="HSplitContainer"] +use_parent_material = true +layout_mode = 2 +tooltip_text = "test" +bbcode_enabled = true + +[node name="GdUnitUpdateClient" type="Node" parent="."] +script = ExtResource("6") + +[connection signal="text_changed" from="HSplitContainer/TextEdit" to="." method="_on_TextEdit_text_changed"] +[connection signal="meta_clicked" from="HSplitContainer/RichTextLabel" to="." method="_on_RichTextLabel_meta_clicked"] +[connection signal="meta_hover_ended" from="HSplitContainer/RichTextLabel" to="." method="_on_RichTextLabel_meta_hover_ended"] +[connection signal="meta_hover_started" from="HSplitContainer/RichTextLabel" to="." method="_on_RichTextLabel_meta_hover_started"] diff --git a/addons/gdUnit4/test/update/resources/bbcode_example.txt b/addons/gdUnit4/test/update/resources/bbcode_example.txt new file mode 100644 index 0000000..921f91c --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_example.txt @@ -0,0 +1,28 @@ +[font_size=28]GdUnit3 v0.9.4 - Release Candidate[/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + + +[font_size=24]Improvements[/font_size] + +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] Added project settings to configure: +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] [b]Verbose Orphans[/b] to enable/disable report detected orphans +[img]res://addons/gdUnit4/tmp-update/119266895-e09d1900-bbec-11eb-91e9-45409ba2edb2.png[/img] +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] [b]Server Connection Timeout Minites[/b] to set test server connection timeout in minutes +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] [b]Test Timeout Seconds[/b] to set the default test case timeout in seconds +[img]res://addons/gdUnit4/tmp-update/119266875-d1b66680-bbec-11eb-856f-8fac9b0ed31c.png[/img] + +test seperator +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + + +[font_size=24]Bugfixes[/font_size] + +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] GdUnit inspecor: + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Fixed invalid test case state visualisation for detected orphan nodes (#63) + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Fixed a ui bug to auto select the first report failure after a test run + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Fixed invalid visualisation state and error counter (#66) +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] TestSuite: + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] Using asserts on stage after() now reporting +[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img] Core: + [img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img] The GdUnit network layer was replaced by a new TCP server/client architecture to enable network-related testing (#64 ) + diff --git a/addons/gdUnit4/test/update/resources/bbcode_header.txt b/addons/gdUnit4/test/update/resources/bbcode_header.txt new file mode 100644 index 0000000..4874ceb --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_header.txt @@ -0,0 +1,40 @@ +[font_size=32]Header 1 Text[/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=32][center]Header 1 Centered Text[/center][/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + + +[font_size=28]Header 2 Text[/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + +[font_size=32][center]Header 2 Centered Text[/center][/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + + +[font_size=32][center]Header 2 Centered Text +Multiline Test +Is here[/center][/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + + +[font_size=24]Header 3 Text[/font_size] + +[font_size=24][center]Header 3 Centered Text[/center][/font_size] + + +[font_size=20]Header 4 Text[/font_size] + +[font_size=20][center]Header 4 Centered Text[/center][/font_size] + + +[font_size=16]Header 5 Text[/font_size] + +[font_size=16][center]Header 5 Centered Text[/center][/font_size] + + +[font_size=12]Header 6 Text[/font_size] + +[font_size=12][center]Header 6 Centered Text[/center][/font_size] + + diff --git a/addons/gdUnit4/test/update/resources/bbcode_md_header.txt b/addons/gdUnit4/test/update/resources/bbcode_md_header.txt new file mode 100644 index 0000000..09399f3 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_md_header.txt @@ -0,0 +1,20 @@ +[font_size=32]Header 1 Text[/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + + +[font_size=28]Header 2 Text[/font_size] +[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img] + + +[font_size=24]Header 3 Text[/font_size] + + +[font_size=20]Header 4 Text[/font_size] + + +[font_size=16]Header 5 Text[/font_size] + + +[font_size=12]Header 6 Text[/font_size] + + diff --git a/addons/gdUnit4/test/update/resources/bbcode_table.txt b/addons/gdUnit4/test/update/resources/bbcode_table.txt new file mode 100644 index 0000000..e7fc778 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/bbcode_table.txt @@ -0,0 +1,9 @@ +[table=2] +[cell][b] type [/b][/cell]|[cell][b] description[/b][/cell] +[cell]--------------[/cell]|[cell]--------------------------------------------[/cell] +[cell]any_color() [/cell]|[cell] Argument matcher to match any Color value[/cell] +[cell]any_vector2() [/cell]|[cell] Argument matcher to match any Vector2 value[/cell] +[cell]any_vector3() [/cell]|[cell] Argument matcher to match any Vector3 value[/cell] +[cell]any_rect2() [/cell]|[cell] Argument matcher to match any Rect2 value[/cell] +[/table] + diff --git a/addons/gdUnit4/test/update/resources/html_header.txt b/addons/gdUnit4/test/update/resources/html_header.txt new file mode 100644 index 0000000..d9206b4 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/html_header.txt @@ -0,0 +1,21 @@ +

Header 1 Text

+

Header 1 Centered Text

+ +

Header 2 Text

+

Header 2 Centered Text

+ +

Header 2 Centered Text +Multiline Test +Is here

+ +

Header 3 Text

+

Header 3 Centered Text

+ +

Header 4 Text

+

Header 4 Centered Text

+ +
Header 5 Text
+
Header 5 Centered Text
+ +
Header 6 Text
+
Header 6 Centered Text
diff --git a/addons/gdUnit4/test/update/resources/icon48.png b/addons/gdUnit4/test/update/resources/icon48.png new file mode 100644 index 0000000..9f8d934 Binary files /dev/null and b/addons/gdUnit4/test/update/resources/icon48.png differ diff --git a/addons/gdUnit4/test/update/resources/icon48.png.import b/addons/gdUnit4/test/update/resources/icon48.png.import new file mode 100644 index 0000000..a5fe1c4 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/icon48.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://4d6aqwgnxyla" +path="res://.godot/imported/icon48.png-e87a81340792ea4239ce79ba4259a0dd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/test/update/resources/icon48.png" +dest_files=["res://.godot/imported/icon48.png-e87a81340792ea4239ce79ba4259a0dd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/test/update/resources/markdown.txt b/addons/gdUnit4/test/update/resources/markdown.txt new file mode 100644 index 0000000..058a8f3 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/markdown.txt @@ -0,0 +1,168 @@ +------ HTML Headers --- +[font_size=24]Header 1 Text[/font_size] +[font_size=24][center]Header 1Centered Text[/center][/font_size] + +[font_size=20]Header 2 Text[/font_size] +[font_size=20][center]Header 2 Centered Text[/center][/font_size] + +[font_size=20][center]Header 2 Centered Text +Multiline Test +Is here[/center][/font_size] + +--- + +------ Markdown ------------- + +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 + + +------ embeded ------ +>This is an **embedded section**. +>The section continues here + +>This is another **embedded section**. +>This section also continues in the second like +>- aba +>This line isn’t embedded any more. +>- tets +> - aha +> - akaka + + + + +------ lists ------ +* an asterisk starts an unordered list +* and this is another item in the list ++ or you can also use the + character +- or the - character + +To start an ordered list, write this: + +1. this starts a list *with* numbers +* this will show as number "2" +* this will show as number "3." +9. any number, +, -, or * will keep the list going. + * just indent by 4 spaces (or tab) to make a sub-list + 1. keep indenting for more sub lists + * here i'm back to the second level + + + +- Asserts: + - Added new `assert_vector2` to verify Vector2 values (#69 ) + - Added new `assert_vector3` to verify Vector3 values (#69 ) + - ahss + - kaka + - kaka + - kaka + - lll + - kkk + + +- Fuzzers: + - Added `rangev2` to generate random Vector2 values + - Added `rangev3` to generate random Vector3 values + - one or more fuzzers are now allowed for a test case (#71) +- GitHub Action + - Added GitHub action to automatic trigger selftest on push events (tests against Godot 3.2.3, 3.3, 3.3.1, 3.3.2) (#74 ) + + + +------ check lists ------ +[ ] A +[x] B +[ ] C + +------ code ------ +This is `code`. + +``This is all `code`.`` + +```javascript +var s = "JavaScript syntax highlighting"; +alert(s); +``` + +```python +s = "Python syntax highlighting" +print s +``` + +``` +No language indicated, so no syntax highlighting. +But let's throw in a tag. +``` + +------ links ------ +Here is a [Link](https://example.com/ "Optional link title"). + +------ image ------ +Inline-style: +![alt text](res://addons/gdUnit4/test/update/resources/icon48.png "Logo Title Text 1") + +![alt text](https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png) + + +Reference-style: +![alt text][logo] + +[logo]:res://addons/gdUnit4/test/update/resources/icon48.png "Logo Title Text 2" + + +------ Horizontal Rules ------ + +--- +Hyphens +*** +Asterisks +___ +Underscores + + + +------ table ------ +|Column 1|Column 2| +|--------|--------| +| A | B | +| C | D | + +Column 1|Column 2 +--------|-------- +A | B +C | D + + +------ foodnodes ------ +You can easily place footnotes [^2] in the continuous text [^1]. +[^1]: Here you can find the text for the footnote. +[^2]: **Footnotes** themselves can also be *formatted*. +And these even include several lines. + + +------ asterisk ------ +This *is* an \*example with an asterisk\**. +This _is_ an \_example with an asterisk\_. + +------ bold ------ +test +**test** +__test__ + +------ italic ------ +test +*test* +_test_ + +------ italic + bold ------ +***Italic and Bold Text*** +___Italic and Bold Text___ + +------ stroke ------ +test +~test~ +~~test~~ diff --git a/addons/gdUnit4/test/update/resources/markdown_example.txt b/addons/gdUnit4/test/update/resources/markdown_example.txt new file mode 100644 index 0000000..54dd073 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/markdown_example.txt @@ -0,0 +1,22 @@ +## GdUnit3 v0.9.4 - Release Candidate + +### Improvements +- Added project settings to configure: + - Verbose Orphans to enable/disable report detected orphans +![image](https://user-images.githubusercontent.com/347037/119266895-e09d1900-bbec-11eb-91e9-45409ba2edb2.png) + - Server Connection Timeout Minites to set test server connection timeout in minutes + - Test Timeout Seconds to set the default test case timeout in seconds +![image](https://user-images.githubusercontent.com/347037/119266875-d1b66680-bbec-11eb-856f-8fac9b0ed31c.png) + +test seperator +--- + +### Bugfixes +- GdUnit inspecor: + - Fixed invalid test case state visualisation for detected orphan nodes (#63) + - Fixed a ui bug to auto select the first report failure after a test run + - Fixed invalid visualisation state and error counter (#66) +- TestSuite: + - Using asserts on stage after() now reporting +- Core: + - The GdUnit network layer was replaced by a new TCP server/client architecture to enable network-related testing (#64 ) diff --git a/addons/gdUnit4/test/update/resources/markdown_table.txt b/addons/gdUnit4/test/update/resources/markdown_table.txt new file mode 100644 index 0000000..6fbdad6 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/markdown_table.txt @@ -0,0 +1,6 @@ + type | description +-- | -- +any_color() | Argument matcher to match any Color value +any_vector2() | Argument matcher to match any Vector2 value +any_vector3() | Argument matcher to match any Vector3 value +any_rect2() | Argument matcher to match any Rect2 value diff --git a/addons/gdUnit4/test/update/resources/md_header.txt b/addons/gdUnit4/test/update/resources/md_header.txt new file mode 100644 index 0000000..b7669b0 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/md_header.txt @@ -0,0 +1,11 @@ +# Header 1 Text + +## Header 2 Text + +### Header 3 Text + +#### Header 4 Text + +##### Header 5 Text + +###### Header 6 Text diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd new file mode 100644 index 0000000..ee555ec --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.5/patch_y.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init(): + super(GdUnit4Version.parse("v0.9.5")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd new file mode 100644 index 0000000..a71e1d4 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.6/patch_x.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init(): + super(GdUnit4Version.parse("v0.9.6")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd new file mode 100644 index 0000000..908571b --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_a.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init(): + super(GdUnit4Version.parse("v0.9.9-a")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd new file mode 100644 index 0000000..9338997 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v0.9.9/patch_b.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init(): + super(GdUnit4Version.parse("v0.9.9-b")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd b/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd new file mode 100644 index 0000000..3098ba7 --- /dev/null +++ b/addons/gdUnit4/test/update/resources/patches/v1.1.4/patch_a.gd @@ -0,0 +1,12 @@ +extends GdUnitPatch + +func _init(): + super(GdUnit4Version.parse("v1.1.4")) + +func execute() -> bool: + var patches := Array() + if Engine.has_meta(PATCH_VERSION): + patches = Engine.get_meta(PATCH_VERSION) + patches.append(version()) + Engine.set_meta(PATCH_VERSION, patches) + return true diff --git a/addons/gdUnit4/test/update/resources/update.zip b/addons/gdUnit4/test/update/resources/update.zip new file mode 100644 index 0000000..f58a854 Binary files /dev/null and b/addons/gdUnit4/test/update/resources/update.zip differ diff --git a/addons/log/log.gd b/addons/log/log.gd new file mode 100644 index 0000000..7a4e5c6 --- /dev/null +++ b/addons/log/log.gd @@ -0,0 +1,484 @@ +@tool +extends Object +class_name Log + +## helpers #################################### + +static func assoc(opts: Dictionary, key: String, val): + var _opts = opts.duplicate(true) + _opts[key] = val + return _opts + +## config #################################### + +static var config = { + max_array_size=20, + dictionary_skip_keys=["layer_0/tile_data"], + } + +static func get_max_array_size(): + return Log.config.get("max_array_size", 20) + +static func get_dictionary_skip_keys(): + return Log.config.get("dictionary_skip_keys", []) + +static func set_color_scheme(scheme): + Log.config["color_scheme"] = scheme + +static func get_config_color_scheme(): + return Log.config.get("color_scheme", {}) + +## colors ########################################################################### + +# terminal safe colors: +# - black +# - red +# - green +# - yellow +# - blue +# - magenta +# - pink +# - purple +# - cyan +# - white +# - orange +# - gray + +static var COLORS_TERMINAL_SAFE = { + "SRC": "cyan", + "ADDONS": "red", + "TEST": "green", + ",": "red", + "(": "red", + ")": "red", + "[": "red", + "]": "red", + "{": "red", + "}": "red", + "&": "orange", + "^": "orange", + "dict_key": "magenta", + "vector_value": "green", + "class_name": "magenta", + TYPE_NIL: "pink", + TYPE_BOOL: "pink", + TYPE_INT: "green", + TYPE_FLOAT: "green", + TYPE_STRING: "pink", + TYPE_VECTOR2: "green", + TYPE_VECTOR2I: "green", + TYPE_RECT2: "green", + TYPE_RECT2I: "green", + TYPE_VECTOR3: "green", + TYPE_VECTOR3I: "green", + TYPE_TRANSFORM2D: "pink", + TYPE_VECTOR4: "green", + TYPE_VECTOR4I: "green", + TYPE_PLANE: "pink", + TYPE_QUATERNION: "pink", + TYPE_AABB: "pink", + TYPE_BASIS: "pink", + TYPE_TRANSFORM3D: "pink", + TYPE_PROJECTION: "pink", + TYPE_COLOR: "pink", + TYPE_STRING_NAME: "pink", + TYPE_NODE_PATH: "pink", + TYPE_RID: "pink", + TYPE_OBJECT: "pink", + TYPE_CALLABLE: "pink", + TYPE_SIGNAL: "pink", + TYPE_DICTIONARY: "pink", + TYPE_ARRAY: "pink", + TYPE_PACKED_BYTE_ARRAY: "pink", + TYPE_PACKED_INT32_ARRAY: "pink", + TYPE_PACKED_INT64_ARRAY: "pink", + TYPE_PACKED_FLOAT32_ARRAY: "pink", + TYPE_PACKED_FLOAT64_ARRAY: "pink", + TYPE_PACKED_STRING_ARRAY: "pink", + TYPE_PACKED_VECTOR2_ARRAY: "pink", + TYPE_PACKED_VECTOR3_ARRAY: "pink", + TYPE_PACKED_COLOR_ARRAY: "pink", + TYPE_MAX: "pink", + } + +static var COLORS_PRETTY_V1 = { + "SRC": "aquamarine", + "ADDONS": "peru", + "TEST": "green_yellow", + ",": "crimson", + "(": "crimson", + ")": "crimson", + "[": "crimson", + "]": "crimson", + "{": "crimson", + "}": "crimson", + "&": "coral", + "^": "coral", + "dict_key": "cadet_blue", + "vector_value": "cornflower_blue", + "class_name": "cadet_blue", + TYPE_NIL: "coral", + TYPE_BOOL: "pink", + TYPE_INT: "cornflower_blue", + TYPE_FLOAT: "cornflower_blue", + TYPE_STRING: "dark_gray", + TYPE_VECTOR2: "cornflower_blue", + TYPE_VECTOR2I: "cornflower_blue", + TYPE_RECT2: "cornflower_blue", + TYPE_RECT2I: "cornflower_blue", + TYPE_VECTOR3: "cornflower_blue", + TYPE_VECTOR3I: "cornflower_blue", + TYPE_TRANSFORM2D: "pink", + TYPE_VECTOR4: "cornflower_blue", + TYPE_VECTOR4I: "cornflower_blue", + TYPE_PLANE: "pink", + TYPE_QUATERNION: "pink", + TYPE_AABB: "pink", + TYPE_BASIS: "pink", + TYPE_TRANSFORM3D: "pink", + TYPE_PROJECTION: "pink", + TYPE_COLOR: "pink", + TYPE_STRING_NAME: "pink", + TYPE_NODE_PATH: "pink", + TYPE_RID: "pink", + TYPE_OBJECT: "pink", + TYPE_CALLABLE: "pink", + TYPE_SIGNAL: "pink", + TYPE_DICTIONARY: "pink", + TYPE_ARRAY: "pink", + TYPE_PACKED_BYTE_ARRAY: "pink", + TYPE_PACKED_INT32_ARRAY: "pink", + TYPE_PACKED_INT64_ARRAY: "pink", + TYPE_PACKED_FLOAT32_ARRAY: "pink", + TYPE_PACKED_FLOAT64_ARRAY: "pink", + TYPE_PACKED_STRING_ARRAY: "pink", + TYPE_PACKED_VECTOR2_ARRAY: "pink", + TYPE_PACKED_VECTOR3_ARRAY: "pink", + TYPE_PACKED_COLOR_ARRAY: "pink", + TYPE_MAX: "pink", + } + +## set color scheme #################################### + +static func set_colors_termsafe(): + set_color_scheme(Log.COLORS_TERMINAL_SAFE) + +static func set_colors_pretty(): + set_color_scheme(Log.COLORS_PRETTY_V1) + +static func color_scheme(opts={}): + var scheme = opts.get("color_scheme", {}) + # fill in any missing vals with the set scheme, then the term-safe fallbacks + scheme.merge(Log.get_config_color_scheme()) + scheme.merge(Log.COLORS_TERMINAL_SAFE) + return scheme + +static func color_wrap(s, opts={}): + var use_color = opts.get("use_color", true) + # don't rebuild the color scheme every time + var colors = opts.get("built_color_scheme", color_scheme(opts)) + + if use_color: + var color = opts.get("color") + if not color: + var s_type = opts.get("typeof", typeof(s)) + if s_type is String: + # type overwrites + color = colors.get(s_type) + elif s_type is int and s_type == TYPE_STRING: + # specific strings/punctuation + var s_trimmed = s.strip_edges() + if s_trimmed in colors: + color = colors.get(s_trimmed) + else: + # fallback string color + color = colors.get(s_type) + else: + # all other types + color = colors.get(s_type) + + if color == null: + print("Log.gd could not determine color for object: %s type: (%s)" % [str(s), typeof(s)]) + + return "[color=%s]%s[/color]" % [color, s] + else: + return s + +## overwrites ########################################################################### + +static var log_overwrites = { + "Vector2": func(msg, opts): + if opts.get("use_color", true): + return '%s%s%s%s%s' % [ + Log.color_wrap("(", opts), + Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(")", opts), + ] + else: + return '(%s,%s)' % [msg.x, msg.y], + } + +static func register_overwrite(key, handler): + # TODO warning on key exists? + # support multiple handlers? + # return success/fail? + # validate the key/handler somehow? + log_overwrites[key] = handler + +## to_pretty ########################################################################### + +# returns the passed object as a decorated string +static func to_pretty(msg, opts={}): + var newlines = opts.get("newlines", false) + var use_color = opts.get("use_color", true) + var indent_level = opts.get("indent_level", 0) + if not "indent_level" in opts: + opts["indent_level"] = indent_level + + var color_scheme = opts.get("built_color_scheme", color_scheme(opts)) + if not "built_color_scheme" in opts: + opts["built_color_scheme"] = color_scheme + + if not is_instance_valid(msg) and typeof(msg) == TYPE_OBJECT: + return str("invalid instance: ", msg) + + if msg == null: + return Log.color_wrap(msg, opts) + + if msg is Object and msg.get_class() in log_overwrites: + return log_overwrites.get(msg.get_class()).call(msg, opts) + elif typeof(msg) in log_overwrites: + return log_overwrites.get(typeof(msg)).call(msg, opts) + + # objects + if msg is Object and msg.has_method("to_pretty"): + return Log.to_pretty(msg.to_pretty(), opts) + if msg is Object and msg.has_method("data"): + return Log.to_pretty(msg.data(), opts) + if msg is Object and msg.has_method("to_printable"): + return Log.to_pretty(msg.to_printable(), opts) + + # arrays + if msg is Array or msg is PackedStringArray: + if len(msg) > Log.get_max_array_size(): + pr("[DEBUG]: truncating large array. total:", len(msg)) + msg = msg.slice(0, Log.get_max_array_size() - 1) + if newlines: + msg.append("...") + + var tmp = Log.color_wrap("[ ", opts) + var last = len(msg) - 1 + for i in range(len(msg)): + if newlines and last > 1: + tmp += "\n\t" + tmp += Log.to_pretty(msg[i], + # duplicate here to prevent indenting-per-msg + # e.g. when printing an array of dictionaries + opts.duplicate(true)) + if i != last: + tmp += Log.color_wrap(", ", opts) + tmp += Log.color_wrap(" ]", opts) + return tmp + + # dictionary + elif msg is Dictionary: + var tmp = Log.color_wrap("{ ", opts) + var ct = len(msg) + var last + if len(msg) > 0: + last = msg.keys()[-1] + for k in msg.keys(): + var val + if k in Log.get_dictionary_skip_keys(): + val = "..." + else: + opts.indent_level += 1 + val = Log.to_pretty(msg[k], opts) + if newlines and ct > 1: + tmp += "\n\t" \ + + range(indent_level)\ + .map(func(_i): return "\t")\ + .reduce(func(a, b): return str(a, b), "") + if use_color: + var key = Log.color_wrap('"%s"' % k, Log.assoc(opts, "typeof", "dict_key")) + tmp += "%s: %s" % [key, val] + else: + tmp += '"%s": %s' % [k, val] + if last and str(k) != str(last): + tmp += Log.color_wrap(", ", opts) + tmp += Log.color_wrap(" }", opts) + return tmp + + # strings + elif msg is String: + if msg == "": + return '""' + if "[color=" in msg and "[/color]" in msg: + # assumes the string is already colorized + # NOT PERFECT! could use a regex for something more robust + return msg + return Log.color_wrap(msg, opts) + elif msg is StringName: + return str(Log.color_wrap("&", opts), '"%s"' % msg) + elif msg is NodePath: + return str(Log.color_wrap("^", opts), '"%s"' % msg) + + # vectors + elif msg is Vector2 or msg is Vector2i: + return log_overwrites.get("Vector2").call(msg, opts) + + elif msg is Vector3 or msg is Vector3i: + if use_color: + return '%s%s%s%s%s%s%s' % [ + Log.color_wrap("(", opts), + Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(")", opts), + ] + else: + return '(%s,%s,%s)' % [msg.x, msg.y, msg.z] + elif msg is Vector4 or msg is Vector4i: + if use_color: + return '%s%s%s%s%s%s%s%s%s' % [ + Log.color_wrap("(", opts), + Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(",", opts), + Log.color_wrap(msg.w, Log.assoc(opts, "typeof", "vector_value")), + Log.color_wrap(")", opts), + ] + else: + return '(%s,%s,%s,%s)' % [msg.x, msg.y, msg.z, msg.w] + + # packed scene + elif msg is PackedScene: + if msg.resource_path != "": + return str(Log.color_wrap("PackedScene:", opts), '%s' % msg.resource_path.get_file()) + elif msg.get_script() != null and msg.get_script().resource_path != "": + return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name")) + else: + return Log.color_wrap(msg, opts) + + # resource + elif msg is Resource: + if msg.get_script() != null and msg.get_script().resource_path != "": + return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name")) + elif msg.resource_path != "": + return str(Log.color_wrap("Resource:", opts), '%s' % msg.resource_path.get_file()) + else: + return Log.color_wrap(msg, opts) + + # refcounted + elif msg is RefCounted: + if msg.get_script() != null and msg.get_script().resource_path != "": + return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name")) + else: + return Log.color_wrap(msg.get_class(), Log.assoc(opts, "typeof", "class_name")) + + # fallback to primitive-type lookup + else: + return Log.color_wrap(msg, opts) + +## to_printable ########################################################################### + +static func log_prefix(stack): + if len(stack) > 1: + var call_site = stack[1] + var basename = call_site["source"].get_file().get_basename() + var line_num = str(call_site.get("line", 0)) + if call_site["source"].match("*/test/*"): + return "{" + basename + ":" + line_num + "}: " + elif call_site["source"].match("*/addons/*"): + return "<" + basename + ":" + line_num + ">: " + else: + return "[" + basename + ":" + line_num + "]: " + +static func to_printable(msgs, opts={}): + var stack = opts.get("stack", []) + var pretty = opts.get("pretty", true) + var newlines = opts.get("newlines", false) + var m = "" + if len(stack) > 0: + var prefix = Log.log_prefix(stack) + var prefix_type + if prefix != null and prefix[0] == "[": + prefix_type = "SRC" + elif prefix != null and prefix[0] == "{": + prefix_type = "TEST" + elif prefix != null and prefix[0] == "<": + prefix_type = "ADDONS" + if pretty: + m += Log.color_wrap(prefix, Log.assoc(opts, "typeof", prefix_type)) + else: + m += prefix + for msg in msgs: + # add a space between msgs + if pretty: + m += "%s " % Log.to_pretty(msg, opts) + else: + m += "%s " % str(msg) + return m.trim_suffix(" ") + +## public print fns ########################################################################### + +static func is_not_default(v): + return not v is String or (v is String and v != "ZZZDEF") + +static func pr(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack()}) + print_rich(m) + +static func info(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack()}) + print_rich(m) + +static func log(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack()}) + print_rich(m) + +static func prn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true}) + print_rich(m) + +static func warn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var rich_msgs = msgs.duplicate() + rich_msgs.push_front("[color=yellow][WARN][/color]") + print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true})) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false}) + push_warning(m) + +static func err(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var rich_msgs = msgs.duplicate() + rich_msgs.push_front("[color=red][ERR][/color]") + print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true})) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false}) + push_error(m) + +static func error(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"): + var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7] + msgs = msgs.filter(Log.is_not_default) + var rich_msgs = msgs.duplicate() + rich_msgs.push_front("[color=red][ERR][/color]") + print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true})) + var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false}) + push_error(m) diff --git a/addons/log/plugin.cfg b/addons/log/plugin.cfg new file mode 100644 index 0000000..6e89483 --- /dev/null +++ b/addons/log/plugin.cfg @@ -0,0 +1,14 @@ +[plugin] + +name="Log.gd" +description="A pretty-printing debug logger. + +Log.pr(\"some str\", some_object) + +- Colorizes printed data based on datatype +- Handles nested data structures (Arrays and Dictionaries) +- Prefixes logs with the callsite's source file +- Opt-in to pretty printing via duck-typing (implement a `to_printable()` method on the object)" +author="Russell Matney" +version="v0.0.5" +script="plugin.gd" diff --git a/addons/log/plugin.gd b/addons/log/plugin.gd new file mode 100644 index 0000000..15356b8 --- /dev/null +++ b/addons/log/plugin.gd @@ -0,0 +1,3 @@ +@tool +extends EditorPlugin + diff --git a/project.godot b/project.godot index 612c2ae..8714135 100644 --- a/project.godot +++ b/project.godot @@ -13,3 +13,7 @@ config_version=5 config/name="Pollen Not Included" config/features=PackedStringArray("4.2", "Forward Plus") config/icon="res://icon.svg" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/log/plugin.cfg")