Source code for interact.unittest

# Copyright (c) 2013 Galah Group LLC
# Copyright (c) 2013 Other contributers as noted in the CONTRIBUTERS file
#
# This file is part of galah-interact-python.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This module contains very useful functions you can use while unittesting
student's code.

.. note::

    In order to use the :mod:`unittest` module, you need to make sure that you
    have SWIG installed, and that you have *Python development headers*
    installed, both of which are probably available through your distribution's
    package manager (``apt-get`` or ``yum`` for example).

"""

import interact._utils as _utils
import os
import imp
import inspect
import atexit
import shutil
import tempfile
import subprocess
import os.path
import distutils.core
import capture

#: The absolute path to the swig executable. When this module is imported, the
#: environmental variable ``PATH`` is searched for a file named ``swig``, this
#: variable will be set to the first one that is found. This variable will equal
#: ``None`` if no such file could be found.
swig_path = _utils.which("swig")

[docs]class CouldNotCompile(RuntimeError): """ Exception raised when a student's code could not be compiled into a single library file. :ivar message: A short message describing the exception. :ivar stderr: The output that was received through standard error. This is output by ``distutils.core.setup``. """ def __init__(self, message, stderr): self.message = message self.stderr = stderr RuntimeError.__init__(self) def __str__(self): output = [ self.message, "---BEGIN STDERR---", self.stderr, "---END STDERR---" ] return "\n".join(output)
def _build_extension(module, mod_ext, working_directory): os.chdir(working_directory) distutils.core.setup( name = module, ext_modules = [mod_ext], py_modules = [module], script_name = "setup.py", script_args = ["build_ext", "--inplace"] ) def _generate_shared_libraries(modules, wrapper_directory): """ Compiles modules and wrappers to shared libraries using distutils. :raises: :class:`CouldNotCompile` if the extension could not be compiled. """ wrapper_directory = _utils.resolve_path(wrapper_directory) for module in modules: so_name = "_%s" % (module, ) wrapper_file = os.path.join(wrapper_directory, module + "_wrap.cxx") mod_ext = distutils.core.Extension( str(so_name), sources = [str(wrapper_file)] ) try: captured = capture.capture_function( _build_extension, str(module), mod_ext, str(wrapper_directory) ) captured.wait() except SystemExit: # Setup will call exit which can make the running script exit rather # suddenly. At least give the user an error with a traceback. raise CouldNotCompile( "Could not compile extension module.", stderr = captured.stderr.read() ) def _generate_swig_wrappers(interface_files, output_directory): """ Generates SWIG Wrapper files (.cxx) and python modules that can be compiled into a shared library by distutils. :raises: ``EnvironmentError`` if swig is not installed. """ if swig_path is None: raise EnvironmentError("No swig executable found.") output_directory = _utils.resolve_path(output_directory) for current_file in interface_files: module_name = _utils.file_name(current_file) output_file = os.path.join( output_directory, "%s_wrap.cxx" % (module_name, ) ) # Let swig generate the wrapper files. subprocess.check_call( [swig_path, "-c++", "-python", "-o", output_file, current_file], cwd = output_directory, stdout = _utils.DEVNULL, stderr = subprocess.STDOUT ) # These are necessary to allow STL types in python STD_INTERFACES = [ "std_deque.i", "std_list.i", "std_map.i", "std_pair.i", "std_set.i", "std_string.i", "std_vector.i", "std_sstream.i" ] # C++ Directives that expose extra functionality in the underlying C++ code. EXPOSURE_DIRECTIVES = [ "#define private public", # Expose private member variables to module "#define protected public", "#define class struct" # Expose unmarked private member variables ] def _generate_swig_interface(file_path, output_directory): """ Generates a SWIG Interface file (.i) that can be compiled with SWIG to a shared library file that can be imported into python for testing. """ file_path = _utils.resolve_path(file_path) output_directory = _utils.resolve_path(output_directory) # Figure out what this module will be named by getting just the filename # (minus extension) of the code file. module_name = _utils.file_name(file_path) # -MM flag returns all dependencies needed to compile file. gpp_process = subprocess.Popen( ["g++", "-MM", file_path], stdout = subprocess.PIPE, stderr = subprocess.STDOUT ) gpp_output = gpp_process.communicate()[0] # Get dependencies, minus the .o file and the white space gpp_output = gpp_output.split(":")[1].strip() dependencies = [i.strip() for i in gpp_output.split(" ") if i.strip() != "\\"] necessary_includes = [] for include in dependencies: necessary_includes.append("#include \"%s\"" % (include)) # TODO: Add comment describing what's going on here. if ".h" in include: include = include.replace(".hpp", ".h") include = include.replace(".h", ".cpp") if file_path not in include and os.path.isfile(include): necessary_includes.append("#include \"%s\"" % (include)) with open(os.path.join(output_directory, module_name + ".i"), "w") as f: f.write("%%module %s\n\n" % (module_name, )) # Ensure we include all of the special swig interface files that allow # us to interop with the C++ Standard Library. for interface in STD_INTERFACES: f.write("%%include \"%s\"\n" % (interface, )) # Write directives inside and out of wrapper for consistency in wrapped # file. f.write("\n".join(EXPOSURE_DIRECTIVES) + "\n") f.write("using namespace std;\n\n") f.write("%{\n") f.write("\n".join(EXPOSURE_DIRECTIVES) + "\n") for include in necessary_includes: f.write("%s\n" % include) f.write("%}\n\n") # SWIG cannot import global include like iostream, but it does need # all local includes local_includes = \ (include for include in necessary_includes if '<' not in include) for include in local_includes: f.write("%s\n" % include.replace("#", "%")) return module_name to_delete = [] def _cleanup(): for i in to_delete: shutil.rmtree(i) atexit.register(_cleanup)
[docs]def load_files(files): """ Compiles and loads functions and classes in code files and makes them callable from within Python. :param files: A list of file paths. All of the files will be compiled and loaded together. These must be absolute paths, see :meth:`Harness.student_files <interact.core.Harness.student_files>`. :returns: A ``dict`` where every file that was passed in is a key in the dictionary (without its file extension) and the value is another ``dict`` where each key is the name of a function or class in the file and the value is a callable you can use to actually execute or create an instance of that function or class. :raises: ``EnvironmentError`` if swig is not properly installed. :raises: :class:`CouldNotCompile` if the student's code could not be compiled into a library file. .. warning:: During testing, oftentimes the execution of loaded code's ``main()`` function failed. We haven't determined what the problem is yet so for now don't use this function to test ``main()`` functions (the :mod:`interact.execute` module should work well instead). .. code-block:: python >>> print open("main.cpp").read() #include <iostream> using namespace std; class Foo { int a_; public: Foo(int a); int get_a() const; }; Foo::Foo(int a) : a_(a) { // Do nothing } int Foo::get_a() const { return a_; } int bar() { Foo foo(3); cout << "foo.get_a() = " << foo.get_a() << endl; return 2; } int main() { return 0; } >>> students_code = interact.unittest.load_files(["main.cpp"]) >>> Foo = students_code["main"]["Foo"] >>> bar = students_code["main"]["bar"] >>> b = Foo(3) >>> b.get_a() 3 >>> rvalue = b.bar() foo.get_a() = 3 >>> print rvalue 2 If you want to test a function that prints things to stdout or reads from stdin (like the ``bar()`` function in the above example) you can use the :mod:`interact.capture` module. """ module_dict = {} # Get a directory we can work within. temp_dir = tempfile.mkdtemp() modules = [] for f in files: modules.append(_generate_swig_interface(f, temp_dir)) interface_files = ((module + ".i") for module in modules) _generate_swig_wrappers(interface_files, temp_dir) _generate_shared_libraries(modules, temp_dir) for module in modules: module_dict[module] = {} # Load up the python module we created whose function will let us access # the C++ ones. created_module = os.path.join(temp_dir, module + ".py") mod = imp.load_source(module, created_module) # Get all functions and classes in this module filter_func = lambda a: inspect.isbuiltin(a) or inspect.isclass(a) for name, impl in inspect.getmembers(mod, filter_func): module_dict[module][name] = impl to_delete.append(temp_dir) return module_dict