basic test-enabled framework

[?]
May 23, 2022, 1:27 AM
AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC

Dependencies

  • [2] UYRAO73Y enable pressing and holding backspace
  • [3] ZNLTRNNK highlight another global
  • [4] BYG5CEMV support for naming points
  • [5] ESETRNLB bugfix: printing the first part of a line at the bottom made it seem non-wrapping
  • [6] TVCPXAAU rename
  • [7] BJ5X5O4A let's prevent the text cursor from ever getting on a drawing
  • [8] 6LJZN727 handle chords
  • [9] IZZVOCLB confirm that we have access to all of the love API
  • [10] JVRL5TWL store device-independent coordinates inside drawings
  • [11] H2DPLWMV snapshot: wrapping long lines at word boundaries
  • [12] 6DE7RBZ6 move mouse_released events to Drawing
  • [13] 5T2E3PDV couple of bugfixes to file-handling
  • [14] G77XIN7M selecting a stroke
  • [15] ZOOY3ME4 new mode: circle arc
  • [16] FJ4L6N74 draw lines by default
  • [17] ICIIP4DB slightly better default sizing of drawings
  • [18] V5TP27FP ctrl-+ and ctrl-- to adjust font size
  • [19] YKRF5V3Z starting to load/save
  • [20] AVQ5MC5D finish uppercasing all globals
  • [21] IYW7X3WL left/right cursor movement, deleting characters
  • [22] 2KRK3OBV don't rely on defaults
  • [23] SNDZOK6Q slightly less strange now that we have the same two ways to move points as any other operation
  • [24] DAENUOGV eliminate assumptions that line length == size in bytes
  • [25] QU7NHFOV show cursor
  • [26] VVXVV2D2 change data model; text can now have metadata
  • [27] M36DBSDE bit more polish to help screen
  • [28] SVJZZDC3 snapshot - no, that's all wrong
  • [29] PGZJ6NAT ensure Filename is writable when opened outside a terminal
  • [30] OFA3PRBS autosave on keystrokes
  • [31] XX7G2FFJ intermingle freehand line drawings with text
  • [32] EF6MFB46 assume we always have a filename
  • [33] Z4KNS42N to open a file without a terminal, drag it on!
  • [34] 2FBLO5FH adjust window size
  • [35] HRWN5V6J Devine's suggestion to try to live with just freehand
  • [36] A2QPFRFJ move
  • [37] EFMLTMZG bugfix: restrict strokes to the drawing they started in
  • [38] WLHI7KD3 new globals: draw partial screen line up top
  • [39] ZUOL7X6V move
  • [40] OTIBCAUJ love2d scaffold
  • [41] O2UFJ6G3 switch from freehand to just straight lines
  • [42] LUNH47XX make text and drawings the same width
  • [43] VHQCNMAR several more modules
  • [44] RT6EV6OP delegate update events to drawings
  • [45] OAHNWDYG .
  • [46] KJKKASHZ reduce ambitions a bit: page up/down need not start screen from the middle of a line
  • [47] WDWXNW7V slightly strange way to move points
  • [48] W4UVZETR 2 regressions:
  • [49] PWHZPJJM always show current filename in window title
  • [50] MGOQ5XAV start uppercasing globals
  • [51] 2RXZ3PGO beginning of a new approach to scroll+wrap
  • [52] NCRKBTHC position cursor more precisely
  • [53] OIB2QPRC start remembering where the cursor is drawn in px
  • [54] JRLBUB6L more intuitive point delete from polygons
  • [55] MNWHXPBL more lightweight; select just the stroke at the mouse
  • [56] JCSLDGAH beginnings of support for multiple shapes
  • [57] XNFTJHC4 split keyboard handling between Text and Drawing
  • [58] T76KKDWZ turn strokes into horizontal and vertical lines
  • [59] 3QNOKBFM beginnings of a test harness
  • [60] QCQHLMST always have a filename
  • [61] KVHUFUFV reorg
  • [62] 2C7CTIQY make space for multiple kinds of width
  • [63] 3RGHOJ25 DRY some code
  • [64] A2TQYJ6J .
  • [65] RCDVDFJQ comment
  • [66] WAZVXUV2 simplest possible way to straighten strokes
  • [*] R5QXEHUI somebody stop me

