Repository: tmontes/ppytty

Issue:
Improve kernel state...
Notes on having state as a module:
* Simple way to get global mutable state (do we want that?).
* Otherwise we would probably need to:
  * Either pass it around...
  * ...or build the scheduler (+ trap handlers + ...) under a common class with state as an attribute.

Wishlist:
* [x] We'll need to be able to "reset" it such that multiple scheduler runs can be tested independently.
* [ ] The name `state` is not that great...

Discussion:

Patch:
diff --git a/src/ppytty/kernel/common.py b/src/ppytty/kernel/common.py
index 83277d8..582b3d6 100644
--- a/src/ppytty/kernel/common.py
+++ b/src/ppytty/kernel/common.py
@@ -5,7 +5,7 @@
 # See LICENSE for details.
 # ----------------------------------------------------------------------------
 
-from . state import tasks, state
+from . state import state
 
 
 
@@ -17,37 +17,37 @@ def runnable_task(task):
 
 def clear_tasks_children(task):
 
-    if not tasks.children[task]:
-        del tasks.children[task]
+    if not state.child_tasks[task]:
+        del state.child_tasks[task]
 
 
 
 def clear_tasks_traps(task):
 
-    for target in (tasks.trap_calls, tasks.trap_results):
+    for target in (state.trap_calls, state.trap_results):
         if task in target:
             del target[task]
 
 
 
-def clear_tasks_waiting_on_time_hq():
+def clear_tasks_waiting_time_hq():
 
-    if not tasks.waiting_on_time:
-        tasks.waiting_on_time_hq.clear()
+    if not state.tasks_waiting_time:
+        state.tasks_waiting_time_hq.clear()
 
 
 
-# TODO: Do we need a "clear_tasks_waiting_on_key_hq"? Probably.
+# TODO: Do we need a "clear_tasks_waiting_key_hq"? Probably.
 
 
 
 def destroy_task_windows(task):
 
-    need_rerender = bool(tasks.windows[task])
+    need_rerender = bool(state.task_windows[task])
 
-    for window in tasks.windows[task]:
+    for window in state.task_windows[task]:
         state.all_windows.remove(window)
-    del tasks.windows[task]
+    del state.task_windows[task]
 
     if need_rerender:
         rerender_all_windows()
diff --git a/src/ppytty/kernel/scheduler.py b/src/ppytty/kernel/scheduler.py
index 9af548d..d96fdc6 100644
--- a/src/ppytty/kernel/scheduler.py
+++ b/src/ppytty/kernel/scheduler.py
@@ -12,7 +12,7 @@ import logging
 from . import hw
 from . import trap_handlers
 from . import common
-from . state import tasks, io_fds, state
+from . state import state
 from . terminal import Terminal
 
 
@@ -30,8 +30,7 @@ class _SchedulerStop(Exception):
 def run(task, post_prompt=None):
 
     with Terminal() as t:
-        state.terminal = t
-        io_fds.set_user_io(t.in_fd, t.out_fd)
+        state.reset_for_terminal(t)
         try:
             success, result = scheduler(task)
             while post_prompt:
@@ -46,17 +45,17 @@ def run(task, post_prompt=None):
 def scheduler(top_task):
 
     top_task = common.runnable_task(top_task)
-    tasks.top_task = top_task
-    tasks.runnable.append(top_task)
+    state.top_task = top_task
+    state.runnable_tasks.append(top_task)
 
-    while (tasks.runnable or tasks.waiting_on_child or tasks.waiting_on_key or
-           tasks.waiting_on_time or tasks.terminated):
+    while (state.runnable_tasks or state.tasks_waiting_child or state.tasks_waiting_key or
+           state.tasks_waiting_time or state.completed_tasks):
         state.now = hw.time_monotonic()
-        if not tasks.runnable:
-            process_tasks_waiting_on_key()
-            process_tasks_waiting_on_time()
+        if not state.runnable_tasks:
+            process_tasks_waiting_key()
+            process_tasks_waiting_time()
             continue
-        task = tasks.runnable.popleft()
+        task = state.runnable_tasks.popleft()
         try:
             trap = run_task_until_trap(task)
         except StopIteration as return_:
@@ -68,14 +67,14 @@ def scheduler(top_task):
         else:
             process_task_trap(task, trap)
 
