예제 #1
0
    def _add_test_module(self, parentNode, testModule):
        """Recursively walk test modules to build display tree."""
        # Need proper tag to get the right select function
        # We don't handle TestSuite, because it should aways be above this
        # try this:  tag = testModule.__class__.__name__
        if isinstance(testModule, TestModule):
            tag = 'TestModule'
        elif isinstance(testModule, TestCase):
            tag = 'TestCase'
        elif isinstance(testModule, TestMethod):
            tag = 'TestMethod'
        else:
            debug("add_test_module: Unknown testModule: %r", testModule)
            return

        debug("add_test_module: %r %r %r as %r", parentNode, tag, testModule, testModule.name)
        testModule_node = self.all_tests_tree.insert(
            parentNode, 'end', testModule.path,
            text=testModule.name,
            tags=[tag, 'active'],
            open=True)          # always insert a node

        if testModule.can_have_children():  # walk the children
            for subModuleName, subModule in sorted(testModule._child_nodes.items()):
                self._add_test_module(testModule_node, subModule)
예제 #2
0
    def execute_commandline(self, labels):
        "Return the command line to execute the specified test labels"
        args = self._pytest_exec + ['--cricket', 'execute']

        debug("cli_args: %r", self.cli_args)
        for aa in self.cli_args:
            value = self.cli_args[aa]
            if value:
                if aa == "junit-xml":
                    jpath = fix_file_path(self.cli_args["junit-xml"])  # fix slashes and timestamps
                    jdir = os.path.dirname(jpath)
                    if not os.path.exists(jdir):  # create directory if needed
                        os.makedirs(jdir)
                    value = jpath

                args.extend(['--'+aa, value])

        # TODO: need way to configure directories to run coverage against and other arguments
        # Breaks executor parsing
        # if self.coverage:
        #     args.append('--cov=')  # coverage over all directories

        if labels:
            args.extend(labels)

        return args
예제 #3
0
    def refresh(self, test_list=None, errors=None):
        """Rediscover the tests in the test suite.
        """
        if test_list is None:
            debug("Running %s to discover tests", self.discover_commandline())
            runner = subprocess.Popen(
                self.discover_commandline(),
                stdin=None,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=False,
            )

            test_list = []
            for line in runner.stdout:
                line = line.strip().decode('utf-8')
                #debug("Got line %r", line)
                test_list.append(line)

            errors = []
            for line in runner.stderr:
                line = line.strip().decode('utf-8')
                debug("Got error %r", line)
                errors.append(line)

            if errors and not test_list:
                raise ModelLoadError('\n'.join(errors))

        timestamp = datetime.now()

        # Make sure there is a data representation for every test in the list.
        for test_id in test_list:
            self.put_test(test_id)

        self.errors = errors if errors is not None else []
예제 #4
0
    def set_active(self, is_active, cascade=True):
        """Explicitly set the active state of the test case.

        Forces all test cases and test modules held by this test module
        to be set to the same active status

        If cascade is True, the parent test module will be prompted
        to check it's current active status.
        """
        debug("%r set_active", self)
        if self._active:
            if not is_active:
                self._active = False
                # self.emit('inactive')
                if cascade:
                    self._source._update_active()
                for testModule in self._child_nodes.values():
                    testModule.set_active(False, cascade=False)
        else:
            if is_active:
                self._active = True
                # self.emit('active')
                if cascade:
                    self._source._update_active()
                for testModule in self._child_nodes.values():
                    testModule.set_active(True, cascade=False)
예제 #5
0
    def put_test(self, test_id):
        """An idempotent insert method for tests.
        Ensures that a test identified as `test_id` exists in the test tree.
        Creates all intermediate nodes as well

        Returns found or created node
        """
        parent = self

        parts = self.split_test_id(test_id)
        part_paths = [d[1] for d in parts]  # just the path strings
        #debug("put_test(%r) splits to: %r", test_id, parts)

        count = 0
        for NodeClass, part in parts:
            try:
                child = parent[part]  # already exists
                #debug("put_test found %r", child)
            except KeyError:  # create and insert
                # need path to this point
                if parent is None or parent.path is None:
                    path = self.join_path(None, part)
                elif NodeClass == TestModule:
                    path = self.join_path((parent.path, part), None)
                else:  # TestMethod
                    path = self.join_path(parent.path, part)

                child = NodeClass(source=self, path=path, name=part)
                parent[part] = child
                debug("put_test created %r", child)
            parent = child
            count += 1

        return child
