Skip to main content

Tests & benchs for python

Project description

CoTests

cotests is a light set of tests and benchmarks for python. The main goal is ease of use.

Features

  • Python3.7+
  • Can run sync functions, coroutines, coroutinefunctions, classes
  • Convenient conversion between min, sec, ms, µs, etc.
  • Comparison table in benchmarks
  • Whole module test

DOX

test_batch() & arguments

Simple run all types of tests.

    # args
    :param funcs: all functions/cases/groups for test or benchmark
    # kwargs (all optional)
    :param str name: Title for test
    :param Iterable global_args: arguments for each function
    :param Mapping global_kwargs: keyword arguments for each function (can merge with own keyword arguments)
    :param Iterable[Iterable] personal_args: list of arguments for each function
    :param Iterable[Mapping] personal_kwargs: list of keyword arguments for each function
    :param Callable pre_test: run before each function; is not added to benchmark time
    :param Callable post_test: run after each function; is not added to benchmark time
    :return: None | Awaitable[None]

bench_batch()

Like test_batch()', but with output a results table, and you can set count of iterations - :param int iterations.

CoTestCase

A class for tests. By default, it runs all methods (including @classmethod or @staticmethod) starting with test_. It has methods (without name parameter; name taken from class name):

  • run_tests - is analog of test_batch
  • run_bench - is analog of bench_batch

CoTestGroup

The main entity - a class for group of tests. It can use all types of tests, as bench_batch(). It has go() for test and go_bench(int) for benchmark.

test_groups()

Function for run CoTestGroups.

test_module

Function for searching and running all tests in the module by directory path. Search for all files t_(.*).py and inside:

  • CoTestGroup objects. If found, other types are ignoring.
  • functions starting with test_
  • CoTestCase classes
import os
from cotests import test_module

dir_path = os.path.dirname(os.path.realpath(__file__))
test_module(dir_path)

Examples

Base using

from cotests import test_batch, bench_batch, CoTestGroup, CoTestCase, test_groups

def test_0(*_, **__): ...
def test_1(*_, **__): ...

async def test_a0(*_, **__): ...
async def test_a1(*_, **__): ...

# Example 1: just test
test_batch(test_0, test_1, test_a0, test_a1)
# Example 1.1: single-run benchmark
bench_batch(test_0, test_1, test_a0, test_a1)
# Example 1.2: benchmark
bench_batch(test_0, test_1, test_a0, test_a1, iterations=50)

# Example 2: CoTestCase
class Case0(CoTestCase):
    def test_0(self): ...
    def test_1(self): ...
    async def test_a0(self): ...
Case0().run_test()
# Example 2.1: benchmark
Case0().run_bench(iterations=50)

# Example 3: CoTestGroup
g_sync = CoTestGroup(test_0, test_1, name='SYNC')
g_async = CoTestGroup(test_a0, test_a1, name='ASYNC')
g_all = CoTestGroup(test_0, test_1, test_a0, test_a1, Case0, name='ALL')

# Example 3.1 - single group
g_sync.go()
# Example 3.2 - multiple
test_groups(g_sync, g_async, g_all)

# Example 4: ALL
test_batch(
    test_0, test_1, test_a0, test_a1,
    Case0,
    g_sync, g_async, g_all,
)

test_batch & bench_batch

from cotests import test_batch, bench_batch

def test_0(): ...
def test_1(): ...
def test_2(): ...

# just test
test_batch(test_0, test_1, test_2,)

# benchy
bench_batch(test_0, test_1, test_2, name='benchy')

# more benchy
bench_batch(
    test_0, test_1, test_2,
    iterations=1000,
    name='more benchy',
)

Output:

⌌-------------- Start CoTest --------------
¦  * test_0:ok - 360.000 ns
¦  * test_1:ok - 340.000 ns
¦  * test_2:ok - 229.998 ns
⌎-- Full time: 79.940 µs

⌌-------------- Start CoBench benchy--------------
¦  * test_0:.ok - 290.000 ns
¦  * test_1:.ok - 290.000 ns
¦  * test_2:.ok - 309.999 ns
¦  +-----------------------------+
¦  |    time    |   f    |   %   |
¦  | 290.000 ns | test_0 | 100.0 |
¦  | 290.000 ns | test_1 | 100.0 |
¦  | 309.999 ns | test_2 | 106.9 |
¦  +-----------------------------+
⌎-- Full time: 99.939 µs

⌌-------------- Start CoBench more benchy--------------
¦  * test_0:..................................................ok - 259.999 ns
¦  * test_1:..................................................ok - 309.999 ns
¦  * test_2:..................................................ok - 220.001 ns
¦  +--------------------------------------------------------------------+
¦  |    full    |    max     |    min     |    avg     |   f    |   %   |
¦  | 137.490 µs | 259.999 ns |  90.000 ns | 137.490 ns | test_0 | 106.0 |
¦  | 140.370 µs | 310.001 ns | 119.999 ns | 140.370 ns | test_1 | 108.2 |
¦  | 129.719 µs | 390.000 ns | 110.002 ns | 129.719 ns | test_2 | 100.0 |
¦  +--------------------------------------------------------------------+
⌎-- Full time: 3.798 ms