-    return tasks.top_task_success, tasks.top_task_result
+    return state.top_task_success, state.top_task_result
 
 
 
 def process_task_trap(task, trap):
 
     log.debug('%r trap: %r', task, trap)
-    tasks.trap_calls[task] = trap
+    state.trap_calls[task] = trap
     trap_name, *trap_args = trap
     trap_handler_name = trap_name.replace("-", "_")
     try:
@@ -94,8 +93,8 @@ def process_task_trap(task, trap):
 
 def run_task_until_trap(task):
 
-    prev_trap_call = tasks.trap_calls.get(task)
-    prev_trap_result = tasks.trap_results.get(task)
+    prev_trap_call = state.trap_calls.get(task)
+    prev_trap_result = state.trap_results.get(task)
     if prev_trap_call is not None:
         log.debug('%r trap %r result: %r', task, prev_trap_call, prev_trap_result)
     elif prev_trap_result is not None:
@@ -106,59 +105,59 @@ def run_task_until_trap(task):
         return task.send(prev_trap_result)
     finally:
         if prev_trap_call:
-            del tasks.trap_calls[task]
+            del state.trap_calls[task]
         if prev_trap_result:
-            del tasks.trap_results[task]
+            del state.trap_results[task]
 
 
 
-def process_tasks_waiting_on_key(keyboard_byte=None):
+def process_tasks_waiting_key(keyboard_byte=None):
 
     if keyboard_byte is None:
         keyboard_byte = _read_keyboard(prompt='?')
 
-    if keyboard_byte and tasks.waiting_on_key:
-        while tasks.waiting_on_key_hq:
-            _, _, key_waiter = heapq.heappop(tasks.waiting_on_key_hq)
-            if key_waiter in tasks.waiting_on_key:
-                tasks.waiting_on_key.remove(key_waiter)
-                tasks.trap_results[key_waiter] = keyboard_byte
-                tasks.runnable.append(key_waiter)
+    if keyboard_byte and state.tasks_waiting_key:
+        while state.tasks_waiting_key_hq:
+            _, _, key_waiter = heapq.heappop(state.tasks_waiting_key_hq)
+            if key_waiter in state.tasks_waiting_key:
+                state.tasks_waiting_key.remove(key_waiter)
+                state.trap_results[key_waiter] = keyboard_byte
+                state.runnable_tasks.append(key_waiter)
                 log.info('%r getting key %r', key_waiter, keyboard_byte)
                 break
 
 
 
-def process_tasks_waiting_on_time():
+def process_tasks_waiting_time():
 
-    if tasks.waiting_on_time:
-        while tasks.waiting_on_time_hq and tasks.waiting_on_time_hq[0][0] < state.now:
-            _, _, time_waiter = heapq.heappop(tasks.waiting_on_time_hq)
-            if time_waiter in tasks.waiting_on_time:
-                tasks.waiting_on_time.remove(time_waiter)
-                common.clear_tasks_waiting_on_time_hq()
-                tasks.runnable.append(time_waiter)
+    if state.tasks_waiting_time:
+        while state.tasks_waiting_time_hq and state.tasks_waiting_time_hq[0][0] < state.now:
+            _, _, time_waiter = heapq.heappop(state.tasks_waiting_time_hq)
+            if time_waiter in state.tasks_waiting_time:
+                state.tasks_waiting_time.remove(time_waiter)
+                common.clear_tasks_waiting_time_hq()
+                state.runnable_tasks.append(time_waiter)
                 log.info('%r waking up', time_waiter)
 
 
 
 def process_task_completion(task, success, result):
 
-    candidate_parent = tasks.parent.get(task)
-    if not candidate_parent and task is not tasks.top_task:
+    candidate_parent = state.parent_task.get(task)
+    if not candidate_parent and task is not state.top_task:
         log.error('%r completed with no parent', task)
-    if candidate_parent in tasks.waiting_on_child:
-        tasks.trap_results[candidate_parent] = (task, success, result)
-        del tasks.parent[task]
-        tasks.children[candidate_parent].remove(task)
+    if candidate_parent in state.tasks_waiting_child:
+        state.trap_results[candidate_parent] = (task, success, result)
+        del state.parent_task[task]
+        state.child_tasks[candidate_parent].remove(task)
         common.clear_tasks_children(candidate_parent)
