Cleanse Your Batavia Test Output Using This One Weird Trick

Batavia has a great framework for running tests. Essentially you write Python that results in some kind of output. The test runs that code through both C Python and Batavia, then you can compare those outputs. In most cases, we want to know that the outputs match. The author of the test doesn't need to worry about hard coding any kind of expectation for what the output should be. This gets tricky however in that the output from C Python and Batavia have to be cleaned up a little bit to ensure they match. For example, the output 3.000 is transformed to 3.0. In the case where I print a float, this is the correct output. But what happens if I hand it "%.3f" % 3. In this case 3.000 is the correct output. The extra zeros shouldn't be dropped. The problem here is that output is naive; nothing about the output tells us what produced it. I want to have sensible defaults for output transformation but also be able to pass in options to skip certain transformations. This way I can avoid having to refactor existing tests, but have more flexibility in new tests when I need them.

The existing transform functions already perform several transforms on output. Asking a writer of a test pass directly into the assertion which parts to skip could get messy. Instead I wrapped it up state into two new classes, JSCleaner and PYCleaner.

class JSCleaner:

    def __init__(self, err_msg=True, memory_ref=True, js_bool=True, decimal=True, float_exp=True, complex_num=True,
        high_percision_float=True, test_ref=True, custom=True):

        self.transforms = {k:v for k, v in locals().items() if k != 'self'}

    def cleanse(self, js_input, substitutions):
        """
        cleanse output from javascript
        """
        ...

class PYCleaner:

    def __init__(self, err_msg=True, memory_ref=True, float_exp=True, complex_num=True,
        high_percision_float=True, test_ref=True, custom=True):

        self.transforms = {k:v for k, v in locals().items() if k != 'self'}

    def cleanse(self, js_input, substitutions):
        """
        cleanse output from javascript
        """
        ...

This way, all transforms are performed be default. Then inside assertions that need to perform those transformations we change them to call the cleanse method instead of calling cleanse_javascript and cleanse_python.

I also created a decorator providing a simple interface for injecting a JSCleaner and PYCleaner object into tests that have needs other than the default.

  def transforms(**transform_args):
      """
      injects a JSCleaner and PYCleaner object into the function
      use this as a decarator to configure which transformations should be performed
      """
      def real_decorator(function):

          def wrapper(self, *args, **kwargs):

              # js_cleaner
              js_excludes = ['py_test_script', 'py_str_excep']
              js_params = {jsk : jsv for jsk, jsv in transform_args.items() if jsk not in js_excludes}
              js_cleaner = JSCleaner(**js_params)

              # py_cleaner
              py_excludes = ['js_bool', 'decimal', 'float_exp']
              py_params = {pyk : pyv for pyk, pyv in transform_args.items() if pyk not in py_excludes}
              py_cleaner = PYCleaner(**py_params)

              res = function(self, js_cleaner, py_cleaner, *args, **kwargs)
              return res
          wrapper.__name__ = function.__name__
          wrapper.__doc__ = function.__doc__
          return wrapper

      return real_decorator

Then if you have a test that needs to skip any transformations:

  @transforms(
      js_bool=False,
      decimal=False,
      float_exp=False,
      memory_ref=False
  )
  def test_basic(self, js_cleaner, py_cleaner):
      tests = adjust("""
      print(">>> 'Hello, world!'")
      print('Hello, world!')
      """)

      # the cleaner objects are passed into the assertion.
      self.assertCodeExecution(tests, js_cleaner=js_cleaner, py_cleaner=py_cleaner)