예제 #6
0
    def test_suite(self, test_suite):
        self._test_suite = test_suite
        debug("view test_suite = %r", test_suite)

        # Get a count of active tests to display in the status bar.
        count, labels = self.test_suite.find_tests(active=True)
        self.run_summary.set('T:%s P:0 F:0 E:0 X:0 U:0 S:0' % count)

        # Populate the initial tree nodes. This is recursive, because
        # the tree could be of arbitrary depth.
        for testModule_name, testModule in sorted(test_suite._child_nodes.items()):
            self._add_test_module('', testModule)

        # Listen for any state changes on nodes in the tree
        TestModule.bind('active', self.on_nodeActive)
        TestCase.bind('active', self.on_nodeActive)
        TestMethod.bind('active', self.on_nodeActive)

        TestModule.bind('inactive', self.on_nodeInactive)
        TestCase.bind('inactive', self.on_nodeInactive)
        TestMethod.bind('inactive', self.on_nodeInactive)

        # Listen for new nodes added to the tree
        TestModule.bind('new', self.on_nodeAdded)
        TestCase.bind('new', self.on_nodeAdded)
        TestMethod.bind('new', self.on_nodeAdded)

        # Listen for any status updates on nodes in the tree.
        TestMethod.bind('status_update', self.on_nodeStatusUpdate)

        # Update the test_suite to make sure coverage status matches the GUI
        self.on_coverageChange()
예제 #7
0
    def join_path(self, parents, parts):
        """Create a path given the parts."""
        # FIXME: this isn't the inverse of split_test_id
        # we need all the parts and to know what is module and what is within the file
        if isinstance(parts, (list, tuple)):
            part = '::'.join(parts)
        else:
            part = parts

        if parents is None:
            debug("pytest.join_path(None, %r) -> %r", parts, part)
            return part

        if isinstance(parents, (list, tuple)):
            # pytest seems to like /, even on Windows
            parent = '/'.join(parents)
        else:
            parent = parents

        if part:
            ret = '{}::{}'.format(parent, part)
        else:
            ret = parent

        debug("pytest.join_path(%r, %r) -> %r", parents, parts, ret)
        return ret
예제 #8
0
    def __init__(self, test_suite, count, labels):
        self.test_suite = test_suite  # The test tree
        self.total_count = count  # The total count of tests under execution
        self.completed_count = 0  # The count of tests that have been executed.
        self.result_count = {}    # The count of specific test results { status : count }
        self.error_buffer = []    # An accumulator for error output from all the tests.
        self.current_test = None  # The TestMethod object currently under execution.
        self.test_start = None    # Info from test start {path : "", start_time : seconds}
        self.start_time = None    # The timestamp when current_test started

        cmd = self.test_suite.execute_commandline(labels)
        debug("Running(%r): %r", os.getcwd(), cmd)
        self.proc = subprocess.Popen(
            cmd,
            stdin=None,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            shell=False,
            bufsize=1,
            close_fds='posix' in sys.builtin_module_names
        )

        # Piped stdout/stderr reads are blocking; therefore, we need to
        # do all our readline calls in a background thread, and use a
        # queue object to store lines that have been read.
        self.stdout = Queue()
        t = Thread(target=enqueue_output, args=(self.proc.stdout, self.stdout))
        t.daemon = True
        t.start()

        self.stderr = Queue()
        t = Thread(target=enqueue_output, args=(self.proc.stderr, self.stderr))
        t.daemon = True
        t.start()
