rlgomes/pocha

Migrating from pocha to pytest

Opened this issue · 0 comments

I started using pocha 2 years ago shortly after it went out of active development. I just got around to migrating from it to pytest (with pytest-asyncio and pytest-mocha). In case someone else is in the same situation here is the script I wrote to migrate my test files. No citation needed for its use.

import os
import re
from collections import OrderedDict
from utilities import convert_to_camel_case
input_file = 'base.py'
input_file = os.path.join('test/', input_file)

with open(input_file) as f:
    input_data = f.read()


output = OrderedDict()
output['root'] = []
output['root'].append('import pytest')
skip_lines = 0
test_name = None
test_subsection = ['root']
has_before_each = {}
for line in input_data.split('\n'):
    # if a previous statement wants to skip lines then do it
    if skip_lines:
        skip_lines -= 1
        continue

    # ignore our custom decorator
    if 'make_sync_from_async' in line:
        continue

    # skip empty lines because they cause problems when deciding when to unindent
    line_sans_leading_spaces = re.sub(r'\s', '', line)
    if not line_sans_leading_spaces:
        output[test_subsection[-1]].append('')
        continue

    # ignore commented out code blocks
    if re.match(r'\s*#', line):
        output[test_subsection[-1]].append(line)
        continue

    # replace pocha with unitttest, but only once
    added_imports = False
    if 'from pocha import' in line:
        if not added_imports:
            output[test_subsection[-1]].append('from unittest import TestCase')
        continue

    # these values will be useful later
    extra_space, = re.match(r'(\s*)', line).groups()
    spacing = int(extra_space.count(' ')/4)
    describe_match = re.match(r".*@describe\('(.*)'\)", line)
    before_match = re.match('.*@before_each', line)
    after_match = re.match('.*@after_each', line)

    # replace describe statements
    if describe_match:
        class_name, = describe_match.groups()
        class_name = class_name.replace('tests', '').replace('test', '').strip().replace(' ', '_').capitalize()
        
        class_name = convert_to_camel_case(class_name)
        test_subsection.append(class_name)

        output[test_subsection[-1]] = []
        output[test_subsection[-1]].append('class Test' + class_name + '(TestCase):')

        mocha_name = ' :: '.join(test_subsection[1:])
        output[test_subsection[-1]].append("    '''" + mocha_name + "'''")
        skip_lines = 1

        continue

    # if the spacing decreases then we need to pop out of the subsection
    should_unindent = spacing < (len(test_subsection)-1)
    if should_unindent:
        test_subsection = test_subsection[:-1]

    # unittest doesn't use nesting like pocha does, so we need to remove spaces from the previous code
    indent = len(test_subsection)-2
    if indent > 0:
        extra_space = ' '*indent*4
        line = re.sub('^'+extra_space, '', line)

    
    # replace before_each statements
    if before_match or (after_match and test_subsection[-1] not in has_before_each):
        has_before_each[test_subsection[-1]] = True
        output[test_subsection[-1]].append('    @pytest.fixture(autouse=True)')
        output[test_subsection[-1]].append('    @pytest.mark.asyncio')
        output[test_subsection[-1]].append('    async def setup(self):')
        skip_lines = 2
    
    if before_match:
        continue

    # replace after_each statements
    if after_match:
        output[test_subsection[-1]].append('        try:')
        output[test_subsection[-1]].append('            yield')
        output[test_subsection[-1]].append('        except:')
        output[test_subsection[-1]].append('            pass')
        skip_lines = 1
        continue
    
    # replace it statements
    match = re.match(r".*@it\((.*)\)", line)
    if match:
        test_name, = match.groups()
        test_name = test_name.strip().strip("'").replace("\\'", '')
        # remove other arguments
        test_name = re.sub(r',.*skip=True.*', '', test_name)

        if 'skip=True' in line:
            output[test_subsection[-1]].append('    @pytest.mark.skip(reason="no way of testing this")')

        continue
    
    # modify the functions for it statements to support pytest-mocha
    if re.match('.*def _\(', line):
        if not test_name:
            continue

        if 'async def' in line:
            output[test_subsection[-1]].append('    @pytest.mark.asyncio')

        function_name = 'test_' + re.sub('[ -/,]', '_', test_name)
        
        line = re.sub(r'_\(.*', function_name + '(self):', line)
        # print(line)
        output[test_subsection[-1]].append(line)
        output[test_subsection[-1]].append("        '''{a}'''".format(a=test_name))
        test_name = None
        continue


    output[test_subsection[-1]].append(line)


output_path, output_file = os.path.split(input_file)
output_file = output_path + '/test_' + output_file
print('pytest -W ignore -k ' + input_file)
with open(output_file, 'w') as f:
    for k, v in output.items():
        f.write('\n'.join(v))
        f.write('\n\n')