Browse files

Merged-in reverend-fixes branch.

------------------------------------------------------------
Use --include-merged or -n0 to see merged revisions.
  • Loading branch information...
1 parent ae68840 commit c315d4ecf93c522e0b7d4651d92eed3acdb66036 @mkwiatkowski committed with Mar 15, 2010
View
1 MANIFEST.in
@@ -3,6 +3,7 @@ include doc/*
include test/assertions.py
include test/factories.py
include test/helper.py
+include test/testing_project.py
include test/__init__.py
include test/data/*.py
include lib2to3/Grammar.txt
View
264 pythoscope/execution.py
@@ -0,0 +1,264 @@
+import itertools
+import time
+import types
+
+from pythoscope.serializer import BuiltinException, ImmutableObject, MapObject,\
+ UnknownObject, SequenceObject, is_immutable, is_sequence,\
+ is_mapping, is_builtin_exception
+from pythoscope.store import Call, Class, Function, FunctionCall,\
+ GeneratorObject, GeneratorObjectInvocation, MethodCall, Project, UserObject
+from pythoscope.util import all_of_type, assert_argument_type, class_name,\
+ generator_has_ended, get_generator_from_frame, is_generator_code,\
+ map_values, module_name
+
+
+class Execution(object):
+ """A single run of a user application.
+
+ To start an execution context, simply create a new Execution() object.
+ >>> e = Execution(Project("."))
+ >>> e.ended is None
+ True
+
+ When you're done tracing, call the finalize() method. Objects protected
+ from the garbage collector will be released and the execution context
+ will be closed:
+ >>> e.finalize()
+ >>> e.ended is not None
+ True
+
+ To erase any information collected during this run, call the destroy()
+ method:
+ >>> e.destroy()
+
+ In create_method_call/create_function_call if we can't find a class or
+ function in Project, we don't care about it. This way we don't record any
+ information about thid-party and dynamically created code.
+ """
+ def __init__(self, project):
+ self.project = project
+
+ self.started = time.time()
+ self.ended = None
+
+ # References to objects and calls created during the run.
+ self.captured_objects = {}
+ self.captured_calls = []
+
+ # After an inspection run, this will be a reference to the top level
+ # call. Call graph can be traveresed by descending to `subcalls`
+ # attribute of a call.
+ self.call_graph = None
+
+ # References to objects we don't want to be garbage collected just yet.
+ self._preserved_objects = []
+
+ def finalize(self):
+ """Mark execution as finished.
+ """
+ self._preserved_objects = []
+ self.ended = time.time()
+ self._fix_generator_objects()
+
+ def destroy(self):
+ """Erase any serialized objects and references created during this run.
+ """
+ self.destroy_references()
+ self.captured_objects = {}
+ self.captured_calls = []
+ self.call_graph = None
+
+ def destroy_references(self):
+ for obj in itertools.chain(self.captured_calls, self.captured_objects.values()):
+ # Method calls will also be erased, implicitly during removal of
+ # their UserObjects.
+ if isinstance(obj, UserObject):
+ obj.klass.user_objects.remove(obj)
+ # FunctionCalls have to be removed from their definition classes.
+ elif isinstance(obj, FunctionCall):
+ obj.definition.calls.remove(obj)
+ # GeneratorObjectInvocations will also be erased, implicitly
+ # during removal of their GeneratObjects.
+ elif isinstance(obj, GeneratorObject):
+ # GeneratorObjects are registered as calls both in Functions
+ # and in UserObjects. Since we remove UserObjects altogether
+ # we only have to care about Functions here.
+ if isinstance(obj.definition, Function):
+ obj.definition.calls.remove(obj)
+ # Other serializables, like ImmutableObject are not referenced from
+ # anywhere outside of calls in self.captured_calls.
+
+ # :: object -> SerializedObject
+ def serialize(self, obj):
+ """Return description of the given object in the form of a subclass of
+ SerializedObject.
+ """
+ return self._retrieve_or_capture(obj, self.create_serialized_object)
+
+ # :: {str: object, ...} -> {str: SerializedObject, ...}
+ def serialize_call_arguments(self, args):
+ return map_values(self.serialize, args)
+
+ # :: object -> UserObject | None
+ def try_serializing_as_user_object(self, obj):
+ """This method either find/creates a UserObject or returns None, without
+ serializing the object to anything else.
+ """
+ sobject = self._retrieve_or_capture(obj, self.create_serialized_user_object)
+ if isinstance(sobject, UserObject):
+ return sobject
+
+ # :: object -> UserObject | None
+ def create_serialized_user_object(self, obj):
+ klass = self.project.find_object(Class, class_name(obj), module_name(obj))
+ if klass:
+ serialized = UserObject(obj, klass)
+ klass.add_user_object(serialized)
+ return serialized
+
+ # :: object -> SerializedObject
+ def create_serialized_object(self, obj):
+ # Generator object has been passed as a value. We don't have enough
+ # information to create a complete GeneratorObject instance here, so
+ # we create a stub to be activated later.
+ if isinstance(obj, types.GeneratorType):
+ return GeneratorObject(obj)
+ user_object = self.create_serialized_user_object(obj)
+ if user_object:
+ return user_object
+ elif is_immutable(obj):
+ return ImmutableObject(obj)
+ elif is_sequence(obj):
+ return SequenceObject(obj, self.serialize)
+ elif is_mapping(obj):
+ return MapObject(obj, self.serialize)
+ elif is_builtin_exception(obj):
+ return BuiltinException(obj, self.serialize)
+ else:
+ return UnknownObject(obj)
+
+ # :: (type, Definition, Callable, args, code, frame) -> Call
+ def create_call(self, call_type, definition, callable, args, code, frame):
+ sargs = self.serialize_call_arguments(args)
+ if is_generator_code(code):
+ generator = get_generator_from_frame(frame)
+ # Each generator invocation is related to some generator object,
+ # so we have to create one if it wasn't captured yet.
+ def create_generator_object(_):
+ gobject = GeneratorObject(generator, definition, sargs, callable)
+ save_generator_inside(gobject, generator)
+ return gobject
+ gobject = self._retrieve_or_capture(generator, create_generator_object)
+ # It may have been captured, but not necessarily invoked yet, so
+ # we activate it if that's the case.
+ if not gobject.is_activated():
+ gobject.activate(definition, sargs, callable)
+ save_generator_inside(gobject, generator)
+ # In case of generators the call is really an invocation (resume) of
+ # a specific generator object. Input arguments were already saved
+ # in the GeneratorObject, and there's no need for duplicating them.
+ call_type = GeneratorObjectInvocation
+ callable = gobject
+ sargs = {}
+ call = call_type(definition, sargs)
+ self.captured_calls.append(call)
+ callable.add_call(call)
+ return call
+
+ # :: (str, object, dict, code, frame) -> MethodCall | None
+ def create_method_call(self, name, obj, args, code, frame):
+ user_object = self.try_serializing_as_user_object(obj)
+
+ # We ignore the call if we can't find the class of this object.
+ if user_object:
+ method = user_object.klass.find_method_by_name(name)
+ if method:
+ return self.create_call(MethodCall, method, user_object, args, code, frame)
+ else:
+ # TODO: We're lacking a definition of a method in a known class,
+ # so at least issue a warning.
+ pass
+
+ # :: (str, dict, code, frame) -> FunctionCall | None
+ def create_function_call(self, name, args, code, frame):
+ if self.project.contains_path(code.co_filename):
+ modulename = self.project._extract_subpath(code.co_filename)
+ function = self.project.find_object(Function, name, modulename)
+ if function:
+ return self.create_call(FunctionCall, function, function,
+ args, code, frame)
+
+ # :: (object, callable) -> SerializedObject | None
+ def _retrieve_or_capture(self, obj, capture_callback):
+ """Return existing description of the given object or create and return
+ new one if the description wasn't captured yet.
+
+ Preserves identity of objects, by storing them in `captured_objects`
+ list.
+
+ Returns None, when an obj wasn't serialized earlier and capture_callback
+ returns None:
+ >>> e = Execution(Project("."))
+ >>> e._retrieve_or_capture(123, lambda x: None) is None
+ True
+ >>> e._preserved_objects
+ []
+ """
+ try:
+ return self.captured_objects[object_id(obj)]
+ except KeyError:
+ captured = capture_callback(obj)
+ if captured:
+ self._preserve(obj)
+ self.captured_objects[object_id(obj)] = captured
+ return captured
+
+ def _preserve(self, obj):
+ """Preserve an object from garbage collection, so its id won't get
+ occupied by any other object.
+ """
+ self._preserved_objects.append(obj)
+
+ def iter_captured_generator_objects(self):
+ return all_of_type(self.captured_objects.values(), GeneratorObject)
+
+ def remove_call_from_call_graph(self, call_to_remove):
+ assert_argument_type(call_to_remove, Call)
+ def remove(calls):
+ try:
+ calls.remove(call_to_remove)
+ return True
+ except ValueError:
+ for call in calls:
+ if remove(call.subcalls):
+ return True
+ remove(self.call_graph)
+
+ def _fix_generator_objects(self):
+ """Remove last yielded values of generator objects, as those are
+ just bogus Nones placed on generator stop.
+ """
+ for gobject in self.iter_captured_generator_objects():
+ if is_exhaused_generator_object(gobject) \
+ and gobject.calls \
+ and gobject.calls[-1].output == ImmutableObject(None):
+ removed_invocation = gobject.calls.pop()
+ self.remove_call_from_call_graph(removed_invocation)
+ # Once we know if the generator is active or not, we can discard it.
+ if hasattr(gobject, '_generator'):
+ del gobject._generator
+
+def object_id(obj):
+ return id(obj)
+
+def save_generator_inside(gobject, generator):
+ # Generator objects return None to the tracer when stopped. That
+ # extra None we have to filter out manually (see
+ # Execution._fix_generator_objects method). We distinguish between active
+ # and stopped generators using the generator_has_ended() function.
+ # It needs the generator object itself, so we save it for later
+ # inspection inside the GeneratorObject.
+ gobject._generator = generator
+
+def is_exhaused_generator_object(gobject):
+ return hasattr(gobject, '_generator') and generator_has_ended(gobject._generator)
View
111 pythoscope/generator/__init__.py
@@ -9,10 +9,11 @@
ImmutableObject, MapObject, UnknownObject, SequenceObject, \
SerializedObject, is_serialized_string
from pythoscope.store import Class, Function, FunctionCall, TestClass, \
- TestMethod, ModuleNotFound, UserObject, MethodCall, Method, GeneratorObject
+ TestMethod, ModuleNotFound, UserObject, MethodCall, Method, GeneratorObject, \
+ GeneratorObjectInvocation
from pythoscope.compat import all, set, sorted
-from pythoscope.util import camelize, compact, counted, flatten, \
- key_for_value, pluralize, underscore
+from pythoscope.util import assert_argument_type, camelize, compact, counted, \
+ flatten, key_for_value, pluralize, underscore
# :: SerializedObject | [SerializedObject] -> bool
@@ -22,9 +23,21 @@ def can_be_constructed(obj):
elif isinstance(obj, SequenceObject):
return all(map(can_be_constructed, obj.contained_objects))
elif isinstance(obj, GeneratorObject):
- return obj.activated
+ return obj.is_activated()
return not isinstance(obj, UnknownObject)
+# :: GeneratorObject -> SerializedObject | None
+def generator_object_exception(gobject):
+ assert_argument_type(gobject, GeneratorObject)
+ for call in gobject.calls:
+ if call.raised_exception():
+ return call.exception
+
+# :: GeneratorObject -> [SerializedObject]
+def generator_object_yields(gobject):
+ assert_argument_type(gobject, GeneratorObject)
+ return [c.output for c in gobject.calls]
+
# :: Definition -> (str, str)
def import_for(definition):
return (definition.module.locator, definition.name)
@@ -117,8 +130,8 @@ def constructor_as_string(object, assigned_names={}):
arguments = join(', ', get_contained_objects_info(object, assigned_names))
return putinto(arguments, object.constructor_format, object.imports)
elif isinstance(object, GeneratorObject):
- if object.activated:
- cs = call_as_string_for(object.definition.name, object.input,
+ if object.is_activated():
+ cs = call_as_string_for(object.definition.name, object.args,
object.definition)
return addimport(cs, import_for(object.definition))
else:
@@ -284,16 +297,11 @@ def call_as_string(object_name, args, assigned_names={}):
... {mutable: 'alist'})
'merge(seq1=alist, seq2=[1, 2, 3])'
"""
- uncomplete = False
- imports = set()
arguments = []
for arg, value in sorted(args.iteritems()):
constructor = constructor_as_string(value, assigned_names)
- uncomplete = uncomplete or constructor.uncomplete
- imports.update(constructor.imports)
- arguments.append("%s=%s" % (arg, constructor))
- return CodeString("%s(%s)" % (object_name, ', '.join(arguments)),
- uncomplete=uncomplete, imports=imports)
+ arguments.append(combine(arg, constructor, template="%s=%s"))
+ return combine(object_name, join(", ", arguments), template="%s(%s)")
# :: MapObject -> {str: SerializedObject}
def map_as_kwargs(mapobject):
@@ -324,15 +332,16 @@ def get_contained_objects(obj):
elif isinstance(obj, UserObject):
calls = compact([obj.get_init_call()]) + obj.get_external_calls()
return get_contained_objects(calls)
- elif isinstance(obj, (FunctionCall, MethodCall)):
+ elif isinstance(obj, (FunctionCall, MethodCall, GeneratorObjectInvocation)):
if obj.raised_exception():
output = obj.exception
else:
output = obj.output
return get_those_and_contained_objects(obj.input.values() + [output])
elif isinstance(obj, GeneratorObject):
- if obj.activated:
- return get_those_and_contained_objects(obj.input.values() + obj.output)
+ if obj.is_activated():
+ return get_those_and_contained_objects(obj.args.values()) +\
+ get_contained_objects(obj.calls)
else:
return []
else:
@@ -453,14 +462,19 @@ def object2id(obj):
'generator'
...but if we have a matching definition, the name is based on it.
- >>> definition = Function('producer')
- >>> gobject.activate(definition, {})
+ >>> definition = Function('producer', is_generator=True)
+ >>> gobject.activate(definition, {}, definition)
>>> object2id(gobject)
'producer_instance'
+
+ Accepts only SerializedObjects as arguments.
+ >>> object2id(42)
+ Traceback (most recent call last):
+ ...
+ TypeError: object2id() should be called with a SerializedObject argument, not 42
"""
- if not isinstance(obj, SerializedObject):
- raise TypeError("object2id() should be called with a SerializedObject argument, not %s" % obj)
- if isinstance(obj, GeneratorObject) and obj.activated:
+ assert_argument_type(obj, SerializedObject)
+ if isinstance(obj, GeneratorObject) and obj.is_activated():
return "%s_instance" % obj.definition.name
else:
return obj.human_readable_id
@@ -572,12 +586,16 @@ def gencall2testname(object_name, args, yields):
return "test_%s_yields_%s" % (underscore(object_name), call_description)
def call2testname(call, object_name):
- # Note: order is significant. We may have a GeneratorObject that raised
- # an exception, and we care about exceptions more.
- if call.raised_exception():
+ # Generators can be treated as calls, which take arguments during
+ # initialization and return a list of yielded values.
+ if isinstance(call, GeneratorObject):
+ exception = generator_object_exception(call)
+ if exception:
+ return exccall2testname(object_name, call.args, exception)
+ else:
+ return gencall2testname(object_name, call.args, generator_object_yields(call))
+ elif call.raised_exception():
return exccall2testname(object_name, call.input, call.exception)
- elif isinstance(call, GeneratorObject):
- return gencall2testname(object_name, call.input, call.output)
else:
return objcall2testname(object_name, call.input, call.output)
@@ -648,18 +666,6 @@ def class_init_stub(klass):
args = init_method.get_call_args()
return call_with_args(klass.name, args)
-# :: (Call, CodeString) -> CodeString
-def decorate_call(call, string):
- if isinstance(call, GeneratorObject):
- invocations = len(call.output)
- if call.raised_exception():
- invocations += 1
- # TODO: generators were added to Python 2.2, while itertools appeared in
- # release 2.3, so we may generate incompatible tests here.
- return putinto(string, "list(islice(%%s, %s))" % invocations,
- set([("itertools", "islice")]))
- return string
-
def should_ignore_method(method):
return method.is_private()
@@ -936,26 +942,41 @@ def _create_assertion(self, name, call, stub=False, assigned_names={}):
Generated assertion will be a stub if input of a call cannot be
constructed or if stub argument is True.
"""
- callstring = decorate_call(call, call_as_string_for(name, call.input,
- call.definition, assigned_names))
+ if isinstance(call, GeneratorObject):
+ callstring = call_as_string_for(name, call.args, call.definition,
+ assigned_names)
+ invocations = len(call.calls)
+ #if call.raised_exception():
+ # invocations += 1
+ # TODO: generators were added to Python 2.2, while itertools appeared in
+ # release 2.3, so we may generate incompatible tests here.
+ callstring = putinto(callstring, "list(islice(%%s, %s))" % invocations,
+ set([("itertools", "islice")]))
+ exception = generator_object_exception(call)
+ output = generator_object_yields(call)
+ else:
+ callstring = call_as_string_for(name, call.input,
+ call.definition, assigned_names)
+ exception = call.exception
+ output = call.output
self.ensure_imports(callstring.imports)
- if call.raised_exception():
- return self._exception_assertion(call.exception, callstring, stub)
+ if exception is not None:
+ return self._exception_assertion(exception, callstring, stub)
else:
if callstring.uncomplete or stub:
assertion_type = 'equal_stub'
else:
assertion_type = 'equal'
- if can_be_constructed(call.output):
+ if can_be_constructed(output):
return (assertion_type,
- constructor_as_string(call.output, assigned_names),
+ constructor_as_string(output, assigned_names),
callstring)
else:
# If we can't test for real values, let's at least test for the right type.
- output_type = type_as_string(call.output)
+ output_type = type_as_string(output)
if isinstance(call, GeneratorObject):
callstring_type = map_types(callstring)
else:
View
19 pythoscope/generator/selector.py
@@ -1,10 +1,10 @@
-from pythoscope.store import Call, Callable, Class, GeneratorObject, TestClass
+from pythoscope.store import Call, Class, Definition, GeneratorObject, TestClass
def testable_objects(module):
- return [o for o in module.objects if is_testable(o)]
+ return [o for o in module.objects if is_testable_object(o)]
-def is_testable(obj):
+def is_testable_object(obj):
if isinstance(obj, TestClass):
return False
elif isinstance(obj, Class):
@@ -13,12 +13,13 @@ def is_testable(obj):
if klass in obj.bases:
return False
return True
- elif isinstance(obj, Callable):
+ elif isinstance(obj, Definition):
return not obj.name.startswith('_')
- elif isinstance(obj, GeneratorObject):
- return obj.raised_exception() or obj.output
- elif isinstance(obj, Call):
- return True
def testable_calls(calls):
- return [c for c in calls if is_testable(c)]
+ return [c for c in calls if is_testable_call(c)]
+
+def is_testable_call(call):
+ if isinstance(call, GeneratorObject):
+ return call.is_activated() and len(call.calls) > 0
+ return True
View
10 pythoscope/inspector/__init__.py
@@ -1,6 +1,7 @@
from pythoscope.inspector import static, dynamic
from pythoscope.logger import log
from pythoscope.store import ModuleNotFound
+from pythoscope.point_of_entry import PointOfEntry
from pythoscope.util import generator_has_ended, last_traceback, \
last_exception_as_string, python_modules_below
@@ -43,10 +44,17 @@ def remove_deleted_points_of_entry(project):
for name in names:
project.remove_point_of_entry(name)
+def ensure_point_of_entry(project, path):
+ name = project._extract_point_of_entry_subpath(path)
+ if not project.contains_point_of_entry(name):
+ poe = PointOfEntry(project=project, name=name)
+ project.add_point_of_entry(poe)
+ return project.get_point_of_entry(name)
+
def add_and_update_points_of_entry(project):
count = 0
for path in python_modules_below(project.get_points_of_entry_path()):
- poe = project.ensure_point_of_entry(path)
+ poe = ensure_point_of_entry(project, path)
if poe.is_out_of_sync():
count += 1
return count
View
12 pythoscope/inspector/dynamic.py
@@ -34,6 +34,10 @@ def raised(self, exception, traceback):
caller.set_exception(exception)
self.last_traceback = traceback
+ def unwind(self, value):
+ while self.stack:
+ self.returned(value)
+
class Inspector(ICallback):
"""Controller of the dynamic inspection process. It receives information
from the tracer and propagates it to Execution and CallStack objects.
@@ -43,6 +47,14 @@ def __init__(self, execution):
self.call_stack = CallStack()
def finalize(self):
+ # TODO: There are ways for the application to terminate (easiest
+ # being os._exit) without unwinding the stack. This means Pythoscope
+ # will be left with some calls registered on the stack without a return.
+ # We remedy the situation by injecting None as the return value for
+ # those calls. In the future we should also associate some kind of
+ # an "exit" side effect with those calls.
+ self.call_stack.unwind(self.execution.serialize(None))
+
# Copy the call graph structure to the Execution instance.
self.execution.call_graph = self.call_stack.top_level_calls
self.execution.finalize()
View
34 pythoscope/point_of_entry.py
@@ -0,0 +1,34 @@
+from pythoscope.execution import Execution
+from pythoscope.store import Localizable
+from pythoscope.util import read_file_contents
+
+
+class PointOfEntry(Localizable):
+ """Piece of code provided by the user that allows dynamic analysis.
+
+ Each point of entry keeps a reference to its last run in the `execution`
+ attribute.
+ """
+ def __init__(self, project, name):
+ Localizable.__init__(self, project, project.subpath_for_point_of_entry(name))
+
+ self.project = project
+ self.name = name
+ self.execution = Execution(project)
+
+ def _get_created(self):
+ "Points of entry are not up-to-date until they're run."
+ return self.execution.ended or 0
+ def _set_created(self, value):
+ pass
+ created = property(_get_created, _set_created)
+
+ def get_path(self):
+ return self.project.path_for_point_of_entry(self.name)
+
+ def get_content(self):
+ return read_file_contents(self.get_path())
+
+ def clear_previous_run(self):
+ self.execution.destroy()
+ self.execution = Execution(self.project)
View
3 pythoscope/snippet.py
@@ -14,7 +14,8 @@
import sys
from cmdline import find_project_directory, PythoscopeDirectoryMissing
-from store import Execution, Project
+from execution import Execution
+from store import Project
from tracer import Tracer
from inspector.dynamic import Inspector
View
362 pythoscope/store.py
@@ -1,22 +1,16 @@
import cPickle
-import itertools
import os
import re
-import time
-import types
from pythoscope.astbuilder import regenerate
from pythoscope.code_trees_manager import FilesystemCodeTreesManager
from pythoscope.compat import set
from pythoscope.localizable import Localizable
from pythoscope.logger import log
-from pythoscope.serializer import BuiltinException, ImmutableObject, MapObject,\
- UnknownObject, SequenceObject, SerializedObject, is_immutable, is_sequence,\
- is_mapping, is_builtin_exception
-from pythoscope.util import DirectoryException, all_of_type, class_name,\
- directories_under, extract_subpath, findfirst, generator_has_ended,\
- get_generator_from_frame, is_generator_code, load_pickle_from, map_values, \
- module_name, read_file_contents, starts_with_path, write_content_to_file
+from pythoscope.serializer import SerializedObject
+from pythoscope.util import all_of_type, assert_argument_type, class_name,\
+ directories_under, extract_subpath, findfirst, load_pickle_from,\
+ starts_with_path, write_content_to_file, DirectoryException
########################################################################
## Project class and helpers.
@@ -130,13 +124,15 @@ def find_module_by_full_path(self, path):
subpath = self._extract_subpath(path)
return self[subpath]
- def ensure_point_of_entry(self, path):
- name = self._extract_point_of_entry_subpath(path)
- if name not in self.points_of_entry:
- poe = PointOfEntry(project=self, name=name)
- self.points_of_entry[name] = poe
+ def contains_point_of_entry(self, name):
+ return name in self.points_of_entry
+
+ def get_point_of_entry(self, name):
return self.points_of_entry[name]
+ def add_point_of_entry(self, poe):
+ self.points_of_entry[poe.name] = poe
+
def remove_point_of_entry(self, name):
poe = self.points_of_entry.pop(name)
poe.clear_previous_run()
@@ -261,17 +257,9 @@ def iter_functions(self):
for function in module.functions:
yield function
- def iter_generator_objects(self):
- for module in self.iter_modules():
- for generator in module.generators:
- for gobject in generator.calls:
- yield gobject
-
def find_object(self, type, name, modulename):
try:
- for obj in all_of_type(self[modulename].objects, type):
- if obj.name == name:
- return obj
+ return self[modulename].find_object(type, name)
except ModuleNotFound:
pass
@@ -609,9 +597,6 @@ def __init__(self, definition, args, output=None, exception=None):
self.subcalls = []
def add_subcall(self, call):
- # Don't add the same GeneratorObject more than once.
- if isinstance(call, GeneratorObject) and call.caller is self:
- return
if call.caller is not None:
raise TypeError("This %s of %s already has a caller." % \
(class_name(call), call.definition.name))
@@ -658,70 +643,75 @@ class MethodCall(Call):
class Callable(object):
"""Dynamic aspect of a callable object. Tracks all calls made to given
callable.
+
+ Each Callable subclass tracks a different type of Calls.
"""
+ calls_type = None
+
def __init__(self, calls=None):
if calls is None:
calls = []
self.calls = calls
def add_call(self, call):
- # Don't add the same GeneratorObject more than once.
- if isinstance(call, GeneratorObject) and call in self.calls:
- return
+ assert_argument_type(call, self.calls_type)
self.calls.append(call)
class Function(Definition, Callable):
def __init__(self, name, args=None, code=None, calls=None, is_generator=False, module=None):
Definition.__init__(self, name, args=args, code=code, is_generator=is_generator)
Callable.__init__(self, calls)
self.module = module
+ if is_generator:
+ self.calls_type = GeneratorObject
+ else:
+ self.calls_type = FunctionCall
def get_unique_calls(self):
return set(self.calls)
def __repr__(self):
return "Function(name=%s, args=%r, calls=%r)" % (self.name, self.args, self.calls)
-class GeneratorObject(Call, SerializedObject):
+class GeneratorObjectInvocation(Call):
+ """Representation of a single generator invocation.
+
+ Each time a generator is resumed a new GeneratorObjectInvocation is created.
+ """
+
+class GeneratorObject(Callable, SerializedObject):
"""Representation of a generator object - a callable with an input and many
outputs (here called "yields").
-
- Although a generator object execution is not a single call, but consists of
- a series of suspensions and resumes, we make it conform to the Call interface
- for simplicity.
"""
- def __init__(self, obj, generator=None, args=None, yields=None, exception=None):
+ calls_type = GeneratorObjectInvocation
+
+ def __init__(self, obj, generator=None, args=None, callable=None):
+ Callable.__init__(self)
SerializedObject.__init__(self, obj)
- self.activated = False
- if generator is not None and args is not None:
- self.activate(generator, args, yields, exception)
+ if generator is not None and args is not None and callable is not None:
+ self.activate(generator, args, callable)
- def activate(self, generator, args, yields=None, exception=None):
- if self.activated:
+ def activate(self, generator, args, callable):
+ assert_argument_type(generator, (Function, Method))
+ assert_argument_type(callable, (Function, UserObject))
+ if self.is_activated():
raise ValueError("This generator has already been activated.")
- if yields is None:
- yields = []
- Call.__init__(self, generator, args, yields, exception)
- self.activated = True
-
- def set_output(self, output):
- self.output.append(output)
+ if not generator.is_generator:
+ raise TypeError("Tried to activate GeneratorObject with %r as a generator definition." % generator)
+ self.definition = generator
+ self.args = args
+ # Finally register this GeneratorObject with its callable context
+ # (which will be a Function or an UserObject). This has to be
+ # done only once for each GeneratorObject.
+ callable.add_call(self)
- def __eq__(self, other):
- if self.activated:
- return Call.__eq__(self, other)
- else:
- return isinstance(other, GeneratorObject) \
- and hash(self) == hash(other)
-
- def __hash__(self):
- return hash((self.timestamp, self.human_readable_id,
- self.module_name, self.type_name))
+ def is_activated(self):
+ return hasattr(self, 'args')
def __repr__(self):
- if self.activated:
- return "GeneratorObject(generator=%r, yields=%r)" % \
- (self.definition.name, self.output)
+ if self.is_activated():
+ return "GeneratorObject(generator=%r, args=%r)" % \
+ (self.definition.name, self.args)
else:
return "GeneratorObject()"
@@ -731,6 +721,8 @@ class UserObject(Callable, SerializedObject):
UserObjects are also callables that aggregate MethodCall instances,
capturing the whole life of an object, from initialization to destruction.
"""
+ calls_type = (MethodCall, GeneratorObject)
+
def __init__(self, obj, klass):
Callable.__init__(self)
SerializedObject.__init__(self, obj)
@@ -754,6 +746,8 @@ def get_external_calls(self):
def is_not_init_call(call):
return call.definition.name != '__init__'
def is_external_call(call):
+ if isinstance(call, GeneratorObject):
+ return True
return (not call.caller) or (call.caller not in self.calls)
return filter(is_not_init_call, filter(is_external_call, self.calls))
@@ -909,6 +903,11 @@ def get_test_cases_for_module(self, module):
"""
return [tc for tc in self.test_cases if module in tc.associated_modules]
+ def find_object(self, type, name):
+ for obj in all_of_type(self.objects, type):
+ if obj.name == name:
+ return obj
+
def save(self):
# Don't save the test file unless it has been changed.
if self.changed:
@@ -920,244 +919,3 @@ def save(self):
raise ModuleSaveError(self.subpath, err.args[0])
self.changed = False
-########################################################################
-## Dynamic inspection classes: Execution and PointOfEntry.
-##
-class Execution(object):
- """A single run of a user application.
-
- To start an execution context, simply create a new Execution() object.
- >>> e = Execution(Project("."))
- >>> e.ended is None
- True
-
- When you're done tracing, call the finalize() method. Objects protected
- from the garbage collector will be released and the execution context
- will be closed:
- >>> e.finalize()
- >>> e.ended is not None
- True
-
- To erase any information collected during this run, call the destroy()
- method:
- >>> e.destroy()
-
- In create_method_call/create_function_call if we can't find a class or
- function in Project, we don't care about it. This way we don't record any
- information about thid-party and dynamically created code.
- """
- def __init__(self, project):
- self.project = project
-
- self.started = time.time()
- self.ended = None
-
- # References to objects and calls created during the run.
- self.captured_objects = {}
- self.captured_calls = []
-
- # After an inspection run, this will be a reference to the top level
- # call. Call graph can be traveresed by descending to `subcalls`
- # attribute of a call.
- self.call_graph = None
-
- # References to objects we don't want to be garbage collected just yet.
- self._preserved_objects = []
-
- def finalize(self):
- """Mark execution as finished.
- """
- self._preserved_objects = []
- self.ended = time.time()
- self._fix_generator_objects()
-
- def destroy(self):
- """Erase any serialized objects and references created during this run.
- """
- self.destroy_references()
- self.captured_objects = {}
- self.captured_calls = []
- self.call_graph = None
-
- def destroy_references(self):
- for obj in itertools.chain(self.captured_calls, self.captured_objects.values()):
- # Method calls will also be erased, implicitly during removal of
- # their UserObjects.
- if isinstance(obj, UserObject):
- obj.klass.user_objects.remove(obj)
- # FunctionCalls and GeneratorObjects have to be removed from their
- # definition classes.
- elif isinstance(obj, (FunctionCall, GeneratorObject)):
- obj.definition.calls.remove(obj)
- # Other serializables, like ImmutableObject are not referenced from
- # anywhere outside of calls in self.captured_calls.
-
- # :: object -> SerializedObject
- def serialize(self, obj):
- """Return description of the given object in the form of a subclass of
- SerializedObject.
- """
- def create_serialized_object():
- return self.create_serialized_object(obj)
- return self._retrieve_or_capture(obj, create_serialized_object)
-
- # :: {str: object, ...} -> {str: SerializedObject, ...}
- def serialize_call_arguments(self, args):
- return map_values(self.serialize, args)
-
- # :: object -> SerializedObject
- def create_serialized_object(self, obj):
- # Generator object has been passed as a value. We don't have enough
- # information to create a GeneratorObject instance here, so create
- # a stub to be activated later.
- if isinstance(obj, types.GeneratorType):
- return GeneratorObject(obj)
- klass = self.project.find_object(Class, class_name(obj), module_name(obj))
- if klass:
- serialized = UserObject(obj, klass)
- klass.add_user_object(serialized)
- return serialized
- elif is_immutable(obj):
- return ImmutableObject(obj)
- elif is_sequence(obj):
- return SequenceObject(obj, self.serialize)
- elif is_mapping(obj):
- return MapObject(obj, self.serialize)
- elif is_builtin_exception(obj):
- return BuiltinException(obj, self.serialize)
- else:
- return UnknownObject(obj)
-
- # :: (type, Definition, Callable, args, code, frame) -> Call
- def create_call(self, call_type, definition, callable, args, code, frame):
- sargs = self.serialize_call_arguments(args)
- if is_generator_code(code):
- # Each generator invocation is related to some generator object,
- # so we have to create one if it wasn't captured yet.
- def create_generator_object():
- generator = get_generator_from_frame(frame)
- gobject = GeneratorObject(generator, definition, sargs)
- save_generator_inside(gobject, frame)
- return gobject
- call = self._retrieve_or_capture(code, create_generator_object)
- # It may have been captured, but not necessarily invoked yet, so
- # we activate it if that's the case.
- if not call.activated:
- call.activate(definition, sargs)
- save_generator_inside(call, frame)
- else:
- call = call_type(definition, sargs)
- self.captured_calls.append(call)
- callable.add_call(call)
- return call
-
- # :: (str, object, dict, code, frame) -> MethodCall | None
- def create_method_call(self, name, obj, args, code, frame):
- serialized_object = self.serialize(obj)
-
- # We ignore the call if we can't find the class of this object.
- if isinstance(serialized_object, UserObject):
- method = serialized_object.klass.find_method_by_name(name)
- if method:
- return self.create_call(MethodCall, method, serialized_object, args, code, frame)
- else:
- # TODO: We're lacking a definition of a method in a known class,
- # so at least issue a warning.
- pass
-
- # :: (str, dict, code, frame) -> FunctionCall | None
- def create_function_call(self, name, args, code, frame):
- if self.project.contains_path(code.co_filename):
- modulename = self.project._extract_subpath(code.co_filename)
- function = self.project.find_object(Function, name, modulename)
- if function:
- return self.create_call(FunctionCall, function, function,
- args, code, frame)
-
- def _retrieve_or_capture(self, obj, capture_callback):
- """Return existing description of the given object or create and return
- new one if the description wasn't captured yet.
-
- Preserves identity of objects, by storing them in `captured_objects`
- list.
- """
- try:
- return self.captured_objects[object_id(obj)]
- except KeyError:
- captured = capture_callback()
- self._preserve(obj)
- self.captured_objects[object_id(obj)] = captured
- return captured
-
- def _preserve(self, obj):
- """Preserve an object from garbage collection, so its id won't get
- occupied by any other object.
- """
- self._preserved_objects.append(obj)
-
- def iter_captured_generator_objects(self):
- return all_of_type(self.captured_objects.values(), GeneratorObject)
-
- def _fix_generator_objects(self):
- """Remove last yielded values of generator objects, as those are
- just bogus Nones placed on generator stop.
- """
- for gobject in self.iter_captured_generator_objects():
- if is_exhaused_generator_object(gobject) \
- and gobject.output \
- and gobject.output[-1] == ImmutableObject(None):
- gobject.output.pop()
- # Once we know if the generator is active or not, we can discard it.
- if hasattr(gobject, '_generator'):
- del gobject._generator
-
-def object_id(obj):
- # The reason why we index generator by its code id is because at the time
- # of GeneratorObject creation we don't have access to the generator itself,
- # only to its code. See `Execution.create_call` for details.
- if isinstance(obj, types.GeneratorType):
- return id(obj.gi_frame.f_code)
- else:
- return id(obj)
-
-def save_generator_inside(gobject, frame):
- # Generator objects return None to the tracer when stopped. That
- # extra None we have to filter out manually (see
- # Execution_fix_generator_objects method). We distinguish between active
- # and stopped generators using the generator_has_ended() function.
- # It needs the generator object itself, so we save it for later
- # inspection inside the GeneratorObject.
- gobject._generator = get_generator_from_frame(frame)
-
-def is_exhaused_generator_object(gobject):
- return hasattr(gobject, '_generator') and generator_has_ended(gobject._generator)
-
-class PointOfEntry(Localizable):
- """Piece of code provided by the user that allows dynamic analysis.
-
- Each point of entry keeps a reference to its last run in the `execution`
- attribute.
- """
- def __init__(self, project, name):
- Localizable.__init__(self, project, project.subpath_for_point_of_entry(name))
-
- self.project = project
- self.name = name
- self.execution = Execution(project)
-
- def _get_created(self):
- "Points of entry are not up-to-date until they're run."
- return self.execution.ended or 0
- def _set_created(self, value):
- pass
- created = property(_get_created, _set_created)
-
- def get_path(self):
- return self.project.path_for_point_of_entry(self.name)
-
- def get_content(self):
- return read_file_contents(self.get_path())
-
- def clear_previous_run(self):
- self.execution.destroy()
- self.execution = Execution(self.project)
View
9 pythoscope/tracer.py
@@ -257,11 +257,10 @@ def tracer(self, frame, event, arg):
self.propagating_exception = False
return super(Python23Tracer, self).tracer(frame, event, arg)
-def Tracer(*args):
- if sys.version_info < (2, 4):
- return Python23Tracer(*args)
- else:
- return StandardTracer(*args)
+if sys.version_info < (2, 4):
+ Tracer = Python23Tracer
+else:
+ Tracer = StandardTracer
class ICallback(object):
"""Interface that Tracer's callback object should adhere to.
View
13 pythoscope/util.py
@@ -308,6 +308,19 @@ def compile_without_warnings(stmt):
warnings.resetwarnings()
return code
+def callers_name():
+ return sys._getframe(2).f_code.co_name
+
+def type_names(types):
+ if isinstance(types, tuple):
+ return '/'.join(map(type_names, types))
+ return types.__name__
+
+def assert_argument_type(obj, expected_type):
+ if not isinstance(obj, expected_type):
+ raise TypeError("%s() should be called with a %s argument, not %s" %
+ (callers_name(), type_names(expected_type), obj))
+
def quoted_block(text):
return ''.join(["> %s" % line for line in text.splitlines(True)])
View
10 test/helper.py
@@ -7,11 +7,12 @@
from StringIO import StringIO
from pythoscope.astbuilder import EmptyCode
+from pythoscope.execution import Execution
from pythoscope.generator import add_tests_to_project
from pythoscope.logger import DEBUG, INFO, get_output, log, set_output
from pythoscope.code_trees_manager import CodeTreesManager, CodeTreeNotFound
-from pythoscope.store import Execution, Function, ModuleNotFound, \
- PointOfEntry, Project
+from pythoscope.point_of_entry import PointOfEntry
+from pythoscope.store import Function, ModuleNotFound, Project
from pythoscope.util import read_file_contents, write_content_to_file
@@ -30,6 +31,11 @@ def P(path):
"Convert given path with slashes to proper format for OS we're running on."
return os.path.join(*path.split("/"))
+def noindent(string):
+ lines = string.splitlines(True)
+ indent = min([len(line) - len(line.lstrip()) for line in lines if len(line.lstrip()) > 0])
+ return ''.join([line[indent:] for line in lines])
+
class PointOfEntryMock(PointOfEntry):
def __init__(self, project=None, name="poe", content=""):
if project is None:
View
187 test/test_dynamic_inspector.py
@@ -3,20 +3,22 @@
from nose import SkipTest
+from pythoscope.execution import Execution
from pythoscope.inspector.static import inspect_code
from pythoscope.inspector.dynamic import inspect_code_in_context, \
inspect_point_of_entry
from pythoscope.serializer import ImmutableObject, MapObject, UnknownObject, \
SequenceObject, BuiltinException
-from pythoscope.store import Class, Execution, Function, \
- GeneratorObject, Method, UserObject, Project
+from pythoscope.store import Class, Function, FunctionCall, GeneratorObject,\
+ GeneratorObjectInvocation, Method, UserObject, Project
from pythoscope.compat import all
from pythoscope.util import findfirst, generator_has_ended, \
last_exception_as_string, last_traceback
from assertions import *
from helper import ProjectInDirectory, PointOfEntryMock, EmptyProjectExecution, \
- IgnoredWarnings, putfile, TempDirectory, P, CapturedLogger
+ IgnoredWarnings, putfile, TempDirectory, P, CapturedLogger, noindent
+from testing_project import TestingProject
########################################################################
@@ -147,8 +149,22 @@ def assert_call_with_string_exception(expected_input, expected_string, call):
def assert_generator_object(expected_input, expected_yields, obj):
assert_instance(obj, GeneratorObject)
- assert_call_arguments(expected_input, obj.input)
- assert_collection_of_serialized(expected_yields, obj.output)
+ assert obj.is_activated()
+ assert_call_arguments(expected_input, obj.args)
+ assert_collection_of_serialized(expected_yields, [c.output for c in obj.calls])
+ assert all([not c.raised_exception() for c in obj.calls])
+
+def assert_function_with_single_generator_object(name, expected_input, expected_yields, execution):
+ function = execution.project.find_object(Function, name)
+ assert function is not None
+ gobject = assert_one_element_and_return(function.calls)
+ assert_generator_object(expected_input, expected_yields, gobject)
+
+def make_execution_with_single_generator_function(name):
+ return TestingProject()\
+ .with_all_catch_module()\
+ .with_object(Function(name, is_generator=True))\
+ .make_new_execution()
def assert_one_element_and_return(collection):
"""Assert that the collection has exactly one element and return this
@@ -593,9 +609,11 @@ def generator(x):
yield x * 2
yield x ** 3
[x for x in generator(2)]
- generator = inspect_returning_single_callable(function_calling_generator)
- assert_instance(generator, Function)
+ execution = make_execution_with_single_generator_function("generator")
+ inspect_code_in_context(function_calling_generator, execution)
+
+ generator = execution.project.find_object(Function, "generator")
gobject = assert_one_element_and_return(generator.calls)
assert_generator_object({'x': 2}, [2, 3, 4, 8], gobject)
@@ -606,13 +624,14 @@ def generator():
yield 1
g = generator()
g.next()
- generator = inspect_returning_single_callable(yields_one)
- assert_instance(generator, Function)
+ execution = make_execution_with_single_generator_function("generator")
+ inspect_code_in_context(yields_one, execution)
+
+ generator = execution.project.find_object(Function, "generator")
gobject = assert_one_element_and_return(generator.calls)
assert_generator_object({}, [1], gobject)
- assert not gobject.raised_exception()
def test_handles_yielded_nones(self):
if hasattr(generator_has_ended, 'unreliable'):
@@ -623,7 +642,12 @@ def generator():
yield None
g = generator()
g.next()
- gobject = inspect_returning_single_call(yields_none)
+
+ execution = make_execution_with_single_generator_function("generator")
+ inspect_code_in_context(yields_none, execution)
+
+ generator = execution.project.find_object(Function, "generator")
+ gobject = assert_one_element_and_return(generator.calls)
assert_generator_object({}, [None], gobject)
@@ -633,9 +657,11 @@ def generator(x):
if False:
yield 'something'
[x for x in generator(123)]
- gobject = inspect_returning_single_call(function_calling_empty_generator)
- assert_generator_object({'x': 123}, [], gobject)
+ execution = make_execution_with_single_generator_function("generator")
+ inspect_code_in_context(function_calling_empty_generator, execution)
+
+ assert_function_with_single_generator_object("generator", {'x': 123}, [], execution)
def test_handles_generator_objects_that_werent_destroyed(self):
def function():
@@ -644,9 +670,11 @@ def generator():
g = generator()
g.next()
globals()['__generator_yielding_one'] = g
- gobject = inspect_returning_single_call(function)
- assert_generator_object({}, [1], gobject)
+ execution = make_execution_with_single_generator_function("generator")
+ inspect_code_in_context(function, execution)
+
+ assert_function_with_single_generator_object("generator", {}, [1], execution)
def test_handles_generator_objects_that_yield_none_and_dont_get_destroyed(self):
if hasattr(generator_has_ended, 'unreliable'):
@@ -658,9 +686,11 @@ def generator():
g = generator()
g.next()
globals()['__generator_yielding_none'] = g
- gobject = inspect_returning_single_call(function)
- assert_generator_object({}, [None], gobject)
+ execution = make_execution_with_single_generator_function("generator")
+ inspect_code_in_context(function, execution)
+
+ assert_function_with_single_generator_object("generator", {}, [None], execution)
def test_handles_generator_methods(self):
def function_calling_generator_method():
@@ -670,7 +700,15 @@ def genmeth(self):
yield 1
yield 0
[x for x in GenClass().genmeth()]
- user_object = inspect_returning_single_callable(function_calling_generator_method)
+
+ execution = TestingProject()\
+ .with_all_catch_module()\
+ .with_object(Class("GenClass", methods=[Method("genmeth", is_generator=True)]))\
+ .make_new_execution()
+ inspect_code_in_context(function_calling_generator_method, execution)
+
+ klass = execution.project.find_object(Class, "GenClass")
+ user_object = assert_one_element_and_return(klass.user_objects)
assert_instance(user_object, UserObject)
gobject = assert_one_element_and_return(user_object.calls)
@@ -686,16 +724,16 @@ def generator(x):
def function(y):
return [x for x in generator(y)]
function(2)
- callables = inspect_returning_callables(function_calling_function_that_uses_generator)
-
- assert_length(callables, 2)
- function = find_first_with_name("function", callables)
+ execution = TestingProject()\
+ .with_all_catch_module()\
+ .with_object(Function("generator", is_generator=True))\
+ .with_object(Function("function"))\
+ .make_new_execution()
+ inspect_code_in_context(function_calling_function_that_uses_generator, execution)
- fcall = assert_one_element_and_return(function.calls)
-
- gobject = assert_one_element_and_return(fcall.subcalls)
- assert_generator_object({'x': 2}, [2, 3, 4, 8], gobject)
+ assert_function_with_single_generator_object("generator",
+ {'x': 2}, [2, 3, 4, 8], execution)
def test_serializes_generator_objects_passed_as_values(self):
def function():
@@ -705,17 +743,102 @@ def generator():
yield 1
yield 2
invoke(generator())
- callables = inspect_returning_callables(function)
- assert_length(callables, 2)
- function = find_first_with_name("invoke", callables)
+ execution = TestingProject()\
+ .with_all_catch_module()\
+ .with_object(Function("invoke"))\
+ .with_object(Function("generator", is_generator=True))\
+ .make_new_execution()
+ inspect_code_in_context(function, execution)
+
+ function = execution.project.find_object(Function, "invoke")
fcall = assert_one_element_and_return(function.calls)
- assert_instance(fcall.input['g'], GeneratorObject)
- gobject = assert_one_element_and_return(fcall.subcalls)
- assert fcall.input['g'] is gobject
+ gobject = fcall.input['g']
+ assert_instance(gobject, GeneratorObject)
assert_generator_object({}, [1, 2], gobject)
+ def test_generator_can_be_invoked_multiple_times_at_the_same_place(self):
+ def function():
+ def sth_else():
+ return False
+ def generator():
+ yield 1
+ yield 2
+ g = generator()
+ g.next()
+ sth_else()
+ g.next()
+
+ execution = TestingProject()\
+ .with_all_catch_module()\
+ .with_object(Function("generator", is_generator=True))\
+ .with_object(Function("sth_else"))\
+ .make_new_execution()
+ inspect_code_in_context(function, execution)
+
+ assert_instance(execution.call_graph[0], GeneratorObjectInvocation)
+ assert_instance(execution.call_graph[1], FunctionCall)
+ assert_instance(execution.call_graph[2], GeneratorObjectInvocation)
+
+ def test_generator_can_be_invoked_in_multiple_places(self):
+ def function():
+ def generator():
+ i = 1
+ while True:
+ yield i
+ i += 1
+ def first(g):
+ return g.next()
+ def second(g):
+ return g.next() + g.next()
+ g = generator()
+ g.next()
+ first(g)
+ second(g)
+
+ execution = TestingProject()\
+ .with_all_catch_module()\
+ .with_object(Function("generator", is_generator=True))\
+ .with_object(Function("first"))\
+ .with_object(Function("second"))\
+ .make_new_execution()
+ inspect_code_in_context(function, execution)
+
+ expected_call_graph = noindent("""
+ generator()
+ first()
+ generator()
+ second()
+ generator()
+ generator()
+ """)
+ assert_equal_strings(expected_call_graph,
+ call_graph_as_string(execution.call_graph))
+
+ def test_distinguishes_between_generator_objects_spawned_with_the_same_generator(self):
+ def function():
+ def generator():
+ yield 1
+ yield 2
+ [x for x in generator()]
+ [x for x in generator()]
+
+ execution = make_execution_with_single_generator_function("generator")
+ inspect_code_in_context(function, execution)
+
+ generator = execution.project.find_object(Function, "generator")
+ assert_length(generator.calls, 2)
+ assert_length(execution.call_graph, 4)
+
+ gcall1 = generator.calls[0]
+ assert gcall1.calls[0] is execution.call_graph[0]
+ assert gcall1.calls[1] is execution.call_graph[1]
+
+ gcall2 = generator.calls[1]
+ assert gcall2.calls[0] is execution.call_graph[2]
+ assert gcall2.calls[1] is execution.call_graph[3]
+
class TestObjectsIdentityPreservation:
def test_handles_passing_sequence_objects_around(self):
callables = inspect_returning_callables(function_calling_functions_that_use_the_same_sequence_object)
View
39 test/test_generator.py
@@ -13,7 +13,7 @@
from pythoscope.serializer import ImmutableObject
from pythoscope.store import Class, Function, Method, ModuleNeedsAnalysis, \
ModuleSaveError, TestClass, TestMethod, MethodCall, FunctionCall, \
- UserObject, GeneratorObject, ModuleNotFound
+ UserObject, GeneratorObject, GeneratorObjectInvocation, ModuleNotFound
from pythoscope.compat import sets, sorted
from pythoscope.util import read_file_contents, get_last_modification_time, \
flatten
@@ -37,14 +37,17 @@ def stable_serialize_call_arguments(execution, args):
serialized_args[key] = execution.serialize(value)
return serialized_args
-def create_method_call(method, args, output, call_type, execution):
+def create_method_call(method, args, output, call_type, execution, user_object):
sargs = stable_serialize_call_arguments(execution, args)
if call_type == 'output':
return MethodCall(method, sargs, output=execution.serialize(output))
elif call_type == 'exception':
return MethodCall(method, sargs, exception=execution.serialize(output))
elif call_type == 'generator':
- return GeneratorObject(None, method, sargs, map(execution.serialize, output))
+ gobject = GeneratorObject(None, method, sargs, user_object)
+ for syield in map(execution.serialize, output):
+ gobject.add_call(GeneratorObjectInvocation(method, {}, syield))
+ return gobject
def ClassWithMethods(classname, methods, call_type='output'):
"""call_type has to be one of 'output', 'exception' or 'generator'.
@@ -53,14 +56,17 @@ def ClassWithMethods(classname, methods, call_type='output'):
method_objects = []
method_calls = []
+ klass = Class(classname, methods=method_objects)
+ user_object = UserObject(None, klass)
+
for name, calls in methods:
- method = Method(name, ['self'] + flatten([a.keys() for a,_ in calls]))
+ method = Method(name, ['self'] + flatten([a.keys() for a,_ in calls]),
+ is_generator=(call_type=='generator'))
method_objects.append(method)
for args, output in calls:
- method_calls.append(create_method_call(method, args, output, call_type, execution))
+ method_calls.append(create_method_call(method, args, output, call_type, execution, user_object))
- klass = Class(classname, methods=method_objects)
- user_object = UserObject(None, klass)
+ klass.add_methods(method_objects)
user_object.calls = method_calls
klass.add_user_object(user_object)
@@ -98,17 +104,21 @@ def GeneratorWithYields(genname, input, yields):
generator = Function(genname, input.keys(), is_generator=True)
gobject = GeneratorObject(None, generator,
stable_serialize_call_arguments(execution, input),
- map(execution.serialize, yields))
- generator.calls = [gobject]
+ generator)
+ for syield in map(execution.serialize, yields):
+ gobject.add_call(GeneratorObjectInvocation(generator, {}, syield))
+ generator.add_call(gobject)
return generator
def GeneratorWithSingleException(genname, input, exception):
execution = EmptyProjectExecution()
generator = Function(genname, input.keys(), is_generator=True)
gobject = GeneratorObject(None, generator,
stable_serialize_call_arguments(execution, input),
- exception=execution.serialize(exception))
- generator.calls = [gobject]
+ generator)
+ gobject.add_call(GeneratorObjectInvocation(generator, {},
+ exception=execution.serialize(exception)))
+ generator.add_call(gobject)
return generator
class TestGeneratorClass:
@@ -388,10 +398,11 @@ def ssca(args):
s = execution.serialize
mygen = Function('mygen', ['a', 'b'], is_generator=True)
- gobject = GeneratorObject(None, mygen, ssca({'a': 42, 'b': False}), [s(42)])
- mygen.calls = [gobject]
+ gobject = GeneratorObject(None, mygen, ssca({'a': 42, 'b': False}), mygen)
+ gobject.add_call(GeneratorObjectInvocation(mygen, {}, s(42)))
+ mygen.add_call(gobject)
function = Function('invoke', ['g'])
- function.calls = [FunctionCall(function, {'g': gobject}, s(True))]
+ function.add_call(FunctionCall(function, {'g': gobject}, s(True)))
result = generate_single_test_module(objects=[function, mygen])
View
88 test/test_point_of_entry.py
@@ -0,0 +1,88 @@
+from pythoscope.astbuilder import EmptyCode
+from pythoscope.point_of_entry import PointOfEntry
+from pythoscope.serializer import ImmutableObject, UnknownObject,\
+ SequenceObject, MapObject
+from pythoscope.store import Class, Function, FunctionCall, GeneratorObject,\
+ Method, UserObject
+
+from assertions import *
+from helper import EmptyProject
+
+
+def inject_user_object(poe, obj, klass):
+ def create_user_object(_):
+ return UserObject(obj, klass)
+ user_object = poe.execution._retrieve_or_capture(obj, create_user_object)
+ klass.add_user_object(user_object)
+ return user_object
+
+def inject_function_call(poe, function, args={}):
+ call = FunctionCall(function, args)
+ poe.execution.captured_calls.append(call)
+ for arg in args.values():
+ poe.execution.captured_objects[id(arg)] = arg
+ function.add_call(call)
+ return call
+
+def inject_generator_object(poe, obj, *args):
+ return poe.execution._retrieve_or_capture(obj,
+ lambda _:GeneratorObject(obj, *args))
+
+class TestPointOfEntry:
+ def _create_project_with_two_points_of_entry(self, *objs):
+ project = EmptyProject()
+ project.create_module("module.py", code=EmptyCode(), objects=list(objs))
+ self.first = PointOfEntry(project, 'first')
+ self.second = PointOfEntry(project, 'second')
+
+ def test_clear_previous_run_removes_user_objects_from_classes(self):
+ klass = Class('SomeClass')
+ self._create_project_with_two_points_of_entry(klass)
+
+ obj1 = inject_user_object(self.first, 1, klass)
+ obj2 = inject_user_object(self.first, 2, klass)
+ obj3 = inject_user_object(self.second, 1, klass)
+
+ self.first.clear_previous_run()
+
+ # Only the UserObject from the second POE remains.
+ assert_equal_sets([obj3], klass.user_objects)
+
+ def test_clear_previous_run_removes_function_calls_from_functions(self):
+ function = Function('some_function')
+ self._create_project_with_two_points_of_entry(function)
+
+ call1 = inject_function_call(self.first, function)
+ call2 = inject_function_call(self.first, function)
+ call3 = inject_function_call(self.second, function)
+
+ self.first.clear_previous_run()
+
+ # Only the FunctionCall from the second POE remains.
+ assert_equal_sets([call3], function.calls)
+
+ def test_clear_previous_run_removes_generator_objects_from_functions(self):
+ function = Function('generator', is_generator=True)
+ method = Method('generator_method', is_generator=True)
+ klass = Class('ClassWithGenerators', methods=[method])
+ self._create_project_with_two_points_of_entry(function, klass)
+
+ user_object = inject_user_object(self.first, 1, klass)
+ inject_generator_object(self.first, 2, function, {}, function)
+ inject_generator_object(self.first, 3, method, {}, user_object)
+
+ self.first.clear_previous_run()
+
+ assert_equal([], klass.user_objects)
+ assert_equal([], function.calls)
+
+ def test_clear_previous_run_ignores_not_referenced_objects(self):
+ function = Function('some_function')
+ self._create_project_with_two_points_of_entry(function)
+
+ args = {'i': ImmutableObject(123), 'u': UnknownObject(None),
+ 's': SequenceObject([], None), 'm': MapObject({}, None)}
+ inject_function_call(self.first, function, args)
+
+ self.first.clear_previous_run()
+ # Make sure it doesn't raise any exceptions.
View
68 test/test_store.py
@@ -1,10 +1,7 @@
-from pythoscope.astbuilder import parse, EmptyCode
+from pythoscope.astbuilder import parse
from pythoscope.code_trees_manager import CodeTreeNotFound
-from pythoscope.store import Class, Function, FunctionCall, Method, Module, \
- CodeTree, PointOfEntry, Project, TestClass, TestMethod, \
- UserObject, code_of, module_of
-from pythoscope.serializer import ImmutableObject, UnknownObject, \
- SequenceObject, MapObject
+from pythoscope.store import Class, Function, Method, Module, CodeTree,\
+ TestClass, TestMethod, code_of, module_of
from pythoscope.generator.adder import add_test_case
from assertions import *
@@ -43,65 +40,6 @@ def test_uses_system_specific_path_separator(self):
module = Module(subpath="some#path.py", project=EmptyProject())
assert_equal("some.path", module.locator)
-def inject_user_object(poe, obj, klass):
- def create_user_object():
- return UserObject(obj, klass)
- user_object = poe.execution._retrieve_or_capture(obj, create_user_object)
- klass.add_user_object(user_object)
- return user_object
-
-def inject_function_call(poe, function, args={}):
- call = FunctionCall(function, args)
- poe.execution.captured_calls.append(call)
- for arg in args.values():
- poe.execution.captured_objects[id(arg)] = arg
- function.add_call(call)
- return call
-
-class TestPointOfEntry:
- def _create_project_with_two_points_of_entry(self, obj):
- project = EmptyProject()
- project.create_module("module.py", code=EmptyCode(), objects=[obj])
- self.first = PointOfEntry(project, 'first')
- self.second = PointOfEntry(project, 'second')
-
- def test_clear_previous_run_removes_user_objects_from_classes(self):
- klass = Class('SomeClass')
- self._create_project_with_two_points_of_entry(klass)
-
- obj1 = inject_user_object(self.first, 1, klass)
- obj2 = inject_user_object(self.first, 2, klass)
- obj3 = inject_user_object(self.second, 1, klass)
-
- self.first.clear_previous_run()
-
- # Only the UserObject from the second POE remains.
- assert_equal_sets([obj3], klass.user_objects)
-
- def test_clear_previous_run_removes_function_calls_from_functions(self):
- function = Function('some_function')
- self._create_project_with_two_points_of_entry(function)
-
- call1 = inject_function_call(self.first, function)
- call2 = inject_function_call(self.first, function)
- call3 = inject_function_call(self.second, function)
-
- self.first.clear_previous_run()
-
- # Only the FunctionCall from the second POE remains.
- assert_equal_sets([call3], function.calls)
-
- def test_clear_previous_run_ignores_not_referenced_objects(self):
- function = Function('some_function')
- self._create_project_with_two_points_of_entry(function)
-
- args = {'i': ImmutableObject(123), 'u': UnknownObject(None),
- 's': SequenceObject([], None), 'm': MapObject({}, None)}
- inject_function_call(self.first, function, args)
-
- self.first.clear_previous_run()
- # Make sure it doesn't raise any exceptions.
-
class TestModuleOf:
def setUp(self):
project = EmptyProject()
View
50 test/testing_project.py
@@ -0,0 +1,50 @@
+import os.path
+
+from pythoscope.astbuilder import EmptyCode
+from pythoscope.execution import Execution
+from pythoscope.store import Project
+
+from helper import MemoryCodeTreesManager
+
+
+class TestingProject(Project):
+ """Project subclass useful during testing.
+
+ It contains handy creation methods, which can all be nested.
+ """
+ __test__ = False
+
+ def __init__(self, path=os.path.realpath(".")):
+ Project.__init__(self, path=path,
+ code_trees_manager_class=MemoryCodeTreesManager)
+ self._last_module = None
+ self._all_catch_module = None
+
+ def with_module(self, path="module.py"):
+ modpath = os.path.join(self.path, path)
+ self._last_module = self.create_module(modpath, code=EmptyCode())
+ return self
+
+ def with_all_catch_module(self):
+ """All object lookups will go through this single module.
+ """
+ if self._all_catch_module is not None:
+ raise ValueError("Already specified an all-catch module.")
+ self.with_module()
+ self._all_catch_module = self._last_module
+ return self
+
+ def with_object(self, obj):
+ if self._last_module is None:
+ raise ValueError("Tried to use with_object() without a module.")
+ self._last_module.add_object(obj)
+ return self
+
+ def make_new_execution(self):
+ return Execution(project=self)
+
+ def find_object(self, type, name, modulename=None):
+ if self._all_catch_module is not None:
+ return self._all_catch_module.find_object(type, name)
+ return Project.find_object(self, type, name, modulename)
+
View
140 tools/gather-metrics.py
@@ -0,0 +1,140 @@
+import commands
+import os
+import shutil
+import sys
+import tarfile
+import tempfile
+
+
+PREFIX = os.path.abspath(os.path.join(os.path.dirname(__file__), 'projects'))
+
+class GatheringError(Exception):
+ pass
+
+class GatheringResults(object):
+ def __init__(self, passed, skipped, errors, failures, coverage):
+ self.passed = passed
+ self.skipped = skipped
+ self.errors = errors
+ self.failures = failures
+ self.coverage = coverage
+ total = property(lambda s: s.passed + s.skipped + s.errors + s.failures)
+
+def notify(message):
+ print '*'*8, message
+
+def check_environment():
+ notify("Checking environment...")
+ # TODO: check for python, nosetests, coverage
+ notify("Environment OK.")
+
+def prepare_project(project):
+ notify("Preparing project...")
+ archive = os.path.join(PREFIX, project) + ".tar.gz"
+ project_dir = tempfile.mkdtemp(prefix="pythoscope-")
+ os.chdir(project_dir)
+ t = tarfile.open(archive)
+ t.extractall(project_dir)
+ t.close()
+ notify("Project ready in %s." % project_dir)
+ return project_dir
+
+def do_pythoscope_init(project_path):
+ notify("Doing pythoscope --init...")
+ status = os.system("pythoscope --init %s" % project_path)
+ notify("Done.")
+ if status != 0:
+ raise GatheringError("Failed at static inspection.")
+
+def put_point_of_entry(poe, project_dir):
+ notify("Copying point of entry %s..." % poe)
+ shutil.copy(os.path.join(PREFIX, poe),
+ os.path.join(project_dir, ".pythoscope", "points-of-entry"))
+ notify("Done.")
+
+def generate_tests_for_file(project_dir, appfile):
+ notify("Generating tests for %s..." % appfile)
+ status, output = commands.getstatusoutput("pythoscope --verbose -t nose %s" % os.path.join(project_dir, appfile))
+ print output
+ if contains_dynamic_inspection_error(output):
+ raise GatheringError("Failed at dynamic inspection.")
+ if status != 0:
+ raise GatheringError("Failed during test generation: exited with code=%d." % status)
+ notify("Done.")
+
+def contains_dynamic_inspection_error(output):
+ return "Point of entry exited with error" in output
+
+def run_nosetests(project_dir, test_path):
+ notify("Running nosetests on the generated test module...")
+ status, output = commands.getstatusoutput("nosetests -w %s %s" % (project_dir, test_path))
+ print output
+ if status != 0:
+ raise GatheringError("Failed during test run: nosetests exited with code=%d." % status)
+ counts = get_test_counts(output)
+ notify("Done.")
+ return counts
+
+def get_test_counts(output):
+ lines = output.splitlines()
+ if 'DeprecationWarning' in lines[0]:
+ line = lines[2]
+ else:
+ line = lines[0]
+ return line.count('.'), line.count('S'), line.count('E'), line.count('F')
+
+def run_nosetests_with_coverage(project_dir, test_path):
+ notify("Running nosetests with coverage on the generated test module...")
+ status, output = commands.getstatusoutput("nosetests --with-coverage --cover-package=reverend -w %s %s" % (project_dir, test_path))
+ print output
+ if status != 0:
+ raise GatheringError("Failed during test run: nosetests+coverage exited with code=%d." % status)
+ coverage = extract_coverage_percent(output)
+ notify("Done.")
+ return coverage
+
+def extract_coverage_percent(output):
+ for line in output.splitlines():
+ if line.startswith("TOTAL"):
+ return line.split()[3]
+ raise GatheringError("Can't find coverage in the output.")
+
+def cleanup_project(project_dir):
+ notify("Cleaning up %s..." % project_dir)
+ shutil.rmtree(project_dir)
+ notify("Done.")
+
+def gather_metrics_from_project(project, poes, appfile, testfile):
+ check_environment()
+ temp_dir = prepare_project(project)
+ project_dir = os.path.join(temp_dir, project)
+ try:
+ do_pythoscope_init(project_dir)
+ for poe in poes:
+ put_point_of_entry(poe, project_dir)
+ generate_tests_for_file(project_dir, appfile)
+ test_path = os.path.join(project_dir, testfile)
+ if not os.path.exists(test_path):
+ raise GatheringError("Failed at test generation: test file not generated.")
+ passed, skipped, errors, failures = run_nosetests(project_dir, test_path)
+ coverage = run_nosetests_with_coverage(project_dir, test_path)
+ return GatheringResults(passed, skipped, errors, failures, coverage)
+ finally:
+ cleanup_project(temp_dir)
+
+def main():
+ try:
+ results = gather_metrics_from_project(project="Reverend-r17924",
+ poes=["Reverend_poe_from_readme.py", "Reverend_poe_from_homepage.py"],
+ appfile="reverend/thomas.py",
+ testfile="tests/test_reverend_thomas.py")
+ print "%d test cases:" % results.total
+ print " %d passing" % results.passed
+ print " %d failing" % (results.failures + results.errors)
+ print " %d stubs" % results.skipped
+ print "%s coverage" % results.coverage
+ except GatheringError, e:
+ print e.args[0]
+
+if __name__ == '__main__':
+ sys.exit(main())
View
BIN tools/projects/Reverend-r17924.tar.gz
Binary file not shown.
View
10 tools/projects/Reverend_poe_from_homepage.py
@@ -0,0 +1,10 @@
+from reverend.thomas import Bayes
+guesser = Bayes()
+guesser.train('french', 'le la les du un une je il elle de en')
+guesser.train('german', 'der die das ein eine')
+guesser.train('spanish', 'el uno una las de la en')
+guesser.train('english', 'the it she he they them are were to')
+guesser.guess('they went to el cantina')
+guesser.guess('they were flying planes')
+guesser.train('english', 'the rain in spain falls mainly on the plain')
+guesser.save('my_guesser.bay')
View
9 tools/projects/Reverend_poe_from_readme.py
@@ -0,0 +1,9 @@
+from reverend.thomas import Bayes
+
+guesser = Bayes()
+guesser.train('fish', 'salmon trout cod carp')
+guesser.train('fowl', 'hen chicken duck goose')
+
+guesser.guess('chicken tikka marsala')
+
+guesser.untrain('fish','salmon carp')

0 comments on commit c315d4e

Please sign in to comment.