From f171ce63416085827aae78f32f5960ff6f240a9d Mon Sep 17 00:00:00 2001 From: Paolo Tranquilli Date: Tue, 26 Apr 2022 18:22:40 +0200 Subject: [PATCH] Swift: add unit tests to code generation Tests can be run with ``` bazel test //swift/codegen:tests ``` Coverage can be checked installing `pytest-cov` and running ``` pytest --cov=swift/codegen swift/codegen/test ``` --- .github/workflows/swift-codegen.yml | 7 +- .pre-commit-config.yaml | 7 + conftest.py | 1 + swift/codegen/.coverage | Bin 0 -> 53248 bytes swift/codegen/BUILD.bazel | 29 ++- swift/codegen/dbschemegen.py | 13 +- swift/codegen/lib/options.py | 12 +- swift/codegen/lib/paths.py | 11 +- swift/codegen/lib/ql.py | 88 +++++++ swift/codegen/lib/render.py | 10 +- swift/codegen/lib/schema.py | 26 +- swift/codegen/qlgen.py | 118 ++------- swift/codegen/requirements.txt | 1 + swift/codegen/test/test_dbschemegen.py | 328 +++++++++++++++++++++++++ swift/codegen/test/test_qlgen.py | 199 +++++++++++++++ swift/codegen/test/test_render.py | 79 ++++++ swift/codegen/test/test_schema.py | 158 ++++++++++++ swift/codegen/test/utils.py | 50 ++++ swift/ql/lib/swift.dbscheme | 20 +- 19 files changed, 1008 insertions(+), 149 deletions(-) create mode 100644 conftest.py create mode 100644 swift/codegen/.coverage create mode 100644 swift/codegen/lib/ql.py create mode 100644 swift/codegen/test/test_dbschemegen.py create mode 100644 swift/codegen/test/test_qlgen.py create mode 100644 swift/codegen/test/test_render.py create mode 100644 swift/codegen/test/test_schema.py create mode 100644 swift/codegen/test/utils.py diff --git a/.github/workflows/swift-codegen.yml b/.github/workflows/swift-codegen.yml index d3e55ca75f19..3714b00d25ce 100644 --- a/.github/workflows/swift-codegen.yml +++ b/.github/workflows/swift-codegen.yml @@ -19,9 +19,14 @@ jobs: 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index d90a7982a572..5676f36512b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,3 +40,10 @@ repos: language: system entry: bazel run //swift/codegen pass_filenames: false + + - id: swift-codegen-unit-tests + name: Run Swift code generation unit tests + files: ^swift/codegen + language: system + entry: bazel test //swift/codegen:tests + pass_filenames: false diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000000..a118fa835ef1 --- /dev/null +++ b/conftest.py @@ -0,0 +1 @@ +# this empty file adds the repo root to PYTHON_PATH when running pytest diff --git a/swift/codegen/.coverage b/swift/codegen/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..1f3d1b3104cf46997f405ce745441d169f80f048 GIT binary patch literal 53248 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 dg187&5yV6g6+uJ path/to/syntax.qll include_file = stub_out.with_suffix(".qll") - all_imports = QlImportList(v for _, v in sorted(imports.items())) + all_imports = ql.QlImportList([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_dbschemegen.py b/swift/codegen/test/test_dbschemegen.py new file mode 100644 index 000000000000..3fb6f798ae49 --- /dev/null +++ b/swift/codegen/test/test_dbschemegen.py @@ -0,0 +1,328 @@ +import pathlib +import sys + +from swift.codegen import dbschemegen +from swift.codegen.lib import dbscheme, paths +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.DbScheme( + 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.DbScheme( + src=schema_file, + includes=[ + dbscheme.DbSchemeInclude( + 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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbTable( + name="objects", + columns=[ + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbTable( + name="objects", + columns=[ + dbscheme.DbColumn('id', '@object', binding=True), + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbTable( + name="objects", + columns=[ + dbscheme.DbColumn('id', '@object', binding=True), + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbTable( + name="objects", + columns=[ + dbscheme.DbColumn('id', '@object', binding=True), + ] + ), + dbscheme.DbTable( + name="object_foos", + keyset=dbscheme.DbKeySet(["id"]), + columns=[ + dbscheme.DbColumn('id', '@object'), + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbTable( + name="objects", + columns=[ + dbscheme.DbColumn('id', '@object', binding=True), + ] + ), + dbscheme.DbTable( + name="object_foos", + keyset=dbscheme.DbKeySet(["id", "index"]), + columns=[ + dbscheme.DbColumn('id', '@object'), + dbscheme.DbColumn('index', 'int'), + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbTable( + name="objects", + columns=[ + dbscheme.DbColumn('id', '@object', binding=True), + dbscheme.DbColumn('one', 'x'), + dbscheme.DbColumn('two', 'y'), + ] + ), + dbscheme.DbTable( + name="object_threes", + keyset=dbscheme.DbKeySet(["id"]), + columns=[ + dbscheme.DbColumn('id', '@object'), + dbscheme.DbColumn('three', 'z'), + ] + ), + dbscheme.DbTable( + name="object_fours", + keyset=dbscheme.DbKeySet(["id", "index"]), + columns=[ + dbscheme.DbColumn('id', '@object'), + dbscheme.DbColumn('index', 'int'), + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbUnion( + 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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbUnion( + lhs="@base", + rhs=["@left", "@right"], + ), + dbscheme.DbTable( + name="bases", + keyset=dbscheme.DbKeySet(["id"]), + columns=[ + dbscheme.DbColumn('id', '@base'), + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbUnion( + lhs="@base", + rhs=["@left", "@right"], + ), + dbscheme.DbTable( + name="base_opts", + keyset=dbscheme.DbKeySet(["id"]), + columns=[ + dbscheme.DbColumn('id', '@base'), + dbscheme.DbColumn('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.DbScheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.DbUnion( + lhs="@base", + rhs=["@left", "@right"], + ), + dbscheme.DbTable( + name="base_reps", + keyset=dbscheme.DbKeySet(["id", "index"]), + columns=[ + dbscheme.DbColumn('id', '@base'), + dbscheme.DbColumn('index', 'int'), + dbscheme.DbColumn('rep', '@prop'), + ] + ) + ], + ) + + +def test_dbcolumn_name(): + assert dbscheme.DbColumn("foo", "some_type").name == "foo" + + +@pytest.mark.parametrize("keyword", dbscheme.dbscheme_keywords) +def test_dbcolumn_keyword_name(keyword): + assert dbscheme.DbColumn(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.DbColumn("foo", type, binding) + assert col.lhstype == lhstype + assert col.rhstype == rhstype + + +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..9379ce317868 --- /dev/null +++ b/swift/codegen/test/test_qlgen.py @@ -0,0 +1,199 @@ +import subprocess +import sys + +import mock + +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 + + +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.QlParam("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.QlImportList() + } + + +def test_one_empty_class(opts, input, renderer): + input.classes = [ + schema.Class("A") + ] + assert generate(opts, renderer) == { + import_file(): ql.QlImportList([stub_import_prefix + "A"]), + stub_path() / "A.qll": ql.QlStub(name="A", base_import=gen_import_prefix + "A"), + ql_output_path() / "A.qll": ql.QlClass(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.QlImportList([stub_import_prefix + cls for cls in "ABCD"]), + stub_path() / "A.qll": ql.QlStub(name="A", base_import=gen_import_prefix + "A"), + stub_path() / "B.qll": ql.QlStub(name="B", base_import=gen_import_prefix + "B"), + stub_path() / "C.qll": ql.QlStub(name="C", base_import=gen_import_prefix + "C"), + stub_path() / "D.qll": ql.QlStub(name="D", base_import=gen_import_prefix + "D"), + ql_output_path() / "A.qll": ql.QlClass(name="A"), + ql_output_path() / "B.qll": ql.QlClass(name="B", bases=["A"], imports=[stub_import_prefix + "A"]), + ql_output_path() / "C.qll": ql.QlClass(name="C", bases=["A"], imports=[stub_import_prefix + "A"]), + ql_output_path() / "D.qll": ql.QlClass(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.QlImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.QlStub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.QlClass(name="MyObject", final=True, properties=[ + ql.QlProperty(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.QlImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.QlStub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.QlClass(name="MyObject", final=True, properties=[ + ql.QlProperty(singular="One", type="x", tablename="my_objects", tableparams=["this", "result", "_", "_"]), + ql.QlProperty(singular="Two", type="y", tablename="my_objects", tableparams=["this", "_", "result", "_"]), + ql.QlProperty(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.QlImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.QlStub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.QlClass(name="MyObject", final=True, properties=[ + ql.QlProperty(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.QlImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.QlStub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.QlClass(name="MyObject", final=True, properties=[ + ql.QlProperty(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.QlImportList([stub_import_prefix + cls for cls in ("Bar", "MyObject")]), + stub_path() / "MyObject.qll": ql.QlStub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + stub_path() / "Bar.qll": ql.QlStub(name="Bar", base_import=gen_import_prefix + "Bar"), + ql_output_path() / "MyObject.qll": ql.QlClass( + name="MyObject", final=True, imports=[stub_import_prefix + "Bar"], properties=[ + ql.QlProperty(singular="Foo", type="Bar", tablename="my_objects", tableparams=["this", "result"]), + ], + ), + ql_output_path() / "Bar.qll": ql.QlClass(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.QlImportList([ + stub_import_prefix + "another.rel.path.A", + stub_import_prefix + "B", + ]), + stub_path() / dir / "A.qll": ql.QlStub(name="A", base_import=gen_import_prefix + "another.rel.path.A"), + stub_path() / "B.qll": ql.QlStub(name="B", base_import=gen_import_prefix + "B"), + ql_output_path() / dir / "A.qll": ql.QlClass(name="A", dir=dir), + ql_output_path() / "B.qll": ql.QlClass(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..9703eafe1b8c --- /dev/null +++ b/swift/codegen/test/test_schema.py @@ -0,0 +1,158 @@ +import io +import pathlib +import sys + +import mock +import pytest + +import swift.codegen.lib.schema as schema +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 -;