예제 #9
0
    def on_testMethodSelected(self, event):
        "Event handler: a test case has been selected in the tree"
        if len(event.widget.selection()) == 1:
            label = event.widget.selection()[0]
            debug("testMethodSelected 1: %r", event.widget.selection()[0])
            testMethod = self.test_suite.get_node_from_label(label)

            self.name.set(testMethod.path)

            try:
                if testMethod.description:
                    self.description.delete('1.0', END)
                    self.description.insert('1.0', testMethod.description)
            except AttributeError:  # not a testMethod node
                return

            if testMethod.status != testMethod.STATUS_UNKNOWN:
                config = STATUS.get(testMethod.status, STATUS_DEFAULT)
                self.test_status_widget.config(foreground=config['color'])
                self.test_status.set(config['symbol'])

            # Show output as test is running
            if ((testMethod.status != testMethod.STATUS_UNKNOWN)
                or testMethod.output or testMethod.error):
                # Test has been executed, so show status windows
                if testMethod._duration is not None:
                    self.duration.set('%0.3fs' % testMethod._duration)
                else:
                    self.duration.set('')

                if testMethod.output:
                    self._show_test_output(testMethod.output)
                else:
                    self._hide_test_output()

                if testMethod.error:
                    self._show_test_errors(testMethod.error)
                else:
                    self._hide_test_errors()
            else:               # Nothing to show
                self.duration.set('Not executed')

                self._hide_test_output()
                self._hide_test_errors()

        else:            # Multiple tests selected, hide result fields
            debug("testMethodSelected: %r", event.widget.selection())

            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()

        # update "run selected" button enabled state
        self.set_selected_button_state()
예제 #10
0
    def __init__(self, source, path, name):
        self._child_labels = []
        self._child_nodes = {}  # {label : node }

        self._source = source  # AKA parent

        self._path = path
        self._name = name
        self._active = True
        debug("%r (source=%r, path=%r, name=%r)", self, source, path, name)
예제 #11
0
 def on_executorTestStart(self, event, test_path):
     """The executor has started running a new test.  Handles test_start"""
     # Update status line, and set the tree item to active.
     self.run_status.set('Running %s...' % test_path)
     try:
         self.all_tests_tree.item(test_path, tags=['TestMethod', 'active'])
         self.current_test_tree.selection_set((test_path, ))  # select only current test
         debug("Set selection to: %r", test_path)             # DEBUG
         self._need_update = True  # request a display update
     except TclError:
         debug("INTERNAL ERROR trying to set tags on %r", test_path)
예제 #12
0
def enqueue_output(out, queue):
    """A utility method for consuming piped output from a subprocess.

    Run this as a thread to get non-blocking data from out.

    Reads content from `out` one line at a time, strip trailing
    whitespace, and puts it onto queue for consumption
    """
    for line in iter(out.readline, b''):  # read until EOF
        queue.put(line.rstrip().decode('utf-8'))
    debug("enqueue_output closing %r", out)
    out.close()
예제 #13
0
    def _read_all_lines(self, q, name=""):
        """Read all the lines in the queue and return as a list."""
        lines = []
        try:
            while True:
                line = q.get(block=False)
                lines.append(line)
                debug("%s%r", name, line)
        except Empty:           # queue is empty
            pass

        return lines
예제 #14
0
    def set_active(self, is_active, cascade=True):
        """Explicitly set the active state of the test method

        If cascade is True, the parent testCase will be prompted
        to check it's current active status.
        """
        debug("%r set_active", self)
        if self._active:
            if not is_active:
                self._active = False
                if cascade:
                    self._source._update_active()
        else:
            if is_active:
                self._active = True
                if cascade:
                    self._source._update_active()
