Beispiel #1
0
    def start(self):
        """
        Start serving pages on an open local port.
        """
        server_thread = threading.Thread(target=self.serve_forever)
        server_thread.daemon = True
        server_thread.start()

        # If we're collecting coverage information
        if self._jscover_path is not None:

            # Create an object to store coverage data we receive
            self.coverage_data = CoverageData()

            # Start each SrcInstrumenter instance if we know where JSCover is
            for suite_name, desc in self.desc_dict.iteritems():

                # Inform the coverage data that we expect this source
                # (report it as 0% if no info received).
                for rel_path in desc.src_paths():
                    self.coverage_data.add_expected_src(desc.root_dir(), rel_path)

                # Create an instrumenter serving files
                # in the suite description root directory
                instr = SrcInstrumenter(desc.root_dir(),
                                        tool_path=self._jscover_path)

                # Start the instrumenter service
                instr.start()

                # Associate the instrumenter with its suite description
                self.src_instr_dict[suite_name] = instr

        else:
            self.src_instr_dict = {}
Beispiel #2
0
    def test_multiple_instances_unique_ip(self):

        # Start the first service
        self.instrumenter.start()

        # Start a second service
        other = SrcInstrumenter(self.TEST_ROOT_DIR,
                                tool_path=self.TEST_TOOL_PATH,
                                subprocess_module=self.subprocess,
                                requests_module=self.requests)
        other.start()

        # Expect that the service process was started twice,
        # with different ports
        self._assert_jscover_called(self.TEST_TOOL_PATH,
                                    self.TEST_ROOT_DIR,
                                    num_calls=2)
Beispiel #3
0
    def setUp(self):

        # Mock the subprocess module
        self.subprocess = mock.Mock()
        self.process = mock.Mock()
        self.subprocess.Popen = mock.Mock(return_value=self.process)
        self.process.communicate = mock.Mock()

        # Configure the tool to return non-error
        self._configure_tool()

        # Mock the requests module
        self.requests = mock.Mock()

        # Set wait between attempts to very short to speed up the tests
        # Since we are using mocks, this shouldn't be an issue
        self._old_wait_between_attempts = SrcInstrumenter.WAIT_BETWEEN_ATTEMPTS
        SrcInstrumenter.WAIT_BETWEEN_ATTEMPTS = 0.01

        # Create, but do not start, the service
        self.instrumenter = SrcInstrumenter(self.TEST_ROOT_DIR,
                                            tool_path=self.TEST_TOOL_PATH,
                                            subprocess_module=self.subprocess,
                                            requests_module=self.requests)
