Skip to content

Commit b8c11de

Browse files
authored
Merge pull request github#11498 from github/redsun82/swift-codegen
Swift: enhance `codegen` UX
2 parents 45e2a13 + bb3aa9e commit b8c11de

File tree

5 files changed

+167
-20
lines changed

5 files changed

+167
-20
lines changed

swift/codegen/codegen.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def _parse_args() -> argparse.Namespace:
4242
help="output directory for generated C++ files, required if trap or cpp is provided to --generate")
4343
p.add_argument("--generated-registry", type=_abspath, default=paths.swift_dir / "ql/.generated.list",
4444
help="registry file containing information about checked-in generated code")
45+
p.add_argument("--force", "-f", action="store_true",
46+
help="generate all files without skipping unchanged files and overwriting modified ones")
4547
return p.parse_args()
4648

4749

swift/codegen/generators/qlgen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ def generate(opts, renderer):
302302

303303
imports = {}
304304

305-
with renderer.manage(generated=generated, stubs=stubs, registry=opts.generated_registry) as renderer:
305+
with renderer.manage(generated=generated, stubs=stubs, registry=opts.generated_registry,
306+
force=opts.force) as renderer:
306307

307308
db_classes = [cls for cls in classes.values() if not cls.ipa]
308309
renderer.render(ql.DbClasses(db_classes), out / "Raw.qll")

swift/codegen/lib/render.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ def _do_write(self, mnemonic: str, contents: str, output: pathlib.Path):
5959
log.debug(f"{mnemonic}: generated {output.name}")
6060

6161
def manage(self, generated: typing.Iterable[pathlib.Path], stubs: typing.Iterable[pathlib.Path],
62-
registry: pathlib.Path) -> "RenderManager":
63-
return RenderManager(self._swift_dir, generated, stubs, registry)
62+
registry: pathlib.Path, force: bool = False) -> "RenderManager":
63+
return RenderManager(self._swift_dir, generated, stubs, registry, force)
6464

6565

6666
class RenderManager(Renderer):
@@ -87,9 +87,10 @@ class Hashes:
8787

8888
def __init__(self, swift_dir: pathlib.Path, generated: typing.Iterable[pathlib.Path],
8989
stubs: typing.Iterable[pathlib.Path],
90-
registry: pathlib.Path):
90+
registry: pathlib.Path, force: bool = False):
9191
super().__init__(swift_dir)
9292
self._registry_path = registry
93+
self._force = force
9394
self._hashes = {}
9495
self.written = set()
9596
self._existing = set()
@@ -103,12 +104,18 @@ def __enter__(self):
103104
return self
104105

105106
def __exit__(self, exc_type, exc_val, exc_tb):
106-
for f in self._existing - self._skipped - self.written:
107-
self._hashes.pop(self._get_path(f), None)
108-
f.unlink(missing_ok=True)
109-
log.info(f"removed {f.name}")
110-
for f in self.written:
111-
self._hashes[self._get_path(f)].post = self._hash_file(f)
107+
if exc_val is None:
108+
for f in self._existing - self._skipped - self.written:
109+
self._hashes.pop(self._get_path(f), None)
110+
f.unlink(missing_ok=True)
111+
log.info(f"removed {f.name}")
112+
for f in self.written:
113+
self._hashes[self._get_path(f)].post = self._hash_file(f)
114+
else:
115+
# if an error was encountered, drop already written files from the registry
116+
# so that they get the chance to be regenerated again during the next run
117+
for f in self.written:
118+
self._hashes.pop(self._get_path(f), None)
112119
self._dump_registry()
113120

114121
def _do_write(self, mnemonic: str, contents: str, output: pathlib.Path):
@@ -126,10 +133,13 @@ def _process_generated(self, generated: typing.Iterable[pathlib.Path]):
126133
for f in generated:
127134
self._existing.add(f)
128135
rel_path = self._get_path(f)
129-
if rel_path not in self._hashes:
136+
if self._force:
137+
pass
138+
elif rel_path not in self._hashes:
130139
log.warning(f"{rel_path} marked as generated but absent from the registry")
131140
elif self._hashes[rel_path].post != self._hash_file(f):
132-
raise Error(f"{rel_path} is generated but was modified, please revert the file")
141+
raise Error(f"{rel_path} is generated but was modified, please revert the file "
142+
"or pass --force to overwrite")
133143