예제 #15
0
    def _handle_test_start(self, pre):
        """Saw input with no current test.

        Arguments:
          pre  Dictionary with parsed json output from plugin

        Returns True if polling should continue
        """
        debug("Got new test: %r", pre)
        try:
            # No active test; first line tells us which test is running.
            path = None
            if 'path' in pre:
                path = pre['path']
            elif 'description' in pre:  # HACK? sometimes path is missing, but this isn't
                path = pre['description']

            if path is None:
                debug("Could not find path: %r", pre)
                self.current_test = None
                return True

            try:
                self.current_test = self.test_suite.get_node_from_label(path)
            except KeyError:
                # pytest likes to return just the last bit, search for it
                debug("Straight lookup of %r failed", path)
                matches = self.test_suite.find_tests_substring(path)
                if len(matches) == 1:
                    self.current_test = self.test_suite.get_node_from_label(
                        matches[0])
                else:
                    debug("Could not resolve path %r: %r", path, matches)
                    self.current_test = None
                    return True

            self.emit('test_start', test_path=self.current_test.path)

        except ValueError as e:
            debug("ValueError: %r", e)
            self.current_test = None
            self.emit('suite_end')
            return True

        return False
예제 #16
0
    def on_testOutputUpdate(self, event, test_path, new_text, was_empty):
        """A running test got output.  Handles test_output_update"""
        current_tree = self.current_test_tree
        if ((len(current_tree.selection()) != 1)
            or (current_tree.selection()[0] != test_path)):  # do nothing if not selected
            debug("test_output_update: not selected")        # DEBUG
            return

        if was_empty:
            # trigger selection event to refresh result page, displaying output box
            # In this case, testMethod.output will be used, new_text is ignored
            debug("test_output_update: re-selecting to show output")        # DEBUG
            current_tree.selection_set(current_tree.selection())
        else:
            self.output.insert(END, new_text)  # insert new contents at end
            # TODO: detect if user has changed position and don't scroll
            self.output.see(END)               # scroll to bottom

        self._need_update = True  # request a display update
예제 #17
0
    def on_executorTestEnd(self, event, test_path, result, remaining_time):
        """The executor has finished running a test. Handles test_end"""
        # Update the progress meter
        self.progress_value.set(self.progress_value.get() + 1)

        self._set_run_summary(remaining_time)  # Update the run summary

        if self.options and self.options.save:  # write output to a file
            testMethod = self.test_suite.get_node_from_label(test_path)

            if testMethod.output:
                fpath = self.options.save
                if '<TESTNAME>' in fpath:
                    fpath = fpath.replace('<TESTNAME>', testMethod._name)
                fpath = fix_file_path(fpath)  # handles DATETIME and slashes
                add2 = os.path.exists(fpath)
                debug("Writing output to %r", fpath)
                with open(fpath, 'a') as fd:
                    if add2:
                        print("================", file=fd)  # TODO: insert result, time, etc
                    fd.write(testMethod.output)

        # If the test that just fininshed is the one (and only one)
        # selected on the tree, update the display.
        current_tree = self.current_test_tree
        if len(current_tree.selection()) == 1:
            # One test selected.
            if current_tree.selection()[0] == test_path:
                # If the test that just finished running is the selected
                # test, force reset the selection, which will generate a
                # selection event, forcing a refresh of the result page.
                debug("on_executorTestEnd: re-selecting to show output")  # DEBUG
                current_tree.selection_set(current_tree.selection())
        else:
            # No or Multiple tests selected
            self.name.set('')
            self.test_status.set('')

            self.duration.set('')
            self.description.delete('1.0', END)

            self._hide_test_output()
            self._hide_test_errors()
예제 #18
0
    def find_tests_substring(self, substring):
        """Find all tests with substring in path."""
        ret = []

        try:
            if self.path and substring in self.path:
                debug("find_tests_sub got %r in %r", substring, self.path)
                ret.append(self.path)
        except AttributeError:
            pass  # TestSuite has no path

        for subModuleName, subModule in sorted(self._child_nodes.items()):
            if subModule.can_have_children():  # walk the children
                ret.extend(subModule.find_tests_substring(substring))
            else:  # TestMethods
                if substring in subModule.path:
                    debug("find_tests_sub got %r in %r", substring,
                          subModule.path)
                    ret.append(subModule.path)

        return ret