bench_batch: arguments

from cotests import test_batch

def test_0(*_, **__): ...
def test_1(*_, **__): ...
def test_2(*_, **__): ...
def test_3(*_, **__): ...
async def test_a0(*_, **__): ...
async def test_a1(*_, **__): ...

# just for convenience, all to 1 list
tests_list = [value for key, value in globals().items()
              if key.startswith('test_') and callable(value) and value.__module__ == __name__]

# test with args: like test_0(1), test_1(1), etc...
test_batch(
    *tests_list,
    global_args=(1,)
)

# test with kwargs: like test_0(a=1), test_1(a=1), etc...
test_batch(
    *tests_list,
    global_kwargs={'a': 1}
)

# It can be combined: like test_0(1, a=1), test_1(1, a=1), etc...
test_batch(
    *tests_list,
    global_args=(1,),
    global_kwargs={'a': 1}
)

# different ways to set test function & combo kwargs
test_batch(
    test_0,  # test_0()
    (test_1, (1, 2,)),  # test_1(1, 2)
    (test_2, {'a': 1}),  # test_2(a=1)
    (test_3, (1, 2), {'a': 1}),  # test_3(1, 2, a=1)
    # async
    test_a0,  # and other options above are available
    test_a0(),  # run like coroutine
    test_a1(1, 2, a=1),  # run like coroutine with arguments
)

# ... with personal args
# test_0(0, a=0), test_0(1, a=1), ..., test_0(5, a=5), test_1(0, a=0), test_0(1, a=1), ..., test_a1(5, a=5)
test_batch(
    *tests_list,
    personal_args=[(x,) for x in range(len(tests_list))],
    personal_kwargs=[{'a': x} for x in range(len(tests_list))],
)

CoTestCase

import asyncio
import time

from cotests import CoTestCase, test_batch


class TObj(CoTestCase):
    # test functions should start with "test_"
    def __init__(self): print('Init Case')
    def __del__(self): print('Del Case')

    # optional additional functions
    async def constructor(self):
        print('Additional constructor')
    def destructor(self):
        print('Additional destructor')
    def pre_test(self):
        print(' :)', end=' ')

    def test_0(self, t: float = .1): time.sleep(t)

    @staticmethod
    def test_1(t: float = .2): time.sleep(t)

    @classmethod
    def test_2(cls, t: float = .3): time.sleep(t)

    def function_no_test(self): ...  # will be ignored

    async def test_a0(self, t: float = .1): await asyncio.sleep(t)

    @classmethod
    async def test_a1(cls, t: float = .2): await asyncio.sleep(t)


TObj().run_test(
    global_args=(.1,),
)
# or
test_batch(
    TObj(),
    global_args=(.1,),
)
# or
test_batch(
    TObj,
    global_args=(.1,),
)

Partial output:

Init Case
Additional constructor

⌌-------------- Start CoTest TObj--------------
¦  * test_0: :) ok - 100.096 ms
¦  * test_1: :) ok - 100.087 ms
¦  * test_2: :) ok - 100.117 ms
¦  * test_a0: :) ok - 100.328 ms
¦  * test_a1: :) ok - 100.272 ms
⌎-- Full time: 501.606 ms
Additional destructor
Del Case

errors?

from cotests import test_batch, CoTestCase

def test_0(): ...
def test_1(): raise Exception('I want error!')

class T0(CoTestCase):
    def test_t0(self): ...
    def test_t1(self): raise ValueError('I want ValueError in case!')

test_batch(test_0, test_1, T0)
⌌-------------- Start CoTest --------------
¦  * test_0:ok - 350.003 ns
¦  * test_1:error: I want error!
¦ 
¦ ⌌-------------- Start CoTest T0--------------
¦ ¦  * test_t0:ok - 460.001 ns
¦ ¦  * test_t1:error: I want ValueError in case!
¦ ⌎-- Full time: 42.750 µs
⌎-- Full time: 139.400 µs
! Errors:
! * test_1 
!   Exception : I want error!
! * T0 / test_t1 
!   ValueError : I want ValueError in case!
⌎----------------------------

is orjson faster?

# content of tests/json.py
import json
import orjson
import os.path
from argparse import ArgumentParser
from cotests import bench_batch


def bench_json(file_path: str):
    with open(file_path, 'rb') as f:
        json.load(f)

def bench_orjson(file_path: str):
    with open(file_path, 'rb') as f:
        orjson.loads(f.read())


if __name__ == '__main__':
    parser = ArgumentParser()
    parser.add_argument('path_file')
    args = parser.parse_args()

    if not os.path.isfile(args.path_file):
        raise FileNotFoundError

    bench_batch(
        bench_json,
        bench_orjson,
        iterations=50,
        global_args={args.path_file},
    )

