Skip to content

Commit 5438ecb

Browse files
committed
Tests & fixes for keys and os api
1 parent b8f9444 commit 5438ecb

File tree

6 files changed

+185
-45
lines changed

6 files changed

+185
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ __pypackages__/
44
/build/
55
/*.egg-info/
66
/dist/
7+
/todo.txt

computercraft/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ async def ws(self, request):
192192
asyncio.create_task(self._sender(ws, api))
193193
async for msg in self._json_messages(ws):
194194
if msg['action'] == 'event':
195-
for task_id in self._event_to_tids.get(msg['event'], ()):
195+
for task_id in api._event_to_tids.get(msg['event'], ()):
196196
await api._result_queues[task_id].put(msg['params'])
197197
elif msg['action'] == 'task_result':
198198
api._result_values[msg['task_id']] = msg['result']

computercraft/subapis/keys.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
from typing import Optional
22

3-
from .base import BaseSubAPI, opt_str_return
3+
from .base import BaseSubAPI, lua_string, opt_int_return, opt_str_return
44

55

66
class KeysAPI(BaseSubAPI):
77
_API = 'keys'
88

9+
async def getCode(self, name: str) -> Optional[int]:
10+
# replaces properties
11+
# keys.space → await api.keys.getCode('space')
12+
return opt_int_return(await self._cc._send_cmd('''
13+
if type({api}[{key}]) == 'number' then
14+
return {api}[{key}]
15+
end
16+
return nil'''.format(api=self._API, key=lua_string(name))))
17+
918
async def getName(self, code: int) -> Optional[str]:
1019
return opt_str_return(await self._send('getName', code))

computercraft/subapis/mixins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ async def isColor(self) -> bool:
3030
async def getSize(self) -> Tuple[int, int]:
3131
return tuple(await self._send('getSize'))
3232

33-
async def scroll(self, n: int):
34-
return nil_return(await self._send('scroll', n))
33+
async def scroll(self, lines: int):
34+
return nil_return(await self._send('scroll', lines))
3535

3636
async def setTextColor(self, color: int):
3737
return nil_return(await self._send('setTextColor', color))

computercraft/subapis/os.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1+
from contextlib import asynccontextmanager
12
from typing import Optional, List
3+
24
from .base import (
35
BaseSubAPI, LuaTable, LuaNum,
46
nil_return, str_return, opt_str_return, number_return, int_return, bool_return
57
)
68

79

8-
class CCEventQueue(BaseSubAPI):
9-
def __init__(self, cc, targetEvent):
10-
super().__init__(cc)
11-
self._targetEvent = targetEvent
12-
13-
async def __aenter__(self):
14-
self._q, self._tid = await self._cc._start_queue(self._targetEvent)
15-
return self
16-
17-
async def __aexit__(self, exc_type, exc, tb):
18-
await self._cc._stop_queue(self._tid)
10+
class CCEventQueue:
11+
def __init__(self, q):
12+
self._q = q
1913

2014
def __aiter__(self):
2115
return self
@@ -42,13 +36,8 @@ async def setComputerLabel(self, label: Optional[str]):
4236
async def run(self, environment: LuaTable, programPath: str, *args: List[str]):
4337
return bool_return(await self._send('run', environment, programPath, *args))
4438

45-
async def loadAPI(self, path: str):
46-
return bool_return(await self._send('loadAPI', path))
47-
48-
async def unloadAPI(self, name: str):
49-
return nil_return(await self._send('unloadAPI', name))
50-
51-
def registerEventQueue(self, targetEvent: str) -> CCEventQueue:
39+
@asynccontextmanager
40+
async def captureEvent(self, targetEvent: str) -> CCEventQueue:
5241
'''
5342
Use this function instead loop over pullEvent/pullEventRaw.
5443
@@ -60,41 +49,56 @@ def registerEventQueue(self, targetEvent: str) -> CCEventQueue:
6049
There exist some dead intervals of time, while pullEvent is going to be transferred to python side.
6150
Lua side can receive and discard timer event since there's no consumer for it.
6251
63-
registerEventQueue gives you reliable way of receiving events without losses.
52+
captureEvent gives you reliable way of receiving events without losses.
6453
Register queue before firing a timer, start a timer, listen for messages in queue.
6554
6655
# it's significant here: start queue before starting a timer
67-
async with api.os.registerEventQueue('timer') as timer_queue:
56+
async with api.os.captureEvent('timer') as timer_queue:
6857
myTimer = await api.os.startTimer(3)
69-
async for e in timer_queue:
70-
if e[1] == myTimer:
58+
async for etid, in timer_queue:
59+
if etid == myTimer:
7160
await api.print('Timer reached')
7261
break
7362
'''
74-
return CCEventQueue(self._cc, targetEvent)
63+
q, tid = await self._cc._start_queue(targetEvent)
64+
try:
65+
yield CCEventQueue(q)
66+
finally:
67+
await self._cc._stop_queue(tid)
7568

7669
async def queueEvent(self, event: str, *params):
77-
return nil_return(await self._send('queueEvent', event, *params))
70+
return nil_return(await self._send('queueEvent', event, *params, omit_nulls=False))
7871

7972
async def clock(self) -> LuaNum:
73+
# number of game ticks * 0.05, roughly seconds
8074
return number_return(await self._send('clock'))
8175

82-
async def startTimer(self, timeout: LuaNum) -> int:
83-
return int_return(await self._send('startTimer', timeout))
84-
85-
async def cancelTimer(self, timerID: int):
86-
return nil_return(await self._send('cancelTimer', timerID))
76+
# regarding ingame parameter below:
77+
# python has great stdlib to deal with real current time
78+
# we keep here only in-game time methods and parameters
8779

8880
async def time(self) -> LuaNum:
89-
return number_return(await self._send('time'))
81+
# in hours 0..24
82+
return number_return(await self._send('time', 'ingame'))
83+
84+
async def day(self) -> int:
85+
return int_return(await self._send('day', 'ingame'))
86+
87+
async def epoch(self) -> int:
88+
return int_return(await self._send('epoch', 'ingame'))
9089

9190
async def sleep(self, seconds: LuaNum):
9291
return nil_return(await self._send('sleep', seconds))
9392

94-
async def day(self) -> int:
95-
return int_return(await self._send('day'))
93+
async def startTimer(self, timeout: LuaNum) -> int:
94+
return int_return(await self._send('startTimer', timeout))
95+
96+
async def cancelTimer(self, timerID: int):
97+
return nil_return(await self._send('cancelTimer', timerID))
9698

9799
async def setAlarm(self, time: LuaNum) -> int:
100+
# takes time of the day in hours 0..24
101+
# returns integer alarmID
98102
return int_return(await self._send('setAlarm', time))
99103

100104
async def cancelAlarm(self, alarmID: int):

testmod.py

Lines changed: 135 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
from contextlib import contextmanager
3+
from time import monotonic
34
from types import FunctionType
45

56
from computercraft.errors import LuaException, CommandException
@@ -15,12 +16,6 @@ async def id(api):
1516
await api.print('Version', await api.os.version())
1617

1718

18-
async def parallel(api):
19-
# Since os.sleep is mostly waiting for events, it doesn't block execution of parallel threads
20-
# and this snippet takes approximately 2 seconds to complete.
21-
await asyncio.gather(api.os.sleep(2), api.os.sleep(2))
22-
23-
2419
async def move(api):
2520
for _ in range(4):
2621
await api.turtle.forward()
@@ -49,6 +44,15 @@ def assert_raises(etype):
4944
raise AssertionError(f'Exception of type {etype} was not raised')
5045

5146

47+
@contextmanager
48+
def assert_takes_time(at_least, at_most):
49+
t = monotonic()
50+
yield
51+
dt = monotonic() - t
52+
# print(at_least, '<=', dt, '<=', at_most)
53+
assert at_least <= dt <= at_most
54+
55+
5256
class AnyInstanceOf:
5357
def __init__(self, cls):
5458
self.c = cls
@@ -506,8 +510,130 @@ async def test_gps_command_computer(api):
506510

507511

508512
async def test_keys_api(api):
509-
assert await api.keys.getName(65) == 'a'
510-
assert await api.keys.getName(32) == 'space'
511-
assert await api.keys.getName(13) is None # wtf?
513+
a = await api.keys.getCode('a')
514+
space = await api.keys.getCode('space')
515+
enter = await api.keys.getCode('enter')
516+
assert await api.keys.getCode('doesnotexist') is None
517+
assert await api.keys.getCode('getName') is None
518+
assert isinstance(a, int)
519+
assert isinstance(space, int)
520+
assert isinstance(enter, int)
521+
522+
assert await api.keys.getName(a) == 'a'
523+
assert await api.keys.getName(space) == 'space'
524+
assert await api.keys.getName(enter) == 'enter'
525+
512526
# for i in range(255):
513527
# print(i, await api.keys.getName(i))
528+
529+
await api.print('Test finished successfully')
530+
531+
532+
async def test_help_api(api):
533+
assert get_class_table(api.help.__class__) \
534+
== await get_object_table(api, 'help')
535+
536+
await api.help.setPath('/rom/help')
537+
538+
assert await api.help.path() == '/rom/help'
539+
540+
assert await api.help.lookup('disk') == 'rom/help/disk.txt'
541+
assert await api.help.lookup('abracadabra') is None
542+
543+
ts = await api.help.topics()
544+
assert isinstance(ts, list)
545+
assert len(ts) > 2
546+
# print(ts)
547+
assert 'disk' in ts
548+
549+
assert await api.help.completeTopic('di') == ['sk']
550+
assert await api.help.completeTopic('abracadabra') == []
551+
552+
assert await api.help.setPath('/kek') is None
553+
assert await api.help.path() == '/kek'
554+
assert await api.help.topics() == ['index']
555+
assert await api.help.setPath('/rom/help') is None
556+
557+
await api.print('Test finished successfully')
558+
559+
560+
async def test_reboot(api):
561+
assert await api.os.reboot() is None
562+
await api.print('Test finished successfully')
563+
564+
565+
async def test_shutdown(api):
566+
assert await api.os.shutdown() is None
567+
await api.print('Test finished successfully')
568+
569+
570+
async def test_os_api(api):
571+
tbl = await get_object_table(api, 'os')
572+
573+
# use methods with get*
574+
del tbl['function']['computerID']
575+
del tbl['function']['computerLabel']
576+
577+
# use captureEvent
578+
del tbl['function']['pullEvent']
579+
del tbl['function']['pullEventRaw']
580+
581+
# we are in python world, loading lua modules is useless
582+
del tbl['function']['loadAPI']
583+
del tbl['function']['unloadAPI']
584+
585+
# remove complex date formatting function in favor of python stdlib
586+
del tbl['function']['date']
587+
588+
tbl['function']['captureEvent'] = True
589+
590+
assert get_class_table(api.os.__class__) == tbl
591+
592+
with assert_takes_time(1.5, 3):
593+
async with api.os.captureEvent('timer') as timer_queue:
594+
timer_id = await api.os.startTimer(2)
595+
async for etid, in timer_queue:
596+
if etid == timer_id:
597+
await api.print('Timer reached')
598+
break
599+
600+
timer_id = await api.os.startTimer(20)
601+
assert isinstance(timer_id, int)
602+
assert await api.os.cancelTimer(timer_id) is None
603+
assert await api.os.cancelTimer(timer_id) is None
604+
605+
alarm_id = await api.os.setAlarm(0.0)
606+
assert isinstance(alarm_id, int)
607+
assert await api.os.cancelAlarm(alarm_id) is None
608+
assert await api.os.cancelAlarm(alarm_id) is None
609+
610+
with assert_takes_time(1.5, 3):
611+
assert await api.os.sleep(2) is None
612+
613+
assert (await api.os.version()).startswith('CraftOS ')
614+
assert isinstance(await api.os.getComputerID(), int)
615+
616+
assert await api.os.setComputerLabel(None) is None
617+
assert await api.os.getComputerLabel() is None
618+
assert await api.os.setComputerLabel('altair') is None
619+
assert await api.os.getComputerLabel() == 'altair'
620+
assert await api.os.setComputerLabel(None) is None
621+
assert await api.os.getComputerLabel() is None
622+
623+
assert isinstance(await api.os.epoch(), int)
624+
assert isinstance(await api.os.day(), int)
625+
assert isinstance(await api.os.time(), (int, float))
626+
assert isinstance(await api.os.clock(), (int, float))
627+
628+
# TODO: run method
629+
630+
await api.print('Test finished successfully')
631+
632+
633+
async def test_parallel(api):
634+
with assert_takes_time(1.5, 3):
635+
# Since os.sleep is mostly waiting for events, it doesn't block execution of parallel threads
636+
# and this snippet takes approximately 2 seconds to complete.
637+
await asyncio.gather(api.os.sleep(2), api.os.sleep(2))
638+
639+
await api.print('Test finished successfully')

0 commit comments

Comments
 (0)