-        tasks.waiting_on_child.remove(candidate_parent)
-        tasks.runnable.append(candidate_parent)
-    elif task is not tasks.top_task:
-        tasks.terminated.append((task, success, result))
+        state.tasks_waiting_child.remove(candidate_parent)
+        state.runnable_tasks.append(candidate_parent)
+    elif task is not state.top_task:
+        state.completed_tasks.append((task, success, result))
     else:
-        tasks.top_task_success = success
-        tasks.top_task_result = result
+        state.top_task_success = success
+        state.top_task_result = result
     common.clear_tasks_traps(task)
     common.destroy_task_windows(task)
 
@@ -188,17 +187,17 @@ def _read_keyboard(prompt=None):
 
     quit_in_progress = False
 
-    if tasks.waiting_on_time:
-        timeout = max(tasks.waiting_on_time_hq[0][0] - state.now, 0)
+    if state.tasks_waiting_time:
+        timeout = max(state.tasks_waiting_time_hq[0][0] - state.now, 0)
     else:
         timeout = None
     save_timeout = timeout
     while True:
         actual_prompt = 'QUIT?' if quit_in_progress else prompt
         with _prompt_context(actual_prompt):
-            fds, _, _ = hw.select_select(io_fds.input, _NO_FDS, _NO_FDS, timeout)
-        if io_fds.user_in in fds:
-            keyboard_byte = hw.os_read(io_fds.user_in, 1)
+            fds, _, _ = hw.select_select(state.in_fds, _NO_FDS, _NO_FDS, timeout)
+        if state.user_in_fd in fds:
+            keyboard_byte = hw.os_read(state.user_in_fd, 1)
             if keyboard_byte == b'q':
                 if quit_in_progress:
                     raise _SchedulerStop()
diff --git a/src/ppytty/kernel/state.py b/src/ppytty/kernel/state.py
index d713e31..7d077ab 100644
--- a/src/ppytty/kernel/state.py
+++ b/src/ppytty/kernel/state.py
@@ -6,116 +6,99 @@
 # ----------------------------------------------------------------------------
 
 import collections
-import types
 
 
 
-tasks = types.SimpleNamespace(
+class _State(object):
 
-    # The task given to the scheduler, to be run.
-    top_task = None,
+    def __init__(self):
 
-    # True if top_task completed, False otherwise (exception or interrupted).
-    top_task_success = None,
+        # ---------------------------------------------------------------------
+        # Task state tracking.
 
-    # Exception if top_task_success is False, else whatever top_task returned.
-    top_task_result = None,
+        # The task given to the scheduler, to be run.
+        self.top_task = None
 
-    # Runnable tasks queue.
-    runnable = collections.deque(),
+        # True if top_task completed, False otherwise (exception or interrupted).
+        self.top_task_success = None
 
-    # Terminated tasks will be here until their parent task waits on them.
-    terminated = [],
+        # Exception if top_task_success is False, else whatever top_task returned.
+        self.top_task_result = None
 
-    # Keys: Tasks, Values: Their parent Task.
-    parent = {},
-    # Keys: Tasks, Values: List of child Tasks, if any.
-    children = collections.defaultdict(list),
+        # Runnable tasks queue.
+        self.runnable_tasks = collections.deque()
 
-    # Tasks waiting on children.
-    waiting_on_child = [],
+        # Completed tasks will be here until their parent task waits on them.
+        self.completed_tasks = []
 
-    # Tasks waiting on keyboard input, and the associated priority queue.
-    waiting_on_key = [],
-    waiting_on_key_hq = [],
+        # Keys: Tasks, Values: Their parent Task.
+        self.parent_task = {}
+        # Keys: Tasks, Values: List of child Tasks, if any.
+        self.child_tasks = collections.defaultdict(list)
 
-    # Tasks sleeping, and the associated priority queue.
-    waiting_on_time = [],
-    waiting_on_time_hq = [],
+        # Tasks waiting on children.
+        self.tasks_waiting_child = []
 
+        # Tasks waiting on keyboard input, and the associated priority queue.
+        self.tasks_waiting_key = []
+        self.tasks_waiting_key_hq = []
 
-    # Keys: Tasks, Values: Their current trap, if any.
-    trap_calls = {},
-    # Keys: Tasks, Values: The value to return to the task, if any.
-    trap_results = {},
+        # Tasks sleeping, and the associated priority queue.
+        self.tasks_waiting_time = []
+        self.tasks_waiting_time_hq = []
 
+        # ---------------------------------------------------------------------
+        # Task trap tracking.
 
-    # Keys: Tasks, Values: List of Task created Windows.
-    windows = collections.defaultdict(list),
-)
+        # Keys: Tasks, Values: Their current trap, if any.
+        self.trap_calls = {}
+        # Keys: Tasks, Values: The value to return to the task, if any.
+        self.trap_results = {}
 
