-
Notifications
You must be signed in to change notification settings - Fork 4
/
rules.bzl
200 lines (176 loc) · 7.38 KB
/
rules.bzl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
load("//:expansion.bzl", "expansion")
# From:
# https://stackoverflow.com/questions/47192668/idiomatic-retrieval-of-the-bazel-execution-path#
def _dirname(path):
prefix, _, _ = path.rpartition("/")
return prefix.rstrip("/")
def _test_files(bats, srcs, attr):
return '"{bats_bin}" {bats_args} "$@" {test_paths}'.format(
bats_bin = bats.short_path,
bats_args = " ".join(attr.bats_args),
test_paths = " ".join(['"{}"'.format(s.short_path) for s in srcs]),
)
# Finds shortest path.
# Used to find commonpath for bats-assert or bats-support.
# This would not always necessarily return the correct path (in other contexts),
# but works for the filegroups used.
# A more robust method of finding the base directory is made more difficult by skylark's
# divergence from python. Bringing in skylab as a dependency was deemed overkill.
# So long as a file in the base dir exists (e.g. load.bash), this is fine.
def _base_dir(files):
result = files[0].dirname
min_len = len(files[0].dirname)
for file in files:
if len(file.dirname) < min_len:
min_len = len(file.dirname)
result = file.dirname
return result
def _bats_test_impl(ctx):
path = ["$PWD/" + _dirname(b.short_path) for b in ctx.files.deps]
sep = ctx.configuration.host_path_separator
expanded_envs = expansion.expand_with_toolchains_and_location_attr(
ctx,
validate_expansion = True,
)
for env_key, env_val in expanded_envs.items():
# Postprocess expanded vals to replace any escaped `$`
# (from bazel notation to bash notation).
expanded_envs[env_key] = env_val.replace("$$", "\\$")
content = "\n".join(
["#!/usr/bin/env bash"] +
["set -e"] +
["export TMPDIR=\"$TEST_TMPDIR\""] +
["export PATH=\"{bats_bins_path}\":$PATH".format(bats_bins_path = sep.join(path))] +
['export {}="{}"'.format(key, val) for key, val in expanded_envs.items()] +
[_test_files(ctx.executable._bats, ctx.files.srcs, ctx.attr)],
)
ctx.actions.write(
output = ctx.outputs.executable,
content = content,
)
dep_transitive_files = []
for dep in ctx.attr.deps:
dep_transitive_files.extend(dep.default_runfiles.files.to_list())
runfiles = ctx.runfiles(
files = ctx.files.srcs,
transitive_files = depset(ctx.files.data + dep_transitive_files),
).merge(ctx.attr._bats.default_runfiles)
return [DefaultInfo(runfiles = runfiles)]
def _bats_with_bats_assert_test_impl(ctx):
base_info = _bats_test_impl(ctx)[0]
bats_assert_base_dir = _base_dir(ctx.attr._bats_assert.files.to_list())
bats_support_base_dir = _base_dir(ctx.attr._bats_support.files.to_list())
test_helper_outputs = []
for src_file in ctx.files.srcs:
test_helper_dir = ctx.actions.declare_directory(
"test_helper",
sibling = src_file,
)
test_helper_outputs.append(test_helper_dir)
ctx.actions.run_shell(
outputs = [test_helper_dir],
inputs = depset(ctx.attr._bats_assert.files.to_list() + ctx.attr._bats_support.files.to_list()),
arguments = [test_helper_dir.path, bats_assert_base_dir, bats_support_base_dir],
command = """
mkdir -p $1/bats-support $1/bats-assert \\
&& cp -r $2/* $1/bats-assert \\
&& cp -r $3/* $1/bats-support
""",
)
runfiles = ctx.runfiles(
files = test_helper_outputs,
).merge(base_info.default_runfiles)
return [DefaultInfo(runfiles = runfiles)]
_bats_test_attrs = {
"data": attr.label_list(allow_files = True),
"deps": attr.label_list(),
"env": attr.string_dict(
doc = "A list of key-value pairs of environment variables to define",
),
"bats_args": attr.string_list(
doc = "List of arguments passed to `bats`",
),
"srcs": attr.label_list(
allow_files = [".bats"],
doc = "Source files to run a BATS test on",
),
"_bats": attr.label(
default = Label("@bats_core//:bats"),
executable = True,
cfg = "exec",
),
}
_bats_with_bats_assert_test_attrs = _bats_test_attrs | {
"_bats_support": attr.label(
default = Label("@bats_support//:load_files"),
),
"_bats_assert": attr.label(
default = Label("@bats_assert//:load_files"),
),
}
_bats_test = rule(
_bats_test_impl,
attrs = _bats_test_attrs,
test = True,
doc = "Runs a BATS test on the supplied source files.",
)
_bats_with_bats_assert_test = rule(
_bats_with_bats_assert_test_impl,
attrs = _bats_with_bats_assert_test_attrs,
test = True,
doc = "Runs a BATS test on the supplied source files, allowing for usage of bats-support and bats-assert.",
)
def bats_test(uses_bats_assert = False, **kwargs):
"""
A rule for creating a test target for running one or more `*.bats` test files.
This rule can run one or more bats files, utilizing the `bats-core` framework. Optionally,
`bats-assert` (extension library) can be used for these tests.
`bats_test()` is a macro that handles proper target definition internally.
Args:
uses_bats_assert (str): Whether this test makes use of `bats_assert` (and `bats_support`).
**kwargs (dict): Additional keyword arguments that are passed to the underyling target.
These attributes may include:
name: (Required) The name for the underlying internal target.
srcs: (Required) The `*.bats` files to be run by this test.
data: (Optional) Files necessary for the test during runtime.
deps: (Optional) Dependency targets for the test.
bats_args: (Optional) Arguments to be passed to the `bats` (bats-core) framework when
running the tests.
env: (Optional) Dictionary of enviroment variables to their set values. Values
are subject to `$(location)` and "Make variable" substitution. This
includes expansion mapping provided via `toolchains`.
toolchains: (Optional) Additional providers for extra logic (e.g. `env` substitution).
*: (Optional) Any other attributes that apply for `*_test` targets.
"""
if not uses_bats_assert:
_bats_test(**kwargs)
else:
_bats_with_bats_assert_test(**kwargs)
# Inspired from `rules_rust`
def bats_test_suite(name, srcs, **kwargs):
"""
A rule for creating a test suite for a set of `bats_test` targets.
The rule can be used to generate `bats_test` targets for each source file and a `test_suite`
which encapsulates all tests.
Args:
name (str): The name of the `test_suite`.
srcs (list): All test sources, typically `glob(["*.bats"])`.
**kwargs (dict): Additional keyword arguments for the underyling `bats_test` targets. The
`tags` argument is also passed to the generated `test_suite` target.
"""
tests = []
for src in srcs:
# Prefixed with `name` to allow parameterization with macros
# The test name should not end with `.bats`
test_name = name + "_" + src[:-5]
bats_test(
name = test_name,
srcs = [src],
**kwargs
)
tests.append(test_name)
native.test_suite(
name = name,
tests = tests,
tags = kwargs.get("tags", None),
)