예제 #19
0
    def on_nodeStatusUpdate(self, event, node):
        """Event handler: a node on the tree has received a status update. 
        Handles status_update"""
        self.all_tests_tree.item(node.path, tags=['TestMethod', STATUS[node.status]['tag']])

        if node.status in TestMethod.FAILING_STATES:
            # Test is in a failing state. Make sure it is on the problem tree,
            # with the correct current status.

            debug("nodeStatusUpdate: %r fail", node.path)
            parts = self.test_suite.split_test_id(node.path)
            parentModule = self.test_suite
            for part in parts:  # walk down tree so we can create missing levels
                testModule = parentModule[part[1]]

                if not self.problem_tests_tree.exists(testModule.path):
                    debug("Create problem node %r under %r", testModule, parentModule)
                    parent_path = parentModule.path if parentModule.path else ''
                    self.problem_tests_tree.insert(
                        parent_path, 'end', testModule.path,
                        text=testModule.name,
                        tags=[testModule.__class__.__name__, 'active'],
                        open=True
                    )

                parentModule = testModule

            self.problem_tests_tree.item(node.path, tags=['TestMethod', STATUS[node.status]['tag']])
        else:
            # Test passed; if it's on the problem tree, remove it.
            debug("nodeStatusUpdate: %r pass", node.path)
            if self.problem_tests_tree.exists(node.path):
                self.problem_tests_tree.delete(node.path)

                # Check all parents of this node. Recursively remove
                # any parent has no children as a result of this deletion.
                has_children = False
                node = node._source
                while node.path and not has_children:
                    if not self.problem_tests_tree.get_children(node.path):
                        self.problem_tests_tree.delete(node.path)
                    else:
                        has_children = True
                    node = node._source
예제 #20
0
def main(Model):
    """Run the main loop of the app.

    Take the project Model as the argument. This model will be
    instantiated as part of the main loop.
    """
    parser = ArgumentParser()

    parser.add_argument("--version", action="store_true",
                        help="Display version number and exit")
    parser.add_argument("--debug", "-d", action="store_true",
                        help="Turn on debug prints (to console).  Also pass python '-u'")
    parser.add_argument("--save",
                        help="Set path to save test output.  <TESTNAME> and <DATETIME> are replaced")
    parser.add_argument("testdir", action="store", default="", nargs='?',
                        help="Test root directory.  Default is current directory")

    Model.add_arguments(parser)
    options = parser.parse_args()

    # Check the shortcut options
    if options.version:
        import cricket
        print(cricket.__version__)
        sys.exit(2)

    if options.debug:
        set_debug(True)

    if options.testdir:
        os.chdir(options.testdir)

    # Set up the root Tk context
    debug("Starting GUI init")
    root = Tk()

    # Construct an empty window
    view = MainWindow(root, options=options)

    # Try to load the test_suite. If any error occurs during
    # test_suite load, show an error dialog
    test_suite = None
    while test_suite is None:
        try:
            debug("Discovering initial test_suite")
            test_suite = Model(options)
            test_suite.refresh()
        except ModelLoadError as e:
            # Load failed; destroy the test_suite and show an error dialog.
            # If the user selects cancel, quit.
            debug("Test_Suite initial failed.  Find error dialog and click on quit")
            test_suite = None
            dialog = TestLoadErrorDialog(root, e.trace)
            if dialog.status == dialog.CANCEL:
                sys.exit(1)
    if test_suite.errors:
        dialog = IgnorableTestLoadErrorDialog(root, '\n'.join(test_suite.errors))
        if dialog.status == dialog.CANCEL:
            sys.exit(1)

    # Set the test_suite for the main window.
    # This populates the tree, and sets listeners for
    # future tree modifications.
    view.test_suite = test_suite
    if is_debug():
        count, labels = test_suite.find_tests(allow_all=True)
        debug("Found %d tests:", count)
        debug("%s", '\n'.join(labels))

    # Run the main loop
    try:
        debug("Starting GUI mainloop")
        view.mainloop()
    except KeyboardInterrupt:
        view.on_quit()