+        # ---------------------------------------------------------------------
+        # Task owned objects.
 
+        # Keys: Tasks, Values: List of Task created Windows.
+        self.task_windows = collections.defaultdict(list)
 
-class io_fds(object):
+        # Task created Window list in back to front rendering order.
+        self.all_windows = []
 
-    input = []
-    output = []
-    user_in = None
-    user_out = None
+        # ---------------------------------------------------------------------
+        # I/O file descriptors.
 
-    # Would be more Pythonic with class-level properties for `user_in` and
-    # `user_out`: that would require a meta-class and maybe that's too much
-    # for such a simple, single-use, use case.
+        self.in_fds = []
+        self.out_fds = []
+        self.user_in_fd = None
+        self.user_out_fd = None
 
-    @classmethod
-    def set_user_io(cls, in_fd, out_fd):
-        cls.user_in = in_fd
-        cls.input.append(in_fd)
-        cls.user_out = out_fd
-        cls.output.append(out_fd)
+        # ---------------------------------------------------------------------
+        # Environment.
 
+        # Global time.
+        self.now = None
 
+        # Interactive input/output user terminal.
+        self.terminal = None
 
-state = types.SimpleNamespace(
 
-    # Global time.
-    now = None,
+    def reset(self):
 
-    # Interactive input/output user terminal.
-    terminal = None,
+        self.__init__()
 
-    # Task created Window list in back to front rendering order.
-    all_windows = [],
-)
 
+    def reset_for_terminal(self, terminal):
 
+        self.__init__()
+        self.terminal = terminal
+        self.user_in_fd = terminal.in_fd
+        self.in_fds.append(terminal.in_fd)
+        self.user_out_fd = terminal.out_fd
+        self.out_fds.append(terminal.out_fd)
 
-def reset():
 
-    tasks.top_task = None
-    tasks.top_task_success = None
-    tasks.top_task_result = None
-    tasks.runnable = collections.deque()
-    tasks.terminated = []
-    tasks.parent = {}
-    tasks.children = collections.defaultdict(list)
-    tasks.waiting_on_child = []
-    tasks.waiting_on_key = []
-    tasks.waiting_on_key_hq = []
-    tasks.waiting_on_time = []
-    tasks.waiting_on_time_hq = []
-    tasks.trap_calls = {}
-    tasks.trap_results = {}
-    tasks.windows = collections.defaultdict(list)
 
-    io_fds.input = []
-    io_fds.output = []
-    io_fds.user_in = None
-    io_fds.user_out = None
-
-    state.now = None
-    state.terminal = None
-    state.all_windows = []
+state = _State()
 
 
 # ----------------------------------------------------------------------------
diff --git a/src/ppytty/kernel/trap_handlers.py b/src/ppytty/kernel/trap_handlers.py
index eaf8c32..b7fb015 100644
--- a/src/ppytty/kernel/trap_handlers.py
+++ b/src/ppytty/kernel/trap_handlers.py
@@ -11,7 +11,7 @@ import logging
 
 from . import common
 from . import scheduler
-from . state import tasks, state, io_fds
+from . state import state
 from . window import Window
 
 
@@ -23,14 +23,14 @@ log = logging.getLogger(__name__)
 def direct_clear(task):
 
     state.terminal.direct_clear()
-    tasks.runnable.append(task)
+    state.runnable_tasks.append(task)
 
 
 
 def direct_print(task, *args):
 
     state.terminal.direct_print(*args)
-    tasks.runnable.append(task)
+    state.runnable_tasks.append(task)
 
 
 
@@ -43,20 +43,20 @@ def window_create(task, left, top, width, height, bg=None):
         log.debug('%r traceback', task, exc_info=True)
         w = None
     else:
-        tasks.windows[task].append(w)
+        state.task_windows[task].append(w)
         state.all_windows.append(w)
 
