Skip to content
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

Add helpers for testing grids #68

Closed
mtbrock opened this issue Oct 7, 2019 · 2 comments · Fixed by #83
Closed

Add helpers for testing grids #68

mtbrock opened this issue Oct 7, 2019 · 2 comments · Fixed by #83
Assignees

Comments

@mtbrock
Copy link
Contributor

mtbrock commented Oct 7, 2019

There should be a base testing class for testing webrid grids. We use the following in one of our projects:

import re
import urllib.parse

from blazeutils.spreadsheets import workbook_to_reader
import flask
import flask_login
from pyquery import PyQuery
import pytest
import sqlalchemy


def query_to_str(statement, bind=None):
    """This function is copied directly from sqlalchemybwc.lib.testing

        returns a string of a sqlalchemy.orm.Query with parameters bound
        WARNING: this is dangerous and ONLY for testing, executing the results
        of this function can result in an SQL Injection attack.
    """
    if isinstance(statement, sqlalchemy.orm.Query):
        if bind is None:
            bind = statement.session.get_bind()
        statement = statement.statement
    elif bind is None:
        bind = statement.bind

    if bind is None:
        raise Exception('bind param (engine or connection object) required when using with an '
                        'unbound statement')

    dialect = bind.dialect
    compiler = statement._compiler(dialect)

    class LiteralCompiler(compiler.__class__):
        def visit_bindparam(
                self, bindparam, within_columns_clause=False,
                literal_binds=False, **kwargs
        ):
            return super(LiteralCompiler, self).render_literal_bindparam(
                bindparam, within_columns_clause=within_columns_clause,
                literal_binds=literal_binds, **kwargs
            )

    compiler = LiteralCompiler(dialect, statement)
    return 'TESTING ONLY BIND: ' + compiler.process(statement)