예제 #21
0
    def poll(self):
        """Poll the runner looking for new test output

        Returns:
          True if polling should continue
          False otherwise
        """

        finished = False  # saw suite end marker
        stopped = False   # process exited (which is bad if not finished)

        # Check to see if the subprocess is still running.
        if self.proc is None:   # process never started (should never happen)
            stopped = True
            debug("Process never started")
        elif self.proc.poll() is not None:  # process has exited
            stopped = True
            debug("Process exited with %d", self.proc.poll())
            # there still might be output in the pipes

        # grab all complete lines so far
        self.error_buffer.extend(self._read_all_lines(self.stderr, name="Stderr: "))
        for line in self._read_all_lines(self.stdout, name="Stdout: "):
            # Start of suite or new test. Next line will be test start
            if line in (PipedTestRunner.START_TEST_RESULTS, PipedTestResult.RESULT_SEPARATOR):
                debug("Test (or suite) start")
                self.current_test = None
                continue

            elif line == PipedTestRunner.END_TEST_RESULTS: # End of test suite execution.
                debug("Test suite finished")
                finished = True
                break

            if line.startswith('\x1b'):  # Some tools insert escape sequences, strip that
                nn = line.find('{')
                if nn > 0:
                    debug("Strip escape from: %r", line)
                    line = line[nn:]

            if line and (line[0] == '{') and (line[-1] == '}'):  # looks like json
                post = None
                try:
                    post = json.loads(line)
                except:         # wasn't valid json, just collect as output
                    debug("Wasn't really Json: %r", line)
                    pass

                if post is not None:
                    if ('start_time' in post) and ('path' in post):  # start of a test
                        if self.current_test is not None:
                            debug("test start didn't follow a test end")
                        self.test_start = post  # save test start info for later
                        self._handle_test_start(post)  # find test and set current_test
                        continue

                    elif ('end_time' in post) and ('status' in post):  # test end
                        # sub test may have multiple results for one start (unittest)
                        if self.current_test is None:
                            debug("test result didn't follow a test start")

                        else:
                            status, error = parse_status_and_error(post)
                            self._handle_test_end(status, error, self.test_start, post)
                            # TODO: aggregate sub test status
                            # we can't clear current_test if there are sub-tests
                        continue
            # if that wasn't json, or json that we recognized, fall through to output capture

            if self.current_test is None: # A test isn't running - send to status update line
                line = line.strip()
                debug("Between test input: %r", line)
                self.emit('test_status_update', update=line)
                continue

            else:
                was_empty = self.current_test.output == ""
                self.current_test.add_output((line, ))
                # prepend newline if adding to existing text
                new_text = ("" if was_empty else '\n') + line
                self.emit('test_output_update',
                          test_path=self.current_test.path, new_text=new_text, was_empty=was_empty)
                continue

        if finished:            # saw suite end
            debug("Finished. %d in error buffer", len(self.error_buffer))
            if self.error_buffer:
                # YUCK:  This puts all stderr output into a popup
                self.emit('suite_end', error='\n'.join(self.error_buffer))
            else:
                self.emit('suite_end')
            return False

        elif stopped:  # subprocess has stopped before we saw finished
            debug("Process stopped. %d in error buffer", len(self.error_buffer))
            if self.error_buffer:
                # YUCK?:  This puts all stderr output into a popup ???
                self.emit('suite_error', error='\n'.join(self.error_buffer))
            else:
                self.emit('suite_error', error='Test output ended unexpectedly')
            return False

        return True           # Still running - requeue polling event.
예제 #22
0
 def on_testModuleClicked(self, event):
     "Event handler: a module has been clicked in the tree"
     label = event.widget.focus()
     testModule = self.test_suite.get_node_from_label(label)
     debug("testModuleClicked: %r, %r", label, testModule)
     testModule.toggle_active()
예제 #23
0
 def on_testMethodClicked(self, event):
     "Event handler: a test method has been clicked in the tree"
     label = event.widget.focus()
     testMethod = self.test_suite.get_node_from_label(label)
     debug("testMethodClicked: %r, %r", label, testMethod)
     testMethod.toggle_active()
예제 #24
0
 def __init__(self):
     debug("TestSuite()")
     TestNode.__init__(self, None, None, None)
     self.errors = []
     self.coverage = False