reorganize app.lua and its comments

[?]
Aug 26, 2023, 10:04 PM
CUFW4EJL75OAA5BS5EXGTM5RMRNJOBBPAXUJADGZ3VLP2ZMKFOTAC

Dependencies

  • [2] CA5T33FG one more implication
  • [3] ZD3ZKA5E update some App framework docs
  • [4] S4IAYCIR delete all tests once they've executed
  • [5] NR43TWCN make App.open_for_* look more like io.open
  • [6] JMUD7T3O get rid of ugly side-effects in tests
  • [7] D2IADHMW link to default love.run for comparison
  • [8] DLBD4ZA6 wait, fix tests
  • [9] 6RYLD5ON change how we handle clicks above top margin
  • [10] 7IDHIAYI rename modifier_down to key_down
  • [11] 4EGQRXDA bugfix: naming points
  • [12] 4KC7I3E2 make colors easier to edit
  • [13] 3QNOKBFM beginnings of a test harness
  • [14] R53OF3ON one bug I've repeatedly run into while testing with Moby Dick
  • [15] JFFUF5AL override mouse state lookups in tests
  • [16] 3QWK3GSA support mouse clicks in file navigator
  • [17] PX7DDEMO autosave slightly less aggressively
  • [18] 2CK5QI7W make love event names consistent
  • [19] DIRTBUP4 remove a condition
  • [20] U7M4M2F7 bugfix: don't rely on Screen_bottom1 while scrolling
  • [21] BC4SO6AR clean up a print
  • [22] 7CLGG7J2 test: autosave after any shape
  • [23] IMEJA43L snapshot
  • [24] FCFJVYKY redundant check
  • [25] ORRSP7FV deduce test names on failures
  • [26] H4R5BHVY no more Text allocations
  • [27] MDXGMZU2 disable all debug prints
  • [28] ZLJGZYQG select text with shift + mouseclick
  • [29] 5UKUADTW distinguish consistently between mouse buttons and other buttons
  • [30] AD34IX2Z couple more tests
  • [31] AVTNUQYR basic test-enabled framework
  • [32] KKMFQDR4 editing source code from within the app
  • [33] 3ZTODUBQ formatting
  • [34] YTSPVDZH first successful pagedown test, first bug found by test
  • [*] JF5L2BBS test harness now supports copy/paste

Change contents

  • replacement in app.lua at line 4
    [7.113][7.113:170](),[7.170][11.1868:2265](),[11.1868][11.1868:2265]()
    -- 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.
    [7.113]
    [11.34]
    -- 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.
    [11.2551]
    [2.115]
    -- 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
    [2.118][11.5643:5829](),[11.5643][11.5643:5829]()
    -- 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.
    [2.118]
    [11.5829]
    -- Not everything is so wrapped yet. Sometimes you still have to use love.*
    -- primitives directly.
  • replacement in app.lua at line 50
    [11.5830][11.5830:5848]()
    App = {screen={}}
    [11.5830]
    [11.5848]
    App = {}
  • edit in app.lua at line 76
    [11.210403]
    [11.5849]
    function App.run_tests()
    local sorted_names = {}
    for name,binding in pairs(_G) do
    if name:find('test_') == 1 then
    table.insert(sorted_names, name)
    end
    end
    table.sort(sorted_names)
    for _,name in ipairs(sorted_names) do
    App.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 methods
    for _,name in ipairs(sorted_names) do
    _G[name] = nil
    end
    end
  • edit in app.lua at line 105
    [11.5975]
    [11.5975]
    -- 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
    [11.6077][6.672:733]()
    -- operations on the LÖVE window within the monitor/display
  • edit in app.lua at line 138
    [6.1190]
    [11.6077]
    -- 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
    [11.1287][11.1287:1374]()
    function App.color(color)
    love.graphics.setColor(color.r, color.g, color.b, color.a)
    [11.1287]
    [11.382]
    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 then
    error('no text at y '..tostring(y))
    end
    for i,s in ipairs(App.screen.contents[screen_row]) do
    contents = contents..s
    end
    check_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
    [11.387]
    [11.6425]
    -- 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
    [11.1975]
    [11.1975]
    -- 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
    [11.7729]
    [11.4353]
    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]()
    end
    function App.key_down(key)
    return App.fake_keys_pressed[key]
  • edit in app.lua at line 219
    [11.2126]
    [11.2126]
    -- 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 = x
    App.fake_mouse_state.y = y
    App.fake_mouse_state[mouse_button] = true
    end
    function App.fake_mouse_release(x,y, mouse_button)
    App.fake_mouse_state.x = x
    App.fake_mouse_state.y = y
    App.fake_mouse_state[mouse_button] = nil
    end
    [11.4618]
    [11.4905]
  • edit in app.lua at line 241
    [11.5184]
    [36.2948]
    end
    function App.fake_mouse_press(x,y, mouse_button)
    App.fake_mouse_state.x = x
    App.fake_mouse_state.y = y
    App.fake_mouse_state[mouse_button] = true
    end
    function App.fake_mouse_release(x,y, mouse_button)
    App.fake_mouse_state.x = x
    App.fake_mouse_state.y = y
    App.fake_mouse_state[mouse_button] = nil
    end
    -- 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' then
    return {
    write = function(self, ...)
    local args = {...}
    for i,s in ipairs(args) do
    App.filesystem[filename] = App.filesystem[filename]..s
    end
    end,
    close = function(self)
    end,
    }
    elseif Current_app == 'source' then
    return {
    write = function(self, s)
    App.filesystem[filename] = App.filesystem[filename]..s
    end,
    close = function(self)
    end,
    }
    end
    end
    function App.open_for_reading(filename)
    if App.filesystem[filename] then
    return {
    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 then
    error('no text at y '..tostring(y))
    end
    for i,s in ipairs(App.screen.contents[screen_row]) do
    contents = contents..s
    end
    check_eq(contents, expected_contents, msg)
    end
    [11.7144]
    [11.2395]
    -- 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 files
    function App.open_for_writing(filename)
    App.filesystem[filename] = ''
    if Current_app == nil or Current_app == 'run' then
    return {
    write = function(self, ...)
    local args = {...}
    for i,s in ipairs(args) do
    App.filesystem[filename] = App.filesystem[filename]..s
    end
    end,
    close = function(self)
    end,
    }
    elseif Current_app == 'source' then
    return {
    write = function(self, s)
    App.filesystem[filename] = App.filesystem[filename]..s
    end,
    close = function(self)
    end,
    }
    end
    [11.2396]
    [11.1467]
    function 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] then
    return {
    lines = function(self)
    return App.filesystem[filename]:gmatch('[^\n]+')
    end,
    close = function(self)
    end,
    }
    end
    end
    function App.run_tests()
    local sorted_names = {}
    for name,binding in pairs(_G) do
    if name:find('test_') == 1 then
    table.insert(sorted_names, name)
    end
    end
    table.sort(sorted_names)
    for _,name in ipairs(sorted_names) do
    App.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 methods
    for _,name in ipairs(sorted_names) do
    _G[name] = nil
    end
    [11.1472]
    [11.7564]
    function colortable(app_color)
    return {app_color.r, app_color.g, app_color.b, app_color.a}