class GridBase(object):
    grid_cls = None
    filters = ()
    sort_tests = ()

    @classmethod
    def setup_class(cls):
        cls.user = User.testing_create()

        if hasattr(cls, 'init'):
            cls.init()

    def assert_in_query(self, look_for, **kwargs):
        pg = self.get_session_grid(**kwargs)
        query_str = query_to_str(pg.build_query())
        assert look_for in query_str, '"{0}" not found in: {1}'.format(look_for, query_str)

    def assert_not_in_query(self, look_for, **kwargs):
        pg = self.get_session_grid(**kwargs)
        query_str = query_to_str(pg.build_query())
        assert look_for not in query_str, '"{0}" found in: {1}'.format(look_for, query_str)

    def assert_regex_in_query(self, look_for, **kwargs):
        pg = self.get_session_grid(**kwargs)
        query_str = query_to_str(pg.build_query())

        if hasattr(look_for, 'search'):
            assert look_for.search(query_str), \
                '"{0}" not found in: {1}'.format(look_for.pattern, query_str)
        else:
            assert re.search(look_for, query_str), \
                '"{0}" not found in: {1}'.format(look_for, query_str)

    def get_session_grid(self, *args, **kwargs):
        flask_login.login_user(kwargs.pop('user', self.user), force=True)
        g = self.grid_cls(*args, **kwargs)
        g.apply_qs_args()
        return g

    def get_pyq(self, grid=None, **kwargs):
        pg = grid or self.get_session_grid(**kwargs)
        html = pg.html()
        return PyQuery('<html>{0}</html>'.format(html))

    def get_sheet(self, grid=None, **kwargs):
        pg = grid or self.get_session_grid(**kwargs)
        xls = pg.xls()
        return workbook_to_reader(xls).sheet_by_index(0)

    def check_filter(self, name, op, value, expected):
        qs_args = [('op({0})'.format(name), op)]
        if isinstance(value, (list, tuple)):
            for v in value:
                qs_args.append(('v1({0})'.format(name), v))
        else:
            qs_args.append(('v1({0})'.format(name), value))

        def sub_func(ex):
            url = '/?' + urllib.parse.urlencode(qs_args)
            with flask.current_app.test_request_context(url):
                if isinstance(ex, re.compile('').__class__):
                    self.assert_regex_in_query(ex)
                else:
                    self.assert_in_query(ex)
                self.get_pyq()  # ensures the query executes and the grid renders without error

        def page_func():
            url = '/?' + urllib.parse.urlencode([('onpage', 2), ('perpage', 1), *qs_args])
            with flask.current_app.test_request_context(url):
                pg = self.get_session_grid()
                if pg.page_count > 1:
                    self.get_pyq()

        if self.grid_cls.pager_on:
            page_func()

        return sub_func(expected)

    def test_filters(self):
        if callable(self.filters):
            cases = self.filters()
        else:
            cases = self.filters
        for name, op, value, expected in cases:
            self.check_filter(name, op, value, expected)

    def check_sort(self, k, ex, asc):
        if not asc:
            k = '-' + k
        d = {'sort1': k}

        def sub_func():
            with flask.current_app.test_request_context('/?' + urllib.parse.urlencode(d)):
                self.assert_in_query('ORDER BY %s%s' % (ex, '' if asc else ' DESC'))
                self.get_pyq()  # ensures the query executes and the grid renders without error

        def page_func():
            url = '/?' + urllib.parse.urlencode({'sort1': k, 'onpage': 2, 'perpage': 1})
            with flask.current_app.test_request_context(url):
                pg = self.get_session_grid()
                if pg.page_count > 1:
                    self.get_pyq()

        if self.grid_cls.pager_on:
            page_func()

        return sub_func()

    @pytest.mark.parametrize('asc', [True, False])
    def test_sort(self, asc):
        for col, expect in self.sort_tests:
            self.check_sort(col, expect, asc)

    def assert_table(self, table, grid=None, **kwargs):
        d = self.get_pyq(grid, **kwargs)

        assert len(d.find('table.records thead th')) == len(table[0])
        for idx, val in enumerate(table[0]):
            assert d.find('table.records thead th').eq(idx).text() == val

        assert len(d.find('table.records tbody tr')) == len(table[1:])
        for row_idx, row in enumerate(table[1:]):
            len(d.find('table.records tbody tr').eq(row_idx)('td')) == len(row)
            for col_idx, val in enumerate(row):
                read = d.find('table.records tbody tr').eq(row_idx)('td').eq(col_idx).text()
                assert read == val, 'row {} col {} {} != {}'.format(row_idx, col_idx, read, val)

    def expect_table_contents(self, expect, grid=None, **kwargs):
        d = self.get_pyq(grid, **kwargs)
        assert len(d.find('table.records tbody tr')) == len(expect)

        for row_idx, row in enumerate(expect):
            td = d.find('table.records tbody tr').eq(row_idx).find('td')
            assert len(td) == len(row)
            for col_idx, val in enumerate(row):
                assert td.eq(col_idx).text() == val
@mtbrock
Copy link
Contributor Author

mtbrock commented Oct 7, 2019

We've had to use a hack like the following to test grids with dates and datetimes:

class PGCompilerTesting(PGCompiler):                                                
    def render_literal_value(self, value, type_):                                   
        """                                                                         
        For date and datetime values, convert to a string                           
        format acceptable to PGSQL. That seems to be this:                          
                                                                                    
            yyyy-mm-dd hh:mi:ss.mmm(24h)                                            
                                                                                    
        For other data types, call the base class implementation.                   
        """                                                                         
        if isinstance(value, datetime.datetime):                                    
            return "'" + value.strftime('%Y-%m-%d %H:%M:%S.%f') + "'"               
        elif isinstance(value, datetime.date):                                      
            return "'" + value.strftime('%Y-%m-%d') + "'"                           
        elif isinstance(value, datetime.time):                                      
            return "'{:%H:%M}'".format(value)                                       
        elif value is None:                                                         
            return 'NULL'                                                           
        else:                                                                       
            return super().render_literal_value(value, type_)                       
                                                                                    
    def visit_table(self, table, asfrom=False, iscrud=False, ashint=False,          
                    fromhints=None, use_schema=True, **kwargs):                     
        """Strip the default schema from table names when it is not needed"""       
        ret_val = super().visit_table(table, asfrom, iscrud, ashint, fromhints, use_schema,    
                                      **kwargs)                                     
        if ret_val.startswith('dbo.'):                                              
            return ret_val[4:]                                                      
        return ret_val                                                              
                                                                                    
    def visit_column(self, column, add_to_result_map=None, include_table=True, **kwargs):    
        """Strip the default schema from table names when it is not needed"""       
        ret_val = super().visit_column(column, add_to_result_map, include_table, **kwargs)    
        if ret_val.startswith('dbo.'):                                              
            return ret_val[4:]                                                      
        return ret_val                                                              
                                                                                    
                                                                                    
