def test_invalid_dict(self): # Pass in some invalid data for invalid in ["", "invalid", ["list"], 5, None]: with self.assertRaises(ValueError): coverage_data = CoverageData() coverage_data.load_from_dict('root_dir', '', invalid)
def test_get_relative_unknown_path(self): # Load the data coverage_data = CoverageData() coverage_data.load_from_dict('/root_dir', '', self.TEST_COVERAGE_DICT) # Unknown path returns None, even if in the root dir self.assertIs(coverage_data.rel_src_path('unknown'), None) self.assertIs(coverage_data.rel_src_path('/root_dir/unknown'), None)
def test_missing_key(self): # Load data with a JSON dict that is missing the line keys coverage_data = CoverageData() coverage_data.load_from_dict('root_dir', '', {'/src1': {'missing key': True}}) # Expect that no coverage information is loaded self.assertEqual(coverage_data.src_list(), []) self.assertIs(coverage_data.line_dict_for_src('root_dir/src1'), None)
def test_get_relative_src_path(self): # Load the data coverage_data = CoverageData() coverage_data.load_from_dict('/root_dir', 'prepend/to/path', self.TEST_COVERAGE_DICT) # Check that we can retrieve the relative source path self.assertEqual(coverage_data.rel_src_path(u'/root_dir/src1.js'), 'prepend/to/path/src1.js') self.assertEqual(coverage_data.rel_src_path(u'/root_dir/subdir/src2.js'), 'prepend/to/path/subdir/src2.js')
def test_covered_and_uncovered_src(self, mock_num_file_lines): # Set an expected source file coverage_data = CoverageData() coverage_data.add_expected_src('/root_dir', 'uncovered.js') # Configure the num file lines for the source file num_lines = 10 mock_num_file_lines.return_value = num_lines # Provide coverage info for other source files, # but not the one we passed to the constructor coverage_data.load_from_dict('/root_dir', '', self.TEST_COVERAGE_DICT) # Expect that all sources are reported (including the uncovered src) self.assertEqual(coverage_data.src_list(), [u'/root_dir/src1.js', u'/root_dir/subdir/src2.js', u'/root_dir/uncovered.js']) # Expect that the source is reported as 0% covered self.assertEqual(coverage_data.line_dict_for_src('/root_dir/uncovered.js'), {line_num: False for line_num in range(num_lines)}) # Expect that total coverage is smaller because # of the uncovered file. expected_coverage = 6.0 / (8.0 + num_lines) self.assertEqual(coverage_data.total_coverage(), expected_coverage)
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 = {}
def assert_output_equals(self, coverage_dict, expected_output): """ Asserts that the output from the coverage reporter is equal to `expected_output`. `coverage_dict` is a dict of the form: { SRC_PATH: [ COVER_INFO, ...] where COVER_INFO is either: * None: no coverage info * integer: number of times line was hit for example: { 'src.js': [ 1, 0, None, 1]} This assumes that the output is XML-parseable; it will parse the XML to ignore whitespace between elements. """ # Munge the dict into the right format coverage_dict = {src_path: {'lineData': line_data} for src_path, line_data in coverage_dict.items()} # Create a `CoverageData` instance. # Since this involves no network or filesystem access # we don't bother mocking it. data = CoverageData() data.load_from_dict('root_dir', '', coverage_dict) # Write the report to the output file in the temp directory self.reporter.write_report(data) # Read the data back in from the output file with open(self.OUTPUT_FILE_NAME) as output_file: output_str = output_file.read() # Run the reports through the XML parser to normalize format output_str = etree.tostring(etree.fromstring(output_str)) expected_output = etree.tostring(etree.fromstring(expected_output)) # Check that the the output matches what we expect self.assertEqual(output_str, expected_output)
def test_suite_name_list(self): # Report data from two suites coverage_data = CoverageData() coverage_data.add_suite_name(2) coverage_data.add_suite_name(3) coverage_data.add_suite_name(3) self.assertEqual(coverage_data.suite_name_list(), [2, 3])
def test_multiple_load_from_dict(self): # Load the data coverage_data = CoverageData() coverage_data.load_from_dict('/root_dir', '', self.TEST_COVERAGE_DICT) # Load additional data covering the lines that were uncovered lines = [0, 1, 0, 1, 1, None] coverage_data.load_from_dict('/root_dir', '', {'/src1.js': {'lineData': lines}}) # Check that the two sources are combined correctly expected = {0: True, 1: True, 2: True, 3: True, 4: True, 5: True} self.assertEqual(coverage_data.line_dict_for_src('/root_dir/src1.js'), expected)
def test_load_from_dict(self): # Load the data coverage_data = CoverageData() coverage_data.load_from_dict('/root_dir', '', self.TEST_COVERAGE_DICT) # Check that it gets parsed correctly self.assertEqual(coverage_data.src_list(), [u'/root_dir/src1.js', u'/root_dir/subdir/src2.js']) self.assertEqual(coverage_data.line_dict_for_src('/root_dir/src1.js'), {0: True, 2: True, 3: False, 5: True}) self.assertEqual(coverage_data.line_dict_for_src('/root_dir/subdir/src2.js'), {0: True, 1: True, 2: True, 3: False})
def test_different_root_dirs(self): # Load data from two different root dirs coverage_data = CoverageData() coverage_data.load_from_dict('/root_1', '', self.TEST_COVERAGE_DICT) coverage_data.load_from_dict('/root_2', '', self.TEST_COVERAGE_DICT) # We should get two separate sources self.assertEqual(coverage_data.src_list(), ['/root_1/src1.js', '/root_1/subdir/src2.js', '/root_2/src1.js', '/root_2/subdir/src2.js']) # But the data in each should be the same expected = {0: True, 2: True, 3: False, 5: True} self.assertEqual(coverage_data.line_dict_for_src('/root_1/src1.js'), expected) self.assertEqual(coverage_data.line_dict_for_src('/root_2/src1.js'), expected)
def test_uncovered_src(self, mock_num_file_lines): # Set an expected source file coverage_data = CoverageData() coverage_data.add_expected_src('/root_dir', 'src1.js') # Configure the num file lines for the source file num_lines = 10 mock_num_file_lines.return_value = num_lines # Provide no coverage information (did NOT call `load_from_dict()`) # Expect that the source is still reported self.assertEqual(coverage_data.src_list(), ['/root_dir/src1.js']) self.assertEqual(coverage_data.rel_src_path('/root_dir/src1.js'), 'src1.js') # Expect that the source is reported as 0% covered self.assertEqual(coverage_data.line_dict_for_src('/root_dir/src1.js'), {line_num: False for line_num in range(num_lines)}) # Expect that total coverage is 0% self.assertEqual(coverage_data.total_coverage(), 0.0)
class SuitePageServer(ThreadingMixIn, HTTPServer): """ Serve test suite pages and included JavaScript files. """ protocol_version = 'HTTP/1.1' # Request response timeout timeout = 5 # Amount of time to wait for clients to POST coverage info # back to the server before timing out. COVERAGE_TIMEOUT = 2.0 # Amount of time to wait between checks that the we # have all the coverage info COVERAGE_WAIT_TIME = 0.1 # Returns the `CoverageData` instance used by the server # to store coverage data received from the test suites. # Since `CoverageData` is thread-safe, it is okay for # other processes to write to it asynchronously. coverage_data = None def __init__(self, suite_desc_list, suite_renderer, jscover_path=None, port=0): """ Initialize the server to serve test runner pages and dependencies described by `suite_desc_list` (list of `SuiteDescription` instances). `jscover_path` is the path to the JSCover JAR file. If not specified, no coverage information will be collected. Use `suite_renderer` (a `SuiteRenderer` instance) to render the test suite pages. """ # Store dependencies self.desc_dict = self._suite_dict_from_list(suite_desc_list) self.renderer = suite_renderer self._jscover_path = jscover_path # Create a dict for source instrumenter services # (One for each suite description) self.src_instr_dict = {} address = ('0.0.0.0', port) HTTPServer.__init__(self, address, SuitePageRequestHandler) 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 = {} def stop(self): """ Stop the server and free the port. """ # Stop each instrumenter service that we started for instr in self.src_instr_dict.values(): instr.stop() # Stop the page server and free the port self.shutdown() self.socket.close() def suite_url_list(self): """ Return a list of URLs (unicode strings), where each URL is a test suite page containing the JS code to run the JavaScript tests. """ return [self.root_url() + u'suite/{}'.format(suite_name) for suite_name in self.desc_dict.keys()] def root_url(self): """ Return the root URL (including host and port) for the server as a unicode string. """ host, port = self.server_address return u"http://{}:{}/".format(host, port) def all_coverage_data(self): """ Returns a `CoverageData` instance containing all coverage data received from running the tests. Blocks until all suites have reported coverage data. If it times out waiting for all data, raises a `TimeoutException`. If we are not collecting coverage, returns None. """ if self.coverage_data is not None: self._block_until(self._has_all_coverage) return self.coverage_data else: return None def _block_until(self, success_func): """ Block until `success_func` returns True. `success_func` should be a lambda with no argument. """ # Remember when we started start_time = time.time() # Until we are successful while not success_func(): # See if we've timed out if time.time() - start_time > self.COVERAGE_TIMEOUT: raise TimeoutError() # Wait a little bit before checking again time.sleep(self.COVERAGE_WAIT_TIME) def _has_all_coverage(self): """ Returns True if and only if every suite has coverage information. """ # Retrieve the indices of each suite for which coverage # information was reported. suite_name_list = self.coverage_data.suite_name_list() # Check that we have an index for every suite # (This is not the most efficient way to do this -- # if it becomes a bottleneck, we can revisit.) return (sorted(suite_name_list) == sorted(self.desc_dict.keys())) @classmethod def _suite_dict_from_list(cls, suite_desc_list): """ Given a list of `SuiteDescription` instances, construct a dictionary mapping suite names to the instances. Raises a `DuplicateSuiteNameError` if two suites have the same name. """ suite_dict = { suite.suite_name(): suite for suite in suite_desc_list } # Check that we haven't repeated keys duplicates = cls._duplicates([suite.suite_name() for suite in suite_desc_list]) if len(duplicates) > 0: msg = "Duplicate suite name(s): {}".format(",".join(duplicates)) raise DuplicateSuiteNameError(msg) return suite_dict @classmethod def _duplicates(cls, name_list): """ Given a list of strings, return a set of duplicates in the list. """ seen = set() duplicates = set() for name in name_list: # Check if we've already seen the name; if so, add it # to the list of duplicates if name in seen: duplicates.add(name) # Add the name to the list of names we've already seen else: seen.add(name) return duplicates
def test_total_coverage(self): coverage_data = CoverageData() coverage_data.load_from_dict('root_dir', '', self.TEST_COVERAGE_DICT) # Total coverage is 6/8 = 0.75 self.assertEqual(coverage_data.total_coverage(), 0.75)
def test_coverage_for_src(self): coverage_data = CoverageData() coverage_data.load_from_dict('root_dir', '', self.TEST_COVERAGE_DICT) # The coverage for root_dir/src1.js is 3/4 = 0.75 self.assertEqual(coverage_data.coverage_for_src('root_dir/src1.js'), 0.75)