diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml new file mode 100644 index 0000000..fd3bb92 --- /dev/null +++ b/.github/workflows/ci_workflow.yml @@ -0,0 +1,26 @@ +name: Run tests + +on: [push] + +jobs: + build_and_run: + name: Build and run tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + lfs: true + - uses: actions/setup-java@v1 + with: + java-version: '17' + + - name: Build && Run + run: | + deployer_url="https://raw.githubusercontent.com/Insality/defold-deployer/4/deployer.sh" + curl -s ${deployer_url} | bash -s lbd --headless --settings ./test/test.ini + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: insality/defold-event \ No newline at end of file diff --git a/.gitignore b/.gitignore index 027138d..1db3f96 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ Thumbs.db manifest.private.der manifest.public.der + +deployer_version_settings.txt + +.deployer_cache +bob.jar +deployer_build_stats.csv +dist \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fa96091 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "Lua.diagnostics.globals": [ + "lua_script_instance", + "sys", + "init", + "describe", + "before", + "after", + "it" + ] +} diff --git a/README.md b/README.md index 96f5320..ba0e455 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ ![](media/logo.png) -[![Github-sponsors](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/insality) [![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/insality) [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/insality) - [![GitHub release (latest by date)](https://img.shields.io/github/v/tag/insality/defold-event?style=for-the-badge&label=Release)](https://github.com/Insality/defold-event/tags) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/insality/defold-event/ci-workflow.yml?branch=master&style=for-the-badge)](https://github.com/Insality/defold-event/actions) +[![codecov](https://img.shields.io/codecov/c/github/Insality/defold-event?style=for-the-badge)](https://codecov.io/gh/Insality/defold-event) + +[![Github-sponsors](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/insality) [![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/insality) [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/insality) # Event @@ -26,7 +28,7 @@ Open your `game.project` file and add the following line to the dependencies field under the project section: -**[Event v2](https://github.com/Insality/defold-event/archive/refs/tags/2.zip)** +**[Event v3](https://github.com/Insality/defold-event/archive/refs/tags/2.zip)** ``` https://github.com/Insality/defold-event/archive/refs/tags/2.zip @@ -70,6 +72,7 @@ Memory allocation tracking is turned off in release builds, regardless of the `g ### Quick API Reference ```lua +-- Event Module event.set_logger(logger) event.create(callback, [callback_context]) event:subscribe(callback, [callback_context]) @@ -78,10 +81,13 @@ event:is_subscribed(callback, [callback_context]) event:trigger(...) event:is_empty() event:clear() + +-- Global Events Module events.subscribe(name, callback, [callback_context]) events.unsubscribe(name, callback, [callback_context]) events.is_subscribed(name, callback, [callback_context]) events.trigger(name, ...) +events.is_empty(name) events.clear(name) events.clear_all() ``` @@ -228,6 +234,8 @@ Trigger the event, causing all subscribed callbacks to be executed. - **Parameters:** Any number of parameters to be passed to the subscribed callbacks. +- **Return Value:** The return value of the last callback executed. + - **Usage Example:** ```lua @@ -351,6 +359,24 @@ Throw a global event with the specified name. All subscribed callbacks will be e events.trigger("on_game_over", "arg1", "arg2") ``` +**events.is_empty** +--- +```lua +events.is_empty(name) +``` +Check if the specified global event has no subscribed callbacks. + +- **Parameters:** + - `name`: The name of the global event to check. + +- **Return Value:** `true` if the global event has no subscribed callbacks, `false` otherwise. + +- **Usage Example:** + +```lua +local is_empty = events.is_empty("on_game_over") +``` + **events.clear** --- ```lua @@ -425,6 +451,15 @@ If you have any issues, questions or suggestions please [create an issue](https: - The `event:subscribe` and `event:unsubscribe` now return boolean value of success +### **V3** +
+ Changelog + + - Event Trigger now returns value of last executed callback + - Add `events.is_empty(name)` function + - Add tests for Event and Global Events modules +
+ ## ❤️ Support project ❤️ diff --git a/USE_CASES.md b/USE_CASES.md index f06e029..74fca88 100644 --- a/USE_CASES.md +++ b/USE_CASES.md @@ -42,6 +42,7 @@ function final(self) end ``` + ### 2. Component-specific Events Design components with built-in events, enabling customizable behavior for instances of the component. This is particularly useful for UI elements like buttons where you want to bind specific actions to events like clicks. @@ -85,3 +86,28 @@ function init(self) end ``` + + +### 3. Lua annotations + +You can use annotations to document your events and make them easier to understand. + +```lua +---This event is triggered when the sound button is clicked. +---@class event.on_sound_click: event +---@field trigger fun(_, is_sound_on: boolean): boolean|nil +---@field subscribe fun(_, callback: fun(is_sound_on: boolean): boolean, _): boolean + +local event = require("event.event") + +---@type event.on_sound_click +local on_sound_click = event.create() + +-- This callback params will be checked by Lua linter +on_sound_click:subscribe(function(is_sound_on) + print("Sound is on: ", is_sound_on) +end) + +-- Trigger params will be checked by Lua linter +on_sound_click:trigger(true) +``` diff --git a/event/annotations.lua b/event/annotations.lua new file mode 100644 index 0000000..2b4b430 --- /dev/null +++ b/event/annotations.lua @@ -0,0 +1,24 @@ +---@class event +---@field create fun(callback: function|nil, callback_context: any|nil): event +---@field subscribe fun(self: event, callback: function, callback_context: any|nil): boolean +---@field unsubscribe fun(self: event, callback: function, callback_context: any|nil): boolean +---@field is_subscribed fun(self: event, callback: function, callback_context: any|nil): boolean +---@field trigger fun(self: event, a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any, i: any, j: any): nil +---@field clear fun(self: event): nil +---@field is_empty fun(self: event): boolean + +---@class events +---@field subscribe fun(event_name: string, callback: function, callback_context: any|nil): boolean +---@field unsubscribe fun(event_name: string, callback: function, callback_context: any|nil): boolean +---@field is_subscribed fun(event_name: string, callback: function, callback_context: any|nil): boolean +---@field trigger fun(event_name: string, ...: any): any @Result of the last callback +---@field clear fun(name: string): nil +---@field clear_all fun(): nil +---@field is_empty fun(name: string): boolean + +---@class event.logger +---@field trace fun(logger: event.logger, message: string, data: any|nil) +---@field debug fun(logger: event.logger, message: string, data: any|nil) +---@field info fun(logger: event.logger, message: string, data: any|nil) +---@field warn fun(logger: event.logger, message: string, data: any|nil) +---@field error fun(logger: event.logger, message: string, data: any|nil) diff --git a/event/event.lua b/event/event.lua index 3f823d3..4798f1d 100644 --- a/event/event.lua +++ b/event/event.lua @@ -5,13 +5,14 @@ if not IS_DEBUG then MEMORY_THRESHOLD_WARNING = 0 end + ---@class event @Event Module ----@field callbacks table local M = {} --- Use empty function to save a bit of memory local EMPTY_FUNCTION = function(_, message, context) end +---@type event.logger M.logger = { trace = EMPTY_FUNCTION, debug = EMPTY_FUNCTION, @@ -21,7 +22,7 @@ M.logger = { } ----@param logger_instance logger +---@param logger_instance event.logger function M.set_logger(logger_instance) M.logger = logger_instance end @@ -31,12 +32,13 @@ end ---Create new event instance. If callback is passed, it will be subscribed to the event. ---@param callback function|nil ---@param callback_context any|nil +---@return event function M.create(callback, callback_context) local instance = setmetatable({ _mapping = nil, -- Used for memory threshold warning, only in debug mode callbacks = nil, }, { - __index = M + __index = M, }) if callback then @@ -123,25 +125,20 @@ function M:is_subscribed(callback, callback_context) end ----Trigger the event. All subscribed callbacks will be called in the order they were subscribed. ----@param a any ----@param b any ----@param c any ----@param d any ----@param e any ----@param f any ----@param g any ----@param h any ----@param i any ----@param j any local last_used_memory = 0 -function M:trigger(a, b, c, d, e, f, g, h, i, j) + +---Trigger the event. All subscribed callbacks will be called in the order they were subscribed. +---@vararg any +---@return any @Result of the last triggered callback +function M:trigger(...) if not self.callbacks then return end local current_script_context = lua_script_instance.Get() + local result = nil + for index = 1, #self.callbacks do local callback = self.callbacks[index] @@ -153,11 +150,11 @@ function M:trigger(a, b, c, d, e, f, g, h, i, j) last_used_memory = collectgarbage("count") end - local ok, errors + local ok, result_or_error if callback.callback_context then - ok, errors = pcall(callback.callback, callback.callback_context, a, b, c, d, e, f, g, h, i, j) + ok, result_or_error = pcall(callback.callback, callback.callback_context, ...) else - ok, errors = pcall(callback.callback, a, b, c, d, e, f, g, h, i, j) + ok, result_or_error = pcall(callback.callback, ...) end if current_script_context ~= callback.script_context then @@ -166,9 +163,12 @@ function M:trigger(a, b, c, d, e, f, g, h, i, j) if not ok then local traceback = debug.traceback() - M.logger:error("An error occurred during event processing", { errors = errors, traceback = traceback }) + M.logger:error("An error occurred during event processing", { errors = result_or_error, traceback = traceback }) -- Print again cause it's just better to see it in the console - print(traceback) + pprint(result_or_error) + pprint(traceback) + else + result = result_or_error end if MEMORY_THRESHOLD_WARNING > 0 then @@ -182,6 +182,8 @@ function M:trigger(a, b, c, d, e, f, g, h, i, j) end end end + + return result end diff --git a/event/events.lua b/event/events.lua index d2ac0d2..1bb3f78 100644 --- a/event/events.lua +++ b/event/events.lua @@ -9,6 +9,7 @@ M.events = {} ---Throws the event ---@param event_name string Event name ---@vararg any @Event params +---@return any @Result of the last triggered callback function M.trigger(event_name, ...) local event = M.events[event_name] @@ -18,7 +19,7 @@ function M.trigger(event_name, ...) }) if event then - event:trigger(...) + return event:trigger(...) end end @@ -87,4 +88,16 @@ function M.is_subscribed(event_name, callback, callback_context) end +---Check if the event is empty +---@param event_name string Event name +---@return boolean @True if event is empty. If event is not exist, return true +function M.is_empty(event_name) + if not M.events[event_name] then + return true + end + + return M.events[event_name]:is_empty() +end + + return M diff --git a/game.project b/game.project index 7e842e4..e8b0aaf 100644 --- a/game.project +++ b/game.project @@ -1,5 +1,5 @@ [bootstrap] -main_collection = /example/example.collectionc +main_collection = /test/test.collectionc [script] shared_state = 1 @@ -13,10 +13,11 @@ input_method = HiddenInputField [project] title = Defold Event -version = 1 +version = 3 publisher = Insality developer = Maksim Tuprikov, Insality dependencies#0 = https://github.com/Insality/defold-log/archive/refs/tags/2.zip +dependencies#1 = https://github.com/britzl/deftest/archive/refs/tags/2.8.0.zip [library] include_dirs = event diff --git a/settings_deployer b/settings_deployer new file mode 100644 index 0000000..1ca6a3f --- /dev/null +++ b/settings_deployer @@ -0,0 +1,14 @@ +# Path to bob folder. It will find and save new bob.jar files inside +bob_folder=./ + +# You can point bob version for project in format "filename:sha" +bob_sha="181:fd1ad4c17bfdcd890ea7176f2672c35102384419" + +# Select Defold channel. Values: stable, beta, alpha +bob_channel="stable" + +# If true, it will check and download latest bob version. It will ignore bob_sha param +use_latest_bob=false + +# Select Defold build server +build_server="https://build.defold.com" \ No newline at end of file diff --git a/test/test.collection b/test/test.collection new file mode 100644 index 0000000..fe2bf3f --- /dev/null +++ b/test/test.collection @@ -0,0 +1,39 @@ +name: "default" +scale_along_z: 0 +embedded_instances { + id: "test" + data: "components {\n" + " id: \"test\"\n" + " component: \"/test/test.gui\"\n" + " position {\n" + " x: 0.0\n" + " y: 0.0\n" + " z: 0.0\n" + " }\n" + " rotation {\n" + " x: 0.0\n" + " y: 0.0\n" + " z: 0.0\n" + " w: 1.0\n" + " }\n" + " property_decls {\n" + " }\n" + "}\n" + "" + position { + x: 0.0 + y: 0.0 + z: 0.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale3 { + x: 1.0 + y: 1.0 + z: 1.0 + } +} diff --git a/test/test.gui b/test/test.gui new file mode 100644 index 0000000..d0f87d1 --- /dev/null +++ b/test/test.gui @@ -0,0 +1,10 @@ +script: "/test/test.gui_script" +background_color { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 +} +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT +max_nodes: 512 diff --git a/test/test.gui_script b/test/test.gui_script new file mode 100644 index 0000000..d671d8b --- /dev/null +++ b/test/test.gui_script @@ -0,0 +1,9 @@ +local deftest = require("deftest.deftest") + +function init(self) + deftest.add(require("test.test_event")) + deftest.add(require("test.test_events")) + + local is_report = sys.get_config("test.report") == "1" + deftest.run({ coverage = { enabled = is_report } }) +end diff --git a/test/test.ini b/test/test.ini new file mode 100644 index 0000000..eeec6a9 --- /dev/null +++ b/test/test.ini @@ -0,0 +1,9 @@ +[bootstrap] +main_collection = /test/test.collectionc + +[display] +height = 256 +width = 256 + +[test] +report = 1 \ No newline at end of file diff --git a/test/test_event.lua b/test/test_event.lua new file mode 100644 index 0000000..b6134f1 --- /dev/null +++ b/test/test_event.lua @@ -0,0 +1,128 @@ +local event = require("event.event") + +return function() + describe("Defold Event", function() + it("Instantiate Event", function() + local test_event = event.create() + assert(test_event) + end) + + it("Subscribe and Unsubscribe", function() + local test_event = event.create() + local f = function() end + + test_event:subscribe(f) + assert(#test_event.callbacks == 1) + + test_event:unsubscribe(f) + assert(#test_event.callbacks == 0) + end) + + it("Trigger", function() + local test_event = event.create() + local counter = 0 + local f = function() counter = counter + 1 end + + test_event:subscribe(f) + test_event:trigger() + assert(counter == 1) + test_event:trigger() + assert(counter == 2) + + test_event:unsubscribe(f) + test_event:trigger() + assert(counter == 2) + end) + + it("Trigger with params", function() + local test_event = event.create() + local counter = 0 + local f = function(a, b) counter = counter + a + b end + + test_event:subscribe(f) + test_event:trigger(1, 2) + assert(counter == 3) + end) + + it("One function can be subscribed only once", function() + local test_event = event.create() + local counter = 0 + local f = function() counter = counter + 1 end + + local is_subscribed = test_event:subscribe(f) + assert(is_subscribed == true) + + is_subscribed = test_event:subscribe(f) + assert(is_subscribed == false) + assert(#test_event.callbacks == 1) + end) + + it("Event is_subscribed", function() + local test_event = event.create() + local f = function() end + + assert(test_event:is_subscribed(f) == false) + + test_event:subscribe(f) + assert(test_event:is_subscribed(f) == true) + end) + + it("Subscribe with context", function() + local test_event = event.create() + local last_context + local f = function(context) last_context = context end + + test_event:subscribe(f, "context") + test_event:trigger("foo", "bar") + assert(last_context == "context") + end) + + it("Event is_empty", function() + local test_event = event.create() + local f = function() end + + assert(test_event:is_empty() == true) + + test_event:subscribe(f) + assert(test_event:is_empty() == false) + end) + + it("Event clear", function() + local test_event = event.create() + local f = function() end + + test_event:subscribe(f) + assert(test_event:is_empty() == false) + + test_event:clear() + assert(test_event:is_empty() == true) + end) + + it("Event trigger returns result", function() + local test_event = event.create() + local f = function() return "result" end + + test_event:subscribe(f) + local result = test_event:trigger() + assert(result == "result") + end) + + it("Event trigger returns last subscriber result", function() + local test_event = event.create() + local f1 = function() return "result1" end + local f2 = function() return "result2" end + + test_event:subscribe(f1) + test_event:subscribe(f2) + + local result = test_event:trigger() + assert(result == "result2") + end) + + it("Event trigger returns nil if no subscribers", function() + local test_event = event.create() + local result = test_event:trigger() + assert(result == nil) + end) + end) +end diff --git a/test/test_events.lua b/test/test_events.lua new file mode 100644 index 0000000..2e74ff6 --- /dev/null +++ b/test/test_events.lua @@ -0,0 +1,125 @@ +local events = require("event.events") + +return function() + describe("Defold Events", function() + before(function() + events.clear_all() + end) + + it("Events Subscribe and Unsubscribe", function() + local f = function() end + + events.subscribe("test", f) + assert(events.is_subscribed("test", f) == true) + + events.unsubscribe("test", f) + assert(events.is_subscribed("test", f) == false) + end) + + it("Events Trigger", function() + local counter = 0 + local f = function() counter = counter + 1 end + + events.subscribe("test", f) + events.trigger("test") + assert(counter == 1) + events.trigger("test") + assert(counter == 2) + + events.unsubscribe("test", f) + events.trigger("test") + assert(counter == 2) + end) + + it("Events Trigger with params", function() + local counter = 0 + local f = function(a, b) counter = counter + a + b end + + events.subscribe("test", f) + events.trigger("test", 1, 2) + assert(counter == 3) + end) + + it("One function can be subscribed only once", function() + local counter = 0 + local f = function() counter = counter + 1 end + + local is_subscribed = events.subscribe("test", f) + assert(is_subscribed == true) + + is_subscribed = events.subscribe("test", f) + assert(is_subscribed == false) + assert(events.is_subscribed("test", f) == true) + end) + + it("Events Clear", function() + local counter = 0 + local f = function() counter = counter + 1 end + + events.subscribe("test", f) + events.trigger("test") + assert(counter == 1) + + events.clear("test") + events.trigger("test") + assert(counter == 1) + end) + + it("Events Clear All", function() + local counter = 0 + local f = function() counter = counter + 1 end + + events.subscribe("test", f) + events.trigger("test") + assert(counter == 1) + + events.clear_all() + events.trigger("test") + assert(counter == 1) + end) + + it("Events is_subscribed", function() + local f = function() end + + assert(events.is_subscribed("test", f) == false) + + events.subscribe("test", f) + assert(events.is_subscribed("test", f) == true) + end) + + it("Events is_empty", function() + assert(events.is_empty("test") == true) + + local f = function() end + events.subscribe("test", f) + assert(events.is_empty("test") == false) + end) + + it("Events Subscribe with context", function() + local last_context + local f = function(context) last_context = context end + + events.subscribe("test", f, "context") + events.trigger("test", "foo", "bar") + assert(last_context == "context") + end) + + it("Events Trigger returns result", function() + local f = function() return "result" end + + events.subscribe("test", f) + local result = events.trigger("test") + assert(result == "result") + end) + + it("Events Trigger return last callback result", function() + local f1 = function() return "result1" end + local f2 = function() return "result2" end + + events.subscribe("test", f1) + events.subscribe("test", f2) + local result = events.trigger("test") + assert(result == "result2") + end) + end) +end