Change contents

  • file addition: test.lua (----------)
    [68.2]
    -- Some primitives for tests.
    --
    -- Success indicators go to the terminal; failures go to the window.
    -- I don't know what I am doing.
    function check(x, msg)
    if x then
    io.write('.')
    else
    error(msg)
    end
    end
    function check_eq(x, expected, msg)
    if eq(x, expected) then
    io.write('.')
    else
    error(msg..'; got "'..x..'"')
    end
    end
    function eq(a, b)
    if type(a) ~= type(b) then return false end
    if type(a) == 'table' then
    if #a ~= #b then return false end
    for k, v in pairs(a) do
    if b[k] ~= v then
    return false
    end
    end
    for k, v in pairs(b) do
    if a[k] ~= v then
    return false
    end
    end
    return true
    end
    return a == b
    end
  • edit in main.lua at line 4
    [6.18]
    [6.18]
    require 'test'
  • edit in main.lua at line 15
    [7.22]
    [7.1]
    function App.initialize(arg)
    love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
    love.keyboard.setKeyRepeat(true)
    -- globals
  • replacement in main.lua at line 64
    [7.1239][7.60:114]()
    Screen_width, Screen_height, Screen_flags = 0, 0, nil
    [7.1239]
    [7.54]
    -- maximize window
    love.window.setMode(0, 0) -- maximize
    Screen_width, Screen_height, Screen_flags = love.window.getMode()
    -- shrink slightly to account for window decoration
    Screen_width = Screen_width-100
    Screen_height = Screen_height-100
    love.window.setMode(Screen_width, Screen_height)
  • replacement in main.lua at line 77
    [7.4][7.3:86]()
    Line_width = nil -- maximum width available to either text or drawings, in pixels
    [7.4]
    [7.1]
    -- maximum width available to either text or drawings, in pixels
    Line_width = math.floor(Screen_width/2/40)*40
  • edit in main.lua at line 86
    [7.48][7.20:44](),[7.95][7.2:23](),[7.23][5.1608:1861](),[5.1861][7.611:638](),[7.611][7.611:638](),[7.638][5.1862:1905](),[7.689][7.505:556](),[5.1905][7.505:556](),[7.2270][7.505:556](),[7.505][7.505:556](),[7.556][7.1:43](),[7.166][7.1:43](),[7.43][5.1906:1977](),[7.42][7.2:75](),[7.43][7.2:75](),[7.102][7.2:75](),[7.135][7.2:75](),[7.208][7.2:75](),[7.608][7.2:75](),[7.761][7.2:75](),[5.1977][7.2:75](),[7.2342][7.2:75](),[7.183][7.2:75](),[7.75][2.3:38]()
    function love.load(arg)
    -- maximize window
    --? love.window.setMode(0, 0) -- maximize
    --? Screen_width, Screen_height, Screen_flags = love.window.getMode()
    --? -- shrink slightly to account for window decoration
    --? Screen_width = Screen_width-100
    --? Screen_height = Screen_height-100
    -- for testing line wrap
    Screen_width = 120
    Screen_height = 200
    love.window.setMode(Screen_width, Screen_height)
    love.window.setTitle('Text with Lines')
    Line_width = 100
    --? Line_width = math.floor(Screen_width/2/40)*40
    love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
    love.keyboard.setKeyRepeat(true)
  • edit in main.lua at line 97
    [7.746]
    [7.2]
  • replacement in main.lua at line 100
    [7.7][7.7:39]()
    function love.filedropped(file)
    [7.7]
    [7.747]
    function App.filedropped(file)
  • replacement in main.lua at line 114
    [7.188][7.188:209]()
    function love.draw()
    [7.188]
    [3.3]
    function App.draw()
  • replacement in main.lua at line 162
    [7.421][7.421:446]()
    function love.update(dt)
    [7.421]
    [7.3]
    function App.update(dt)
  • replacement in main.lua at line 166
    [7.8][7.3:49]()
    function love.mousepressed(x,y, mouse_button)
    [7.8]
    [7.49]
    function App.mousepressed(x,y, mouse_button)
  • replacement in main.lua at line 182
    [7.451][7.1585:1626]()
    function love.mousereleased(x,y, button)
    [7.451]
    [7.3]
    function App.mousereleased(x,y, button)
  • replacement in main.lua at line 186
    [4.135][4.135:162]()
    function love.textinput(t)
    [4.135]
    [4.162]
    function App.textinput(t)
  • replacement in main.lua at line 197
    [7.90][7.90:123]()
    function keychord_pressed(chord)
    [7.3121]
    [5.2179]
    function App.keychord_pressed(chord)
  • replacement in main.lua at line 255
    [7.361][7.330:371](),[7.330][7.330:371]()
    function love.keyreleased(key, scancode)
    [7.361]
    [7.327]
    function App.keyreleased(key, scancode)
  • replacement in keychord.lua at line 3
    [7.488][7.488:538]()
    function love.keypressed(key, scancode, isrepeat)
    [7.488]
    [7.538]
    function App.keypressed(key, scancode, isrepeat)
  • replacement in keychord.lua at line 8
    [7.801][7.801:844]()
    keychord_pressed(combine_modifiers(key))
    [7.801]
    [7.844]
    App.keychord_pressed(App.combine_modifiers(key))
  • replacement in keychord.lua at line 11
    [7.849][7.849:881]()
    function combine_modifiers(key)
    [7.849]
    [7.881]
    function App.combine_modifiers(key)
  • replacement in app.lua at line 1
    [7.2][7.3:34]()
    -- main entrypoint from LÖVE
    [7.2]
    [7.34]
    -- main entrypoint for LÖVE
    --
    -- Most apps can just use the default, 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.
  • replacement in app.lua at line 14
    [7.54][7.54:127]()
    if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
    [7.54]
    [7.127]
    -- Tests always run at the start.
    App.run_tests()
    App.disable_tests()
    if App.initialize then App.initialize(love.arg.parseGameArguments(arg), arg) end
  • replacement in app.lua at line 38
    [7.551][7.551:636]()
    if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
    [7.551]
    [7.636]
    if App.update then App.update(dt) end -- will pass 0 if love.timer is disabled
  • replacement in app.lua at line 44
    [7.784][7.784:824]()
    if love.draw then love.draw() end
    [7.784]
    [7.824]
    if App.draw then App:draw() end
  • edit in app.lua at line 52
    [7.925]
    -- 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'}
    -- App.screen.init{
    -- width=100
    -- height=30
    -- }
    -- App.font{
    -- height=15
    -- }
    -- App.run_with_keypress('pagedown')
    -- App.check_screen_contents{
    -- y0='ghi'
    -- y15=''
    -- }
    -- 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.
    --
    -- 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.
    App = {screen={}}
    function App.initialize_for_test()
    App.screen.init({width=100, height=50})
    App.screen.contents = {} -- clear screen
    end
    function App.screen.init(dims)
    App.screen.width = dims.width
    App.screen.height = dims.height
    end
    function App.screen.print(msg, x,y)
    local screen_row = 'y'..tostring(y)
    local screen = App.screen
    if screen.contents[screen_row] == nil then
    screen.contents[screen_row] = {}
    for i=0,screen.width-1 do
    screen.contents[screen_row][i] = ''
    end
    end
    if x < screen.width then
    screen.contents[screen_row][x] = msg
    end
    end
    -- LÖVE's Text primitive retains no trace of the string it was created from,
    -- so we'll wrap it for our tests.
    --
    -- This implies that we need to hook any operations we need on Text objects.
    function App.newText(font, s)
    return {type='text', data=s, text=love.graphics.newText(font, s)}
    end
    function App.screen.draw(obj, x,y)
    if type(obj) == 'userdata' then
    -- ignore most things as graphics the test harness can't handle
    elseif obj.type == 'text' then
    App.screen.print(obj.data, x,y)
    else
    print(obj.type)
    assert(false)
    end
    end
    function App.run_after_textinput(t)
    App.textinput(t)
    App.screen.contents = {}
    App.draw()
    end
    function App.width(text)
    return text.text:getWidth()
    end
    function App.screen.check(y, expected_contents, msg)
    local screen_row = 'y'..tostring(y)
    local contents = ''
    for i,s in ipairs(App.screen.contents[screen_row]) do
    contents = contents..s
    end
    check_eq(contents, expected_contents, msg)
    end
    function App.run_tests()
    for name,binding in pairs(_G) do
    if name:find('test_') == 1 then
    App.initialize_for_test()
    binding()
    end
    end
    print()
    end
    -- call this once all tests are run
    -- can't run any tests after this
    function App.disable_tests()
    -- have LÖVE delegate all handlers to App if they exist
    for name in pairs(love.handlers) do
    if App[name] then
    love.handlers[name] = App[name]
    end
    end
    -- test methods are disallowed outside tests
    App.screen.init = nil
    App.run_after_textinput = nil
    -- other methods dispatch to real hardware
    App.screen.print = love.graphics.print
    App.newText = love.graphics.newText
    App.screen.draw = love.graphics.draw
    App.width = function(text) return text:getWidth() end
    end