basic test-enabled framework
[?]
May 23, 2022, 1:27 AM
AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5ACDependencies
- [2]
UYRAO73Yenable pressing and holding backspace - [3]
ZNLTRNNKhighlight another global - [4]
BYG5CEMVsupport for naming points - [5]
ESETRNLBbugfix: printing the first part of a line at the bottom made it seem non-wrapping - [6]
TVCPXAAUrename - [7]
BJ5X5O4Alet's prevent the text cursor from ever getting on a drawing - [8]
6LJZN727handle chords - [9]
IZZVOCLBconfirm that we have access to all of the love API - [10]
JVRL5TWLstore device-independent coordinates inside drawings - [11]
H2DPLWMVsnapshot: wrapping long lines at word boundaries - [12]
6DE7RBZ6move mouse_released events to Drawing - [13]
5T2E3PDVcouple of bugfixes to file-handling - [14]
G77XIN7Mselecting a stroke - [15]
ZOOY3ME4new mode: circle arc - [16]
FJ4L6N74draw lines by default - [17]
ICIIP4DBslightly better default sizing of drawings - [18]
V5TP27FPctrl-+ and ctrl-- to adjust font size - [19]
YKRF5V3Zstarting to load/save - [20]
AVQ5MC5Dfinish uppercasing all globals - [21]
IYW7X3WLleft/right cursor movement, deleting characters - [22]
2KRK3OBVdon't rely on defaults - [23]
SNDZOK6Qslightly less strange now that we have the same two ways to move points as any other operation - [24]
DAENUOGVeliminate assumptions that line length == size in bytes - [25]
QU7NHFOVshow cursor - [26]
VVXVV2D2change data model; text can now have metadata - [27]
M36DBSDEbit more polish to help screen - [28]
SVJZZDC3snapshot - no, that's all wrong - [29]
PGZJ6NATensure Filename is writable when opened outside a terminal - [30]
OFA3PRBSautosave on keystrokes - [31]
XX7G2FFJintermingle freehand line drawings with text - [32]
EF6MFB46assume we always have a filename - [33]
Z4KNS42Nto open a file without a terminal, drag it on! - [34]
2FBLO5FHadjust window size - [35]
HRWN5V6JDevine's suggestion to try to live with just freehand - [36]
A2QPFRFJmove - [37]
EFMLTMZGbugfix: restrict strokes to the drawing they started in - [38]
WLHI7KD3new globals: draw partial screen line up top - [39]
ZUOL7X6Vmove - [40]
OTIBCAUJlove2d scaffold - [41]
O2UFJ6G3switch from freehand to just straight lines - [42]
LUNH47XXmake text and drawings the same width - [43]
VHQCNMARseveral more modules - [44]
RT6EV6OPdelegate update events to drawings - [45]
OAHNWDYG. - [46]
KJKKASHZreduce ambitions a bit: page up/down need not start screen from the middle of a line - [47]
WDWXNW7Vslightly strange way to move points - [48]
W4UVZETR2 regressions: - [49]
PWHZPJJMalways show current filename in window title - [50]
MGOQ5XAVstart uppercasing globals - [51]
2RXZ3PGObeginning of a new approach to scroll+wrap - [52]
NCRKBTHCposition cursor more precisely - [53]
OIB2QPRCstart remembering where the cursor is drawn in px - [54]
JRLBUB6Lmore intuitive point delete from polygons - [55]
MNWHXPBLmore lightweight; select just the stroke at the mouse - [56]
JCSLDGAHbeginnings of support for multiple shapes - [57]
XNFTJHC4split keyboard handling between Text and Drawing - [58]
T76KKDWZturn strokes into horizontal and vertical lines - [59]
3QNOKBFMbeginnings of a test harness - [60]
QCQHLMSTalways have a filename - [61]
KVHUFUFVreorg - [62]
2C7CTIQYmake space for multiple kinds of width - [63]
3RGHOJ25DRY some code - [64]
A2TQYJ6J. - [65]
RCDVDFJQcomment - [66]
WAZVXUV2simplest possible way to straighten strokes - [*]
R5QXEHUIsomebody 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 thenio.write('.')elseerror(msg)endendfunction check_eq(x, expected, msg)if eq(x, expected) thenio.write('.')elseerror(msg..'; got "'..x..'"')endendfunction eq(a, b)if type(a) ~= type(b) then return false endif type(a) == 'table' thenif #a ~= #b then return false endfor k, v in pairs(a) doif b[k] ~= v thenreturn falseendendfor k, v in pairs(b) doif a[k] ~= v thenreturn falseendendreturn trueendreturn a == bend - edit in main.lua at line 4
require 'test' - edit in main.lua at line 15
function App.initialize(arg)love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true)-- globals - replacement in main.lua at line 64
Screen_width, Screen_height, Screen_flags = 0, 0, nil-- maximize windowlove.window.setMode(0, 0) -- maximizeScreen_width, Screen_height, Screen_flags = love.window.getMode()-- shrink slightly to account for window decorationScreen_width = Screen_width-100Screen_height = Screen_height-100love.window.setMode(Screen_width, Screen_height) - replacement in main.lua at line 77
Line_width = nil -- maximum width available to either text or drawings, in pixels-- maximum width available to either text or drawings, in pixelsLine_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 wrapScreen_width = 120Screen_height = 200love.window.setMode(Screen_width, Screen_height)love.window.setTitle('Text with Lines')Line_width = 100--? Line_width = math.floor(Screen_width/2/40)*40love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true) - edit in main.lua at line 97
- replacement in main.lua at line 100
function love.filedropped(file)function App.filedropped(file) - replacement in main.lua at line 114
function love.draw()function App.draw() - replacement in main.lua at line 162
function love.update(dt)function App.update(dt) - replacement in main.lua at line 166
function love.mousepressed(x,y, mouse_button)function App.mousepressed(x,y, mouse_button) - replacement in main.lua at line 182
function love.mousereleased(x,y, button)function App.mousereleased(x,y, button) - replacement in main.lua at line 186
function love.textinput(t)function App.textinput(t) - replacement in main.lua at line 197
function keychord_pressed(chord)function App.keychord_pressed(chord) - replacement in main.lua at line 255
function love.keyreleased(key, scancode)function App.keyreleased(key, scancode) - replacement in keychord.lua at line 3
function love.keypressed(key, scancode, isrepeat)function App.keypressed(key, scancode, isrepeat) - replacement in keychord.lua at line 8
keychord_pressed(combine_modifiers(key))App.keychord_pressed(App.combine_modifiers(key)) - replacement in keychord.lua at line 11
function combine_modifiers(key)function App.combine_modifiers(key) - replacement in app.lua at line 1
-- main entrypoint from LÖVE-- 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
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end-- 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
if love.update then love.update(dt) end -- will pass 0 if love.timer is disabledif App.update then App.update(dt) end -- will pass 0 if love.timer is disabled - replacement in app.lua at line 44
if love.draw then love.draw() endif 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 screenendfunction App.screen.init(dims)App.screen.width = dims.widthApp.screen.height = dims.heightendfunction App.screen.print(msg, x,y)local screen_row = 'y'..tostring(y)local screen = App.screenif screen.contents[screen_row] == nil thenscreen.contents[screen_row] = {}for i=0,screen.width-1 doscreen.contents[screen_row][i] = ''endendif x < screen.width thenscreen.contents[screen_row][x] = msgendend-- 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)}endfunction App.screen.draw(obj, x,y)if type(obj) == 'userdata' then-- ignore most things as graphics the test harness can't handleelseif obj.type == 'text' thenApp.screen.print(obj.data, x,y)elseprint(obj.type)assert(false)endendfunction App.run_after_textinput(t)App.textinput(t)App.screen.contents = {}App.draw()endfunction App.width(text)return text.text:getWidth()endfunction 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]) docontents = contents..sendcheck_eq(contents, expected_contents, msg)endfunction App.run_tests()for name,binding in pairs(_G) doif name:find('test_') == 1 thenApp.initialize_for_test()binding()endendprint()end-- call this once all tests are run-- can't run any tests after thisfunction App.disable_tests()-- have LÖVE delegate all handlers to App if they existfor name in pairs(love.handlers) doif App[name] thenlove.handlers[name] = App[name]endend-- test methods are disallowed outside testsApp.screen.init = nilApp.run_after_textinput = nil-- other methods dispatch to real hardwareApp.screen.print = love.graphics.printApp.newText = love.graphics.newTextApp.screen.draw = love.graphics.drawApp.width = function(text) return text:getWidth() endend