134144
def _process_stubs(self, stubs: typing.Iterable[pathlib.Path]):
135145
for f in stubs:
@@ -138,10 +148,13 @@ def _process_stubs(self, stubs: typing.Iterable[pathlib.Path]):
138148
self._hashes.pop(rel_path, None)
139149
continue
140150
self._existing.add(f)
141-
if rel_path not in self._hashes:
151+
if self._force:
152+
pass
153+
elif rel_path not in self._hashes:
142154
log.warning(f"{rel_path} marked as stub but absent from the registry")
143155
elif self._hashes[rel_path].post != self._hash_file(f):
144-
raise Error(f"{rel_path} is a stub marked as generated, but it was modified")
156+
raise Error(f"{rel_path} is a stub marked as generated, but it was modified, "
157+
"please remove the `// generated` header, revert the file or pass --force to overwrite it")
145158

146159
@staticmethod
147160
def is_customized_stub(file: pathlib.Path) -> bool:
@@ -165,12 +178,18 @@ def _hash_string(data: str) -> str:
165178
return h.hexdigest()
166179

167180
def _load_registry(self):
168-
with open(self._registry_path) as reg:
169-
for line in reg:
170-
filename, prehash, posthash = line.split()
171-
self._hashes[pathlib.Path(filename)] = self.Hashes(prehash, posthash)
181+
if self._force:
182+
return
183+
try:
184+
with open(self._registry_path) as reg:
185+
for line in reg:
186+
filename, prehash, posthash = line.split()
187+
self._hashes[pathlib.Path(filename)] = self.Hashes(prehash, posthash)
188+
except FileNotFoundError:
189+
pass
172190

173191
def _dump_registry(self):
192+
self._registry_path.parent.mkdir(parents=True, exist_ok=True)
174193
with open(self._registry_path, 'w') as out:
175194
for f, hashes in sorted(self._hashes.items()):
176195
print(f, hashes.pre, hashes.post, file=out)

swift/codegen/test/test_qlgen.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def qlgen_opts(opts):
4848
opts.generated_registry = generated_registry_path()
4949
opts.ql_format = True
5050
opts.swift_dir = paths.swift_dir
51+
opts.force = False
5152
return opts
5253

5354

@@ -430,7 +431,9 @@ def test_format_error(opts, generate, render_manager, run_mock):
430431
generate([schema.Class('A')])
431432

432433

433-
def test_manage_parameters(opts, generate, renderer):
434+
@pytest.mark.parametrize("force", [False, True])
435+
def test_manage_parameters(opts, generate, renderer, force):
436+
opts.force = force
434437
ql_a = opts.ql_output / "A.qll"
435438
ql_b = opts.ql_output / "B.qll"
436439
stub_a = opts.ql_stub_output / "A.qll"
@@ -448,7 +451,7 @@ def test_manage_parameters(opts, generate, renderer):
448451
generate([schema.Class('A')])
449452
assert renderer.mock_calls == [
450453
mock.call.manage(generated={ql_a, ql_b, test_a, test_b, import_file()}, stubs={stub_a, stub_b},
451-
registry=opts.generated_registry)
454+
registry=opts.generated_registry, force=force)
452455
]
453456

454457

swift/codegen/test/test_render.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,24 @@ def test_managed_render(pystache_renderer, sut):
7676
]
7777

7878

79+
def test_managed_render_with_no_registry(pystache_renderer, sut):
80+
data = mock.Mock(spec=("template",))
81+
text = "some text"
82+
pystache_renderer.render_name.side_effect = (text,)
83+
output = paths.swift_dir / "some/output.txt"
84+
registry = paths.swift_dir / "a/registry.list"
85+
86+
with sut.manage(generated=(), stubs=(), registry=registry) as renderer:
87+
renderer.render(data, output)
88+
assert renderer.written == {output}
89+
assert_file(output, text)
90+
91+
assert_file(registry, f"some/output.txt {hash(text)} {hash(text)}\n")
92+
assert pystache_renderer.mock_calls == [
93+
mock.call.render_name(data.template, data, generator=paths.exe_file.relative_to(paths.swift_dir)),
94+
]
95+
96+
7997
def test_managed_render_with_post_processing(pystache_renderer, sut):
8098
data = mock.Mock(spec=("template",))
8199
text = "some text"
@@ -192,6 +210,45 @@ def test_managed_render_with_modified_stub_file_not_marked_as_generated(pystache
192210
assert_file(registry, "")
193211

194212

213+
class MyError(Exception):
214+
pass
215+
216+
217+
def test_managed_render_exception_drops_written_from_registry(pystache_renderer, sut):
218+
data = mock.Mock(spec=("template",))
219+
text = "some text"
220+
pystache_renderer.render_name.side_effect = (text,)
221+
output = paths.swift_dir / "some/output.txt"
222+
registry = paths.swift_dir / "a/registry.list"
223+
write(output, text)
224+
write(registry, "a a a\n"
225+
f"some/output.txt whatever {hash(text)}\n"
226+
"b b b")
227+
228+
with pytest.raises(MyError):
229+
with sut.manage(generated=(), stubs=(), registry=registry) as renderer:
230+
renderer.render(data, output)
231+
raise MyError
232+
233+
assert_file(registry, "a a a\nb b b\n")
234+
235+
236+
def test_managed_render_exception_does_not_erase(pystache_renderer, sut):
237+
output = paths.swift_dir / "some/output.txt"
238+
stub = paths.swift_dir / "some/stub.txt"
239+
registry = paths.swift_dir / "a/registry.list"
240+
write(output)
241+
write(stub, "// generated bla bla")
242+
write(registry)
243+
244+
with pytest.raises(MyError):
245+
with sut.manage(generated=(output,), stubs=(stub,), registry=registry) as renderer:
246+
raise MyError
247+
248+
assert output.is_file()
249+
assert stub.is_file()
250+
251+
195252
def test_render_with_extensions(pystache_renderer, sut):
196253
data = mock.Mock(spec=("template", "extensions"))
197254
data.template = "test_template"
@@ -210,5 +267,70 @@ def test_render_with_extensions(pystache_renderer, sut):
210267
assert_file(expected_output, expected_contents)
211268

212269

270+
def test_managed_render_with_force_not_skipping_generated_file(pystache_renderer, sut):
271+
data = mock.Mock(spec=("template",))
272+
output = paths.swift_dir / "some/output.txt"
273+
some_output = "some output"
274+
registry = paths.swift_dir / "a/registry.list"
275+
write(output, some_output)
276+
write(registry, f"some/output.txt {hash(some_output)} {hash(some_output)}\n")
277+
278+
pystache_renderer.render_name.side_effect = (some_output,)
279+
280+
with sut.manage(generated=(output,), stubs=(), registry=registry, force=True) as renderer:
281+
renderer.render(data, output)
282+
assert renderer.written == {output}
283+
assert_file(output, some_output)
284+
285+
assert_file(registry, f"some/output.txt {hash(some_output)} {hash(some_output)}\n")
286+
assert pystache_renderer.mock_calls == [
287+
mock.call.render_name(data.template, data, generator=paths.exe_file.relative_to(paths.swift_dir)),
288+
]
289+
290+
291+
def test_managed_render_with_force_not_skipping_stub_file(pystache_renderer, sut):
292+
data = mock.Mock(spec=("template",))
293+
stub = paths.swift_dir / "some/stub.txt"
294+
some_output = "// generated some output"
295+
some_processed_output = "// generated some processed output"
296+
registry = paths.swift_dir / "a/registry.list"
297+
write(stub, some_processed_output)
298+
write(registry, f"some/stub.txt {hash(some_output)} {hash(some_processed_output)}\n")
299+
300+
pystache_renderer.render_name.side_effect = (some_output,)
301+
302+
with sut.manage(generated=(), stubs=(stub,), registry=registry, force=True) as renderer:
303+
renderer.render(data, stub)
304+
assert renderer.written == {stub}
305+
assert_file(stub, some_output)
306+
307+
assert_file(registry, f"some/stub.txt {hash(some_output)} {hash(some_output)}\n")
308+
assert pystache_renderer.mock_calls == [
309+
mock.call.render_name(data.template, data, generator=paths.exe_file.relative_to(paths.swift_dir)),
310+
]
311+
312+
313+
def test_managed_render_with_force_ignores_modified_generated_file(sut):
314+
output = paths.swift_dir / "some/output.txt"
315+
some_processed_output = "// some processed output"
316+
registry = paths.swift_dir / "a/registry.list"
317+
write(output, "// something else")
318+
write(registry, f"some/output.txt whatever {hash(some_processed_output)}\n")
319+
320+
with sut.manage(generated=(output,), stubs=(), registry=registry, force=True):
321+
pass
322+
323+
324+
def test_managed_render_with_force_ignores_modified_stub_file_still_marked_as_generated(sut):
325+
stub = paths.swift_dir / "some/stub.txt"
326+
some_processed_output = "// generated some processed output"
327+
registry = paths.swift_dir / "a/registry.list"
328+
write(stub, "// generated something else")
329+
write(registry, f"some/stub.txt whatever {hash(some_processed_output)}\n")
330+
331+
with sut.manage(generated=(), stubs=(stub,), registry=registry, force=True):
332+
pass
333+
334+
213335
if __name__ == '__main__':
214336
sys.exit(pytest.main([__file__] + sys.argv[1:]))

0 commit comments

Comments
 (0)