-    tasks.trap_results[task] = w
-    tasks.runnable.append(task)
+    state.trap_results[task] = w
+    state.runnable_tasks.append(task)
 
 
 
 def window_destroy(task, window):
 
-    if not window in tasks.windows[task]:
+    if not window in state.task_windows[task]:
         raise RuntimeError('cannot destroy non-owned windows')
 
-    tasks.windows[task].remove(window)
+    state.task_windows[task].remove(window)
     state.all_windows.remove(window)
 
     # One window is gone: need to re-render everything to account for it.
@@ -65,13 +65,13 @@ def window_destroy(task, window):
     # - Just clear needed terminal lines and rerender overlapping windows.
     common.rerender_all_windows()
 
-    tasks.runnable.append(task)
+    state.runnable_tasks.append(task)
 
 
 
 def window_render(task, window, full=False):
 
-    if not window in tasks.windows[task]:
+    if not window in state.task_windows[task]:
         raise RuntimeError('cannot render non-owned windows')
 
     # Render `window` and all other windows that:
@@ -94,84 +94,84 @@ def window_render(task, window, full=False):
 
     state.terminal.render()
 
-    tasks.runnable.append(task)
+    state.runnable_tasks.append(task)
 
 
 
 def sleep(task, seconds):
 
     wake_at = state.now + seconds
-    tasks.waiting_on_time.append(task)
-    heapq.heappush(tasks.waiting_on_time_hq, (wake_at, id(task), task))
+    state.tasks_waiting_time.append(task)
+    heapq.heappush(state.tasks_waiting_time_hq, (wake_at, id(task), task))
 
 
 
 def read_key(task, priority):
 
-    tasks.waiting_on_key.append(task)
-    heapq.heappush(tasks.waiting_on_key_hq, (priority, id(task), task))
+    state.tasks_waiting_key.append(task)
+    heapq.heappush(state.tasks_waiting_key_hq, (priority, id(task), task))
 
 
 
 def put_key(task, pushed_back_key):
 
-    scheduler.process_tasks_waiting_on_key(pushed_back_key)
-    tasks.runnable.append(task)
+    scheduler.process_tasks_waiting_key(pushed_back_key)
+    state.runnable_tasks.append(task)
 
 
 
 def task_spawn(task, child_task):
 
     child_task = common.runnable_task(child_task)
-    tasks.parent[child_task] = task
-    tasks.children[task].append(child_task)
-    tasks.runnable.append(child_task)
-    tasks.runnable.append(task)
+    state.parent_task[child_task] = task
+    state.child_tasks[task].append(child_task)
+    state.runnable_tasks.append(child_task)
+    state.runnable_tasks.append(task)
 
 
 
 def task_wait(task):
 
     child = None
-    for candidate, success, result in tasks.terminated:
-        if tasks.parent[candidate] is task:
+    for candidate, success, result in state.completed_tasks:
+        if state.parent_task[candidate] is task:
             child = candidate
             break
     if child is not None:
-        tasks.trap_results[task] = (child, success, result)
-        del tasks.parent[child]
-        tasks.children[task].remove(child)
+        state.trap_results[task] = (child, success, result)
+        del state.parent_task[child]
+        state.child_tasks[task].remove(child)
         common.clear_tasks_children(task)
-        tasks.runnable.append(task)
-        tasks.terminated.remove((child, success, result))
+        state.runnable_tasks.append(task)
+        state.completed_tasks.remove((child, success, result))
         common.clear_tasks_traps(child)
     else:
-        tasks.waiting_on_child.append(task)
+        state.tasks_waiting_child.append(task)
 
 
 
 def task_destroy(task, child_task, keep_running=True):
 
-    if tasks.parent[child_task] is not task:
+    if state.parent_task[child_task] is not task:
         raise RuntimeError('cannot kill non-child tasks')
 
-    if child_task in tasks.children:
-        for grand_child_task in tasks.children[child_task]:
+    if child_task in state.child_tasks:
+        for grand_child_task in state.child_tasks[child_task]:
             task_destroy(child_task, grand_child_task, keep_running=False)
