diff --git a/.github/workflows/swift-codegen.yml b/.github/workflows/swift-codegen.yml index d3e55ca75f19..bdbde747ca1d 100644 --- a/.github/workflows/swift-codegen.yml +++ b/.github/workflows/swift-codegen.yml @@ -15,13 +15,18 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v3 with: - python-version: '~3.7' + python-version: '~3.8' cache: 'pip' - uses: ./.github/actions/fetch-codeql - uses: bazelbuild/setup-bazelisk@v2 - - name: Check code generation + - name: Install dependencies run: | pip install -r swift/codegen/requirements.txt + - name: Run unit tests + run: | + bazel test //swift/codegen:tests --test_output=errors + - name: Check that code was generated + run: | bazel run //swift/codegen git add swift git diff --exit-code --stat HEAD diff --git a/.gitignore b/.gitignore index 33ef1770c959..5642399a5f68 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ # python virtual environment folder .venv/ +# binary files created by pytest-cov +.coverage + # It's useful (though not required) to be able to unpack codeql in the ql checkout itself /codeql/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d90a7982a572..04937810115d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,13 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -exclude: /test/.*$(? path/to/syntax.qll include_file = stub_out.with_suffix(".qll") - all_imports = QlImportList(v for _, v in sorted(imports.items())) + all_imports = ql.ImportList([v for _, v in sorted(imports.items())]) renderer.render(all_imports, include_file) renderer.cleanup(existing) diff --git a/swift/codegen/requirements.txt b/swift/codegen/requirements.txt index df5110f65355..b8959a4b15df 100644 --- a/swift/codegen/requirements.txt +++ b/swift/codegen/requirements.txt @@ -1,3 +1,4 @@ pystache pyyaml inflection +pytest diff --git a/swift/codegen/test/test_dbscheme.py b/swift/codegen/test/test_dbscheme.py new file mode 100644 index 000000000000..ca1002aa58e3 --- /dev/null +++ b/swift/codegen/test/test_dbscheme.py @@ -0,0 +1,52 @@ +import sys +from copy import deepcopy + +from swift.codegen.lib import dbscheme +from swift.codegen.test.utils import * + + +def test_dbcolumn_name(): + assert dbscheme.Column("foo", "some_type").name == "foo" + + +@pytest.mark.parametrize("keyword", dbscheme.dbscheme_keywords) +def test_dbcolumn_keyword_name(keyword): + assert dbscheme.Column(keyword, "some_type").name == keyword + "_" + + +@pytest.mark.parametrize("type,binding,lhstype,rhstype", [ + ("builtin_type", False, "builtin_type", "builtin_type ref"), + ("builtin_type", True, "builtin_type", "builtin_type ref"), + ("@at_type", False, "int", "@at_type ref"), + ("@at_type", True, "unique int", "@at_type"), +]) +def test_dbcolumn_types(type, binding, lhstype, rhstype): + col = dbscheme.Column("foo", type, binding) + assert col.lhstype == lhstype + assert col.rhstype == rhstype + + +def test_keyset_has_first_id_marked(): + ids = ["a", "b", "c"] + ks = dbscheme.KeySet(ids) + assert ks.ids[0].first + assert [id.id for id in ks.ids] == ids + + +def test_table_has_first_column_marked(): + columns = [dbscheme.Column("a", "x"), dbscheme.Column("b", "y", binding=True), dbscheme.Column("c", "z")] + expected = deepcopy(columns) + table = dbscheme.Table("foo", columns) + expected[0].first = True + assert table.columns == expected + + +def test_union_has_first_case_marked(): + rhs = ["a", "b", "c"] + u = dbscheme.Union(lhs="x", rhs=rhs) + assert u.rhs[0].first + assert [c.type for c in u.rhs] == rhs + + +if __name__ == '__main__': + sys.exit(pytest.main()) diff --git a/swift/codegen/test/test_dbschemegen.py b/swift/codegen/test/test_dbschemegen.py new file mode 100644 index 000000000000..d8f0089d8636 --- /dev/null +++ b/swift/codegen/test/test_dbschemegen.py @@ -0,0 +1,307 @@ +import sys + +from swift.codegen import dbschemegen +from swift.codegen.lib import dbscheme +from swift.codegen.test.utils import * + + +def generate(opts, renderer): + (out, data), = run_generation(dbschemegen.generate, opts, renderer).items() + assert out is opts.dbscheme + return data + + +def test_empty(opts, input, renderer): + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[], + ) + + +def test_includes(opts, input, renderer): + includes = ["foo", "bar"] + input.includes = includes + for i in includes: + write(opts.schema.parent / i, i + " data") + + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[ + dbscheme.SchemeInclude( + src=schema_dir / i, + data=i + " data", + ) for i in includes + ], + declarations=[], + ) + + +def test_empty_final_class(opts, input, renderer): + input.classes = [ + schema.Class("Object"), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Table( + name="objects", + columns=[ + dbscheme.Column('id', '@object', binding=True), + ] + ) + ], + ) + + +def test_final_class_with_single_scalar_field(opts, input, renderer): + input.classes = [ + + schema.Class("Object", properties=[ + schema.SingleProperty("foo", "bar"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Table( + name="objects", + columns=[ + dbscheme.Column('id', '@object', binding=True), + dbscheme.Column('foo', 'bar'), + ] + ) + ], + ) + + +def test_final_class_with_single_class_field(opts, input, renderer): + input.classes = [ + schema.Class("Object", properties=[ + schema.SingleProperty("foo", "Bar"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Table( + name="objects", + columns=[ + dbscheme.Column('id', '@object', binding=True), + dbscheme.Column('foo', '@bar'), + ] + ) + ], + ) + + +def test_final_class_with_optional_field(opts, input, renderer): + input.classes = [ + schema.Class("Object", properties=[ + schema.OptionalProperty("foo", "bar"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Table( + name="objects", + columns=[ + dbscheme.Column('id', '@object', binding=True), + ] + ), + dbscheme.Table( + name="object_foos", + keyset=dbscheme.KeySet(["id"]), + columns=[ + dbscheme.Column('id', '@object'), + dbscheme.Column('foo', 'bar'), + ] + ), + ], + ) + + +def test_final_class_with_repeated_field(opts, input, renderer): + input.classes = [ + schema.Class("Object", properties=[ + schema.RepeatedProperty("foo", "bar"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Table( + name="objects", + columns=[ + dbscheme.Column('id', '@object', binding=True), + ] + ), + dbscheme.Table( + name="object_foos", + keyset=dbscheme.KeySet(["id", "index"]), + columns=[ + dbscheme.Column('id', '@object'), + dbscheme.Column('index', 'int'), + dbscheme.Column('foo', 'bar'), + ] + ), + ], + ) + + +def test_final_class_with_more_fields(opts, input, renderer): + input.classes = [ + schema.Class("Object", properties=[ + schema.SingleProperty("one", "x"), + schema.SingleProperty("two", "y"), + schema.OptionalProperty("three", "z"), + schema.RepeatedProperty("four", "w"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Table( + name="objects", + columns=[ + dbscheme.Column('id', '@object', binding=True), + dbscheme.Column('one', 'x'), + dbscheme.Column('two', 'y'), + ] + ), + dbscheme.Table( + name="object_threes", + keyset=dbscheme.KeySet(["id"]), + columns=[ + dbscheme.Column('id', '@object'), + dbscheme.Column('three', 'z'), + ] + ), + dbscheme.Table( + name="object_fours", + keyset=dbscheme.KeySet(["id", "index"]), + columns=[ + dbscheme.Column('id', '@object'), + dbscheme.Column('index', 'int'), + dbscheme.Column('four', 'w'), + ] + ), + ], + ) + + +def test_empty_class_with_derived(opts, input, renderer): + input.classes = [ + schema.Class( + name="Base", + derived={"Left", "Right"}), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Union( + lhs="@base", + rhs=["@left", "@right"], + ), + ], + ) + + +def test_class_with_derived_and_single_property(opts, input, renderer): + input.classes = [ + schema.Class( + name="Base", + derived={"Left", "Right"}, + properties=[ + schema.SingleProperty("single", "Prop"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Union( + lhs="@base", + rhs=["@left", "@right"], + ), + dbscheme.Table( + name="bases", + keyset=dbscheme.KeySet(["id"]), + columns=[ + dbscheme.Column('id', '@base'), + dbscheme.Column('single', '@prop'), + ] + ) + ], + ) + + +def test_class_with_derived_and_optional_property(opts, input, renderer): + input.classes = [ + schema.Class( + name="Base", + derived={"Left", "Right"}, + properties=[ + schema.OptionalProperty("opt", "Prop"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Union( + lhs="@base", + rhs=["@left", "@right"], + ), + dbscheme.Table( + name="base_opts", + keyset=dbscheme.KeySet(["id"]), + columns=[ + dbscheme.Column('id', '@base'), + dbscheme.Column('opt', '@prop'), + ] + ) + ], + ) + + +def test_class_with_derived_and_repeated_property(opts, input, renderer): + input.classes = [ + schema.Class( + name="Base", + derived={"Left", "Right"}, + properties=[ + schema.RepeatedProperty("rep", "Prop"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Union( + lhs="@base", + rhs=["@left", "@right"], + ), + dbscheme.Table( + name="base_reps", + keyset=dbscheme.KeySet(["id", "index"]), + columns=[ + dbscheme.Column('id', '@base'), + dbscheme.Column('index', 'int'), + dbscheme.Column('rep', '@prop'), + ] + ) + ], + ) + + +if __name__ == '__main__': + sys.exit(pytest.main()) diff --git a/swift/codegen/test/test_ql.py b/swift/codegen/test/test_ql.py new file mode 100644 index 000000000000..47b9ff887165 --- /dev/null +++ b/swift/codegen/test/test_ql.py @@ -0,0 +1,97 @@ +import sys +from copy import deepcopy + +from swift.codegen.lib import ql +from swift.codegen.test.utils import * + + +def test_property_has_first_param_marked(): + params = [ql.Param("a", "x"), ql.Param("b", "y"), ql.Param("c", "z")] + expected = deepcopy(params) + expected[0].first = True + prop = ql.Property("Prop", "foo", "props", ["this"], params=params) + assert prop.params == expected + + +def test_property_has_first_table_param_marked(): + tableparams = ["a", "b", "c"] + prop = ql.Property("Prop", "foo", "props", tableparams) + assert prop.tableparams[0].first + assert [p.param for p in prop.tableparams] == tableparams + assert all(p.type is None for p in prop.tableparams) + + +@pytest.mark.parametrize("params,expected_local_var", [ + (["a", "b", "c"], "x"), + (["a", "x", "c"], "x_"), + (["a", "x", "x_", "c"], "x__"), + (["a", "x", "x_", "x__"], "x___"), +]) +def test_property_local_var_avoids_params_collision(params, expected_local_var): + prop = ql.Property("Prop", "foo", "props", ["this"], params=[ql.Param(p) for p in params]) + assert prop.local_var == expected_local_var + + +def test_property_not_a_class(): + tableparams = ["x", "result", "y"] + prop = ql.Property("Prop", "foo", "props", tableparams) + assert not prop.type_is_class + assert [p.param for p in prop.tableparams] == tableparams + + +def test_property_is_a_class(): + tableparams = ["x", "result", "y"] + prop = ql.Property("Prop", "Foo", "props", tableparams) + assert prop.type_is_class + assert [p.param for p in prop.tableparams] == ["x", prop.local_var, "y"] + + +@pytest.mark.parametrize("name,expected_article", [ + ("Argument", "An"), + ("Element", "An"), + ("Integer", "An"), + ("Operator", "An"), + ("Unit", "A"), + ("Whatever", "A"), +]) +def test_property_indefinite_article(name, expected_article): + prop = ql.Property(name, "Foo", "props", ["x"], plural="X") + assert prop.indefinite_article == expected_article + + +def test_property_no_plural_no_indefinite_article(): + prop = ql.Property("Prop", "Foo", "props", ["x"]) + assert prop.indefinite_article is None + + +def test_class_sorts_bases(): + bases = ["B", "Ab", "C", "Aa"] + expected = ["Aa", "Ab", "B", "C"] + cls = ql.Class("Foo", bases=bases) + assert cls.bases == expected + + +def test_class_has_first_property_marked(): + props = [ + ql.Property(f"Prop{x}", f"Foo{x}", f"props{x}", [f"{x}"]) for x in range(4) + ] + expected = deepcopy(props) + expected[0].first = True + cls = ql.Class("Class", properties=props) + assert cls.properties == expected + + +def test_class_db_id(): + cls = ql.Class("ThisIsMyClass") + assert cls.db_id == "@this_is_my_class" + +def test_root_class(): + cls = ql.Class("Class") + assert cls.root + +def test_non_root_class(): + cls = ql.Class("Class", bases=["A"]) + assert not cls.root + +if __name__ == '__main__': + sys.exit(pytest.main()) diff --git a/swift/codegen/test/test_qlgen.py b/swift/codegen/test/test_qlgen.py new file mode 100644 index 000000000000..94cd6641afca --- /dev/null +++ b/swift/codegen/test/test_qlgen.py @@ -0,0 +1,198 @@ +import subprocess +import sys + +from swift.codegen import qlgen +from swift.codegen.lib import ql, paths +from swift.codegen.test.utils import * + + +@pytest.fixture(autouse=True) +def run_mock(): + with mock.patch("subprocess.run") as ret: + yield ret + + +# these are lambdas so that they will use patched paths when called +stub_path = lambda: paths.swift_dir / "ql/lib/stub/path" +ql_output_path = lambda: paths.swift_dir / "ql/lib/other/path" +import_file = lambda: stub_path().with_suffix(".qll") +stub_import_prefix = "stub.path." +gen_import_prefix = "other.path." +index_param = ql.Param("index", "int") + + +def generate(opts, renderer, written=None): + opts.ql_stub_output = stub_path() + opts.ql_output = ql_output_path() + renderer.written = written or [] + return run_generation(qlgen.generate, opts, renderer) + + +def test_empty(opts, input, renderer): + assert generate(opts, renderer) == { + import_file(): ql.ImportList() + } + + +def test_one_empty_class(opts, input, renderer): + input.classes = [ + schema.Class("A") + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + "A"]), + stub_path() / "A.qll": ql.Stub(name="A", base_import=gen_import_prefix + "A"), + ql_output_path() / "A.qll": ql.Class(name="A", final=True), + } + + +def test_hierarchy(opts, input, renderer): + input.classes = [ + schema.Class("D", bases={"B", "C"}), + schema.Class("C", bases={"A"}, derived={"D"}), + schema.Class("B", bases={"A"}, derived={"D"}), + schema.Class("A", derived={"B", "C"}), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + cls for cls in "ABCD"]), + stub_path() / "A.qll": ql.Stub(name="A", base_import=gen_import_prefix + "A"), + stub_path() / "B.qll": ql.Stub(name="B", base_import=gen_import_prefix + "B"), + stub_path() / "C.qll": ql.Stub(name="C", base_import=gen_import_prefix + "C"), + stub_path() / "D.qll": ql.Stub(name="D", base_import=gen_import_prefix + "D"), + ql_output_path() / "A.qll": ql.Class(name="A"), + ql_output_path() / "B.qll": ql.Class(name="B", bases=["A"], imports=[stub_import_prefix + "A"]), + ql_output_path() / "C.qll": ql.Class(name="C", bases=["A"], imports=[stub_import_prefix + "A"]), + ql_output_path() / "D.qll": ql.Class(name="D", final=True, bases=["B", "C"], + imports=[stub_import_prefix + cls for cls in "BC"]), + + } + + +def test_single_property(opts, input, renderer): + input.classes = [ + schema.Class("MyObject", properties=[schema.SingleProperty("foo", "bar")]), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.Class(name="MyObject", final=True, properties=[ + ql.Property(singular="Foo", type="bar", tablename="my_objects", tableparams=["this", "result"]), + ]) + } + + +def test_single_properties(opts, input, renderer): + input.classes = [ + schema.Class("MyObject", properties=[ + schema.SingleProperty("one", "x"), + schema.SingleProperty("two", "y"), + schema.SingleProperty("three", "z"), + ]), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.Class(name="MyObject", final=True, properties=[ + ql.Property(singular="One", type="x", tablename="my_objects", tableparams=["this", "result", "_", "_"]), + ql.Property(singular="Two", type="y", tablename="my_objects", tableparams=["this", "_", "result", "_"]), + ql.Property(singular="Three", type="z", tablename="my_objects", tableparams=["this", "_", "_", "result"]), + ]) + } + + +def test_optional_property(opts, input, renderer): + input.classes = [ + schema.Class("MyObject", properties=[schema.OptionalProperty("foo", "bar")]), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.Class(name="MyObject", final=True, properties=[ + ql.Property(singular="Foo", type="bar", tablename="my_object_foos", tableparams=["this", "result"]), + ]) + } + + +def test_repeated_property(opts, input, renderer): + input.classes = [ + schema.Class("MyObject", properties=[schema.RepeatedProperty("foo", "bar")]), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.Class(name="MyObject", final=True, properties=[ + ql.Property(singular="Foo", plural="Foos", type="bar", tablename="my_object_foos", params=[index_param], + tableparams=["this", "index", "result"]), + ]) + } + + +def test_single_class_property(opts, input, renderer): + input.classes = [ + schema.Class("MyObject", properties=[schema.SingleProperty("foo", "Bar")]), + schema.Class("Bar"), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + cls for cls in ("Bar", "MyObject")]), + stub_path() / "MyObject.qll": ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + stub_path() / "Bar.qll": ql.Stub(name="Bar", base_import=gen_import_prefix + "Bar"), + ql_output_path() / "MyObject.qll": ql.Class( + name="MyObject", final=True, imports=[stub_import_prefix + "Bar"], properties=[ + ql.Property(singular="Foo", type="Bar", tablename="my_objects", tableparams=["this", "result"]), + ], + ), + ql_output_path() / "Bar.qll": ql.Class(name="Bar", final=True) + } + + +def test_class_dir(opts, input, renderer): + dir = pathlib.Path("another/rel/path") + input.classes = [ + schema.Class("A", derived={"B"}, dir=dir), + schema.Class("B", bases={"A"}), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([ + stub_import_prefix + "another.rel.path.A", + stub_import_prefix + "B", + ]), + stub_path() / dir / "A.qll": ql.Stub(name="A", base_import=gen_import_prefix + "another.rel.path.A"), + stub_path() / "B.qll": ql.Stub(name="B", base_import=gen_import_prefix + "B"), + ql_output_path() / dir / "A.qll": ql.Class(name="A", dir=dir), + ql_output_path() / "B.qll": ql.Class(name="B", final=True, bases=["A"], + imports=[stub_import_prefix + "another.rel.path.A"]) + } + + +def test_format(opts, input, renderer, run_mock): + opts.codeql_binary = "my_fake_codeql" + run_mock.return_value.stderr = "some\nlines\n" + generate(opts, renderer, written=["foo", "bar"]) + assert run_mock.mock_calls == [ + mock.call(["my_fake_codeql", "query", "format", "--in-place", "--", "foo", "bar"], + check=True, stderr=subprocess.PIPE, text=True), + ] + + +def test_empty_cleanup(opts, input, renderer): + generate(opts, renderer) + assert renderer.mock_calls[-1] == mock.call.cleanup(set()) + + +def test_empty_cleanup(opts, input, renderer, tmp_path): + opts.ql_output = tmp_path / "gen" + opts.ql_stub_output = tmp_path / "stub" + renderer.written = [] + ql_a = opts.ql_output / "A.qll" + ql_b = opts.ql_output / "B.qll" + stub_a = opts.ql_stub_output / "A.qll" + stub_b = opts.ql_stub_output / "B.qll" + write(ql_a) + write(ql_b) + write(stub_a, "// generated\nfoo\n") + write(stub_b, "bar\n") + run_generation(qlgen.generate, opts, renderer) + assert renderer.mock_calls[-1] == mock.call.cleanup({ql_a, ql_b, stub_a}) + + +if __name__ == '__main__': + sys.exit(pytest.main()) diff --git a/swift/codegen/test/test_render.py b/swift/codegen/test/test_render.py new file mode 100644 index 000000000000..3f12845df0bb --- /dev/null +++ b/swift/codegen/test/test_render.py @@ -0,0 +1,79 @@ +import sys +from unittest import mock + +import pytest + +from swift.codegen.lib import paths +from swift.codegen.lib import render + + +@pytest.fixture +def pystache_renderer_cls(): + with mock.patch("pystache.Renderer") as ret: + yield ret + + +@pytest.fixture +def pystache_renderer(pystache_renderer_cls): + ret = mock.Mock() + pystache_renderer_cls.side_effect = (ret,) + return ret + + +@pytest.fixture +def sut(pystache_renderer): + return render.Renderer() + + +def test_constructor(pystache_renderer_cls, sut): + pystache_init, = pystache_renderer_cls.mock_calls + assert set(pystache_init.kwargs) == {'search_dirs', 'escape'} + assert pystache_init.kwargs['search_dirs'] == str(paths.templates_dir) + an_object = object() + assert pystache_init.kwargs['escape'](an_object) is an_object + assert sut.written == set() + + +def test_render(pystache_renderer, sut): + data = mock.Mock() + output = mock.Mock() + with mock.patch("builtins.open", mock.mock_open()) as output_stream: + sut.render(data, output) + assert pystache_renderer.mock_calls == [ + mock.call.render_name(data.template, data, generator=paths.exe_file), + ], pystache_renderer.mock_calls + assert output_stream.mock_calls == [ + mock.call(output, 'w'), + mock.call().__enter__(), + mock.call().write(pystache_renderer.render_name.return_value), + mock.call().__exit__(None, None, None), + ] + assert sut.written == {output} + + +def test_written(sut): + data = [mock.Mock() for _ in range(4)] + output = [mock.Mock() for _ in data] + with mock.patch("builtins.open", mock.mock_open()) as output_stream: + for d, o in zip(data, output): + sut.render(d, o) + assert sut.written == set(output) + + +def test_cleanup(sut): + data = [mock.Mock() for _ in range(4)] + output = [mock.Mock() for _ in data] + with mock.patch("builtins.open", mock.mock_open()) as output_stream: + for d, o in zip(data, output): + sut.render(d, o) + expected_erased = [mock.Mock() for _ in range(3)] + existing = set(expected_erased + output[2:]) + sut.cleanup(existing) + for f in expected_erased: + assert f.mock_calls == [mock.call.unlink(missing_ok=True)] + for f in output: + assert f.unlink.mock_calls == [] + + +if __name__ == '__main__': + sys.exit(pytest.main()) diff --git a/swift/codegen/test/test_schema.py b/swift/codegen/test/test_schema.py new file mode 100644 index 000000000000..9b52bcbbaa17 --- /dev/null +++ b/swift/codegen/test/test_schema.py @@ -0,0 +1,155 @@ +import sys + +from swift.codegen.test.utils import * + +root_name = schema.root_class_name + + +@pytest.fixture +def load(tmp_path): + file = tmp_path / "schema.yml" + + def ret(yml): + write(file, yml) + return schema.load(file) + + return ret + + +def test_empty_schema(load): + ret = load("{}") + assert ret.classes == [schema.Class(root_name)] + assert ret.includes == set() + + +def test_one_empty_class(load): + ret = load(""" +MyClass: {} +""") + assert ret.classes == [ + schema.Class(root_name, derived={'MyClass'}), + schema.Class('MyClass', bases={root_name}), + ] + + +def test_two_empty_classes(load): + ret = load(""" +MyClass1: {} +MyClass2: {} +""") + assert ret.classes == [ + schema.Class(root_name, derived={'MyClass1', 'MyClass2'}), + schema.Class('MyClass1', bases={root_name}), + schema.Class('MyClass2', bases={root_name}), + ] + + +def test_two_empty_chained_classes(load): + ret = load(""" +MyClass1: {} +MyClass2: + _extends: MyClass1 +""") + assert ret.classes == [ + schema.Class(root_name, derived={'MyClass1'}), + schema.Class('MyClass1', bases={root_name}, derived={'MyClass2'}), + schema.Class('MyClass2', bases={'MyClass1'}), + ] + + +def test_empty_classes_diamond(load): + ret = load(""" +A: {} +B: {} +C: + _extends: + - A + - B +""") + assert ret.classes == [ + schema.Class(root_name, derived={'A', 'B'}), + schema.Class('A', bases={root_name}, derived={'C'}), + schema.Class('B', bases={root_name}, derived={'C'}), + schema.Class('C', bases={'A', 'B'}), + ] + + +def test_dir(load): + ret = load(""" +A: + _dir: other/dir +""") + assert ret.classes == [ + schema.Class(root_name, derived={'A'}), + schema.Class('A', bases={root_name}, dir=pathlib.Path("other/dir")), + ] + + +def test_directory_filter(load): + ret = load(""" +_directories: + first/dir: '[xy]' + second/dir: foo$ + third/dir: bar$ +Afoo: {} +Bbar: {} +Abar: {} +Bfoo: {} +Ax: {} +Ay: {} +A: {} +""") + assert ret.classes == [ + schema.Class(root_name, derived={'Afoo', 'Bbar', 'Abar', 'Bfoo', 'Ax', 'Ay', 'A'}), + schema.Class('Afoo', bases={root_name}, dir=pathlib.Path("second/dir")), + schema.Class('Bbar', bases={root_name}, dir=pathlib.Path("third/dir")), + schema.Class('Abar', bases={root_name}, dir=pathlib.Path("third/dir")), + schema.Class('Bfoo', bases={root_name}, dir=pathlib.Path("second/dir")), + schema.Class('Ax', bases={root_name}, dir=pathlib.Path("first/dir")), + schema.Class('Ay', bases={root_name}, dir=pathlib.Path("first/dir")), + schema.Class('A', bases={root_name}, dir=pathlib.Path()), + ] + + +def test_directory_filter_override(load): + ret = load(""" +_directories: + one/dir: ^A$ +A: + _dir: other/dir +""") + assert ret.classes == [ + schema.Class(root_name, derived={'A'}), + schema.Class('A', bases={root_name}, dir=pathlib.Path("other/dir")), + ] + + +def test_lowercase_rejected(load): + with pytest.raises(AssertionError): + load("aLowercase: {}") + + +def test_digit_rejected(load): + with pytest.raises(AssertionError): + load("1digit: {}") + + +def test_properties(load): + ret = load(""" +A: + one: string + two: int? + three: bool* +""") + assert ret.classes == [ + schema.Class(root_name, derived={'A'}), + schema.Class('A', bases={root_name}, properties=[ + schema.SingleProperty('one', 'string'), + schema.OptionalProperty('two', 'int'), + schema.RepeatedProperty('three', 'bool'), + ]), + ] + + +if __name__ == '__main__': + sys.exit(pytest.main()) diff --git a/swift/codegen/test/utils.py b/swift/codegen/test/utils.py new file mode 100644 index 000000000000..d4b40a9e155e --- /dev/null +++ b/swift/codegen/test/utils.py @@ -0,0 +1,50 @@ +import pathlib +from unittest import mock + +import pytest + +from swift.codegen.lib import render, schema + +schema_dir = pathlib.Path("a", "dir") +schema_file = schema_dir / "schema.yml" + + +def write(out, contents=""): + out.parent.mkdir(parents=True, exist_ok=True) + with open(out, "w") as out: + out.write(contents) + + +@pytest.fixture +def renderer(): + return mock.Mock(spec=render.Renderer()) + + +@pytest.fixture +def opts(): + return mock.MagicMock() + + +@pytest.fixture(autouse=True) +def override_paths(tmp_path): + with mock.patch("swift.codegen.lib.paths.swift_dir", tmp_path): + yield + + +@pytest.fixture +def input(opts, tmp_path): + opts.schema = tmp_path / schema_file + with mock.patch("swift.codegen.lib.schema.load") as load_mock: + load_mock.return_value = schema.Schema([]) + yield load_mock.return_value + assert load_mock.mock_calls == [ + mock.call(opts.schema) + ], load_mock.mock_calls + + +def run_generation(generate, opts, renderer): + output = {} + + renderer.render.side_effect = lambda data, out: output.__setitem__(out, data) + generate(opts, renderer) + return output diff --git a/swift/ql/lib/swift.dbscheme b/swift/ql/lib/swift.dbscheme index 6c0dc88a8be7..0aff926e6c0e 100644 --- a/swift/ql/lib/swift.dbscheme +++ b/swift/ql/lib/swift.dbscheme @@ -15,6 +15,16 @@ answer_to_life_the_universe_and_everything( // from codegen/schema.yml +@element = + @argument +| @file +| @generic_context +| @iterable_decl_context +| @locatable +| @location +| @type +; + files( unique int id: @file, string name: string ref @@ -1886,13 +1896,3 @@ integer_literal_exprs( unique int id: @integer_literal_expr, string string_value: string ref ); - -@element = - @argument -| @file -| @generic_context -| @iterable_decl_context -| @locatable -| @location -| @type -;