Skip to content

asyncio: we should dogfood our own asyncio implementation during automated tests #7059

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 10 additions & 17 deletions extmod/moduasyncio.c
Original file line number Diff line number Diff line change
Expand Up @@ -70,32 +70,20 @@ STATIC mp_obj_t task_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf);
/******************************************************************************/
// Ticks for task ordering in pairing heap

#if !CIRCUITPY || (defined(__unix__) || defined(__APPLE__))
STATIC mp_obj_t ticks(void) {
return MP_OBJ_NEW_SMALL_INT(mp_hal_ticks_ms() & (MICROPY_PY_UTIME_TICKS_PERIOD - 1));
}

STATIC mp_int_t ticks_diff(mp_obj_t t1_in, mp_obj_t t0_in) {
mp_uint_t t0 = MP_OBJ_SMALL_INT_VALUE(t0_in);
mp_uint_t t1 = MP_OBJ_SMALL_INT_VALUE(t1_in);
mp_int_t diff = ((t1 - t0 + MICROPY_PY_UTIME_TICKS_PERIOD / 2) & (MICROPY_PY_UTIME_TICKS_PERIOD - 1))
- MICROPY_PY_UTIME_TICKS_PERIOD / 2;
return diff;
}
#else
#define _TICKS_PERIOD (1lu << 29)
#define _TICKS_MAX (_TICKS_PERIOD - 1)
#define _TICKS_HALFPERIOD (_TICKS_PERIOD >> 1)

#define ticks() supervisor_ticks_ms()
STATIC mp_obj_t ticks(void) {
return MP_OBJ_NEW_SMALL_INT(mp_hal_ticks_ms() & _TICKS_MAX);
}

STATIC mp_int_t ticks_diff(mp_obj_t t1_in, mp_obj_t t0_in) {
mp_uint_t t0 = MP_OBJ_SMALL_INT_VALUE(t0_in);
mp_uint_t t1 = MP_OBJ_SMALL_INT_VALUE(t1_in);
mp_int_t diff = ((t1 - t0 + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD;
return diff;
}
#endif

STATIC int task_lt(mp_pairheap_t *n1, mp_pairheap_t *n2) {
mp_obj_task_t *t1 = (mp_obj_task_t *)n1;
Expand Down Expand Up @@ -302,8 +290,13 @@ STATIC mp_obj_t task_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
STATIC mp_obj_t task_iternext(mp_obj_t self_in) {
mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in);
if (TASK_IS_DONE(self)) {
// Task finished, raise return value to caller so it can continue.
nlr_raise(self->data);
if (self->data == mp_const_none) {
// Task finished but has already been sent to the loop's exception handler.
mp_raise_StopIteration(MP_OBJ_NULL);
} else {
// Task finished, raise return value to caller so it can continue.
nlr_raise(self->data);
}
} else {
// Put calling task on waiting queue.
mp_obj_t cur_task = mp_obj_dict_get(uasyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_cur_task));
Expand Down
8 changes: 6 additions & 2 deletions extmod/uasyncio/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,12 @@ def __await__(self):

def __next__(self):
if not self.state:
# Task finished, raise return value to caller so it can continue.
raise self.data
if self.data is None:
# Task finished but has already been sent to the loop's exception handler.
raise StopIteration
else:
# Task finished, raise return value to caller so it can continue.
raise self.data
else:
# Put calling task on waiting queue.
self.state.push_head(core.cur_task)
Expand Down
2 changes: 1 addition & 1 deletion frozen/Adafruit_CircuitPython_RFM69
2 changes: 1 addition & 1 deletion frozen/Adafruit_CircuitPython_SD
2 changes: 1 addition & 1 deletion frozen/Adafruit_CircuitPython_Ticks
7 changes: 6 additions & 1 deletion ports/unix/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,10 @@ include $(TOP)/py/mkrules.mk

.PHONY: test test_full

TEST_EXTRA ?=
test: $(PROG) $(TOP)/tests/run-tests.py
$(eval DIRNAME=ports/$(notdir $(CURDIR)))
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py $(TEST_EXTRA)

test_full: $(PROG) $(TOP)/tests/run-tests.py
$(eval DIRNAME=ports/$(notdir $(CURDIR)))
Expand All @@ -309,6 +310,10 @@ test_full: $(PROG) $(TOP)/tests/run-tests.py
cd $(TOP)/tests && MICROPY_MICROPYTHON=../$(DIRNAME)/$(PROG) ./run-tests.py --via-mpy $(RUN_TESTS_MPY_CROSS_FLAGS) --emit native -d basics float micropython
cat $(TOP)/tests/basics/0prelim.py | ./$(PROG) | grep -q 'abc'

.PHONY: print-failures clean-failures
print-failures clean-failures:
../../tests/run-tests.py --$@

test_gcov: test_full
gcov -o $(BUILD)/py $(TOP)/py/*.c
gcov -o $(BUILD)/extmod $(TOP)/extmod/*.c
Expand Down
42 changes: 42 additions & 0 deletions tests/extmod/uasyncio_task_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# In MicroPython, a non-awaited task with a pending exception will raise to
# the loop's exception handler the second time it is scheduled. This is
# because without reference counting we have no way to know when the task is
# truly "non awaited" -- i.e. we only know that it wasn't awaited in the time
# it took to be re-scheduled.

# If the task _is_ subsequently awaited, then the await should succeed without
# raising.

try:
import uasyncio as asyncio
except ImportError:
try:
import asyncio
except ImportError:
print("SKIP")
raise SystemExit


def custom_handler(loop, context):
print("exception handler", type(context["exception"]).__name__)


async def main():
loop = asyncio.get_event_loop()
loop.set_exception_handler(custom_handler)

async def task():
print("raise")
raise OSError

print("create")
t = asyncio.create_task(task())
print("sleep 1")
await asyncio.sleep(0)
print("sleep 2")
await asyncio.sleep(0)
print("await")
await t # should not raise.


asyncio.run(main())
6 changes: 6 additions & 0 deletions tests/extmod/uasyncio_task_exception.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
create
sleep 1
raise
sleep 2
exception handler OSError
await
24 changes: 20 additions & 4 deletions tests/run-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,17 @@ def send_get(what):

# run the actual test
try:
output_mupy = subprocess.check_output(cmdlist, stderr=subprocess.STDOUT)
result = subprocess.run(
cmdlist,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
check=True,
timeout=10,
)
output_mupy = result.stdout
except subprocess.TimeoutExpired as er:
had_crash = True
output_mupy = (er.output or b"") + b"TIMEOUT"
except subprocess.CalledProcessError as er:
had_crash = True
output_mupy = er.output + b"CRASH"
Expand Down Expand Up @@ -869,9 +879,15 @@ def main():
tests = args.files

if not args.keep_path:
# clear search path to make sure tests use only builtin modules and those in extmod
os.environ["MICROPYPATH"] = os.pathsep + ".frozen" + os.pathsep + base_path("../extmod")

# clear search path to make sure tests use only builtin modules and those that can be frozen
os.environ["MICROPYPATH"] = os.pathsep.join(
[
"",
".frozen",
base_path("../frozen/Adafruit_CircuitPython_asyncio"),
base_path("../frozen/Adafruit_CircuitPython_Ticks"),
]
)
try:
os.makedirs(args.result_dir, exist_ok=True)
res = run_tests(pyb, tests, args, args.result_dir, args.jobs)
Expand Down