-            del tasks.parent[grand_child_task]
-        del tasks.children[child_task]
-
-    if child_task in tasks.runnable:
-        tasks.runnable.remove(child_task)
-    elif child_task in tasks.waiting_on_child:
-        tasks.waiting_on_child.remove(child_task)
-    elif child_task in tasks.waiting_on_key:
-        tasks.waiting_on_key.remove(child_task)
-    elif child_task in tasks.waiting_on_time:
-        tasks.waiting_on_time.remove(child_task)
-        common.clear_tasks_waiting_on_time_hq()
+            del state.parent_task[grand_child_task]
+        del state.child_tasks[child_task]
+
+    if child_task in state.runnable_tasks:
+        state.runnable_tasks.remove(child_task)
+    elif child_task in state.tasks_waiting_child:
+        state.tasks_waiting_child.remove(child_task)
+    elif child_task in state.tasks_waiting_key:
+        state.tasks_waiting_key.remove(child_task)
+    elif child_task in state.tasks_waiting_time:
+        state.tasks_waiting_time.remove(child_task)
+        common.clear_tasks_waiting_time_hq()
     else:
-        terminated = [t for (t, _, _) in tasks.terminated if t is child_task]
+        terminated = [t for (t, _, _) in state.completed_tasks if t is child_task]
         if terminated:
             log.error('%r will not stop terminated task %r', task, child_task)
         return
@@ -179,8 +179,8 @@ def task_destroy(task, child_task, keep_running=True):
     common.clear_tasks_traps(child_task)
     common.destroy_task_windows(child_task)
     if keep_running:
-        tasks.terminated.append((child_task, False, ('destroyed-by', task)))
-        tasks.runnable.append(task)
+        state.completed_tasks.append((child_task, False, ('destroyed-by', task)))
+        state.runnable_tasks.append(task)
         log.info('%r destroyed by %r', child_task, task)
     else:
         log.info('%r destroyed from parent %r destroy', child_task, task)
@@ -192,15 +192,15 @@ _SEPARATOR = '-' * 60
 def state_dump(task, tag=''):
 
     def _task_status(task):
-        if task in tasks.runnable:
+        if task in state.runnable_tasks:
             return 'RR'
-        if task in tasks.waiting_on_child:
+        if task in state.tasks_waiting_child:
             return 'WC'
-        if task in tasks.waiting_on_key:
+        if task in state.tasks_waiting_key:
             return 'WK'
-        if task in tasks.waiting_on_time:
+        if task in state.tasks_waiting_time:
             return 'WT'
-        if task in (t for (t, _, _) in tasks.terminated):
+        if task in (t for (t, _, _) in state.completed_tasks):
             return 'TT'
         return '??'
 
@@ -208,8 +208,8 @@ def state_dump(task, tag=''):
         indent = ' ' * 4 * level
         status = _task_status(task)
         log.critical(f'{status} {indent}{task}')
-        if task in tasks.children:
-            for child in tasks.children[task]:
+        if task in state.child_tasks:
+            for child in state.child_tasks[task]:
                 _log_task_lines(child, level+1)
 
     def _log_object_vars(name, obj):
@@ -223,15 +223,13 @@ def state_dump(task, tag=''):
     tag_string = f'{tag} | ' if tag else ''
 
     log.critical(f'-[ {tag_string}DUMP STATE | TASK HIERARCHY ]'.ljust(60, '-'))
-    _log_task_lines(tasks.top_task)
+    _log_task_lines(state.top_task)
     log.critical(f'-[ {tag_string}DUMP STATE | DATA STRUCTURES ]'.ljust(60, '-'))
-    _log_object_vars('tasks', tasks)
     _log_object_vars('state', state)
-    _log_object_vars('io_fds', io_fds)
     log.critical(f'-[ {tag_string}DUMP STATE | DONE ]'.ljust(60, '-'))
 
     if task is not None:
-        tasks.runnable.append(task)
+        state.runnable_tasks.append(task)
 
 
 # ----------------------------------------------------------------------------


Test patch:
diff --git a/tests/kernel/test_state.py b/tests/kernel/test_state.py
index 7207ee2..2395bb0 100644
--- a/tests/kernel/test_state.py
+++ b/tests/kernel/test_state.py
@@ -7,7 +7,7 @@
 
 import unittest
 
-from ppytty.kernel import state
+from ppytty.kernel.state import state
 
 
 
@@ -65,35 +65,29 @@ def _change_dict(object, attr_name):
 #   Keys: object attribute name
 #   Values: 2-tuple with (is_clear_assertion, change_its_value_callable)
 
