Skip to content

Commit cde5ba7

Browse files
authored
Merge pull request github#8889 from redsun82/swift-codegen-unit-tests
Swift: add unit tests to code generation
2 parents 51bb33a + 0100c71 commit cde5ba7

22 files changed

+1172
-184
lines changed

.github/workflows/swift-codegen.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ jobs:
1515
- uses: actions/checkout@v2
1616
- uses: actions/setup-python@v3
1717
with:
18-
python-version: '~3.7'
18+
python-version: '~3.8'
1919
cache: 'pip'
2020
- uses: ./.github/actions/fetch-codeql
2121
- uses: bazelbuild/setup-bazelisk@v2
22-
- name: Check code generation
22+
- name: Install dependencies
2323
run: |
2424
pip install -r swift/codegen/requirements.txt
25+
- name: Run unit tests
26+
run: |
27+
bazel test //swift/codegen:tests --test_output=errors
28+
- name: Check that code was generated
29+
run: |
2530
bazel run //swift/codegen
2631
git add swift
2732
git diff --exit-code --stat HEAD

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
# python virtual environment folder
2121
.venv/
2222

23+
# binary files created by pytest-cov
24+
.coverage
25+
2326
# It's useful (though not required) to be able to unpack codeql in the ql checkout itself
2427
/codeql/
2528

.pre-commit-config.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# See https://pre-commit.com for more information
22
# See https://pre-commit.com/hooks.html for more hooks
3-
exclude: /test/.*$(?<!\.ql)(?<!\.qll)(?<!\.qlref)
43
repos:
54
- repo: https://github.com/pre-commit/pre-commit-hooks
65
rev: v3.2.0
76
hooks:
87
- id: trailing-whitespace
8+
exclude: /test/.*$(?<!\.ql)(?<!\.qll)(?<!\.qlref)
99
- id: end-of-file-fixer
10+
exclude: /test/.*$(?<!\.ql)(?<!\.qll)(?<!\.qlref)
1011

1112
- repo: https://github.com/pre-commit/mirrors-clang-format
1213
rev: v13.0.1
@@ -24,6 +25,7 @@ repos:
2425

2526
- id: sync-files
2627
name: Fix files required to be identical
28+
files: \.(qll?|qhelp)$
2729
language: system
2830
entry: python3 config/sync-files.py --latest
2931
pass_filenames: false
@@ -40,3 +42,10 @@ repos:
4042
language: system
4143
entry: bazel run //swift/codegen
4244
pass_filenames: false
45+
46+
- id: swift-codegen-unit-tests
47+
name: Run Swift code generation unit tests
48+
files: ^swift/codegen/.*\.py$
49+
language: system
50+
entry: bazel test //swift/codegen:tests
51+
pass_filenames: false

conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# this empty file adds the repo root to PYTHON_PATH when running pytest

swift/codegen/BUILD.bazel

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
11
py_binary(
22
name = "codegen",
3-
srcs = glob(["**/*.py"]),
3+
srcs = glob([
4+
"lib/*.py",
5+
"*.py",
6+
]),
7+
)
8+
9+
py_library(
10+
name = "test_utils",
11+
testonly = True,
12+
srcs = ["test/utils.py"],
13+
deps = [":codegen"],
14+
)
15+
16+
[
17+
py_test(
18+
name = src[len("test/"):-len(".py")],
19+
size = "small",
20+
srcs = [src],
21+
deps = [
22+
":codegen",
23+
":test_utils",
24+
],
25+
)
26+
for src in glob(["test/test_*.py"])
27+
]
28+
29+
test_suite(
30+
name = "tests",
431
)

swift/codegen/dbschemegen.py

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import inflection
55

6-
from lib import paths, schema, generator
7-
from lib.dbscheme import *
6+
from swift.codegen.lib import paths, schema, generator
7+
from swift.codegen.lib.dbscheme import *
88

99
log = logging.getLogger(__name__)
1010

