/moler

Moler – library to help in building automated tests

Primary LanguagePythonBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

Build Status Coverage Status BCH compliance Codacy Badge License

Table of Contents

  1. Moler
  2. API design reasoning
  3. Designed API

Moler

Moler is library to help in building automated tests. Name is coined by Grzegorz Latuszek with high impact of Bartosz Odziomek, Michał Ernst and Mateusz Smet.

Moler comes from: moler_origin

  • Mole 🇬🇧
    • has tunnel-connections to search for data (:bug:) it is processing
    • can detect different bugs hidden under ground
    • as we want this library to search/detect bugs in tested software
  • Moler in spanish 🇪🇸 means:
    • grind, reduce to powder
    • as this library should grind tested software to find it's bugs

Moler key features

  • Event observers & callbacks (alarms are events example)
    • to allow for online reaction (not offline postprocessing)
  • Commands as self-reliant object
    • to allow for command triggering and parsing encapsulated in single object (lower maintenance cost)
  • Run observers/commands in the background
    • to allow for test logic decomposition into multiple commands running in parallel
    • to allow for handling unexpected system behavior (reboots, alarms)
  • State machines -> automatic auto-connecting after dropped connection
    • to increase framework auto-recovery and help in troubleshooting "what went wrong"
  • Automatic logging of all connections towards devices used by tests
    • to decrease investigation time by having logs focused on different parts of system under test

Library content

Library provides "bricks" for building automated tests:

  • have clearly defined responsibilities
  • have similar API
  • follow same construction pattern (so new ones are easy to create)

API design reasoning

The main goal of command is its usage simplicity: just run it and give me back its result.

Command hides from its caller:

  • a way how it realizes "runs"
  • how it gets data of output to be parsed
  • how it parses that data

Command shows to its caller:

  • API to start/stop it or await for its completion
  • API to query for its result or result readiness

Command works as Futures and promises

After starting, we await for its result which is parsed out command output provided usually as dict. Running that command and parsing its output may take some time, so till that point result computation is yet incomplete.

Command as future

  • it starts some command on device/shell over connection (as future-function starts it's execution)
  • it parses data incoming over such connection (as future-function does it's processing)
  • it stores result of that parsing (as future-function concludes in calculation result)
  • it provides means to return that result (as future-function does via 'return' or 'yield' statement)
  • it's result is not ready "just-after" calling command (as it is with future in contrast to function)

So command should have future API.

Quote from "Professional Python" by Luke Sneeringer:

The Future is a standalone object. It is independent of the actual function that is running. It does nothing but store the state and result information.

Command differs in that it is both:

  • function-like object performing computation
  • future-like object storing result of that computation.

Command vs. Connection-observer

Command is just "active version" of connection observer.

Connection observer is passive since it just observes connection for some data; data that may just asynchronously appear (alarms, reboots or anything you want). Intention here is split of responsibility: one observer is looking for alarms, another one for reboots.

Command is active since it actively triggers some output on connection by sending command-string over that connection. So, it activates some action on device-behind-connection. That action is "command" in device terminology. Like ping on bash console/device. And it produces that "command" output. That output is what Moler's Command as connection-observer is looking for.

Most well known Python's futures

API concurrent.futures.Future asyncio.Future
storing result set_result() set_result()
result retrieval result() result()
storing failure cause set_exception() set_exception()
failure cause retrieval exception() exception()
stopping cancel() cancel()
check if stopped cancelled() cancelled()
check if running running() 🚫 (but AbstractEventLoop.running())
check if completed done() done()
subscribe completion add_done_callback() add_done_callback()
unsubscribe completion 🚫 remove_done_callback()

Starting callable to be run "as future" is done by entities external to future-object

API concurrent.futures
start via Executor objects (thread/proc)
asyncio
start via module-lvl functions or ev-loop
start callable submit(fn, *args, **kwargs)
Schedules callable to be executed as
fn(*args **kwargs) -> Future
ensure_future(coro_or_future) -> Task
future = run_coroutine_threadsafe(coro, loop)
start same callable
on data iterator
map(fn, *iterables, timeout) -> iterator join_future = asyncio.gather(*map(f, iterable))
loop.run_until_complete(join_future)

Awaiting completion of future is done by entities external to future-object

API concurrent.futures
awaiting by module level functions
asyncio
awaiting by module-lvl functions or ev-loop
await completion done, not_done = wait(futures, timeout) -> futures done, not_done = await wait(futures)
results = await gather(futures)
result = await future
result = yield from future
result = await coroutine
result = yield from coroutine
result = yield from wait_for(future, timeout)
loop.run_until_complete(future) -> blocking run
process as they
complete
for done in as_completed(futures, timeout) -> futures for done in as_completed(futures, timeout) -> futures

Fundamental difference of command

Contrary to concurrent.futures and asyncio we don't want command to be run by some external entity. We want it to be self-executable for usage simplicity. We want to take command and just say to it:

  • "run" or "run in background"
  • and not "Hi, external runner, would you run/run-background that command for me"

Designed API

  1. create command object
command = Command()
  1. run it synchronously/blocking and get result in one shot behaves like function call since Command is callable.

Run-as-callable gives big advantage since it fits well in python ecosystem.

result = command()

function example:

map(ping_cmd, all_machines_to_validate_reachability)
  1. run it asynchronously/nonblocking
command_as_future = command.start()
  1. shift from background to foreground

asyncio: variant looks like:

result = await future
done_futures, pending = yield from asyncio.wait(futures)
result = yield from asyncio.wait_for(future, 60.0)

and concurrent.futures variant looks like:

done_futures, pending = wait(futures)

Moler's API maps to above well-known API

result = command.await_done(timeout)
  • it is "internal" to command "Hi command, that is what I want from you" (above APIs say "Hi you there, that is what I want you to do with command")
  • it directly (Zen of Python) shows what we are awaiting for
  • timeout is required parameter (not as in concurrent.futures) since we don't expect endless execution of command (user must know what is worst case timeout to await command completion)