diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ef351da..588723a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,7 +20,7 @@ jobs: - uses: leafo/gh-actions-lua@v10 with: - luaVersion: "5.1.5" + luaVersion: "5.4.7" - uses: leafo/gh-actions-luarocks@v4 - @@ -30,5 +30,7 @@ jobs: - run: | ./cli.sh test-self --coverage - - run: | + - + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + run: | bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/DOCS.yml b/.github/workflows/DOCS.yml index a74bac0..807b77a 100644 --- a/.github/workflows/DOCS.yml +++ b/.github/workflows/DOCS.yml @@ -25,6 +25,7 @@ jobs: run: | docker run --rm -v $(pwd):/app -w /app rodrigodornelles/doxygen:lua doxygen - + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} @@ -42,6 +43,7 @@ jobs: run: | docker run --rm -v $(pwd):/app -w /app rodrigodornelles/doxygen:lua ./cli.sh build --core html5 ./examples/pong/game.lua --dist ./html/ - + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/examples/launcher/game.lua b/examples/launcher/game.lua index a19e302..35dfedb 100644 --- a/examples/launcher/game.lua +++ b/examples/launcher/game.lua @@ -122,7 +122,7 @@ local function loop(std, game) next_state(game, 3) elseif game._state == 3 then halt_state(game)(function() - std.csv(game._csv, game._list) + std.csv.decode(game._csv, game._list) game._csv = '' next_state(game, 4) end) @@ -181,18 +181,18 @@ local function draw(std, game) std.draw.color(std.color.white) std.draw.text(8, 8, 'parsing csv...') elseif game._state == 4 then - local s = 0 - std.draw.clear(std.color.darkgray) + std.draw.clear(0x333333FF) std.draw.color(std.color.white) + std.draw.font('Tiresias', 12) local index = 1 while index <= #game._list do std.draw.text(16, 8 + (index * 14), game._list[index].title) - std.draw.text(100, 8 + (index * 14), game._list[index].version) - s=std.math.max(std.draw.text(200, 8 + (index * 12), game._list[index].author), s) + std.draw.text(200, 8 + (index * 14), game._list[index].version) + std.draw.text(300, 8 + (index * 14), game._list[index].author) index = index + 1 end std.draw.color(std.color.red) - std.draw.rect(1, 16, 5 + (game._menu * 16), 200 + s, 14) + std.draw.rect(1, 16, 9 + (game._menu * 14), game.width - 32, 16) elseif game._state == 5 then std.draw.clear(std.color.blue) std.draw.color(std.color.white) diff --git a/src/cli/main.lua b/src/cli/main.lua index 73f8f34..6d99ac2 100644 --- a/src/cli/main.lua +++ b/src/cli/main.lua @@ -32,8 +32,11 @@ local core_list = { 'src/engine/core/ginga/main.ncl' } }, + native={ + src='src/engine/core/native/main.lua', + }, html5_webos={ - src='src/engine/core/html5/main.lua', + src='src/engine/core/native/main.lua', post_exe='webos24 $(pwd)/dist', pipeline={ zeebo_meta.late(game):file(dist..'index.html'):file(dist..'appinfo.json'):pipe() @@ -47,7 +50,7 @@ local core_list = { } }, html5_tizen={ - src='src/engine/core/html5/main.lua', + src='src/engine/core/native/main.lua', pipeline={ zeebo_meta.late(game):file(dist..'index.html'):file(dist..'config.xml'):pipe(), function() os.execute('cd '..dist..';~/tizen-studio/tools/ide/bin/tizen.sh package -t wgt;true') end @@ -62,7 +65,7 @@ local core_list = { } }, html5_ginga={ - src='src/engine/core/html5/main.lua', + src='src/engine/core/native/main.lua', post_exe='ginga dist/main.ncl -s '..screen, pipeline={ zeebo_meta.late(game):file(dist..'index.html'):pipe() @@ -75,7 +78,7 @@ local core_list = { } }, html5={ - src='src/engine/core/html5/main.lua', + src='src/engine/core/native/main.lua', pipeline={ zeebo_meta.late(game):file(dist..'index.html'):pipe() }, @@ -196,6 +199,29 @@ elseif command == 'build' then end os.exit(os.execute(core.post_exe) and 0 or 1) end +elseif command == "help" then + local help_message = "Available commands:\n" .. + "- run: Executes the specified core. If no core is specified, defaults to 'love'.\n" .. + "- clear | clean: Clears the specified distribution directory.\n" .. + "- meta: Displays metadata for the current game.\n" .. + "- bundler: Builds the game using the bundler.\n" .. + "- test-self: Runs tests located in the './tests' directory.\n" .. + "- build: Builds the game and prepares it for distribution.\n" .. + "\n" .. + "Available cores:\n" .. + "- repl: Runs the REPL core.\n" .. + "- love: Runs the Love2D core.\n" .. + "- ginga: Runs the Ginga core.\n" .. + "- html5_webos: Builds for HTML5 on WebOS.\n" .. + "- html5_tizen: Builds for HTML5 on Tizen.\n" .. + "- html5_ginga: Runs the Ginga core for HTML5.\n" .. + "- html5: Runs the standard HTML5 core.\n" .. + "- nintendo_wii: Builds for the Nintendo Wii.\n" .. + "\n" .. + "Usage:\n" .. + "- To run a command, use: ./cli.sh --core [options]\n" .. + "- For example: ./cli.sh build ./examples/asteroids/game.lua --core ginga" + print(help_message) else print('command not found: '..command) os.exit(1) diff --git a/src/engine/core/ginga/main.lua b/src/engine/core/ginga/main.lua index d9621f5..55ebc50 100644 --- a/src/engine/core/ginga/main.lua +++ b/src/engine/core/ginga/main.lua @@ -1,5 +1,5 @@ local zeebo_module = require('src/lib/engine/module') -local engine_csv = require('src/lib/engine/csv') +local engine_encoder = require('src/lib/engine/encoder') local engine_fps = require('src/lib/engine/fps') local engine_math = require('src/lib/engine/math') local engine_game = require('src/lib/engine/game') @@ -10,6 +10,8 @@ local engine_loop = require('src/engine/core/ginga/loop') local engine_draw = require('src/engine/core/ginga/draw') local engine_draw_fps = require('src/lib/engine/draw_fps') local engine_draw_poly = require('src/lib/engine/draw_poly') +local library_csv = require('src/third_party/csv/rodrigodornelles') +local library_json = require('src/third_party/json/rxi') local protocol_http_ginga = require('src/lib/protocol/http_ginga') local application = nil local game = require('src/lib/object/game') @@ -87,7 +89,8 @@ local function install(evt, gamefile) :package('@draw_fps', engine_draw_fps) :package('@draw_poly', engine_draw_poly, polygons) :package('load', zeebo_module.load) - :package('csv', engine_csv) + :package('csv', engine_encoder, library_csv) + :package('json', engine_encoder, library_json) :package('math', engine_math.clib) :package('random', engine_math.clib_random) :package('http', engine_http, protocol_http_ginga) diff --git a/src/engine/core/html5/engine.js b/src/engine/core/html5/engine.js index 2fb61e9..be0e426 100644 --- a/src/engine/core/html5/engine.js +++ b/src/engine/core/html5/engine.js @@ -21,32 +21,36 @@ document.addEventListener('DOMContentLoaded', async () => { }, () => canvas_ctx.stroke() ] - const canvas_std = { - clear: (color) => { - canvas_ctx.fillStyle = '#' + color.toString(16).padStart(8, '0') - canvas_ctx.fillRect(0, 0, canvas_element.width, canvas_element.height) - }, - color: (color) => { - const hex = '#' + color.toString(16).padStart(8, '0') - canvas_ctx.strokeStyle = hex - canvas_ctx.fillStyle = hex - }, - line: (x1, y1, x2, y2) => { - canvas_ctx.beginPath() - canvas_ctx.moveTo(x1, y1) - canvas_ctx.lineTo(x2, y2) - canvas_ctx.stroke() - }, - rect: (mode, x, y, w, h) => mode === 1? - canvas_ctx.strokeRect(x, y, w, h): - canvas_ctx.fillRect(x, y, w, h), - font: (name, size) => {}, - text: (x, y, text) => { - const { width } = canvas_ctx.measureText(text || x) - x && y && canvas_ctx.fillText(text, x, y) - return width - }, - poly: (mode, verts, x, y, scale = 1, angle = 0, ox = 0, oy = 0) => { + + lua.global.set('native_draw_start', () => {}) + lua.global.set('native_draw_flush', () => {}) + lua.global.set('native_draw_clear', (color) => { + canvas_ctx.fillStyle = '#' + color.toString(16).padStart(8, '0') + canvas_ctx.fillRect(0, 0, canvas_element.width, canvas_element.height) + }) + lua.global.set('native_draw_color', (color) => { + const hex = '#' + color.toString(16).padStart(8, '0') + canvas_ctx.strokeStyle = hex + canvas_ctx.fillStyle = hex + }) + lua.global.set('native_draw_line', (x1, y1, x2, y2) => { + canvas_ctx.beginPath() + canvas_ctx.moveTo(x1, y1) + canvas_ctx.lineTo(x2, y2) + canvas_ctx.stroke() + }) + lua.global.set('native_draw_rect', (mode, x, y, w, h) => mode === 1? + canvas_ctx.strokeRect(x, y, w, h): + canvas_ctx.fillRect(x, y, w, h) + ) + lua.global.set('native_draw_font', (name, size) => {}) + lua.global.set('native_draw_text', (x, y, text) => { + const { width } = canvas_ctx.measureText(text || x) + x && y && canvas_ctx.fillText(text, x, y) + return width + }) + lua.global.set('native_dict_poly', { + poly2: (mode, verts, x, y, scale = 1, angle = 0, ox = 0, oy = 0) => { let index = 0 canvas_ctx.beginPath() while (index < verts.length) { @@ -63,8 +67,8 @@ document.addEventListener('DOMContentLoaded', async () => { } canvas_close[mode]() } - } - const browser_protocol_http = { + }) + lua.global.set('native_dict_http', { handler: (self) => { const method = self.method const headers = new Headers(self.headers_dict) @@ -92,7 +96,11 @@ document.addEventListener('DOMContentLoaded', async () => { self.resolve() }) } - } + }) + lua.global.set('native_dict_json', { + encode: JSON.stringify, + decode: JSON.parse + }) if (body_element.clientWidth > body_element.clientHeight) { canvas_element.height = body_element.clientHeight @@ -103,14 +111,21 @@ document.addEventListener('DOMContentLoaded', async () => { canvas_element.width = body_element.clientWidth } - lua.global.set('game_lua', game_lua) - lua.global.set('browser_canvas', canvas_std) - lua.global.set('browser_protocol_http', browser_protocol_http) - const engine_callbacks = await lua.doString(engine_lua) - engine_callbacks.init(canvas_element.width, canvas_element.height) + await lua.doString(engine_lua) + const engine_callbacks = { + init: lua.global.get('native_callback_init'), + update: lua.global.get('native_callback_loop'), + draw: lua.global.get('native_callback_draw'), + keyboard: lua.global.get('native_callback_keyboard'), + } + engine_callbacks.init(canvas_element.width, canvas_element.height, game_lua) setTimeout(() => { const keys = [ + [403, 'red'], + [404, 'green'], + [405, 'yellow'], + [406, 'blue'], ['KeyZ', 'red'], ['KeyX', 'green'], ['KeyC', 'yellow'], @@ -122,10 +137,9 @@ document.addEventListener('DOMContentLoaded', async () => { ['ArrowRight', 'right'], ]; - const keyHandler = (ev) => { - const key = keys.find(key => key[0] == ev.code) - key && engine_callbacks.keyboard(key[1], Number(ev.type === 'keydown')) - } + const keyHandler = (ev) => keys + .filter(key => [ev.code, ev.keyCode].includes(key[0])) + .map(key => engine_callbacks.keyboard(key[1], Number(ev.type === 'keydown'))) window.addEventListener('keydown', keyHandler) window.addEventListener('keyup', keyHandler) @@ -139,5 +153,5 @@ document.addEventListener('DOMContentLoaded', async () => { window.requestAnimationFrame(tick) } - window.requestAnimationFrame(tick) + tick() }) diff --git a/src/engine/core/html5/main.lua b/src/engine/core/html5/main.lua deleted file mode 100644 index babee23..0000000 --- a/src/engine/core/html5/main.lua +++ /dev/null @@ -1,58 +0,0 @@ -local application = nil -local zeebo_module = require('src/lib/engine/module') -local engine_game = require('src/lib/engine/game') -local engine_math = require('src/lib/engine/math') -local engine_color = require('src/lib/object/color') -local engine_math = require('src/lib/engine/math') -local engine_http = require('src/lib/engine/http') -local engine_csv = require('src/lib/engine/csv') -local application_default = require('src/lib/object/application') -local color = require('src/lib/object/color') -local game = require('src/lib/object/game') -local std = require('src/lib/object/std') - -local function browser_update(milis) - game.milis = milis - if application.callbacks.loop then - application.callbacks.loop(std, game) - end - return game.dt -end - -local function browser_draw() - application.callbacks.draw(std, game) -end - -local function browser_keyboard(key, value) - std.key.press[key] = value -end - -local function browser_init(width, height) - application = (load(game_lua))() - zeebo_module.require(std, game, application) - :package('@game', engine_game) - :package('@math', engine_math) - :package('@color', engine_color) - :package('load', zeebo_module.load) - :package('math', engine_math.clib) - :package('random', engine_math.clib_random) - :package('http', engine_http, browser_protocol_http) - :package('csv', engine_csv) - :run() - - std.draw = browser_canvas - game.width = width - game.height = height - game.fps = 60 - game.dt = 16 - application.callbacks.init(std, game) -end - -local P = { - init=browser_init, - update=browser_update, - keyboard=browser_keyboard, - draw=browser_draw -} - -return P diff --git a/src/engine/core/love/main.lua b/src/engine/core/love/main.lua index fa025d6..7b2a62d 100644 --- a/src/engine/core/love/main.lua +++ b/src/engine/core/love/main.lua @@ -8,10 +8,12 @@ local engine_keys = require('src/engine/core/love/keys') local engine_loop = require('src/engine/core/love/loop') local engine_color = require('src/lib/object/color') local engine_http = require('src/lib/engine/http') -local engine_csv = require('src/lib/engine/csv') +local engine_encoder = require('src/lib/engine/encoder') local engine_draw_fps = require('src/lib/engine/draw_fps') local engine_draw_poly = require('src/lib/engine/draw_poly') -local protocol_curl = require('src/lib/protocol/http_curl') +local protocol_curl_love = require('src/lib/protocol/http_curl_love') +local library_csv = require('src/third_party/csv/rodrigodornelles') +local library_json = require('src/third_party/json/rxi') local game = require('src/lib/object/game') local std = require('src/lib/object/std') @@ -46,8 +48,9 @@ function love.load(args) :package('load', zeebo_module.load) :package('math', engine_math.clib) :package('random', engine_math.clib_random) - :package('http', engine_http, protocol_curl) - :package('csv', engine_csv) + :package('http', engine_http, protocol_curl_love) + :package('csv', engine_encoder, library_csv) + :package('json', engine_encoder, library_json) :run() game.width, game.height = w, h diff --git a/src/engine/core/native/main.lua b/src/engine/core/native/main.lua new file mode 100644 index 0000000..aaea223 --- /dev/null +++ b/src/engine/core/native/main.lua @@ -0,0 +1,68 @@ +local zeebo_module = require('src/lib/engine/module') +local engine_game = require('src/lib/engine/game') +local engine_math = require('src/lib/engine/math') +local engine_color = require('src/lib/object/color') +local engine_math = require('src/lib/engine/math') +local engine_http = require('src/lib/engine/http') +local engine_encoder = require('src/lib/engine/encoder') +local engine_draw_fps = require('src/lib/engine/draw_fps') +local engine_draw_poly = require('src/lib/engine/draw_poly') +local library_csv = require('src/third_party/csv/rodrigodornelles') +local application_default = require('src/lib/object/application') +local color = require('src/lib/object/color') +local game = require('src/lib/object/game') +local std = require('src/lib/object/std') +local application = nil + +function native_callback_loop(milis) + game.milis = milis + application.callbacks.loop(std, game) + return game.dt +end + +function native_callback_draw() + native_draw_start() + application.callbacks.draw(std, game) + native_draw_flush() +end + +function native_callback_resize(width, height) + game.width = width + game.height = height +end + +function native_callback_keyboard(key, value) + std.key.press[key] = value +end + +function native_callback_init(width, height, game_lua) + application = zeebo_module.loadgame(game_lua) + + zeebo_module.require(std, game, application) + :package('@game', engine_game) + :package('@math', engine_math) + :package('@color', engine_color) + :package('@draw_fps', engine_draw_fps) + :package('@draw_poly', engine_draw_poly, native_dict_poly) + :package('load', zeebo_module.load) + :package('math', engine_math.clib) + :package('random', engine_math.clib_random) + :package('http', engine_http, native_dict_http) + :package('json', engine_encoder, native_dict_json) + :package('xml', engine_encoder, native_dict_xml) + :package('csv', engine_encoder, library_csv) + :run() + + std.draw.clear=native_draw_clear + std.draw.color=native_draw_color + std.draw.font=native_draw_font + std.draw.text=native_draw_text + std.draw.rect=native_draw_rect + std.draw.line=native_draw_line + + game.width = width + game.height = height + game.fps = 60 + game.dt = 16 + application.callbacks.init(std, game) +end diff --git a/src/lib/engine/draw_poly.lua b/src/lib/engine/draw_poly.lua index e93f190..e51ca7d 100644 --- a/src/lib/engine/draw_poly.lua +++ b/src/lib/engine/draw_poly.lua @@ -66,7 +66,7 @@ local function install(std, game, application, config) local draw_poly = decorator_poo(config.object, config.poly) or decorator_line(draw_line) std = std or {} std.draw = std.draw or {} - std.draw.poly = decorator_poly(draw_poly, std, config.modes, config.repeats) + std.draw.poly = config.poly2 or decorator_poly(draw_poly, std, config.modes, config.repeats) return {poly=std.draw.poly} end diff --git a/src/lib/engine/encoder.lua b/src/lib/engine/encoder.lua new file mode 100644 index 0000000..aad58a1 --- /dev/null +++ b/src/lib/engine/encoder.lua @@ -0,0 +1,14 @@ +local function install(std, game, application, library, name) + std = std or {} + std[name] = { + encode=library.encode, + decode=library.decode + } + return {[name]=std[name]} +end + +local P = { + install=install +} + +return P diff --git a/src/lib/engine/fps.lua b/src/lib/engine/fps.lua index 9943d34..1ee1c78 100644 --- a/src/lib/engine/fps.lua +++ b/src/lib/engine/fps.lua @@ -1,29 +1,29 @@ -local function fps_counter(fps_limit, fps_obj, uptime) - if uptime >= fps_obj.period + 1000 then - fps_obj.period = uptime - fps_obj.total = fps_obj.count - fps_obj.count = 0 +local function fps_counter(fps_limit, fps_tracker, current_time) + if current_time >= fps_tracker.last_check + 1000 then + fps_tracker.last_check = current_time + fps_tracker.total_fps = fps_tracker.frame_count + fps_tracker.frame_count = 0 - if fps_obj.drop_count == 0 then + if fps_tracker.drop_count == 0 then return true end - if fps_obj.total + fps_obj.drop_count > fps_limit then - fps_obj.falls = 0 + if fps_tracker.total_fps + fps_tracker.drop_count > fps_limit then + fps_tracker.fall_streak = 0 return true end - fps_obj.falls = fps_obj.falls + 1 + fps_tracker.fall_streak = fps_tracker.fall_streak + 1 - if fps_obj.drop_time < fps_obj.falls then - fps_obj.falls = 0 + if fps_tracker.fall_streak >= fps_tracker.allowed_falls then + fps_tracker.fall_streak = 0 return false end end - fps_obj.count = fps_obj.count + 1 - fps_obj.delta = uptime - fps_obj.passed - fps_obj.passed = uptime + fps_tracker.frame_count = fps_tracker.frame_count + 1 + fps_tracker.time_delta = current_time - fps_tracker.last_frame_time + fps_tracker.last_frame_time = current_time return true end @@ -35,8 +35,8 @@ local function install(std, game, application, config_fps) application.internal = application.internal or {} config_fps.inverse_list = {} - local fps_obj = {total=0,count=0,period=0,passed=0,delta=0,falls=0,drop=0} - fps_obj.drop_time=application.config and application.config.fps_time or 1 + local fps_obj = {total_fps=0,frame_count=0,last_check=0,last_frame_time=0,time_delta=0,fall_streak=0,drop=0} + fps_obj.allowed_falls=application.config and application.config.fps_time or 1 fps_obj.drop_count=application.config and application.config.fps_drop or 2 while index <= #config_fps.list do @@ -47,8 +47,8 @@ local function install(std, game, application, config_fps) application.internal.fps_controler=function(milis) local index = config_fps.inverse_list[game.fps_max] game.milis = event.uptime() - game.fps = fps_obj.total - game.dt = fps_obj.delta + game.fps = fps_obj.total_fps + game.dt = fps_obj.time_delta if not fps_counter(game.fps_max, fps_obj, game.milis) then if index < #config_fps.list then game.fps_max = config_fps.list[index + 1] diff --git a/src/lib/engine/module.lua b/src/lib/engine/module.lua index 9ba6d89..06f4613 100644 --- a/src/lib/engine/module.lua +++ b/src/lib/engine/module.lua @@ -4,6 +4,10 @@ local zeebo_pipeline = require('src/lib/util/pipeline') --! @brief search by game in filesystem / lua modules --! @li https://love2d.org/wiki/love.filesystem.getSource local function loadgame(game_file) + if type(game_file) == 'table' then + return game_file + end + local cwd = '.' local application = type(game_file) == 'function' and game_file local game_title = game_file and game_file:gsub('%.lua$', '') or 'game' @@ -51,7 +55,7 @@ local function package(self, module_name, module, custom) if not system and not self.lib_required[name] then return end local try_install = function() - module.install(self.std, self.game, self.application, custom) + module.install(self.std, self.game, self.application, custom, module_name) end if not pcall(try_install) then return end diff --git a/src/lib/object/color.lua b/src/lib/object/color.lua index 9faf8d3..d764626 100644 --- a/src/lib/object/color.lua +++ b/src/lib/object/color.lua @@ -4,9 +4,9 @@ --! @{ local white = 0xFFFFFFFF -local lightgray = 0xC8CCCCCC -local gray = 0x828282CC -local darkgray = 0x505050CC +local lightgray = 0xC8CCCCFF +local gray = 0x828282FF +local darkgray = 0x505050FF local yellow = 0xFDF900FF local gold = 0xFFCB00FF local orange = 0xFFA100FF diff --git a/src/lib/protocol/http_curl.lua b/src/lib/protocol/http_curl.lua index 44fef0b..d49abd1 100644 --- a/src/lib/protocol/http_curl.lua +++ b/src/lib/protocol/http_curl.lua @@ -1,26 +1,13 @@ local http_util = require('src/lib/util/http') local function http_handler(self) - local index = 1 - local cmd = 'curl -L \x2D\x2Dsilent \x2D\x2Dinsecure -w "\n%{http_code}" ' - local protocol = self.method == 'HEAD' and '\x2D\x2DHEAD' or '-X ' .. self.method local params = http_util.url_search_param(self.param_list, self.param_dict) - - local headers, index = ' ', 1 + local command, cleanup = http_util.create_request(self.method, self.url..params) + .add_custom_headers(self.header_list, self.header_dict) + .add_body_content(self.body_content) + .to_curl_cmd() - while self.header_list and index <= #self.header_list do - local header = self.header_list[index] - headers = headers..'-H "'..header..': ' - headers = headers..self.header_dict[self.header_list[index]]..'" ' - index = index + 1 - end - - local body = '' - if self.method == 'POST' and self.body_content then - body = '-d \''..self.body_content..'\' ' - end - - local handle = io and io.popen and io.popen(cmd..protocol..headers..body..self.url..params) + local handle = io and io.popen and io.popen(command) if handle then local stdout = handle:read("*a") @@ -32,13 +19,15 @@ local function http_handler(self) self.std.http.error = stderr or stdout or 'unknown error!' else self.std.http.ok = 200 <= status and status < 300 - self.std.http.body = stdout:sub(1, index - 1) + self.std.http.body = stdout:sub(1, index - 2) self.std.http.status = status end else self.std.http.ok = false self.std.http.error = 'failed to spawn process!' end + + cleanup() end local P = { diff --git a/src/lib/protocol/http_curl_love.lua b/src/lib/protocol/http_curl_love.lua new file mode 100644 index 0000000..def5235 --- /dev/null +++ b/src/lib/protocol/http_curl_love.lua @@ -0,0 +1,87 @@ +local http_util = require('src/lib/util/http') + +local function http_handler(self) + local params = http_util.url_search_param(self.param_list, self.param_dict) + local command, cleanup = http_util.create_request(self.method, self.url..params) + .add_custom_headers(self.header_list, self.header_dict) + .add_body_content(self.body_content) + .to_curl_cmd() + + local threadCode = 'local command, channel = ...\n' + ..'local handle = io and io.popen and io.popen(command)\n' + ..'if handle then\n' + ..' local stdout = handle:read(\'*a\')\n' + ..' local ok, stderr = handle:close()\n' + ..' love.thread.getChannel(channel..\'ok\'):push(ok)\n' + ..' love.thread.getChannel(channel..\'stdout\'):push(stdout)\n' + ..' love.thread.getChannel(channel..\'stderr\'):push(stderr)\n' + ..'else\n' + ..' love.thread.getChannel(channel..\'ok\'):push(false)\n' + ..' love.thread.getChannel(channel..\'stderr\'):push(\'command failed\')\n' + ..'end' + + self.promise() + self.application.internal.http.queue[#self.application.internal.http.queue + 1] = self + thread = love.thread.newThread(threadCode) + thread:start(command, tostring(self)) + + cleanup() +end + +local function http_callback(self) + local channel = tostring(self) + local ok = love.thread.getChannel(channel..'ok'):pop() + if ok ~= nil then + local stdout = love.thread.getChannel(channel..'stdout'):pop() or '' + local stderr = love.thread.getChannel(channel..'stderr'):pop() or '' + local index = stdout:find("[^\n]*$") or 1 + local status = tonumber(stdout:sub(index)) + if not ok then + self.std.http.ok = false + self.std.http.error = stderr or stdout or 'unknown error!' + else + self.std.http.ok = http_util.is_ok(status) + self.std.http.body = stdout:sub(1, index - 2) + self.std.http.status = status + end + self.resolve() + return true + end +end + +local function install(std, game, application) + application.callbacks.loop = application.callbacks.loop or function () end + application.internal = application.internal or {} + application.internal.http = {queue = {}} + + local update = function() + local index = 1 + while index <= #application.internal.http.queue do + if http_callback(application.internal.http.queue[index]) then + table.remove(application.internal.http.queue, index) + end + index = index + 1 + end + end + + if love then + if love.update then + local old_update = love.update + love.update = function(dt) + old_update(dt) + update() + end + else + love.update = update + end + end + + return {update=update} +end + +local P = { + handler=http_handler, + install=install +} + +return P diff --git a/src/lib/protocol/http_ginga.lua b/src/lib/protocol/http_ginga.lua index 90383d3..78de3cb 100644 --- a/src/lib/protocol/http_ginga.lua +++ b/src/lib/protocol/http_ginga.lua @@ -71,19 +71,23 @@ --! } --! @endjson local http_util = require('src/lib/util/http') +local lua_util = require('src/lib/util/lua') --! @cond local function http_connect(self) local params = http_util.url_search_param(self.param_list, self.param_dict) - local headers = http_util.headers(self.header_list, self.header_dict, { - 'Host', self.p_host..params, false, - 'Accept', '*/*', true, - 'Cache-Control', 'max-age=0', false, - 'User-Agent', 'Mozilla/4.0 (compatible; MSIE 4.0; Windows 95; Win 9x 4.90)', true, - 'Content-Length', tostring(#self.body_content), false, - 'Connection', 'close', false - }) - local request = 'GET '..self.p_uri..' HTTP/1.1\r\n'..headers..'\r\n'..self.body_content..'\r\n\r\n' + local request, cleanup = http_util.create_request(self.method, self.p_uri..params) + .add_imutable_header('Host', self.p_host) + .add_imutable_header('Cache-Control', 'max-age=0') + .add_mutable_header('Accept', '*/*') + .add_mutable_header('Accept-Charset', 'utf-8', lua_util.has_support_utf8()) + .add_mutable_header('Accept-Charset', 'windows-1252, cp1252') + .add_mutable_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 4.0; Windows 95; Win 9x 4.90)') + .add_custom_headers(self.header_list, self.header_dict) + .add_imutable_header('Content-Length', tostring(#self.body_content), #self.body_content > 0) + .add_imutable_header('Connection', 'close') + .add_body_content(self.body_content) + .to_http_protocol() event.post({ class = 'tcp', @@ -91,6 +95,8 @@ local function http_connect(self) connection = self.evt.connection, value = request, }) + + cleanup() end --! @endcond @@ -114,7 +120,7 @@ end local function http_headers(self) self.p_header = self.p_data:sub(1, self.p_header_pos -1) self.p_status = tonumber(self.p_header:match('^HTTP/%d.%d (%d+) %w*')) - self.p_content_size = tonumber(self.p_header:match('Content%-Length: (%d+)')) + self.p_content_size = tonumber(self.p_header:match('Content%-Length: (%d+)') or 0) end --! @endcond @@ -246,8 +252,8 @@ end local function http_handler(self) local protocol, location = self.url:match('(%w*)://?(.*)') local url, uri = (location or self.url):match('^([^/]+)(.*)$') - local host, port_str = url:match("^(.-):?(%d*)$") - local port = tonumber(port_str and #port_str > 0 and port_str or 80) + local host, port_str = url:match("^([%w%.]+)([:0-9]*)$") + local port = tonumber((port_str and #port_str > 0 and port_str:sub(2, #port_str)) or 80) self.p_url = url self.p_uri = uri or '/' diff --git a/src/lib/util/http.lua b/src/lib/util/http.lua index 1d77614..25090f5 100644 --- a/src/lib/util/http.lua +++ b/src/lib/util/http.lua @@ -10,52 +10,125 @@ local function url_search_param(param_list, param_dict) local index, params = 1, '' while param_list and param_dict and index <= #param_list do local param = param_list[index] + local value = param_dict[param] if #params == 0 then - params = param..'?' + params = params..'?' else - params = param..'&' + params = params..'&' end - params = params..param..'='..param_dict[param_list[index]] + params = params..param..'='..(value or '') index = index + 1 end return params end ---! @todo document this function -local function headers(header_list, header_dict, config) - local headers = '' +local function create_request(method, uri) + local self = { + body_content = '', + header_list = {}, + header_dict = {}, + header_imutable = {} + } - if not header_list or not header_dict then - header_list = {} - header_dict = {} + self.add_body_content = function (body) + self.body_content = self.body_content..(body or '') + return self end - local index = 1 - while index <= #config do - local header = config[index] - local default = config[index + 1] - local mutable = config[index + 2] - local value = default - if mutable then - value = header_dict[header] or default - end - headers = headers..header..': '..value..'\r\n' - index = index + 3 + self.add_imutable_header = function (header, value, cond) + if cond == false then return self end + if self.header_imutable[header] == nil then + self.header_list[#self.header_list + 1] = header + self.header_dict[header] = value + elseif self.header_imutable[header] == false then + self.header_dict[header] = value + end + self.header_imutable[header] = true + return self end - local index = 1 - while index <= #header_list do - local header = header_list[index] - headers = headers..header..': '..header_dict[header]..'\r\n' - index = index + 1 + self.add_mutable_header = function (header, value, cond) + if cond == false then return self end + if self.header_imutable[header] == nil then + self.header_list[#self.header_list + 1] = header + self.header_imutable[header] = false + self.header_dict[header] = value + end + return self + end + + self.add_custom_headers = function(header_list, header_dict) + local index = 1 + while header_list and #header_list >= index do + local header = header_list[index] + local value = header_dict[header] + + if self.header_imutable[header] == nil then + self.header_list[#self.header_list + 1] = header + self.header_imutable[header] = false + self.header_dict[header] = value + elseif self.header_imutable[header] == false then + self.header_dict[header] = value + end + + index = index + 1 + end + return self + end + + self.to_http_protocol = function () + local index = 1 + local request = method..' '..uri..' HTTP/1.1\r\n' + + while index <= #self.header_list do + local header = self.header_list[index] + local value = self.header_dict[header] + request = request..header..': '..value..'\r\n' + index = index + 1 + end + + request = request..'\r\n' + + if method ~= 'GET' and method ~= 'HEAD' and #self.body_content > 0 then + request = request..self.body_content..'\r\n\r\n' + end + self = nil + return request, function() end + end + + self.to_curl_cmd = function () + local index = 1 + local request = 'curl -L \x2D\x2Dsilent \x2D\x2Dinsecure -w "\n%{http_code}" ' + + if method == 'HEAD' then + request = request..'\x2D\x2DHEAD ' + else + request = request..'-X '..method..' ' + end + + while index <= #self.header_list do + local header = self.header_list[index] + local value = self.header_dict[header] + request = request..'-H "'..header..': '..value..'" ' + index = index + 1 + end + + if method ~= 'GET' and method ~= 'HEAD' and #self.body_content > 0 then + request = request..'-d \''..self.body_content..'\' ' + end + + request = request..uri + + self = nil + return request, function() end end - return headers + return self end return { is_ok=is_ok, is_redirect=is_redirect, url_search_param=url_search_param, - headers=headers + create_request=create_request } diff --git a/src/lib/util/lua.lua b/src/lib/util/lua.lua new file mode 100644 index 0000000..3d7d830 --- /dev/null +++ b/src/lib/util/lua.lua @@ -0,0 +1,17 @@ +local function has_support_utf8() + if jit then + return true + end + + if tonumber(_VERSION:match('Lua 5.(%d+)')) >= 3 then + return true + end + + return false +end + +local P = { + has_support_utf8=has_support_utf8 +} + +return P diff --git a/src/lib/engine/csv.lua b/src/third_party/csv/rodrigodornelles.lua similarity index 82% rename from src/lib/engine/csv.lua rename to src/third_party/csv/rodrigodornelles.lua index b5c8cb4..61de413 100644 --- a/src/lib/engine/csv.lua +++ b/src/third_party/csv/rodrigodornelles.lua @@ -1,8 +1,4 @@ ---! @defgroup std ---! @{ - ---! @pre require: @c csv -local function csv(in_str, out_table) +local function decode(in_str, out_table) local index1 = 1 local headers = {} local pattern = '[^,]+' @@ -38,16 +34,9 @@ local function csv(in_str, out_table) until not line end ---! @} - -local function install(std) - std = std or {} - std.csv = csv - return {csv=csv} -end - local P = { - install=install + decode=decode, + encode=function() error('not implemented!') end } return P diff --git a/src/third_party/json/rxi.lua b/src/third_party/json/rxi.lua new file mode 100644 index 0000000..711ef78 --- /dev/null +++ b/src/third_party/json/rxi.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- 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. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/tests/bug_lib_protocol_http_ginga.lua b/tests/bug_lib_protocol_http_ginga.lua new file mode 100644 index 0000000..8432016 --- /dev/null +++ b/tests/bug_lib_protocol_http_ginga.lua @@ -0,0 +1,43 @@ +local luaunit = require('luaunit') +local protocol_http = require('src/lib/protocol/http_ginga') + +function test_bug_53_incorrect_url_ipv4() + local application = {} + local http_handler = protocol_http.install({}, {}, application) + local self = { + url='http://192.168.0.1', + promise = function () end, + application = application + } + + http_handler(self) + luaunit.assertEquals(self.p_host, '192.168.0.1') + luaunit.assertEquals(self.p_port, 80) +end + +function test_bug_53_incorrect_url_ipv4_with_port() + local application = {} + local http_handler = protocol_http.install({}, {}, application) + local self = { + url='http://192.168.0.2:8808', + promise = function () end, + application = application + } + + http_handler(self) + luaunit.assertEquals(self.p_host, '192.168.0.2') + luaunit.assertEquals(self.p_port, 8808) +end + +function test_bug_59_empty_content_response() + local application = {} + local http_handler = protocol_http.install({}, {}, application) + local self = { + p_header_pos = 35, + p_data = 'HTTP/1.0 204 OK\r\nConection: Close\r\n' + } + application.internal.http.callbacks.http_headers(self) + luaunit.assertEquals(self.p_content_size, 0) +end + +os.exit(luaunit.LuaUnit.run()) diff --git a/tests/test_lib_engine_csv.lua b/tests/test_lib_engine_csv.lua index ee76793..f0508b3 100644 --- a/tests/test_lib_engine_csv.lua +++ b/tests/test_lib_engine_csv.lua @@ -1,7 +1,8 @@ local luaunit = require('luaunit') -local engine_csv = require('src/lib/engine/csv') +local encoder = require('src/lib/engine/encoder') +local csv = require('src/third_party/csv/rodrigodornelles') -zeebo_csv = engine_csv.install() +local std = encoder.install(nil, nil, nil, csv, 'csv') function test_simple_csv() local result = {} @@ -19,7 +20,7 @@ function test_simple_csv() } } - zeebo_csv.csv(content, result) + std.csv.decode(content, result) luaunit.assertEquals(result, expected) end diff --git a/tests/test_lib_protocol_http_curl.lua b/tests/test_lib_protocol_http_curl.lua index 6557025..677baf9 100644 --- a/tests/test_lib_protocol_http_curl.lua +++ b/tests/test_lib_protocol_http_curl.lua @@ -11,7 +11,7 @@ local mock_popen = mock_io.open({ read=function () return 'method not allowed!\n403' end, close=function () return true, nil end }, - ['curl -L --silent --insecure -w "\n%{http_code}" -X POST pudim.com.brz&z=zoom'] = { + ['curl -L --silent --insecure -w "\n%{http_code}" -X POST pudim.com.br?foo=zig&bar=zag&z=zoom'] = { read=function () return 'me too!\n201' end, close=function () return true, nil end }, @@ -38,7 +38,7 @@ function test_http_get_200() luaunit.assertEquals(std.http.ok, true) luaunit.assertEquals(std.http.error, nil) luaunit.assertEquals(std.http.status, 200) - luaunit.assertEquals(std.http.body, 'i love pudim!\n') + luaunit.assertEquals(std.http.body, 'i love pudim!') end function test_http_post_201() @@ -60,7 +60,7 @@ function test_http_post_201() luaunit.assertEquals(std.http.ok, true) luaunit.assertEquals(std.http.error, nil) luaunit.assertEquals(std.http.status, 201) - luaunit.assertEquals(std.http.body, 'me too!\n') + luaunit.assertEquals(std.http.body, 'me too!') end function test_http_post_403() @@ -78,7 +78,7 @@ function test_http_post_403() luaunit.assertEquals(std.http.ok, false) luaunit.assertEquals(std.http.error, nil) luaunit.assertEquals(std.http.status, 403) - luaunit.assertEquals(std.http.body, 'method not allowed!\n') + luaunit.assertEquals(std.http.body, 'method not allowed!') end function test_http_head_error() diff --git a/tests/test_lib_util_http.lua b/tests/test_lib_util_http.lua new file mode 100644 index 0000000..e1832ee --- /dev/null +++ b/tests/test_lib_util_http.lua @@ -0,0 +1,74 @@ +local luaunit = require('luaunit') +local zeebo_util_http = require('src/lib/util/http') + +function test_no_params() + local query = zeebo_util_http.url_search_param({}, {}) + luaunit.assertEquals(query, '') +end + +function test_one_param() + local query = zeebo_util_http.url_search_param({'foo'}, {foo='bar'}) + luaunit.assertEquals(query, '?foo=bar') +end + +function test_three_params() + local query = zeebo_util_http.url_search_param({'foo', 'z'}, {foo='bar', z='zoom'}) + luaunit.assertEquals(query, '?foo=bar&z=zoom') +end + +function test_four_params_with_null() + local query = zeebo_util_http.url_search_param({'foo', 'z', 'zig'}, {foo='bar', z='zoom'}) + luaunit.assertEquals(query, '?foo=bar&z=zoom&zig=') +end + +function test_create_request_overrides() + local request = zeebo_util_http.create_request('GET', '/') + .add_imutable_header('h1', '1') + .add_mutable_header('h1', '2') + .add_mutable_header('h2', '3') + .add_imutable_header('h2', '4') + .add_mutable_header('h2', '5') + .add_imutable_header('h3', '6') + .add_mutable_header('h4', '7') + .add_custom_headers({'h3', 'h4'}, {h3='8', h4='9'}) + .add_custom_headers({'h5', 'h6'}, {h5='10', h6='11'}) + .add_imutable_header('h3', '13') + .add_mutable_header('h5', '14') + .add_imutable_header('h6', '15') + .add_mutable_header('h7', '16') + .to_http_protocol() + + luaunit.assertEquals(request, 'GET / HTTP/1.1\r\nh1: 1\r\nh2: 4\r\nh3: 6\r\nh4: 9\r\nh5: 10\r\nh6: 15\r\nh7: 16\r\n\r\n') +end + +function test_create_request_conditions() + local request = zeebo_util_http.create_request('HEAD', '/') + .add_imutable_header('h1', '1', nil) + .add_imutable_header('h2', '2', false) + .add_imutable_header('h3', '3', true) + .add_mutable_header('h4', '4', nil) + .add_mutable_header('h5', '5', false) + .add_mutable_header('h6', '6', true) + .to_http_protocol() + + luaunit.assertEquals(request, 'HEAD / HTTP/1.1\r\nh1: 1\r\nh3: 3\r\nh4: 4\r\nh6: 6\r\n\r\n') +end + +function test_create_request_add_body_post() + local request = zeebo_util_http.create_request('POST', '/') + .add_imutable_header('Content-Length', 9) + .add_body_content('foo bar z') + .to_http_protocol() + + luaunit.assertEquals(request, 'POST / HTTP/1.1\r\nContent-Length: 9\r\n\r\nfoo bar z\r\n\r\n') +end + +function test_create_request_no_body_in_get() + local request = zeebo_util_http.create_request('GET', '/') + .add_body_content('foo bar z') + .to_http_protocol() + + luaunit.assertEquals(request, 'GET / HTTP/1.1\r\n\r\n') +end + +os.exit(luaunit.LuaUnit.run()) diff --git a/tests/test_lib_util_lua.lua b/tests/test_lib_util_lua.lua new file mode 100644 index 0000000..0b8724d --- /dev/null +++ b/tests/test_lib_util_lua.lua @@ -0,0 +1,22 @@ +local luaunit = require('luaunit') +local lua_util = require('src/lib/util/lua') + +function test_no_support_utf8() + jit = nil + _VERSION = 'Lua 5.1' + luaunit.assertEquals(lua_util.has_support_utf8(), false) +end + +function test_lua_jit_support_utf8() + jit = true + _VERSION = 'Lua 5.1' + luaunit.assertEquals(lua_util.has_support_utf8(), true) +end + +function test_lua_5_3_support_utf8() + jit = nil + _VERSION = 'Lua 5.3' + luaunit.assertEquals(lua_util.has_support_utf8(), true) +end + +os.exit(luaunit.LuaUnit.run())