Tests still have a lot of side-effects on the real screen. We'll gradually clean those up.
AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC UYRAO73Y4LMTBBSH5RGNSNR532NFLRU5N5CJW3VIG72GZZXC654AC ZNLTRNNKAKSMWOVZKYKFPSIOSRFS73YTFADWE4N5V3BT4UX57Q4AC BYG5CEMVXANDTBI2ORNVMEY6K3EBRIHZHS4QBK27VONJC5537COQC ESETRNLB3MIJ2SID6HJMMP52FEVUBLGK2HLWD75KDQZAKQMKSF2QC TVCPXAAU4P3K5MFYINH2MWDK3KGTQ2GE74TUNERYOONG2G5EYKMQC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC 6LJZN727CRPYR34LV75CQF55YZI3E7MGESYZSFSYAE73SNEZE3FAC VVXVV2D2F5Y6D6N5VVPUPK3N6GMDTG2YCYPQDYTYEKVKBYHRRYEAC JCSLDGAH2F6AIY4Z6XM6K4LOMW7EFY3E4NF5YXLMHLTYTX3A4Z3QC AVQ5MC5DWNLI6LUUIPGBLGP4LKRPGWBY4THNY25OBT2FAVHC6MCAC OIB2QPRCB4MAVZV5NCEKSAL45ITT6V4BYSET3Q2VCT3WBOIC4QVQC JVRL5TWLBTWMTHJDZSDN5XQDMEIIPVAZBKUP75HMO7JHURAYWG5QC LUNH47XXUUITDLE6NBI3J7GJQWQ45OQAGXY2HJI4HRPOR2GUULPAC V5TP27FPD6GPATHPABRW2FKP7BIKY53KL47UVEU5DF4WXEZF7CKAC YKRF5V3ZZQIQ3UGAFYTQT5PUQVHCP2VHFDX77EY2C3X543HUDYKQC 2FBLO5FHINYONP5ETPEL4GIPUXKVXKPUL4Q5EKU5K2GHGSL3ODBQC KJKKASHZCC5JD6G6PWQ4TA42NVI2CNTAZ667GA76H272DD6KCNRQC SVJZZDC3K6AKAXHGRNAZKRE2ZXEKJANNLG7LSSUZJARFBL5F7C4AC PWHZPJJM7UZW2VILT5TZBDZHIXXXBASVSBQB3A5EEMOFJTI6ADYAC M36DBSDECWQYCVIPWUYPZXSR547P76RBVYQ63B5KJGVTFWIXCSJQC 2KRK3OBVPHQIDGCH2FBTP2AXKPEXS3OEPLBKU7UWCLKQA4MANGSAC W4UVZETRKOSWDPLAM5LGAPCQEJWIVFCXUJDVZQASEIKALYEU34KAC 2C7CTIQYDDYVQJNKX2OSHZ6VMAMPOGNUVTSFAUV7HQCPMZR2YRUAC Z4KNS42NJZTQKUQZ7B5NYU2U4VOCUQCBFT2D7423MAXKF7NQ5ZJAC RT6EV6OPUYCXYZOX2PHFXJ7KT77KHNEVINEGQXIQLHQVKPGTN6VQC KVHUFUFVOSY6GB4XI2QK4T4WCLIYOV3NZR67TX6AQHAQDWJMEOBQC OAHNWDYG3V6EJQXJ333FD7M44E3VBZUWSKZ45SSFXVK5Q2HJUI4AC 6DE7RBZ6RHNEICJ7EUMCTROK43LW4LYINULIF2QEQOKCXWLUYUXAC HRWN5V6J6VMXS7WNSRGI7WMUSZ2OI52JJ4IK352VVSDZI4EF5HHQC JRLBUB6LR2JIAKVQNKF3T4BDICUIJ3HEMRRHX56YP5M5SP7ZS3WAC 3QNOKBFMKBGXBVJIRHR2444JRRMBTABHE4674NR3DT67RRM2X6GAC -- 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
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)
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)
-- 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.
-- 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