-STATE_OBJECTS = {
-    'tasks': {
-        'top_task': (_assert_is_none, _change_scalar),
-        'top_task_success': (_assert_is_none, _change_scalar),
-        'top_task_result': (_assert_is_none, _change_scalar),
-        'runnable': (_assert_empty_list, _change_list),
-        'terminated': (_assert_empty_list, _change_list),
-        'parent': (_assert_empty_dict, _change_dict),
-        'children': (_assert_empty_dict, _change_dict),
-        'waiting_on_child': (_assert_empty_list, _change_list),
-        'waiting_on_key': (_assert_empty_list, _change_list),
-        'waiting_on_key_hq': (_assert_empty_list, _change_list),
-        'waiting_on_time': (_assert_empty_list, _change_list),
-        'waiting_on_time_hq': (_assert_empty_list, _change_list),
-        'trap_calls': (_assert_empty_dict, _change_dict),
-        'trap_results': (_assert_empty_dict, _change_dict),
-        'windows': (_assert_empty_dict, _change_dict),
-    },
-    'io_fds': {
-        'input': (_assert_empty_list, _change_list),
-        'output': (_assert_empty_list, _change_list),
-        'user_in': (_assert_is_none, _change_scalar),
-        'user_out': (_assert_is_none, _change_scalar),
-    },
-    'state': {
-        'now': (_assert_is_none, _change_scalar),
-        'terminal': (_assert_is_none, _change_scalar),
-        'all_windows': (_assert_empty_list, _change_list),
-    }
+STATE_ATTRS = {
+    'top_task': (_assert_is_none, _change_scalar),
+    'top_task_success': (_assert_is_none, _change_scalar),
+    'top_task_result': (_assert_is_none, _change_scalar),
+    'runnable_tasks': (_assert_empty_list, _change_list),
+    'completed_tasks': (_assert_empty_list, _change_list),
+    'parent_task': (_assert_empty_dict, _change_dict),
+    'child_tasks': (_assert_empty_dict, _change_dict),
+    'tasks_waiting_child': (_assert_empty_list, _change_list),
+    'tasks_waiting_key': (_assert_empty_list, _change_list),
+    'tasks_waiting_key_hq': (_assert_empty_list, _change_list),
+    'tasks_waiting_time': (_assert_empty_list, _change_list),
+    'tasks_waiting_time_hq': (_assert_empty_list, _change_list),
+    'trap_calls': (_assert_empty_dict, _change_dict),
+    'trap_results': (_assert_empty_dict, _change_dict),
+    'task_windows': (_assert_empty_dict, _change_dict),
+    'all_windows': (_assert_empty_list, _change_list),
+    'in_fds': (_assert_empty_list, _change_list),
+    'out_fds': (_assert_empty_list, _change_list),
+    'user_in_fd': (_assert_is_none, _change_scalar),
+    'user_out_fd': (_assert_is_none, _change_scalar),
+    'now': (_assert_is_none, _change_scalar),
+    'terminal': (_assert_is_none, _change_scalar),
 }
 
 
@@ -148,28 +142,19 @@ class TestRun(unittest.TestCase):
 
     def test_state_attrs_are_these(self):
 
-        for attr_name, attr_dict in STATE_OBJECTS.items():
-            state_object = getattr(state, attr_name)
-            with self.subTest(attr_name=attr_name):
-                _assert_attrs_are_these(attr_name, state_object, attr_dict.keys())
+        _assert_attrs_are_these('state', state, STATE_ATTRS.keys())
 
 
     def test_state_attrs_are_clean(self):
 
-        for attr_name, attr_dict in STATE_OBJECTS.items():
-            state_object = getattr(state, attr_name)
-            with self.subTest(attr_name=attr_name):
-                _assert_attrs_are_clean(attr_name, state_object, attr_dict)
+        _assert_attrs_are_clean('state', state, STATE_ATTRS)
 
 
     def test_state_reset(self):
 
-        for attr_name, attr_dict in STATE_OBJECTS.items():
-            state_object = getattr(state, attr_name)
-            with self.subTest(attr_name=attr_name):
-                _change_all_attrs(attr_name, state_object, attr_dict)
-                state.reset()
-                _assert_attrs_are_clean(attr_name, state_object, attr_dict)
+        _change_all_attrs('state', state, STATE_ATTRS)
+        state.reset()
+        _assert_attrs_are_clean('state', state, STATE_ATTRS)
 
 
 # ----------------------------------------------------------------------------