test_mozwebidlcodegen.py (10595B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import json 6 import os 7 import shutil 8 import sys 9 import tempfile 10 import unittest 11 12 import mozpack.path as mozpath 13 from mozfile import NamedTemporaryFile, load_source 14 from mozunit import MockedOpen, main 15 from mozwebidlcodegen import WebIDLCodegenManager, WebIDLCodegenManagerState 16 17 OUR_DIR = mozpath.abspath(mozpath.dirname(__file__)) 18 TOPSRCDIR = mozpath.normpath(mozpath.join(OUR_DIR, "..", "..", "..", "..")) 19 20 21 class TestWebIDLCodegenManager(unittest.TestCase): 22 TEST_STEMS = { 23 "Child", 24 "Parent", 25 "ExampleBinding", 26 "TestEvent", 27 } 28 29 @property 30 def _static_input_paths(self): 31 s = { 32 mozpath.join(OUR_DIR, p) 33 for p in os.listdir(OUR_DIR) 34 if p.endswith(".webidl") 35 } 36 37 return s 38 39 @property 40 def _config_path(self): 41 config = mozpath.join(OUR_DIR, "TestBindings.conf") 42 self.assertTrue(os.path.exists(config)) 43 44 return config 45 46 def _get_manager_args(self): 47 tmp = tempfile.mkdtemp() 48 self.addCleanup(shutil.rmtree, tmp) 49 50 cache_dir = mozpath.join(tmp, "cache") 51 os.mkdir(cache_dir) 52 53 ip = self._static_input_paths 54 55 inputs = ( 56 ip, 57 {mozpath.splitext(mozpath.basename(p))[0] for p in ip}, 58 set(), 59 set(), 60 ) 61 62 return dict( 63 config_path=self._config_path, 64 webidl_root=cache_dir, 65 inputs=inputs, 66 exported_header_dir=mozpath.join(tmp, "exports"), 67 codegen_dir=mozpath.join(tmp, "codegen"), 68 state_path=mozpath.join(tmp, "state.json"), 69 make_deps_path=mozpath.join(tmp, "codegen.pp"), 70 make_deps_target="codegen.pp", 71 cache_dir=cache_dir, 72 ) 73 74 def _get_manager(self): 75 return WebIDLCodegenManager(**self._get_manager_args()) 76 77 def test_unknown_state_version(self): 78 """Loading a state file with a too new version resets state.""" 79 args = self._get_manager_args() 80 81 p = args["state_path"] 82 83 with open(p, "w", newline="\n") as fh: 84 json.dump( 85 { 86 "version": WebIDLCodegenManagerState.VERSION + 1, 87 "foobar": "1", 88 }, 89 fh, 90 ) 91 92 manager = WebIDLCodegenManager(**args) 93 94 self.assertEqual(manager._state["version"], WebIDLCodegenManagerState.VERSION) 95 self.assertNotIn("foobar", manager._state) 96 97 def test_generate_build_files(self): 98 """generate_build_files() does the right thing from empty.""" 99 manager = self._get_manager() 100 result = manager.generate_build_files() 101 self.assertEqual(len(result.inputs), 4) 102 103 output = manager.expected_build_output_files() 104 self.assertEqual(result.created, output) 105 self.assertEqual(len(result.updated), 0) 106 self.assertEqual(len(result.unchanged), 0) 107 108 for f in output: 109 self.assertTrue(os.path.isfile(f)) 110 111 for f in manager.GLOBAL_DECLARE_FILES: 112 self.assertIn(mozpath.join(manager._exported_header_dir, f), output) 113 114 for f in manager.GLOBAL_DEFINE_FILES: 115 self.assertIn(mozpath.join(manager._codegen_dir, f), output) 116 117 for s in self.TEST_STEMS: 118 self.assertTrue( 119 os.path.isfile( 120 mozpath.join(manager._exported_header_dir, "%sBinding.h" % s) 121 ) 122 ) 123 self.assertTrue( 124 os.path.isfile( 125 mozpath.join(manager._exported_header_dir, "%sBindingFwd.h" % s) 126 ) 127 ) 128 self.assertTrue( 129 os.path.isfile(mozpath.join(manager._codegen_dir, "%sBinding.cpp" % s)) 130 ) 131 132 self.assertTrue(os.path.isfile(manager._state_path)) 133 134 with open(manager._state_path) as fh: 135 state = json.load(fh) 136 self.assertEqual(state["version"], 3) 137 self.assertIn("webidls", state) 138 139 child = state["webidls"]["Child.webidl"] 140 self.assertEqual(len(child["inputs"]), 2) 141 self.assertEqual(len(child["outputs"]), 3) 142 self.assertEqual(child["sha1"], "c34c40b0fa0ac57c2834ee282efe0681e4dacc35") 143 144 def test_generate_build_files_load_state(self): 145 """State should be equivalent when instantiating a new instance.""" 146 args = self._get_manager_args() 147 m1 = WebIDLCodegenManager(**args) 148 self.assertEqual(len(m1._state["webidls"]), 0) 149 m1.generate_build_files() 150 151 m2 = WebIDLCodegenManager(**args) 152 self.assertGreater(len(m2._state["webidls"]), 2) 153 self.assertEqual(m1._state, m2._state) 154 155 def test_no_change_no_writes(self): 156 """If nothing changes, no files should be updated.""" 157 args = self._get_manager_args() 158 m1 = WebIDLCodegenManager(**args) 159 m1.generate_build_files() 160 161 m2 = WebIDLCodegenManager(**args) 162 result = m2.generate_build_files() 163 164 self.assertEqual(len(result.inputs), 0) 165 self.assertEqual(len(result.created), 0) 166 self.assertEqual(len(result.updated), 0) 167 168 def test_output_file_regenerated(self): 169 """If an output file disappears, it is regenerated.""" 170 args = self._get_manager_args() 171 m1 = WebIDLCodegenManager(**args) 172 m1.generate_build_files() 173 174 rm_count = 0 175 for p in m1._state["webidls"]["Child.webidl"]["outputs"]: 176 rm_count += 1 177 os.unlink(p) 178 179 for p in m1.GLOBAL_DECLARE_FILES: 180 rm_count += 1 181 os.unlink(mozpath.join(m1._exported_header_dir, p)) 182 183 m2 = WebIDLCodegenManager(**args) 184 result = m2.generate_build_files() 185 self.assertEqual(len(result.created), rm_count) 186 187 def test_only_rebuild_self(self): 188 """If an input file changes, only rebuild that one file.""" 189 args = self._get_manager_args() 190 m1 = WebIDLCodegenManager(**args) 191 m1.generate_build_files() 192 193 child_path = None 194 for p in m1._input_paths: 195 if p.endswith("Child.webidl"): 196 child_path = p 197 break 198 199 self.assertIsNotNone(child_path) 200 child_content = open(child_path).read() 201 202 with MockedOpen({child_path: child_content + "\n/* */"}): 203 m2 = WebIDLCodegenManager(**args) 204 result = m2.generate_build_files() 205 self.assertEqual(result.inputs, set([child_path])) 206 self.assertEqual(len(result.updated), 0) 207 self.assertEqual(len(result.created), 0) 208 209 def test_rebuild_dependencies(self): 210 """Ensure an input file used by others results in others rebuilding.""" 211 args = self._get_manager_args() 212 m1 = WebIDLCodegenManager(**args) 213 m1.generate_build_files() 214 215 parent_path = None 216 child_path = None 217 for p in m1._input_paths: 218 if p.endswith("Parent.webidl"): 219 parent_path = p 220 elif p.endswith("Child.webidl"): 221 child_path = p 222 223 self.assertIsNotNone(parent_path) 224 parent_content = open(parent_path).read() 225 226 with MockedOpen({parent_path: parent_content + "\n/* */"}): 227 m2 = WebIDLCodegenManager(**args) 228 result = m2.generate_build_files() 229 self.assertEqual(result.inputs, {child_path, parent_path}) 230 self.assertEqual(len(result.updated), 0) 231 self.assertEqual(len(result.created), 0) 232 233 def test_python_change_regenerate_everything(self): 234 """If a Python file changes, we should attempt to rebuild everything.""" 235 236 # We don't want to mutate files in the source directory because we want 237 # to be able to build from a read-only filesystem. So, we install a 238 # dummy module and rewrite the metadata to say it comes from the source 239 # directory. 240 # 241 # Hacking imp to accept a MockedFile doesn't appear possible. So for 242 # the first iteration we read from a temp file. The second iteration 243 # doesn't need to import, so we are fine with a mocked file. 244 fake_path = mozpath.join(OUR_DIR, "fakemodule.py") 245 with NamedTemporaryFile("wt") as fh: 246 fh.write("# Original content") 247 fh.flush() 248 mod = load_source("mozwebidlcodegen.fakemodule", fh.name) 249 mod.__file__ = fake_path 250 251 args = self._get_manager_args() 252 m1 = WebIDLCodegenManager(**args) 253 with MockedOpen({fake_path: "# Original content"}): 254 # MockedOpen is not compatible with distributed filesystem 255 # access, so force the number of processes used to generate 256 # files to 1. 257 try: 258 result = m1.generate_build_files(processes=1) 259 l = len(result.inputs) 260 261 with open(fake_path, "w", newline="\n") as fh: 262 fh.write("# Modified content") 263 264 m2 = WebIDLCodegenManager(**args) 265 result = m2.generate_build_files(processes=1) 266 self.assertEqual(len(result.inputs), l) 267 268 result = m2.generate_build_files(processes=1) 269 self.assertEqual(len(result.inputs), 0) 270 finally: 271 del sys.modules["mozwebidlcodegen.fakemodule"] 272 273 def test_copy_input(self): 274 """Ensure a copied .webidl file is handled properly.""" 275 276 # This test simulates changing the type of a WebIDL from static to 277 # preprocessed. In that scenario, the original file still exists but 278 # it should no longer be consulted during codegen. 279 280 args = self._get_manager_args() 281 m1 = WebIDLCodegenManager(**args) 282 m1.generate_build_files() 283 284 old_path = None 285 for p in args["inputs"][0]: 286 if p.endswith("Parent.webidl"): 287 old_path = p 288 break 289 self.assertIsNotNone(old_path) 290 291 new_path = mozpath.join(args["cache_dir"], "Parent.webidl") 292 shutil.copy2(old_path, new_path) 293 294 args["inputs"][0].remove(old_path) 295 args["inputs"][0].add(new_path) 296 297 m2 = WebIDLCodegenManager(**args) 298 result = m2.generate_build_files() 299 self.assertEqual(len(result.updated), 0) 300 301 302 if __name__ == "__main__": 303 main()