# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from __future__ import unicode_literals import imp import json import os import shutil import sys import tempfile import unittest import mozpack.path as mozpath from mozwebidlcodegen import ( WebIDLCodegenManager, WebIDLCodegenManagerState, ) from mozfile import NamedTemporaryFile from mozunit import ( MockedOpen, main, ) OUR_DIR = mozpath.abspath(mozpath.dirname(__file__)) TOPSRCDIR = mozpath.normpath(mozpath.join(OUR_DIR, '..', '..', '..', '..')) class TestWebIDLCodegenManager(unittest.TestCase): TEST_STEMS = { 'Child', 'Parent', 'ExampleBinding', 'TestEvent', } @property def _static_input_paths(self): s = {mozpath.join(OUR_DIR, p) for p in os.listdir(OUR_DIR) if p.endswith('.webidl')} return s @property def _config_path(self): config = mozpath.join(TOPSRCDIR, 'dom', 'bindings', 'Bindings.conf') self.assertTrue(os.path.exists(config)) return config def _get_manager_args(self): tmp = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tmp) cache_dir = mozpath.join(tmp, 'cache') os.mkdir(cache_dir) ip = self._static_input_paths inputs = ( ip, {mozpath.splitext(mozpath.basename(p))[0] for p in ip}, set(), set(), ) return dict( config_path=self._config_path, inputs=inputs, exported_header_dir=mozpath.join(tmp, 'exports'), codegen_dir=mozpath.join(tmp, 'codegen'), state_path=mozpath.join(tmp, 'state.json'), make_deps_path=mozpath.join(tmp, 'codegen.pp'), make_deps_target='codegen.pp', cache_dir=cache_dir, ) def _get_manager(self): return WebIDLCodegenManager(**self._get_manager_args()) def test_unknown_state_version(self): """Loading a state file with a too new version resets state.""" args = self._get_manager_args() p = args['state_path'] with open(p, 'wb') as fh: json.dump({ 'version': WebIDLCodegenManagerState.VERSION + 1, 'foobar': '1', }, fh) manager = WebIDLCodegenManager(**args) self.assertEqual(manager._state['version'], WebIDLCodegenManagerState.VERSION) self.assertNotIn('foobar', manager._state) def test_generate_build_files(self): """generate_build_files() does the right thing from empty.""" manager = self._get_manager() result = manager.generate_build_files() self.assertEqual(len(result.inputs), 5) output = manager.expected_build_output_files() self.assertEqual(result.created, output) self.assertEqual(len(result.updated), 0) self.assertEqual(len(result.unchanged), 0) for f in output: self.assertTrue(os.path.isfile(f)) for f in manager.GLOBAL_DECLARE_FILES: self.assertIn(mozpath.join(manager._exported_header_dir, f), output) for f in manager.GLOBAL_DEFINE_FILES: self.assertIn(mozpath.join(manager._codegen_dir, f), output) for s in self.TEST_STEMS: self.assertTrue(os.path.isfile(mozpath.join( manager._exported_header_dir, '%sBinding.h' % s))) self.assertTrue(os.path.isfile(mozpath.join( manager._codegen_dir, '%sBinding.cpp' % s))) self.assertTrue(os.path.isfile(manager._state_path)) with open(manager._state_path, 'rb') as fh: state = json.load(fh) self.assertEqual(state['version'], 1) self.assertIn('webidls', state) child = state['webidls']['Child.webidl'] self.assertEqual(len(child['inputs']), 2) self.assertEqual(len(child['outputs']), 2) self.assertEqual(child['sha1'], 'c41527cad3bc161fa6e7909e48fa11f9eca0468b') def test_generate_build_files_load_state(self): """State should be equivalent when instantiating a new instance.""" args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) self.assertEqual(len(m1._state['webidls']), 0) m1.generate_build_files() m2 = WebIDLCodegenManager(**args) self.assertGreater(len(m2._state['webidls']), 2) self.assertEqual(m1._state, m2._state) def test_no_change_no_writes(self): """If nothing changes, no files should be updated.""" args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) m1.generate_build_files() m2 = WebIDLCodegenManager(**args) result = m2.generate_build_files() self.assertEqual(len(result.inputs), 0) self.assertEqual(len(result.created), 0) self.assertEqual(len(result.updated), 0) def test_output_file_regenerated(self): """If an output file disappears, it is regenerated.""" args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) m1.generate_build_files() rm_count = 0 for p in m1._state['webidls']['Child.webidl']['outputs']: rm_count += 1 os.unlink(p) for p in m1.GLOBAL_DECLARE_FILES: rm_count += 1 os.unlink(mozpath.join(m1._exported_header_dir, p)) m2 = WebIDLCodegenManager(**args) result = m2.generate_build_files() self.assertEqual(len(result.created), rm_count) def test_only_rebuild_self(self): """If an input file changes, only rebuild that one file.""" args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) m1.generate_build_files() child_path = None for p in m1._input_paths: if p.endswith('Child.webidl'): child_path = p break self.assertIsNotNone(child_path) child_content = open(child_path, 'rb').read() with MockedOpen({child_path: child_content + '\n/* */'}): m2 = WebIDLCodegenManager(**args) result = m2.generate_build_files() self.assertEqual(result.inputs, set([child_path])) self.assertEqual(len(result.updated), 0) self.assertEqual(len(result.created), 0) def test_rebuild_dependencies(self): """Ensure an input file used by others results in others rebuilding.""" args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) m1.generate_build_files() parent_path = None child_path = None for p in m1._input_paths: if p.endswith('Parent.webidl'): parent_path = p elif p.endswith('Child.webidl'): child_path = p self.assertIsNotNone(parent_path) parent_content = open(parent_path, 'rb').read() with MockedOpen({parent_path: parent_content + '\n/* */'}): m2 = WebIDLCodegenManager(**args) result = m2.generate_build_files() self.assertEqual(result.inputs, {child_path, parent_path}) self.assertEqual(len(result.updated), 0) self.assertEqual(len(result.created), 0) def test_python_change_regenerate_everything(self): """If a Python file changes, we should attempt to rebuild everything.""" # We don't want to mutate files in the source directory because we want # to be able to build from a read-only filesystem. So, we install a # dummy module and rewrite the metadata to say it comes from the source # directory. # # Hacking imp to accept a MockedFile doesn't appear possible. So for # the first iteration we read from a temp file. The second iteration # doesn't need to import, so we are fine with a mocked file. fake_path = mozpath.join(OUR_DIR, 'fakemodule.py') with NamedTemporaryFile('wt') as fh: fh.write('# Original content') fh.flush() mod = imp.load_source('mozwebidlcodegen.fakemodule', fh.name) mod.__file__ = fake_path args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) with MockedOpen({fake_path: '# Original content'}): old_exists = os.path.exists try: def exists(p): if p == fake_path: return True return old_exists(p) os.path.exists = exists result = m1.generate_build_files() l = len(result.inputs) with open(fake_path, 'wt') as fh: fh.write('# Modified content') m2 = WebIDLCodegenManager(**args) result = m2.generate_build_files() self.assertEqual(len(result.inputs), l) result = m2.generate_build_files() self.assertEqual(len(result.inputs), 0) finally: os.path.exists = old_exists del sys.modules['mozwebidlcodegen.fakemodule'] def test_copy_input(self): """Ensure a copied .webidl file is handled properly.""" # This test simulates changing the type of a WebIDL from static to # preprocessed. In that scenario, the original file still exists but # it should no longer be consulted during codegen. args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) m1.generate_build_files() old_path = None for p in args['inputs'][0]: if p.endswith('Parent.webidl'): old_path = p break self.assertIsNotNone(old_path) new_path = mozpath.join(args['cache_dir'], 'Parent.webidl') shutil.copy2(old_path, new_path) args['inputs'][0].remove(old_path) args['inputs'][0].add(new_path) m2 = WebIDLCodegenManager(**args) result = m2.generate_build_files() self.assertEqual(len(result.updated), 0) if __name__ == '__main__': main()