Run:

python3 -m tests.json /path/to/large-file.json

Output:

⌌-------------- Start CoBench --------------
¦  * bench_json:..................................................ok - 11.288 sec
¦  * bench_orjson:..................................................ok - 7.796 sec
¦  +--------------------------------------------------------------------------+
¦  |    full    |    max     |    min     |    avg     |      f       |   %   |
¦  | 11.288 sec | 235.821 ms | 208.986 ms | 225.759 ms | bench_json   | 144.8 |
¦  |  7.796 sec | 167.107 ms | 149.568 ms | 155.927 ms | bench_orjson | 100.0 |
¦  +--------------------------------------------------------------------------+
⌎-- Full time: 19.091 sec

async

import asyncio
import time
from cotests import test_batch, bench_batch


async def test_0(sleep_time: float = .02):
    await asyncio.sleep(sleep_time)
def test_1(sleep_time: float = .03):
    time.sleep(sleep_time)


if __name__ == '__main__':
    fun_async = (
        test_0,
        (test_0, (.15,)),  # set custom args
    )
    fun_sync = (
        test_1,
        (test_1, (.12,)),
    )

    test_batch(*fun_sync, name='ONLY SYNC')

    bench_batch(
        *fun_async,  # coroutinefunctions can reuse
        test_0(.05),  # coroutine with reuse - error
        iterations=2,
        name='ASYNC W/T LOOP',
    )

    async def main():
        # if `bench_batch()` with coroutines run in running loop, you need to use `await`
        await bench_batch(
            *fun_async,
            *fun_sync,
            test_0(.05),  # coroutine without reuse - ok
            name='ASYNC WITH LOOP',
        )
        # without coroutines = without await
        test_batch(*fun_sync, name='SYNC WITH LOOP',)
    asyncio.run(main())

PreTest & PostTest

import asyncio
import time
from cotests import bench_batch, CoTestGroup

def test_0(): print('T0', end='-')
def test_1(): print('T1', end='-')
async def atest_0(): print('T0', end='-')
async def atest_1(): print('T1', end='-')

async def rba():
    await asyncio.sleep(.1)
    print('B', end='~')

def rb():
    time.sleep(.1)
    print('B', end='-')
def ra():
    time.sleep(.1)
    print('A', end=' ')

tests = (test_0, test_1, atest_0, atest_1)

# groups to use in test_module()
g0 = CoTestGroup(*tests, pre_test=rb, post_test=ra, name='SYNC')
g1 = CoTestGroup(*tests, pre_test=rba, post_test=ra, name='ASYNC')

if __name__ == '__main__':
    bench_batch(g0, g1, iterations=3)

Partial output:

¦ ⌌-------------- Start CoBench SYNC--------------
¦ ¦  * test_0:B-T0-A .B-T0-A .B-T0-A .ok - 11.700 µs
¦ ¦  * test_1:B-T1-A .B-T1-A .B-T1-A .ok - 10.380 µs
¦ ¦  * atest_0:B-T0-A .B-T0-A .B-T0-A .ok - 51.610 µs
¦ ¦  * atest_1:B-T1-A .B-T1-A .B-T1-A .ok - 44.270 µs
...
¦ ⌌-------------- Start CoBench ASYNC--------------
¦ ¦  * test_0:B~T0-A .B~T0-A .B~T0-A .ok - 10.680 µs
¦ ¦  * test_1:B~T1-A .B~T1-A .B~T1-A .ok - 16.020 µs
¦ ¦  * atest_0:B~T0-A .B~T0-A .B~T0-A .ok - 22.160 µs
¦ ¦  * atest_1:B~T1-A .B~T1-A .B~T1-A .ok - 21.009 µs
...

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cotests-0.3.2.tar.gz (16.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cotests-0.3.2-py3-none-any.whl (18.2 kB view details)

Uploaded Python 3

File details

Details for the file cotests-0.3.2.tar.gz.

File metadata

  • Download URL: cotests-0.3.2.tar.gz
  • Upload date:
  • Size: 16.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.7.17

File hashes

Hashes for cotests-0.3.2.tar.gz
Algorithm Hash digest
SHA256 e488e9e2563b1e51b46e6ac049ea8b0caff14b6ef888c3b7aad29c15e5eb5239
MD5 8a312105cf1663434effe95c9156f672
BLAKE2b-256 64d82abf5c24fc425079daf48b7fbc1eb20f656819c7c7ac38553b3e66441178

See more details on using hashes here.

File details

Details for the file cotests-0.3.2-py3-none-any.whl.

File metadata

  • Download URL: cotests-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 18.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.7.17

File hashes

Hashes for cotests-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 217d0784ba984ca39e96226c0bf191c141f263e03a7453f8f65213e440fc5ffc
MD5 834432e449afb4cc676dcecda287381f
BLAKE2b-256 2323a36a6024626a06d5aaca6f564caa4cbb4c73f95bd7f6f026c95362570650

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page