Beispiel #4
0
class SrcInstrumenterTest(unittest.TestCase):

    TEST_ROOT_DIR = '/tmp/test'
    TEST_TOOL_PATH = '/usr/bin/jscover'
    TEST_INSTRUMENTED_SRC = 'instrumented JS src'

    ADDRESS_IN_USE_ERROR = dedent("""
        Exception in thread "main" java.lang.RuntimeException: java.net.BindException: Address already in use
            at jscover.Main.runServer(Main.java:470)
            at jscover.Main.runMain(Main.java:420)
            at jscover.Main.main(Main.java:411)
        Caused by: java.net.BindException: Address already in use
            at java.net.PlainSocketImpl.socketBind(Native Method)
            at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:383)
            at java.net.ServerSocket.bind(ServerSocket.java:328)
            at java.net.ServerSocket.<init>(ServerSocket.java:194)
            at java.net.ServerSocket.<init>(ServerSocket.java:106)
            at jscover.server.WebDaemon.start(WebDaemon.java:356)
            at jscover.Main.runServer(Main.java:468)
            ... 2 more
    """).strip()

    def setUp(self):

        # Mock the subprocess module
        self.subprocess = mock.Mock()
        self.process = mock.Mock()
        self.subprocess.Popen = mock.Mock(return_value=self.process)
        self.process.communicate = mock.Mock()

        # Configure the tool to return non-error
        self._configure_tool()

        # Mock the requests module
        self.requests = mock.Mock()

        # Set wait between attempts to very short to speed up the tests
        # Since we are using mocks, this shouldn't be an issue
        self._old_wait_between_attempts = SrcInstrumenter.WAIT_BETWEEN_ATTEMPTS
        SrcInstrumenter.WAIT_BETWEEN_ATTEMPTS = 0.01

        # Create, but do not start, the service
        self.instrumenter = SrcInstrumenter(self.TEST_ROOT_DIR,
                                            tool_path=self.TEST_TOOL_PATH,
                                            subprocess_module=self.subprocess,
                                            requests_module=self.requests)

    def tearDown(self):

        # Reset the old wait time between attempts
        SrcInstrumenter.WAIT_BETWEEN_ATTEMPTS = self._old_wait_between_attempts

    def test_start_service(self):

        # Start the service
        self.instrumenter.start()

        # Expect that the service process was started
        self._assert_jscover_called(self.TEST_TOOL_PATH,
                                    self.TEST_ROOT_DIR)

        # Stop the instrumenter service
        self.instrumenter.stop()

        # Expect that the service process was terminated
        self.process.terminate.assert_called_once_with()

    def test_get_instrumented_src(self):

        # Configure the `requests` HTTP library to return a
        # pre-defined response
        self._configure_http_response(200, self.TEST_INSTRUMENTED_SRC)

        # Try to instrument a source
        self.instrumenter.start()
        result = self.instrumenter.instrumented_src('src.js')

        # Expect that the type we get back is unicode
        self.assertTrue(isinstance(result, unicode))

        # Expect that we get the right source back
        self.assertEqual(result, self.TEST_INSTRUMENTED_SRC)

        # Expect that a GET request was made at the correct URL
        args, _ = self.requests.get.call_args
        self.assertEqual(len(args), 1)

        matches = re.match(r'http://127.0.0.1:\d+/src.js', args[0])
        self.assertIsNot(
            matches, None,
            msg="URL not in expected form: {}".format(args[0])
        )

    def test_instrumenter_returns_unicode(self):

        # Configure the `requests` HTTP library to return a
        # unicode response
        # (This used to cause a UnicodeDecode error)
        instrumented_unicode = u'tes\u0142 \u014Cf inst\0158umented uni\0186ode'
        self._configure_http_response(200, instrumented_unicode)

        # Try to instrument a source
        self.instrumenter.start()
        result = self.instrumenter.instrumented_src('src.js')

        # Expect that we get the right source back
        self.assertEqual(result, instrumented_unicode)

    def test_multiple_instances_unique_ip(self):

        # Start the first service
        self.instrumenter.start()

        # Start a second service
        other = SrcInstrumenter(self.TEST_ROOT_DIR,
                                tool_path=self.TEST_TOOL_PATH,
                                subprocess_module=self.subprocess,
                                requests_module=self.requests)
        other.start()

        # Expect that the service process was started twice,
        # with different ports
        self._assert_jscover_called(self.TEST_TOOL_PATH,
                                    self.TEST_ROOT_DIR,
                                    num_calls=2)

    def test_retry_on_port_conflict(self):

        # Configure the tool to return an error the first time
        # because something else is running on this port
        self._configure_tool(error_msg=self.ADDRESS_IN_USE_ERROR,
                             first_failure=True)

        # Start the service
        self.instrumenter.start()

        # Expect that the service process was started twice,
        # with different ports.
        self._assert_jscover_called(self.TEST_TOOL_PATH,
                                    self.TEST_ROOT_DIR,
                                    num_calls=2)

    def test_max_retry_port_conflict(self):

        # Configure the tool to always return an error
        self._configure_tool(error_msg=self.ADDRESS_IN_USE_ERROR)

        # Start the service
        with self.assertRaises(SrcInstrumenterError):
            self.instrumenter.start()

        # Expect that the tool was called several times with different
        # port numbers.
        self._assert_jscover_called(self.TEST_TOOL_PATH,
                                    self.TEST_ROOT_DIR,
                                    num_calls=SrcInstrumenter.MAX_START_ATTEMPTS)

    def test_command_not_found(self):

        # Configure the tool to raise an OSError (tool path not found)
        self.subprocess.Popen.side_effect = OSError

        # Expect an error
        with self.assertRaises(SrcInstrumenterError):
            self.instrumenter.start()

        # Expect that we only called the process exactly once
        self._assert_jscover_called(self.TEST_TOOL_PATH,
                                    self.TEST_ROOT_DIR,
                                    num_calls=1)

    def test_http_not_found_error(self):

        # Configure the `requests` HTTP library to return a
        # 404 not found response
        self._configure_http_response(404, "")

        # Try to instrument a source
        with self.assertRaises(SrcInstrumenterError):
            self.instrumenter.instrumented_src('src.js')

    def test_http_connection_refused_retry(self):

        # Raise a connection error on the first connection
        # Then return a success on the second attempt
        self._configure_http_response(200, self.TEST_INSTRUMENTED_SRC)
        self.requests.get.side_effect = [requests.exceptions.ConnectionError,
                                         self.requests.get.return_value]

        # Get the instrumented source (expect a retry on the first failure)
        self.instrumenter.start()
        result = self.instrumenter.instrumented_src('/src.js')

        # Expect that we get the right source back
        self.assertEqual(result, self.TEST_INSTRUMENTED_SRC)

    def test_http_connection_refused_max_retry(self):

        # Raise a connection error on every attempt
        self.requests.get.side_effect = requests.exceptions.ConnectionError

        # Expect that the instrumenter eventually gives up and raises an error
        with self.assertRaises(SrcInstrumenterError):
            self.instrumenter.instrumented_src('/src.js')

    def test_error_when_not_started(self):

        # Set up a valid response, so we don't fail for other reasons
        self._configure_http_response(200, self.TEST_INSTRUMENTED_SRC)

        # Expect that the instrumenter gives an error
        # if we do not call `start()` first
        with self.assertRaises(SrcInstrumenterError):
            self.instrumenter.instrumented_src('/src.js')

    def _configure_http_response(self, status_code, content):
        """
        Configure the `requests` library to respond with the given
        HTTP `status_code` and `content`.
        """
        response_mock = mock.MagicMock(requests.models.Response)
        response_mock.status_code = status_code
        response_mock.text = content
        self.requests.get.return_value = response_mock

    def _configure_tool(self, error_msg=None, first_failure=False):
        """
        Configure the JSCover subprocess to either continue running
        indefinitely (like JSCover will if it can start successfully)
        or terminate and output to stderr.

        `error_msg` is the error for the tool to return.
        If `first_failure` is `True`, configure the tool to return
        an error the first time it's called but succeed afterwards.
        Otherwise, always return an error.

        For testing purposes, the tool will not block indefinitely;
        it will raise an `Exception` indicating that the software
        under test would deadlock.
        """

        # Configure the tool to continue running indefinitely
        if error_msg is None:
            self.process.poll.return_value = None
            msg = "Do not call communicate() -- it will block indefinitely!"
            self.process.communicate.side_effect = Exception(msg)

        # Configure the tool to return immediately and have
        # an error return value
        else:

            # Fail the first time, then succeed
            if first_failure:
                self.process.communicate.side_effect = [("", error_msg), ("", "")]
                self.process.poll.side_effect = [1, None]

            # Always return an error
            else:
                self.process.communicate.side_effect = None
                self.process.communicate.return_value = ("", error_msg)
                self.process.poll.return_value = 1

    def _assert_jscover_called(self, tool_path, document_root, num_calls=1):
        """
        Assert that the JSCover tool was called `num_calls` times
        and configured to serve files in `document_root` to a local port.

        If called multiple times, expect that a unique port number was used.
        """

        # Check that we have the correct number of calls
        self.assertEqual(len(self.subprocess.Popen.call_args_list), num_calls)

        # Keep track of the ports we've already used
        used_ports = []

        # Verify that each call has the correct form
        for args, kwargs in self.subprocess.Popen.call_args_list:

            # First arg should be the list of call components
            self.assertEqual(len(args), 1)
            call = args[0]

            # Should send stdout and stderr back to the caller
            self.assertEqual(kwargs, {'stdout': None,
                                      'stderr': self.subprocess.PIPE})

            # Should be correct number of args
            self.assertEqual(len(call), 6)

            # First arguments specify the tool
            self.assertEqual(call[0:4],
                             ['java', '-jar', tool_path, '-ws'])

            # Next argument should be the local port
            ports = re.findall(r'--port=(\d+)', call[4])
            self.assertEqual(len(ports), 1)
            port_num = int(ports[0])
            self.assertTrue(10000 <= port_num <= 40000)
            self.assertFalse(port_num in used_ports)

            # Then the document root
            self.assertEqual(call[5], '--document-root=' + document_root)

            # Remember that we've seen this port
            used_ports.append(port_num)