@@ -19,67 +19,66 @@ def dbtype(typename):
1919
def cls_to_dbscheme(cls: schema.Class):
2020
""" Yield all dbscheme entities needed to model class `cls` """
2121
if cls.derived:
22-
yield DbUnion(dbtype(cls.name), (dbtype(c) for c in cls.derived))
22+
yield Union(dbtype(cls.name), (dbtype(c) for c in cls.derived))
2323
# output a table specific to a class only if it is a leaf class or it has 1-to-1 properties
2424
# Leaf classes need a table to bind the `@` ids
2525
# 1-to-1 properties are added to a class specific table
2626
# in other cases, separate tables are used for the properties, and a class specific table is unneeded
2727
if not cls.derived or any(f.is_single for f in cls.properties):
2828
binding = not cls.derived
29-
keyset = DbKeySet(["id"]) if cls.derived else None
30-
yield DbTable(
29+
keyset = KeySet(["id"]) if cls.derived else None
30+
yield Table(
3131
keyset=keyset,
3232
name=inflection.tableize(cls.name),
3333
columns=[
34-
DbColumn("id", type=dbtype(cls.name), binding=binding),
34+
Column("id", type=dbtype(cls.name), binding=binding),
3535
] + [
36-
DbColumn(f.name, dbtype(f.type)) for f in cls.properties if f.is_single
36+
Column(f.name, dbtype(f.type)) for f in cls.properties if f.is_single
3737
]
3838
)
3939
# use property-specific tables for 1-to-many and 1-to-at-most-1 properties
4040
for f in cls.properties:
4141
if f.is_optional:
42-
yield DbTable(
43-
keyset=DbKeySet(["id"]),
42+
yield Table(
43+
keyset=KeySet(["id"]),
4444
name=inflection.tableize(f"{cls.name}_{f.name}"),
4545
columns=[
46-
DbColumn("id", type=dbtype(cls.name)),
47-
DbColumn(f.name, dbtype(f.type)),
46+
Column("id", type=dbtype(cls.name)),
47+
Column(f.name, dbtype(f.type)),
4848
],
4949
)
5050
elif f.is_repeated:
51-
yield DbTable(
52-
keyset=DbKeySet(["id", "index"]),
51+
yield Table(
52+
keyset=KeySet(["id", "index"]),
5353
name=inflection.tableize(f"{cls.name}_{f.name}"),
5454
columns=[
55-
DbColumn("id", type=dbtype(cls.name)),
56-
DbColumn("index", type="int"),
57-
DbColumn(inflection.singularize(f.name), dbtype(f.type)),
55+
Column("id", type=dbtype(cls.name)),
56+
Column("index", type="int"),
57+
Column(inflection.singularize(f.name), dbtype(f.type)),
5858
]
5959
)
6060

6161

6262
def get_declarations(data: schema.Schema):
63-
return [d for cls in data.classes.values() for d in cls_to_dbscheme(cls)]
63+
return [d for cls in data.classes for d in cls_to_dbscheme(cls)]
6464

6565

6666
def get_includes(data: schema.Schema, include_dir: pathlib.Path):
6767
includes = []
6868
for inc in data.includes:
6969
inc = include_dir / inc
7070
with open(inc) as inclusion:
71-
includes.append(DbSchemeInclude(src=inc.relative_to(paths.swift_dir), data=inclusion.read()))
71+
includes.append(SchemeInclude(src=inc.relative_to(paths.swift_dir), data=inclusion.read()))
7272
return includes
7373

7474

7575
def generate(opts, renderer):
76-
input = opts.schema.resolve()
77-
out = opts.dbscheme.resolve()
76+
input = opts.schema
77+
out = opts.dbscheme
7878

79-
with open(input) as src:
80-
data = schema.load(src)
79+
data = schema.load(input)
8180

82-
dbscheme = DbScheme(src=input.relative_to(paths.swift_dir),
81+
dbscheme = Scheme(src=input.relative_to(paths.swift_dir),
8382
includes=get_includes(data, include_dir=input.parent),
8483
declarations=get_declarations(data))
8584

swift/codegen/lib/dbscheme.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
@dataclass
13-
class DbColumn:
13+
class Column:
1414
schema_name: str
1515
type: str
1616
binding: bool = False
@@ -36,69 +36,69 @@ def rhstype(self):
3636

3737

3838
@dataclass
39-
class DbKeySetId:
39+
class KeySetId:
4040
id: str
4141
first: bool = False
4242

4343

4444
@dataclass
45-
class DbKeySet:
46-
ids: List[DbKeySetId]
45+
class KeySet:
46+
ids: List[KeySetId]
4747

4848
def __post_init__(self):
4949
assert self.ids
50-
self.ids = [DbKeySetId(x) for x in self.ids]
50+
self.ids = [KeySetId(x) for x in self.ids]
5151
self.ids[0].first = True
5252

5353

54-
class DbDecl:
54+
class Decl:
5555
is_table = False
5656
is_union = False
5757

5858

5959
@dataclass
60-
class DbTable(DbDecl):
60+
class Table(Decl):
6161
is_table: ClassVar = True
6262

6363
name: str
64-
columns: List[DbColumn]
65-
keyset: DbKeySet = None
64+
columns: List[Column]
65+
keyset: KeySet = None
6666

6767
def __post_init__(self):
6868
if self.columns:
6969
self.columns[0].first = True
7070

7171

7272
@dataclass
73-
class DbUnionCase:
73+
class UnionCase:
7474
type: str
7575
first: bool = False
7676

7777

7878
@dataclass
79-
class DbUnion(DbDecl):
79+
class Union(Decl):
8080
is_union: ClassVar = True
8181

8282
lhs: str
83-
rhs: List[DbUnionCase]
83+
rhs: List[UnionCase]
8484

8585
def __post_init__(self):
8686
assert self.rhs
87-
self.rhs = [DbUnionCase(x) for x in self.rhs]
87+
self.rhs = [UnionCase(x) for x in self.rhs]
8888
self.rhs.sort(key=lambda c: c.type)
8989
self.rhs[0].first = True
9090

9191

9292
@dataclass
93-
class DbSchemeInclude:
93+
class SchemeInclude:
9494
src: str
9595
data: str
9696

9797

9898
@dataclass
99-
class DbScheme:
99+
class Scheme:
100100
template: ClassVar = 'dbscheme'
101101

102102
src: str
103-
includes: List[DbSchemeInclude]
104-
declarations: List[DbDecl]
103+
includes: List[SchemeInclude]
104+
declarations: List[Decl]

swift/codegen/lib/options.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@
1010

1111
def _init_options():
1212
Option("--verbose", "-v", action="store_true")
13-
Option("--schema", tags=["schema"], type=pathlib.Path, default=paths.swift_dir / "codegen/schema.yml")
14-
Option("--dbscheme", tags=["dbscheme"], type=pathlib.Path, default=paths.swift_dir / "ql/lib/swift.dbscheme")
15-
Option("--ql-output", tags=["ql"], type=pathlib.Path, default=paths.swift_dir / "ql/lib/codeql/swift/generated")
16-
Option("--ql-stub-output", tags=["ql"], type=pathlib.Path, default=paths.swift_dir / "ql/lib/codeql/swift/elements")
13+
Option("--schema", tags=["schema"], type=_abspath, default=paths.swift_dir / "codegen/schema.yml")
14+
Option("--dbscheme", tags=["dbscheme"], type=_abspath, default=paths.swift_dir / "ql/lib/swift.dbscheme")
15+
Option("--ql-output", tags=["ql"], type=_abspath, default=paths.swift_dir / "ql/lib/codeql/swift/generated")
16+
Option("--ql-stub-output", tags=["ql"], type=_abspath, default=paths.swift_dir / "ql/lib/codeql/swift/elements")
1717
Option("--codeql-binary", tags=["ql"], default="codeql")
1818

1919

20+
def _abspath(x):
21+
return pathlib.Path(x).resolve()
22+
23+
2024
_options = collections.defaultdict(list)
2125

2226

swift/codegen/lib/paths.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
import os
66

77
try:
8-
_workspace_dir = pathlib.Path(os.environ['BUILD_WORKSPACE_DIRECTORY']) # <- means we are using bazel run
8+
_workspace_dir = pathlib.Path(os.environ['BUILD_WORKSPACE_DIRECTORY']).resolve() # <- means we are using bazel run
99
swift_dir = _workspace_dir / 'swift'
10-
lib_dir = swift_dir / 'codegen' / 'lib'
1110
except KeyError:
1211
_this_file = pathlib.Path(__file__).resolve()
1312
swift_dir = _this_file.parents[2]
14-
lib_dir = _this_file.parent
1513

14+
lib_dir = swift_dir / 'codegen' / 'lib'
15+
templates_dir = lib_dir / 'templates'
1616

17-
exe_file = pathlib.Path(sys.argv[0]).resolve()
17+
try:
18+
exe_file = pathlib.Path(sys.argv[0]).resolve().relative_to(swift_dir)
19+
except ValueError:
20+
exe_file = pathlib.Path(sys.argv[0]).name

0 commit comments

Comments
 (0)