class PGCompilerMixin:                                                              
    @classmethod                                                                    
    def setup_class(cls):                                                           
        if db.engine.dialect.name == 'postgresql':                                  
            cls._real_statement_compiler = db.engine.dialect.statement_compiler     
            db.engine.dialect.statement_compiler = PGCompilerTesting                
                                                                                    
    @classmethod                                                                    
    def teardown_class(cls):                                                        
        if db.engine.dialect.name == 'postgresql':                                  
            db.engine.dialect.statement_compiler = cls._real_statement_compiler

@guruofgentoo
Copy link
Member

Can also add the compiler mixins we've used for MSSQL.

@bchopson bchopson self-assigned this Oct 25, 2019
bchopson added a commit that referenced this issue Oct 25, 2019
bchopson added a commit that referenced this issue Nov 4, 2019
bchopson added a commit that referenced this issue Nov 13, 2019
- Add GridBase as grid test base class
- Add PGCompilerMixin
- Add MSSQLCompilerMixin
- Example tests for the dialect mixins in test_testing (requires
  changing the database backend)

Refs #68
bchopson added a commit that referenced this issue Nov 13, 2019
- Add GridBase as grid test base class
- Add PGCompilerMixin
- Add MSSQLCompilerMixin
- Example tests for the dialect mixins in test_testing (requires
  changing the database backend)

Refs #68
bchopson added a commit that referenced this issue Nov 13, 2019
- Add GridBase as grid test base class
- Add PGCompilerMixin
- Add MSSQLCompilerMixin
- Example tests for the dialect mixins in test_testing (requires
  changing the database backend)

Refs #68
bchopson added a commit that referenced this issue Nov 13, 2019
- Add GridBase as grid test base class
- Add PGCompilerMixin
- Add MSSQLCompilerMixin
- Example tests for the dialect mixins in test_testing (requires
  changing the database backend)

Refs #68
bchopson added a commit that referenced this issue Sep 21, 2020
- Add GridBase as grid test base class
- Add PGCompilerMixin
- Add MSSQLCompilerMixin
- Example tests for the dialect mixins in test_testing (requires
  changing the database backend)

Refs #68
bchopson added a commit that referenced this issue Sep 21, 2020
- Add GridBase as grid test base class
- Add PGCompilerMixin
- Add MSSQLCompilerMixin
- Example tests for the dialect mixins in test_testing (requires
  changing the database backend)

Refs #68
guruofgentoo pushed a commit that referenced this issue Oct 9, 2020
- Add GridBase as grid test base class
- Add PGCompilerMixin
- Add MSSQLCompilerMixin
- Example tests for the dialect mixins in test_testing (requires
  changing the database backend)

Refs #68
guruofgentoo added a commit that referenced this issue Oct 9, 2020
- keep dialect-specific treatment of mssql for unicode string matching
- add tests to verify "expect" method assertions
refs #68
guruofgentoo added a commit that referenced this issue Oct 9, 2020
- keep dialect-specific treatment of mssql for unicode string matching
- add tests to verify "expect" method assertions
refs #68
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants