Skip to content

Commit 7cc2058

Browse files
committed
Codegen: add @qltest.test_with
This allows to group together related AST classes to reuse the same test source and extraction. For example this is useful for `EnumDecl/EnumCaseDecl/EnumElementDecl`, where this is applied to.
1 parent 319b799 commit 7cc2058

26 files changed

+256
-13
lines changed

misc/codegen/generators/qlgen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@ def generate(opts, renderer):
381381
for c in data.classes.values():
382382
if _should_skip_qltest(c, data.classes):
383383
continue
384-
test_dir = test_out / c.group / c.name
384+
test_with = data.classes[c.test_with] if c.test_with else c
385+
test_dir = test_out / test_with.group / test_with.name
385386
test_dir.mkdir(parents=True, exist_ok=True)
386387
if all(f.suffix in (".txt", ".ql", ".actual", ".expected") for f in test_dir.glob("*.*")):
387388
log.warning(f"no test source in {test_dir.relative_to(test_out)}")

misc/codegen/lib/schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class Class:
9393
doc: List[str] = field(default_factory=list)
9494
default_doc_name: Optional[str] = None
9595
hideable: bool = False
96+
test_with: Optional[str] = None
9697

9798
@property
9899
def final(self):
@@ -110,6 +111,7 @@ def check_types(self, known: typing.Iterable[str]):
110111
if self.synth.on_arguments is not None:
111112
for t in self.synth.on_arguments.values():
112113
_check_type(t, known)
114+
_check_type(self.test_with, known)
113115

114116

115117
@dataclass

misc/codegen/lib/schemadefs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def f(cls: type) -> type:
148148
_Pragma("qltest_skip")
149149
_Pragma("qltest_collapse_hierarchy")
150150
_Pragma("qltest_uncollapse_hierarchy")
151+
qltest.test_with = lambda cls: _annotate(test_with=cls)
151152

152153
ql.default_doc_name = lambda doc: _annotate(doc_name=doc)
153154
ql.hideable = _annotate(hideable=True)

misc/codegen/loaders/schemaloader.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ def modify(self, prop: schema.Property):
1919
prop.name = self.name.rstrip("_")
2020

2121

22+
def _get_name(x: str | type | None):
23+
if x is None:
24+
return None
25+
if isinstance(x, str):
26+
return x
27+
return x.__name__
28+
29+
2230
def _get_class(cls: type) -> schema.Class:
2331
if not isinstance(cls, type):
2432
raise schema.Error(f"Only class definitions allowed in schema, found {cls}")
@@ -38,6 +46,7 @@ def _get_class(cls: type) -> schema.Class:
3846
# getattr to inherit from bases
3947
group=getattr(cls, "_group", ""),
4048
hideable=getattr(cls, "_hideable", False),
49+
test_with=_get_name(getattr(cls, "_test_with", None)),
4150
# in the following we don't use `getattr` to avoid inheriting
4251
pragmas=cls.__dict__.get("_pragmas", []),
4352
synth=cls.__dict__.get("_synth", None),
@@ -107,6 +116,13 @@ def _fill_hideable_information(classes: typing.Dict[str, schema.Class]):
107116
todo.append(supercls)
108117

109118

119+
def _check_test_with(classes: typing.Dict[str, schema.Class]):
120+
for cls in classes.values():
121+
if cls.test_with is not None and classes[cls.test_with].test_with is not None:
122+
raise schema.Error(f"{cls.name} has test_with {cls.test_with} which in turn "
123+
f"has test_with {classes[cls.test_with].test_with}, use that directly")
124+
125+
110126
def load(m: types.ModuleType) -> schema.Schema:
111127
includes = set()
112128
classes = {}
@@ -136,6 +152,7 @@ def load(m: types.ModuleType) -> schema.Schema:
136152

137153
_fill_synth_information(classes)
138154
_fill_hideable_information(classes)
155+
_check_test_with(classes)
139156

140157
return schema.Schema(includes=includes, classes=_toposort_classes_by_group(classes), null=null)
141158

misc/codegen/test/test_qlgen.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,18 @@ def test_test_class_hierarchy_uncollapse_at_final(opts, generate_tests):
688688
}
689689

690690

691+
def test_test_with(opts, generate_tests):
692+
write(opts.ql_test_output / "B" / "test.swift")
693+
assert generate_tests([
694+
schema.Class("Base", derived={"A", "B"}),
695+
schema.Class("A", bases=["Base"], test_with="B"),
696+
schema.Class("B", bases=["Base"]),
697+
]) == {
698+
"B/A.ql": a_ql_class_tester(class_name="A"),
699+
"B/B.ql": a_ql_class_tester(class_name="B"),
700+
}
701+
702+
691703
def test_property_description(generate_classes):
692704
description = ["Lorem", "Ipsum"]
693705
assert generate_classes([

misc/codegen/test/test_schemaloader.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,5 +727,83 @@ class NonHideable(Root):
727727
}
728728

729729

730+
def test_test_with():
731+
@load
732+
class data:
733+
class Root:
734+
pass
735+
736+
class A(Root):
737+
pass
738+
739+
@defs.qltest.test_with(A)
740+
class B(Root):
741+
pass
742+
743+
@defs.qltest.test_with("D")
744+
class C(Root):
745+
pass
746+
747+
class D(Root):
748+
pass
749+
750+
assert data.classes == {
751+
"Root": schema.Class("Root", derived=set("ABCD")),
752+
"A": schema.Class("A", bases=["Root"]),
753+
"B": schema.Class("B", bases=["Root"], test_with="A"),
754+
"C": schema.Class("C", bases=["Root"], test_with="D"),
755+
"D": schema.Class("D", bases=["Root"]),
756+
}
757+
758+
759+
def test_test_with_unknown_string():
760+
with pytest.raises(schema.Error):
761+
@load
762+
class data:
763+
class Root:
764+
pass
765+
766+
@defs.qltest.test_with("B")
767+
class A(Root):
768+
pass
769+
770+
771+
def test_test_with_unknown_class():
772+
with pytest.raises(schema.Error):
773+
class B:
774+
pass
775+
776+
@load
777+
class data:
778+
class Root:
779+
pass
780+
781+
@defs.qltest.test_with(B)
782+
class A(Root):
783+
pass
784+
785+
786+
def test_test_with_double():
787+
with pytest.raises(schema.Error):
788+
class B:
789+
pass
790+
791+
@load
792+
class data:
793+
class Root:
794+
pass
795+
796+
class A(Root):
797+
pass
798+
799+
@defs.qltest.test_with("C")
800+
class B(Root):
801+
pass
802+
803+
@defs.qltest.test_with(A)
804+
class C(Root):
805+
pass
806+
807+
730808
if __name__ == '__main__':
731809
sys.exit(pytest.main([__file__] + sys.argv[1:]))

swift/ql/.generated.list

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

swift/ql/.gitattributes

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

swift/ql/test/extractor-tests/generated/decl/EnumCaseDecl/MISSING_SOURCE.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
| enums.swift:2:5:2:18 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 2 |
2+
| enums.swift:3:5:3:26 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 3 |
3+
| enums.swift:8:5:8:18 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 2 |
4+
| enums.swift:9:5:9:26 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 3 |
5+
| enums.swift:13:5:13:22 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |
6+
| enums.swift:14:5:14:21 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |
7+
| enums.swift:15:5:15:35 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |
8+
| enums.swift:19:5:19:10 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |
9+
| enums.swift:20:5:20:16 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |
10+
| enums.swift:24:5:24:25 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |
11+
| enums.swift:25:5:25:24 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |
12+
| enums.swift:26:5:26:44 | case ... | getModule: | file://:0:0:0:0 | enums | getNumberOfMembers: | 0 | getNumberOfElements: | 1 |

0 commit comments

Comments
 (0)