reorganize app.lua and its comments
[?]
Aug 26, 2023, 10:04 PM
CUFW4EJL75OAA5BS5EXGTM5RMRNJOBBPAXUJADGZ3VLP2ZMKFOTACDependencies
- [2]
CA5T33FGone more implication - [3]
ZD3ZKA5Eupdate some App framework docs - [4]
S4IAYCIRdelete all tests once they've executed - [5]
NR43TWCNmake App.open_for_* look more like io.open - [6]
JMUD7T3Oget rid of ugly side-effects in tests - [7]
D2IADHMWlink to default love.run for comparison - [8]
DLBD4ZA6wait, fix tests - [9]
6RYLD5ONchange how we handle clicks above top margin - [10]
7IDHIAYIrename modifier_down to key_down - [11]
4EGQRXDAbugfix: naming points - [12]
4KC7I3E2make colors easier to edit - [13]
3QNOKBFMbeginnings of a test harness - [14]
R53OF3ONone bug I've repeatedly run into while testing with Moby Dick - [15]
JFFUF5ALoverride mouse state lookups in tests - [16]
3QWK3GSAsupport mouse clicks in file navigator - [17]
PX7DDEMOautosave slightly less aggressively - [18]
2CK5QI7Wmake love event names consistent - [19]
DIRTBUP4remove a condition - [20]
U7M4M2F7bugfix: don't rely on Screen_bottom1 while scrolling - [21]
BC4SO6ARclean up a print - [22]
7CLGG7J2test: autosave after any shape - [23]
IMEJA43Lsnapshot - [24]
FCFJVYKYredundant check - [25]
ORRSP7FVdeduce test names on failures - [26]
H4R5BHVYno more Text allocations - [27]
MDXGMZU2disable all debug prints - [28]
ZLJGZYQGselect text with shift + mouseclick - [29]
5UKUADTWdistinguish consistently between mouse buttons and other buttons - [30]
AD34IX2Zcouple more tests - [31]
AVTNUQYRbasic test-enabled framework - [32]
KKMFQDR4editing source code from within the app - [33]
3ZTODUBQformatting - [34]
YTSPVDZHfirst successful pagedown test, first bug found by test - [*]
JF5L2BBStest harness now supports copy/paste
Change contents
- replacement in app.lua at line 4
-- but we need to override it to install a test harness.---- A test harness needs to check what the 'real' code did.-- To do this it needs to hook into primitive operations performed by code.-- Our hooks all go through the `App` global. When running tests they operate-- on fake screen, keyboard and so on. Once all tests pass, the App global-- will hook into the real screen, keyboard and so on.---- Scroll below this function for more details.-- but we need to override it to:-- * run all tests (functions starting with 'test_') on startup, and-- * save some state that makes it possible to switch between the main app-- and a source editor, while giving each the illusion of complete-- control. - replacement in app.lua at line 43[11.2551]→[11.2551:4318](∅→∅),[11.4318]→[3.13:69](∅→∅),[3.69]→[11.4318:4389](∅→∅),[11.4318]→[11.4318:4389](∅→∅),[11.4389]→[3.70:92](∅→∅),[3.92]→[11.4406:4434](∅→∅),[11.4406]→[11.4406:4434](∅→∅),[11.4434]→[3.93:171](∅→∅),[3.171]→[11.1133:1175](∅→∅),[11.4434]→[11.1133:1175](∅→∅),[11.1175]→[3.172:241](∅→∅),[3.241]→[11.4552:5643](∅→∅),[11.4552]→[11.4552:5643](∅→∅),[11.5643]→[2.13:115](∅→∅)
-- I've been building LÖVE apps for a couple of months now, and often feel-- stupid. I seem to have a smaller short-term memory than most people, and-- LÖVE apps quickly grow to a point where I need longer and longer chunks of-- focused time to make changes to them. The reason: I don't have a way to-- write tests yet. So before I can change any piece of an app, I have to-- bring into my head all the ways it can break. This isn't the case on other-- platforms, where I can be productive in 5- or 10-minute increments. Because-- I have tests.---- Most test harnesses punt on testing I/O, and conventional wisdom is to test-- business logic, not I/O. However, any non-trivial app does non-trivial I/O-- that benefits from tests. And tests aren't very useful if it isn't obvious-- after reading them what the intent is. Including the I/O allows us to write-- tests that mimic how people use our program.---- There's a major open research problem in testing I/O: how to write tests-- for graphics. Pixel-by-pixel assertions get too verbose, and they're often-- brittle because you don't care about the precise state of every last pixel.-- Except when you do. Pixels are usually -- but not always -- the trees-- rather than the forest.---- I'm not in the business of doing research, so I'm going to shave off a-- small subset of the problem for myself here: how to write tests about text-- (ignoring font, color, etc.) on a graphic screen.---- For example, here's how you may write a test of a simple text paginator-- like `less`:-- function test_paginator()-- -- initialize environment-- App.filesystem['/tmp/foo'] = filename([[-- >abc-- >def-- >ghi-- >jkl-- ]])-- App.args = {'/tmp/foo'}-- -- define a screen with room for 2 lines of text-- App.screen.init{-- width=100-- height=30-- }-- App.font.init{-- height=15-- }-- -- check that screen shows next 2 lines of text after hitting pagedown-- App.run_after_keychord('pagedown')-- App.screen.check(0, 'ghi')-- App.screen.check(15, 'jkl')-- end---- All functions starting with 'test_' (no modules) will run before the app-- runs "for real". Each such test is a fake run of our entire program. It can-- set as much of the environment as it wants, then run the app. Here we've-- got a 30px screen and a 15px font, so the screen has room for 2 lines. The-- file we're viewing has 4 lines. We assert that hitting the 'pagedown' key-- shows the third and fourth lines.---- Programs can still perform graphics, and all graphics will work in the real-- program. We can't yet write tests for graphics, though. Those pixels are-- basically always blank in tests. Really, there isn't even any-- representation for them. All our fake screens know about is lines of text,-- and what (x,y) coordinates they start at. There's some rudimentary support-- for concatenating all blobs of text that start at the same 'y' coordinate,-- but beware: text at y=100 is separate and non-overlapping with text at-- y=101. You have to use the test harness within these limitations for your-- tests to faithfully model the real world.---- One drawback of this approach: the y coordinate used depends on font size,-- which feels brittle.-- The rest of this file wraps around various LÖVE primitives to support-- automated tests. Often tests will run with a fake version of a primitive-- that redirects to the real love.* version once we're done with tests. - replacement in app.lua at line 47
-- In the fullness of time App will support all side-effecting primitives-- exposed by LÖVE, but so far it supports just a rudimentary set of things I-- happen to have needed so far.-- Not everything is so wrapped yet. Sometimes you still have to use love.*-- primitives directly. - replacement in app.lua at line 50
App = {screen={}}App = {} - edit in app.lua at line 76
function App.run_tests()local sorted_names = {}for name,binding in pairs(_G) doif name:find('test_') == 1 thentable.insert(sorted_names, name)endendtable.sort(sorted_names)for _,name in ipairs(sorted_names) doApp.initialize_for_test()--? print('=== '..name)--? _G[name]()xpcall(_G[name], function(err) prepend_debug_info_to_test_failure(name, err) end)end-- clean up all test methodsfor _,name in ipairs(sorted_names) do_G[name] = nilendend - edit in app.lua at line 105
-- App.screen.resize and App.screen.move seem like better names than-- love.window.setMode and love.window.setPosition respectively. They'll-- be side-effect-free during tests, and they'll save their results in-- attributes of App.screen for easy access.App.screen={}-- Use App.screen.init in tests to initialize the fake screen. - edit in app.lua at line 118
-- operations on the LÖVE window within the monitor/display - edit in app.lua at line 138
-- If you use App.screen.print instead of love.graphics.print,-- tests will be able to check what was printed using App.screen.check below.---- One drawback of this approach: the y coordinate used depends on font size,-- which feels brittle. - replacement in app.lua at line 159
function App.color(color)love.graphics.setColor(color.r, color.g, color.b, color.a)function App.screen.check(y, expected_contents, msg)--? print('checking for "'..expected_contents..'" at y '..tostring(y))local screen_row = 'y'..tostring(y)local contents = ''if App.screen.contents[screen_row] == nil thenerror('no text at y '..tostring(y))endfor i,s in ipairs(App.screen.contents[screen_row]) docontents = contents..sendcheck_eq(contents, expected_contents, msg) - replacement in app.lua at line 172[11.387]→[11.387:480](∅→∅),[11.480]→[11.6421:6425](∅→∅),[11.1374]→[11.6421:6425](∅→∅),[11.6421]→[11.6421:6425](∅→∅)
function colortable(app_color)return {app_color.r, app_color.g, app_color.b, app_color.a}end-- If you access the time using App.getTime instead of love.timer.getTime,-- tests will be able to move the time back and forwards as needed using-- App.wait_fake_time below. - edit in app.lua at line 188[36.2820][36.2820]
-- If you access the clipboard using App.getClipboardText and-- App.setClipboardText instead of love.system.getClipboardText and-- love.system.setClipboardText respectively, tests will be able to manipulate-- the clipboard by reading/writing App.clipboard. - edit in app.lua at line 200
-- In tests I mostly send chords all at once to the keyboard handlers.-- However, you'll occasionally need to check if a key is down outside a handler.-- If you use App.key_down instead of love.keyboard.isDown, tests will be able to-- simulate keypresses using App.fake_key_press and App.fake_key_release-- below. This isn't very realistic, though, and it's up to tests to-- orchestrate key presses that correspond to the handlers they invoke. - edit in app.lua at line 209
function App.key_down(key)return App.fake_keys_pressed[key]end - edit in app.lua at line 218[11.4494]→[11.2058:2062](∅→∅),[11.7802]→[11.2058:2062](∅→∅),[11.2058]→[11.2058:2062](∅→∅),[11.2062]→[10.341:368](∅→∅),[10.368]→[11.7803:7839](∅→∅),[11.4527]→[11.7803:7839](∅→∅)
endfunction App.key_down(key)return App.fake_keys_pressed[key] - edit in app.lua at line 219
-- Tests mostly will invoke mouse handlers directly. However, you'll-- occasionally need to check if a mouse button is down outside a handler.-- If you use App.mouse_down instead of love.mouse.isDown, tests will be able to-- simulate mouse clicks using App.fake_mouse_press and App.fake_mouse_release-- below. This isn't very realistic, though, and it's up to tests to-- orchestrate presses that correspond to the handlers they invoke. - replacement in app.lua at line 228[11.4618]→[11.704:753](∅→∅),[11.753]→[11.4661:4719](∅→∅),[11.4661]→[11.4661:4719](∅→∅),[11.4719]→[11.754:798](∅→∅),[11.798]→[11.4757:4761](∅→∅),[11.4757]→[11.4757:4761](∅→∅),[11.4761]→[11.799:850](∅→∅),[11.850]→[11.4806:4864](∅→∅),[11.4806]→[11.4806:4864](∅→∅),[11.4864]→[11.851:894](∅→∅),[11.894]→[11.4901:4905](∅→∅),[11.4901]→[11.4901:4905](∅→∅)
function App.fake_mouse_press(x,y, mouse_button)App.fake_mouse_state.x = xApp.fake_mouse_state.y = yApp.fake_mouse_state[mouse_button] = trueendfunction App.fake_mouse_release(x,y, mouse_button)App.fake_mouse_state.x = xApp.fake_mouse_state.y = yApp.fake_mouse_state[mouse_button] = nilend - edit in app.lua at line 241[11.5184][36.2948]
endfunction App.fake_mouse_press(x,y, mouse_button)App.fake_mouse_state.x = xApp.fake_mouse_state.y = yApp.fake_mouse_state[mouse_button] = trueendfunction App.fake_mouse_release(x,y, mouse_button)App.fake_mouse_state.x = xApp.fake_mouse_state.y = yApp.fake_mouse_state[mouse_button] = nilend-- If you use App.open_for_reading and App.open_for_writing instead of other-- various Lua and LÖVE helpers, tests will be able to check the results of-- file operations inside the App.filesystem table.function App.open_for_writing(filename)App.filesystem[filename] = ''if Current_app == nil or Current_app == 'run' thenreturn {write = function(self, ...)local args = {...}for i,s in ipairs(args) doApp.filesystem[filename] = App.filesystem[filename]..sendend,close = function(self)end,}elseif Current_app == 'source' thenreturn {write = function(self, s)App.filesystem[filename] = App.filesystem[filename]..send,close = function(self)end,}endendfunction App.open_for_reading(filename)if App.filesystem[filename] thenreturn {lines = function(self)return App.filesystem[filename]:gmatch('[^\n]+')end,close = function(self)end,}end - edit in app.lua at line 293[36.2952][11.6983]
-- Some helpers to trigger an event and then refresh the screen. Akin to one-- iteration of the event loop. - replacement in app.lua at line 339[11.7144]→[11.7144:7197](∅→∅),[11.7197]→[11.2934:3007](∅→∅),[11.3007]→[11.7197:7257](∅→∅),[11.7197]→[11.7197:7257](∅→∅),[11.7257]→[11.3048:3143](∅→∅),[11.3143]→[11.7257:7391](∅→∅),[11.7257]→[11.7257:7391](∅→∅),[11.7391]→[11.2391:2395](∅→∅)
function App.screen.check(y, expected_contents, msg)--? print('checking for "'..expected_contents..'" at y '..tostring(y))local screen_row = 'y'..tostring(y)local contents = ''if App.screen.contents[screen_row] == nil thenerror('no text at y '..tostring(y))endfor i,s in ipairs(App.screen.contents[screen_row]) docontents = contents..sendcheck_eq(contents, expected_contents, msg)end-- miscellaneous internal helpers - replacement in app.lua at line 341[11.2396]→[11.2396:2482](∅→∅),[11.2482]→[8.34:461](∅→∅),[8.461]→[11.1314:1385](∅→∅),[11.1314]→[11.1314:1385](∅→∅),[11.1385]→[8.462:541](∅→∅)
-- fake filesfunction App.open_for_writing(filename)App.filesystem[filename] = ''if Current_app == nil or Current_app == 'run' thenreturn {write = function(self, ...)local args = {...}for i,s in ipairs(args) doApp.filesystem[filename] = App.filesystem[filename]..sendend,close = function(self)end,}elseif Current_app == 'source' thenreturn {write = function(self, s)App.filesystem[filename] = App.filesystem[filename]..send,close = function(self)end,}endfunction App.color(color)love.graphics.setColor(color.r, color.g, color.b, color.a) - replacement in app.lua at line 345[11.1472]→[11.1472:1512](∅→∅),[11.1512]→[5.19:240](∅→∅),[5.240]→[11.7391:7421](∅→∅),[11.2656]→[11.7391:7421](∅→∅),[11.7391]→[11.7391:7421](∅→∅),[11.7421]→[11.7894:7920](∅→∅),[11.7920]→[11.7421:7492](∅→∅),[11.7421]→[11.7421:7492](∅→∅),[11.7492]→[11.7921:8041](∅→∅),[11.8041]→[11.17:47](∅→∅),[11.47]→[11.1966:1994](∅→∅),[11.1994]→[9.1289:1308](∅→∅),[9.1308]→[11.60666:60752](∅→∅),[11.1994]→[11.60666:60752](∅→∅),[11.47]→[11.60666:60752](∅→∅),[11.62]→[11.7548:7554](∅→∅),[11.60752]→[11.7548:7554](∅→∅),[11.7548]→[11.7548:7554](∅→∅),[11.7564]→[4.17:113](∅→∅)
function App.open_for_reading(filename)if App.filesystem[filename] thenreturn {lines = function(self)return App.filesystem[filename]:gmatch('[^\n]+')end,close = function(self)end,}endendfunction App.run_tests()local sorted_names = {}for name,binding in pairs(_G) doif name:find('test_') == 1 thentable.insert(sorted_names, name)endendtable.sort(sorted_names)for _,name in ipairs(sorted_names) doApp.initialize_for_test()--? print('=== '..name)--? _G[name]()xpcall(_G[name], function(err) prepend_debug_info_to_test_failure(name, err) end)end-- clean up all test methodsfor _,name in ipairs(sorted_names) do_G[name] = nilendfunction colortable(app_color)return {app_color.r, app_color.g, app_color.b, app_color.a}