Initial check-in.

This commit is contained in:
Tony Allevato 2017-10-10 07:59:31 -07:00
commit 82b3ad6e9e
25 changed files with 2144 additions and 0 deletions

9
AUTHORS Normal file
View File

@ -0,0 +1,9 @@
# This the official list of Bazel authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
# Names should be added to this file as:
# Name or Organization <email address>
# The email address is not required for organizations.
Google Inc.

16
BUILD Normal file
View File

@ -0,0 +1,16 @@
licenses(["notice"])
exports_files([
"LICENSE",
"lib.bzl",
])
filegroup(
name = "test_deps",
srcs = [
"BUILD",
"//lib:test_deps",
] + glob(["*.bzl"]),
test_only = True,
visibility = ["//visibility:public"],
)

27
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,27 @@
Want to contribute? Great! First, read this page (including the small print at the end).
### Before you contribute
**Before we can use your code, you must sign the
[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
(CLA)**, which you can do online.
The CLA is necessary mainly because you own the copyright to your changes,
even after your contribution becomes part of our codebase, so we need your
permission to use and distribute your code. We also need to be sure of
various other things — for instance that you'll tell us if you know that
your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch
with us first. Use the issue tracker to explain your idea so we can help and
possibly guide you.
### Code reviews and other contributions.
**All submissions, including submissions by project members, require review.**
Please follow the instructions in [the contributors documentation](http://bazel.io/contributing.html).
### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).

15
CONTRIBUTORS Normal file
View File

@ -0,0 +1,15 @@
# People who have agreed to one of the CLAs and can contribute patches.
# The AUTHORS file lists the copyright holders; this file
# lists people. For example, Google employees are listed here
# but not in AUTHORS, because Google holds the copyright.
#
# https://developers.google.com/open-source/cla/individual
# https://developers.google.com/open-source/cla/corporate
#
# Names should be added to this file as:
# Name <email address>
Dmitry Lomov <dslomov@google.com>
Jon Brandvein <brandjon@google.com>
Laurent Le Brun <laurentlb@google.com>
Tony Allevato <allevato@google.com>

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

72
README.md Normal file
View File

@ -0,0 +1,72 @@
# Skylib
Skylib is a standard library that provides functions useful for manipulating
collections, file paths, and other features that are useful when writing custom
build rules in Bazel.
> This library is currently under early development. Be aware that the APIs
> in these modules may change during this time.
Each of the `.bzl` files in the `lib` directory defines a "module"&mdash;a
`struct` that contains a set of related functions and/or other symbols that can
be loaded as a single unit, for convenience. The top-level file `lib.bzl` acts
as an index from which the other modules can be imported.
To use the functionality here, import the modules you need from `lib.bzl` and
access the symbols by dotting into those structs:
```python
load("@bazel_skylib//:lib.bzl", "paths", "shell")
p = paths.basename("foo.bar")
s = shell.quote(p)
```
## List of modules
* [collections](blob/master/lib/collections.bzl)
* [dicts](blob/master/lib/dicts.bzl)
* [paths](blob/master/lib/paths.bzl)
* [selects](blob/master/lib/selects.bzl)
* [sets](blob/master/lib/sets.bzl)
* [shell](blob/master/lib/shell.bzl)
* [unittest](blob/master/lib/unittest.bzl)
## Writing a new module
Steps to add a module to Skylib:
1. Create a new `.bzl` file in the `lib` directory.
1. Write the functions or other symbols (such as constants) in that file,
defining them privately (prefixed by an underscore).
1. Create the exported module struct, mapping the public names of the symbols
to their implementations. For example, if your module was named `things` and
had a function named `manipulate`, your `things.bzl` file would look like
this:
```python
def _manipulate():
...
things = struct(
manipulate=_manipulate,
)
```
1. Add a line to `lib.bzl` to make the new module accessible from the index:
```python
load("@bazel_skylib//lib:things.bzl", "things")
```
1. Clients can then use the module by loading it from `lib.bzl`:
```python
load("@bazel_skylib//:lib.bzl", "things")
things.manipulate()
```
1. Add unit tests for your module in the `tests` directory.

1
WORKSPACE Normal file
View File

@ -0,0 +1 @@
workspace(name = "bazel_skylib")

27
lib.bzl Normal file
View File

@ -0,0 +1,27 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Index from which multiple modules can be loaded."""
load("//lib:collections.bzl", "collections")
load("//lib:dicts.bzl", "dicts")
load("//lib:paths.bzl", "paths")
load("//lib:selects.bzl", "selects")
load("//lib:sets.bzl", "sets")
load("//lib:shell.bzl", "shell")
load("//lib:structs.bzl", "structs")
# The unittest module is treated differently to give more convenient names to
# the assert functions, while keeping them in the same .bzl file.
load("//lib:unittest.bzl", "asserts", "unittest")

8
lib/BUILD Normal file
View File

@ -0,0 +1,8 @@
licenses(["notice"])
filegroup(
name = "test_deps",
srcs = ["BUILD"] + glob(["*.bzl"]),
test_only = True,
visibility = ["//visibility:public"],
)

70
lib/collections.bzl Normal file
View File

@ -0,0 +1,70 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skylib module containing functions that operate on collections."""
def _after_each(separator, iterable):
"""Inserts `separator` after each item in `iterable`.
Args:
separator: The value to insert after each item in `iterable`.
iterable: The list into which to intersperse the separator.
Returns:
A new list with `separator` after each item in `iterable`.
"""
result = []
for x in iterable:
result.append(x)
result.append(separator)
return result
def _before_each(separator, iterable):
"""Inserts `separator` before each item in `iterable`.
Args:
separator: The value to insert before each item in `iterable`.
iterable: The list into which to intersperse the separator.
Returns:
A new list with `separator` before each item in `iterable`.
"""
result = []
for x in iterable:
result.append(separator)
result.append(x)
return result
def _uniq(iterable):
"""Returns a list of unique elements in `iterable`.
Requires all the elements to be hashable.
Args:
iterable: An iterable to filter.
Returns:
A new list with all unique elements from `iterable`.
"""
unique_elements = {element: None for element in iterable}
return unique_elements.keys()
collections = struct(
after_each=_after_each,
before_each=_before_each,
uniq=_uniq,
)

42
lib/dicts.bzl Normal file
View File

@ -0,0 +1,42 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skylib module containing functions that operate on dictionaries."""
def _add(*dictionaries):
"""Returns a new `dict` that has all the entries of the given dictionaries.
If the same key is present in more than one of the input dictionaries, the
last of them in the argument list overrides any earlier ones.
This function is designed to take zero or one arguments as well as multiple
dictionaries, so that it follows arithmetic identities and callers can avoid
special cases for their inputs: the sum of zero dictionaries is the empty
dictionary, and the sum of a single dictionary is a copy of itself.
Args:
*dictionaries: Zero or more dictionaries to be added.
Returns:
A new `dict` that has all the entries of the given dictionaries.
"""
result = {}
for d in dictionaries:
result.update(d)
return result
dicts = struct(
add=_add,
)

245
lib/paths.bzl Normal file
View File

@ -0,0 +1,245 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skylib module containing file path manipulation functions.
NOTE: The functions in this module currently only support paths with Unix-style
path separators (forward slash, "/"); they do not handle Windows-style paths
with backslash separators or drive letters.
"""
def _basename(p):
"""Returns the basename (i.e., the file portion) of a path.
Note that if `p` ends with a slash, this function returns an empty string.
This matches the behavior of Python's `os.path.basename`, but differs from
the Unix `basename` command (which would return the path segment preceding
the final slash).
Args:
p: The path whose basename should be returned.
Returns:
The basename of the path, which includes the extension.
"""
return p.rpartition("/")[-1]
def _dirname(p):
"""Returns the dirname of a path.
The dirname is the portion of `p` up to but not including the file portion
(i.e., the basename). Any slashes immediately preceding the basename are not
included, unless omitting them would make the dirname empty.
Args:
p: The path whose dirname should be returned.
Returns:
The dirname of the path.
"""
prefix, sep, _ = p.rpartition("/")
if not prefix:
return sep
else:
# If there are multiple consecutive slashes, strip them all out as Python's
# os.path.dirname does.
return prefix.rstrip("/")
def _is_absolute(path):
"""Returns `True` if `path` is an absolute path.
Args:
path: A path (which is a string).
Returns:
`True` if `path` is an absolute path.
"""
return path.startswith("/")
def _join(path, *others):
"""Joins one or more path components intelligently.
This function mimics the behavior of Python's `os.path.join` function on POSIX
platform. It returns the concatenation of `path` and any members of `others`,
inserting directory separators before each component except the first. The
separator is not inserted if the path up until that point is either empty or
already ends in a separator.
If any component is an absolute path, all previous components are discarded.
Args:
path: A path segment.
*others: Additional path segments.
Returns:
A string containing the joined paths.
"""
result = path
for p in others:
if _is_absolute(p):
result = p
elif not result or result.endswith("/"):
result += p
else:
result += "/" + p
return result
def _normalize(path):
"""Normalizes a path, eliminating double slashes and other redundant segments.
This function mimics the behavior of Python's `os.path.normpath` function on
POSIX platforms; specifically:
- If the entire path is empty, "." is returned.
- All "." segments are removed, unless the path consists solely of a single
"." segment.
- Trailing slashes are removed, unless the path consists solely of slashes.
- ".." segments are removed as long as there are corresponding segments
earlier in the path to remove; otherwise, they are retained as leading ".."
segments.
- Single and double leading slashes are preserved, but three or more leading
slashes are collapsed into a single leading slash.
- Multiple adjacent internal slashes are collapsed into a single slash.
Args:
path: A path.
Returns:
The normalized path.
"""
if not path:
return "."
if path.startswith("//") and not path.startswith("///"):
initial_slashes = 2
elif path.startswith("/"):
initial_slashes = 1
else:
initial_slashes = 0
is_relative = (initial_slashes == 0)
components = path.split("/")
new_components = []
for component in components:
if component in ("", "."):
continue
if component == "..":
if new_components and new_components[-1] != "..":
# Only pop the last segment if it isn't another "..".
new_components.pop()
elif is_relative:
# Preserve leading ".." segments for relative paths.
new_components.append(component)
else:
new_components.append(component)
path = "/".join(new_components)
if not is_relative:
path = ("/" * initial_slashes) + path
return path or "."
def _relativize(path, start):
"""Returns the portion of `path` that is relative to `start`.
Because we do not have access to the underlying file system, this
implementation differs slightly from Python's `os.path.relpath` in that it
will fail if `path` is not beneath `start` (rather than use parent segments to
walk up to the common file system root).
Relativizing paths that start with parent directory references is not allowed.
Args:
path: The path to relativize.
start: The ancestor path against which to relativize.
Returns:
The portion of `path` that is relative to `start`.
"""
segments = _normalize(path).split("/")
start_segments = _normalize(start).split("/")
if start_segments == ["."]:
start_segments = []
start_length = len(start_segments)
if (path.startswith("..") or start.startswith("..")):
fail("Cannot relativize paths above the current (unknown) directory")
if (path.startswith("/") != start.startswith("/") or
len(segments) < start_length):
fail("Path '%s' is not beneath '%s'" % (path, start))
for ancestor_segment, segment in zip(start_segments, segments):
if ancestor_segment != segment:
fail("Path '%s' is not beneath '%s'" % (path, start))
length = len(segments) - start_length
result_segments = segments[-length:]
return "/".join(result_segments)
def _replace_extension(p, new_extension):
"""Replaces the extension of the file at the end of a path.
If the path has no extension, the new extension is added to it.
Args:
p: The path whose extension should be replaced.
new_extension: The new extension for the file. The new extension should
begin with a dot if you want the new filename to have one.
Returns:
The path with the extension replaced (or added, if it did not have one).
"""
return _split_extension(p)[0] + new_extension
def _split_extension(p):
"""Splits the path `p` into a tuple containing the root and extension.
Leading periods on the basename are ignored, so
`path.split_extension(".bashrc")` returns `(".bashrc", "")`.
Args:
p: The path whose root and extension should be split.
Returns:
A tuple `(root, ext)` such that the root is the path without the file
extension, and `ext` is the file extension (which, if non-empty, contains
the leading dot). The returned tuple always satisfies the relationship
`root + ext == p`.
"""
b = _basename(p)
last_dot_in_basename = b.rfind(".")
# If there is no dot or the only dot in the basename is at the front, then
# there is no extension.
if last_dot_in_basename <= 0:
return (p, "")
dot_distance_from_end = len(b) - last_dot_in_basename
return (p[:-dot_distance_from_end], p[-dot_distance_from_end:])
paths = struct(
basename=_basename,
dirname=_dirname,
is_absolute=_is_absolute,
join=_join,
normalize=_normalize,
relativize=_relativize,
replace_extension=_replace_extension,
split_extension=_split_extension,
)

83
lib/selects.bzl Executable file
View File

@ -0,0 +1,83 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skylib module containing convenience interfaces for select()."""
def _with_or(input_dict):
"""Drop-in replacement for `select()` that supports ORed keys.
Args:
input_dict: The same dictionary `select()` takes, except keys may take
either the usual form `"//foo:config1"` or
`("//foo:config1", "//foo:config2", ...)` to signify
`//foo:config1` OR `//foo:config2` OR `...`.
Example:
```build
deps = selects.with_or({
"//configs:one": [":dep1"],
("//configs:two", "//configs:three"): [":dep2or3"],
"//configs:four": [":dep4"],
"//conditions:default": [":default"]
})
```
Key labels may appear at most once anywhere in the input.
Returns:
A native `select()` that expands
`("//configs:two", "//configs:three"): [":dep2or3"]`
to
```build
"//configs:two": [":dep2or3"],
"//configs:three": [":dep2or3"],
```
"""
return select(_with_or_dict(input_dict))
def _with_or_dict(input_dict):
"""Variation of `with_or` that returns the dict of the `select()`.
Unlike `select()`, the contents of the dict can be inspected by Skylark
macros.
Args:
input_dict: Same as `with_or`.
Returns:
A dictionary usable by a native `select()`.
"""
output_dict = {}
for (key, value) in input_dict.items():
if type(key) == type(()):
for config_setting in key:
if config_setting in output_dict.keys():
fail("key %s appears multiple times" % config_setting)
output_dict[config_setting] = value
else:
if key in output_dict.keys():
fail("key %s appears multiple times" % config_setting)
output_dict[key] = value
return output_dict
selects = struct(
with_or=_with_or,
with_or_dict=_with_or_dict
)

145
lib/sets.bzl Normal file
View File

@ -0,0 +1,145 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skylib module containing common set algorithms.
CAUTION: Operating on sets, particularly sets contained in providers, may
asymptotically slow down the analysis phase. While constructing large sets with
addition/union is fast (there is no linear-time copy involved), the
`difference` function and various comparison predicates involve linear-time
traversals.
For convenience, the functions in this module can take either sets or lists as
inputs; operations that take lists treat them as if they were sets (i.e.,
duplicate elements are ignored). Functions that return new sets always return
them as the `set` type, regardless of the types of the inputs.
"""
def _precondition_only_sets_or_lists(*args):
"""Verifies that all arguments are either sets or lists.
The build will fail if any of the arguments is neither a set nor a list.
Args:
*args: A list of values that must be sets or lists.
"""
for a in args:
t = type(a)
if t not in("depset", "list"):
fail("Expected arguments to be depset or list, but found type %s: %r" %
(t, a))
def _is_equal(a, b):
"""Returns whether two sets are equal.
Args:
a: A depset or a list.
b: A depset or a list.
Returns:
True if `a` is equal to `b`, False otherwise.
"""
_precondition_only_sets_or_lists(a, b)
return sorted(depset(a)) == sorted(depset(b))
def _is_subset(a, b):
"""Returns whether `a` is a subset of `b`.
Args:
a: A depset or a list.
b: A depset or a list.
Returns:
True if `a` is a subset of `b`, False otherwise.
"""
_precondition_only_sets_or_lists(a, b)
for e in a:
if e not in b:
return False
return True
def _disjoint(a, b):
"""Returns whether two sets are disjoint.
Two sets are disjoint if they have no elements in common.
Args:
a: A set or list.
b: A set or list.
Returns:
True if `a` and `b` are disjoint, False otherwise.
"""
_precondition_only_sets_or_lists(a, b)
for e in a:
if e in b:
return False
return True
def _intersection(a, b):
"""Returns the intersection of two sets.
Args:
a: A set or list.
b: A set or list.
Returns:
A set containing the elements that are in both `a` and `b`.
"""
_precondition_only_sets_or_lists(a, b)
return depset([e for e in a if e in b])
def _union(*args):
"""Returns the union of several sets.
Args:
*args: An arbitrary number of sets or lists.
Returns:
The set union of all sets or lists in `*args`.
"""
_precondition_only_sets_or_lists(*args)
r = depset()
for a in args:
r += a
return r
def _difference(a, b):
"""Returns the elements in `a` that are not in `b`.
Args:
a: A set or list.
b: A set or list.
Returns:
A set containing the elements that are in `a` but not in `b`.
"""
_precondition_only_sets_or_lists(a, b)
return depset([e for e in a if e not in b])
sets = struct(
difference = _difference,
disjoint = _disjoint,
intersection = _intersection,
is_equal = _is_equal,
is_subset = _is_subset,
union = _union,
)

55
lib/shell.bzl Normal file
View File

@ -0,0 +1,55 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skylib module containing shell utility functions."""
def _array_literal(iterable):
"""Creates a string from a sequence that can be used as a shell array.
For example, `shell.array_literal(["a", "b", "c"])` would return the string
`("a" "b" "c")`, which can be used in a shell script wherever an array
literal is needed.
Note that all elements in the array are quoted (using `shell.quote`) for
safety, even if they do not need to be.
Args:
iterable: A sequence of elements. Elements that are not strings will be
converted to strings first, by calling `str()`.
Returns:
A string that represents the sequence as a shell array; that is,
parentheses containing the quoted elements.
"""
return "(" + " ".join([_quote(str(i)) for i in iterable]) + ")"
def _quote(s):
"""Quotes the given string for use in a shell command.
This function quotes the given string (in case it contains spaces or other
shell metacharacters.)
Args:
s: The string to quote.
Returns:
A quoted version of the string that can be passed to a shell command.
"""
return "'" + s.replace("'", "'\\''") + "'"
shell = struct(
array_literal=_array_literal,
quote=_quote,
)

36
lib/structs.bzl Normal file
View File

@ -0,0 +1,36 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Skylib module containing functions that operate on structs."""
def _to_dict(s):
"""Converts a `struct` to a `dict`.
Args:
s: A `struct`.
Returns:
A `dict` whose keys and values are the same as the fields in `s`. The
transformation is only applied to the struct's fields and not to any
nested values.
"""
attributes = dir(s)
attributes.remove("to_json")
attributes.remove("to_proto")
return {key: getattr(s, key) for key in attributes}
structs = struct(
to_dict=_to_dict,
)

270
lib/unittest.bzl Normal file
View File

@ -0,0 +1,270 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit testing support.
Unlike most Skylib files, this exports two modules: `unittest` which contains
functions to declare and define unit tests, and `asserts` which contains the
assertions used to within tests.
"""
load(":sets.bzl", "sets")
def _make(impl, attrs=None):
"""Creates a unit test rule from its implementation function.
Each unit test is defined in an implementation function that must then be
associated with a rule so that a target can be built. This function handles
the boilerplate to create and return a test rule and captures the
implementation function's name so that it can be printed in test feedback.
The optional `attrs` argument can be used to define dependencies for this
test, in order to form unit tests of rules.
An example of a unit test:
```
def _your_test(ctx):
env = unittest.begin(ctx)
# Assert statements go here
unittest.end(env)
your_test = unittest.make(_your_test)
```
Recall that names of test rules must end in `_test`.
Args:
impl: The implementation function of the unit test.
attrs: An optional dictionary to supplement the attrs passed to the
unit test's `rule()` constructor.
Returns:
A rule definition that should be stored in a global whose name ends in
`_test`.
"""
# Derive the name of the implementation function for better test feedback.
# Skylark currently stringifies a function as "<function NAME>", so we use
# that knowledge to parse the "NAME" portion out. If this behavior ever
# changes, we'll need to update this.
# TODO(bazel-team): Expose a ._name field on functions to avoid this.
impl_name = str(impl)
impl_name = impl_name.partition("<function ")[-1]
impl_name = impl_name.rpartition(">")[0]
attrs = dict(attrs) if attrs else {}
attrs["_impl_name"] = attr.string(default=impl_name)
return rule(
impl,
attrs=attrs,
_skylark_testable=True,
test=True,
)
def _suite(name, *test_rules):
"""Defines a `test_suite` target that contains multiple tests.
After defining your test rules in a `.bzl` file, you need to create targets
from those rules so that `blaze test` can execute them. Doing this manually
in a BUILD file would consist of listing each test in your `load` statement
and then creating each target one by one. To reduce duplication, we recommend
writing a macro in your `.bzl` file to instantiate all targets, and calling
that macro from your BUILD file so you only have to load one symbol.
For the case where your unit tests do not take any (non-default) attributes --
i.e., if your unit tests do not test rules -- you can use this function to
create the targets and wrap them in a single test_suite target. In your
`.bzl` file, write:
```
def your_test_suite():
unittest.suite(
"your_test_suite",
your_test,
your_other_test,
yet_another_test,
)
```
Then, in your `BUILD` file, simply load the macro and invoke it to have all
of the targets created:
```
load("//path/to/your/package:tests.bzl", "your_test_suite")
your_test_suite()
```
If you pass _N_ unit test rules to `unittest.suite`, _N_ + 1 targets will be
created: a `test_suite` target named `${name}` (where `${name}` is the name
argument passed in here) and targets named `${name}_test_${i}`, where `${i}`
is the index of the test in the `test_rules` list, which is used to uniquely
name each target.
Args:
name: The name of the `test_suite` target, and the prefix of all the test
target names.
*test_rules: A list of test rules defines by `unittest.test`.
"""
test_names = []
for index, test_rule in enumerate(test_rules):
test_name = "%s_test_%d" % (name, index)
test_rule(name=test_name)
test_names.append(test_name)
native.test_suite(
name=name,
tests=[":%s" % t for t in test_names]
)
def _begin(ctx):
"""Begins a unit test.
This should be the first function called in a unit test implementation
function. It initializes a "test environment" that is used to collect
assertion failures so that they can be reported and logged at the end of the
test.
Args:
ctx: The Skylark context. Pass the implementation function's `ctx` argument
in verbatim.
Returns:
A test environment struct that must be passed to assertions and finally to
`unittest.end`. Do not rely on internal details about the fields in this
struct as it may change.
"""
return struct(ctx=ctx, failures=[])
def _end(env):
"""Ends a unit test and logs the results.
This must be called before the end of a unit test implementation function so
that the results are reported.
Args:
env: The test environment returned by `unittest.begin`.
"""
cmd = "\n".join([
"cat << EOF",
"\n".join(env.failures),
"EOF",
"exit %d" % len(env.failures),
])
env.ctx.file_action(
output=env.ctx.outputs.executable,
content=cmd,
executable=True,
)
def _fail(env, msg):
"""Unconditionally causes the current test to fail.
Args:
env: The test environment returned by `unittest.begin`.
msg: The message to log describing the failure.
"""
full_msg = "In test %s: %s" % (env.ctx.attr._impl_name, msg)
print(full_msg)
env.failures.append(full_msg)
def _assert_true(env,
condition,
msg="Expected condition to be true, but was false."):
"""Asserts that the given `condition` is true.
Args:
env: The test environment returned by `unittest.begin`.
condition: A value that will be evaluated in a Boolean context.
msg: An optional message that will be printed that describes the failure.
If omitted, a default will be used.
"""
if not condition:
_fail(env, msg)
def _assert_false(env,
condition,
msg="Expected condition to be false, but was true."):
"""Asserts that the given `condition` is false.
Args:
env: The test environment returned by `unittest.begin`.
condition: A value that will be evaluated in a Boolean context.
msg: An optional message that will be printed that describes the failure.
If omitted, a default will be used.
"""
if condition:
_fail(env, msg)
def _assert_equals(env, expected, actual, msg=None):
"""Asserts that the given `expected` and `actual` values are equal.
Args:
env: The test environment returned by `unittest.begin`.
expected: The expected value of some computation.
actual: The actual value returned by some computation.
msg: An optional message that will be printed that describes the failure.
If omitted, a default will be used.
"""
if expected != actual:
expectation_msg = 'Expected "%s", but got "%s"' % (expected, actual)
if msg:
full_msg = "%s (%s)" % (msg, expectation_msg)
else:
full_msg = expectation_msg
_fail(env, full_msg)
def _assert_set_equals(env, expected, actual, msg=None):
"""Asserts that the given `expected` and `actual` sets are equal.
Args:
env: The test environment returned by `unittest.begin`.
expected: The expected set resulting from some computation.
actual: The actual set returned by some computation.
msg: An optional message that will be printed that describes the failure.
If omitted, a default will be used.
"""
if type(actual) != type(depset()) or not sets.is_equal(expected, actual):
expectation_msg = "Expected %r, but got %r" % (expected, actual)
if msg:
full_msg = "%s (%s)" % (msg, expectation_msg)
else:
full_msg = expectation_msg
_fail(env, full_msg)
asserts = struct(
equals=_assert_equals,
false=_assert_false,
set_equals=_assert_set_equals,
true=_assert_true,
)
unittest = struct(
make=_make,
suite=_suite,
begin=_begin,
end=_end,
fail=_fail,
)

23
tests/BUILD Normal file
View File

@ -0,0 +1,23 @@
load(":collections_tests.bzl", "collections_test_suite")
load(":dicts_tests.bzl", "dicts_test_suite")
load(":paths_tests.bzl", "paths_test_suite")
load(":selects_tests.bzl", "selects_test_suite")
load(":sets_tests.bzl", "sets_test_suite")
load(":shell_tests.bzl", "shell_test_suite")
load(":structs_tests.bzl", "structs_test_suite")
licenses(["notice"])
collections_test_suite()
dicts_test_suite()
paths_test_suite()
selects_test_suite()
sets_test_suite()
shell_test_suite()
structs_test_suite()

View File

@ -0,0 +1,81 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for collections.bzl."""
load("//:lib.bzl", "collections", "asserts", "unittest")
def _after_each_test(ctx):
"""Unit tests for collections.after_each."""
env = unittest.begin(ctx)
asserts.equals(env, [], collections.after_each("1", []))
asserts.equals(env, ["a", "1"], collections.after_each("1", ["a"]))
asserts.equals(env, ["a", "1", "b", "1"],
collections.after_each("1", ["a", "b"]))
# We don't care what type the separator is, we just put it there; so None
# should be just as valid as anything else.
asserts.equals(env, ["a", None, "b", None],
collections.after_each(None, ["a", "b"]))
unittest.end(env)
after_each_test = unittest.make(_after_each_test)
def _before_each_test(ctx):
"""Unit tests for collections.before_each."""
env = unittest.begin(ctx)
asserts.equals(env, [], collections.before_each("1", []))
asserts.equals(env, ["1", "a"], collections.before_each("1", ["a"]))
asserts.equals(env, ["1", "a", "1", "b"],
collections.before_each("1", ["a", "b"]))
# We don't care what type the separator is, we just put it there; so None
# should be just as valid as anything else.
asserts.equals(env, [None, "a", None, "b"],
collections.before_each(None, ["a", "b"]))
unittest.end(env)
before_each_test = unittest.make(_before_each_test)
def _uniq_test(ctx):
env = unittest.begin(ctx)
asserts.equals(env, collections.uniq([0, 1, 2, 3]), [0, 1, 2, 3])
asserts.equals(env, collections.uniq([]), [])
asserts.equals(env, collections.uniq([1, 1, 1, 1, 1]), [1])
asserts.equals(env, collections.uniq([True, 5, "foo", 5, False, struct(a=1),
True, struct(b=2), "bar", (1,), "foo",
struct(a=1), (1,)]),
[True, 5, "foo", False, struct(a=1), struct(b=2),
"bar", (1,)])
unittest.end(env)
uniq_test = unittest.make(_uniq_test)
def collections_test_suite():
"""Creates the test targets and test suite for collections.bzl tests."""
unittest.suite(
"collections_tests",
after_each_test,
before_each_test,
uniq_test,
)

67
tests/dicts_tests.bzl Normal file
View File

@ -0,0 +1,67 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for dicts.bzl."""
load("//:lib.bzl", "dicts", "asserts", "unittest")
def _add_test(ctx):
"""Unit tests for dicts.add."""
env = unittest.begin(ctx)
# Test zero- and one-argument behavior.
asserts.equals(env, {}, dicts.add())
asserts.equals(env, {"a": 1}, dicts.add({"a": 1}))
# Test simple two-argument behavior.
asserts.equals(env, {"a": 1, "b": 2}, dicts.add({"a": 1}, {"b": 2}))
# Test simple more-than-two-argument behavior.
asserts.equals(env, {"a": 1, "b": 2, "c": 3, "d": 4},
dicts.add({"a": 1}, {"b": 2}, {"c": 3}, {"d": 4}))
# Test same-key overriding.
asserts.equals(env, {"a": 100}, dicts.add({"a": 1}, {"a": 100}))
asserts.equals(env, {"a": 10}, dicts.add({"a": 1}, {"a": 100}, {"a": 10}))
asserts.equals(env, {"a": 100, "b": 10},
dicts.add({"a": 1}, {"a": 100}, {"b": 10}))
asserts.equals(env, {"a": 10}, dicts.add({"a": 1}, {}, {"a": 10}))
asserts.equals(env, {"a": 10, "b": 5},
dicts.add({"a": 1}, {"a": 10, "b": 5}))
# Test some other boundary cases.
asserts.equals(env, {"a": 1}, dicts.add({"a": 1}, {}))
# Since dictionaries are passed around by reference, make sure that the
# result of dicts.add is always a *copy* by modifying it afterwards and
# ensuring that the original argument doesn't also reflect the change. We do
# this to protect against someone who might attempt to optimize the function
# by returning the argument itself in the one-argument case.
original = {"a": 1}
result = dicts.add(original)
result["a"] = 2
asserts.equals(env, 1, original["a"])
unittest.end(env)
add_test = unittest.make(_add_test)
def dicts_test_suite():
"""Creates the test targets and test suite for dicts.bzl tests."""
unittest.suite(
"dicts_tests",
add_test,
)

280
tests/paths_tests.bzl Normal file
View File

@ -0,0 +1,280 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for paths.bzl."""
load("//:lib.bzl", "paths", "asserts", "unittest")
def _basename_test(ctx):
"""Unit tests for paths.basename."""
env = unittest.begin(ctx)
# Verify some degenerate cases.
asserts.equals(env, "", paths.basename(""))
asserts.equals(env, "", paths.basename("/"))
asserts.equals(env, "bar", paths.basename("foo///bar"))
# Verify some realistic cases.
asserts.equals(env, "foo", paths.basename("foo"))
asserts.equals(env, "foo", paths.basename("/foo"))
asserts.equals(env, "foo", paths.basename("bar/foo"))
asserts.equals(env, "foo", paths.basename("/bar/foo"))
# Verify that we correctly duplicate Python's os.path.basename behavior,
# where a trailing slash means the basename is empty.
asserts.equals(env, "", paths.basename("foo/"))
asserts.equals(env, "", paths.basename("/foo/"))
unittest.end(env)
basename_test = unittest.make(_basename_test)
def _dirname_test(ctx):
"""Unit tests for paths.dirname."""
env = unittest.begin(ctx)
# Verify some degenerate cases.
asserts.equals(env, "", paths.dirname(""))
asserts.equals(env, "/", paths.dirname("/"))
asserts.equals(env, "foo", paths.dirname("foo///bar"))
# Verify some realistic cases.
asserts.equals(env, "", paths.dirname("foo"))
asserts.equals(env, "/", paths.dirname("/foo"))
asserts.equals(env, "bar", paths.dirname("bar/foo"))
asserts.equals(env, "/bar", paths.dirname("/bar/foo"))
# Verify that we correctly duplicate Python's os.path.dirname behavior,
# where a trailing slash means the dirname is the same as the original
# path (without the trailing slash).
asserts.equals(env, "foo", paths.dirname("foo/"))
asserts.equals(env, "/foo", paths.dirname("/foo/"))
unittest.end(env)
dirname_test = unittest.make(_dirname_test)
def _is_absolute_test(ctx):
"""Unit tests for paths.is_absolute."""
env = unittest.begin(ctx)
# Try a degenerate case.
asserts.false(env, paths.is_absolute(""))
# Try some relative paths.
asserts.false(env, paths.is_absolute("foo"))
asserts.false(env, paths.is_absolute("foo/"))
asserts.false(env, paths.is_absolute("foo/bar"))
# Try some absolute paths.
asserts.true(env, paths.is_absolute("/"))
asserts.true(env, paths.is_absolute("/foo"))
asserts.true(env, paths.is_absolute("/foo/"))
asserts.true(env, paths.is_absolute("/foo/bar"))
unittest.end(env)
is_absolute_test = unittest.make(_is_absolute_test)
def _join_test(ctx):
"""Unit tests for paths.join."""
env = unittest.begin(ctx)
# Try a degenerate case.
asserts.equals(env, "", paths.join(""))
# Try some basic paths.
asserts.equals(env, "foo", paths.join("foo"))
asserts.equals(env, "foo/bar", paths.join("foo", "bar"))
asserts.equals(env, "foo/bar/baz", paths.join("foo", "bar", "baz"))
# Make sure an initially absolute path stays absolute.
asserts.equals(env, "/foo", paths.join("/foo"))
asserts.equals(env, "/foo/bar", paths.join("/foo", "bar"))
# Make sure an absolute path later in the list resets the result.
asserts.equals(env, "/baz", paths.join("foo", "bar", "/baz"))
asserts.equals(env, "/baz", paths.join("foo", "/bar", "/baz"))
asserts.equals(env, "/bar/baz", paths.join("foo", "/bar", "baz"))
asserts.equals(env, "/bar", paths.join("/foo", "/bar"))
# Make sure a leading empty segment doesn't make it absolute.
asserts.equals(env, "foo", paths.join("", "foo"))
# Try some trailing slash scenarios.
asserts.equals(env, "foo/", paths.join("foo", ""))
asserts.equals(env, "foo/", paths.join("foo/"))
asserts.equals(env, "foo/", paths.join("foo/", ""))
asserts.equals(env, "foo//", paths.join("foo//", ""))
asserts.equals(env, "foo//", paths.join("foo//"))
asserts.equals(env, "foo/bar/baz/", paths.join("foo/", "bar/", "baz", ""))
asserts.equals(env, "foo/bar/baz/", paths.join("foo/", "bar/", "baz/"))
asserts.equals(env, "foo/bar/baz/", paths.join("foo/", "bar/", "baz/", ""))
# Make sure that adjacent empty segments don't add extra path separators.
asserts.equals(env, "foo/", paths.join("foo", "", ""))
asserts.equals(env, "foo", paths.join("", "", "foo"))
asserts.equals(env, "foo/bar", paths.join("foo", "", "", "bar"))
unittest.end(env)
join_test = unittest.make(_join_test)
def _normalize_test(ctx):
"""Unit tests for paths.normalize."""
env = unittest.begin(ctx)
# Try the most basic case.
asserts.equals(env, ".", paths.normalize(""))
# Try some basic adjacent-slash removal.
asserts.equals(env, "foo/bar", paths.normalize("foo//bar"))
asserts.equals(env, "foo/bar", paths.normalize("foo////bar"))
# Try some "." removal.
asserts.equals(env, "foo/bar", paths.normalize("foo/./bar"))
asserts.equals(env, "foo/bar", paths.normalize("./foo/bar"))
asserts.equals(env, "foo/bar", paths.normalize("foo/bar/."))
asserts.equals(env, "/", paths.normalize("/."))
# Try some ".." removal.
asserts.equals(env, "bar", paths.normalize("foo/../bar"))
asserts.equals(env, "foo", paths.normalize("foo/bar/.."))
asserts.equals(env, ".", paths.normalize("foo/.."))
asserts.equals(env, ".", paths.normalize("foo/bar/../.."))
asserts.equals(env, "..", paths.normalize("foo/../.."))
asserts.equals(env, "/", paths.normalize("/foo/../.."))
asserts.equals(env, "../../c", paths.normalize("a/b/../../../../c/d/.."))
# Make sure one or two initial slashes are preserved, but three or more are
# collapsed to a single slash.
asserts.equals(env, "/foo", paths.normalize("/foo"))
asserts.equals(env, "//foo", paths.normalize("//foo"))
asserts.equals(env, "/foo", paths.normalize("///foo"))
# Trailing slashes should be removed unless the entire path is a trailing
# slash.
asserts.equals(env, "/", paths.normalize("/"))
asserts.equals(env, "foo", paths.normalize("foo/"))
asserts.equals(env, "foo/bar", paths.normalize("foo/bar/"))
unittest.end(env)
normalize_test = unittest.make(_normalize_test)
def _relativize_test(ctx):
"""Unit tests for paths.relativize."""
env = unittest.begin(ctx)
# Make sure that relative-to-current-directory works in all forms.
asserts.equals(env, "foo", paths.relativize("foo", ""))
asserts.equals(env, "foo", paths.relativize("foo", "."))
# Try some regular cases.
asserts.equals(env, "bar", paths.relativize("foo/bar", "foo"))
asserts.equals(env, "baz", paths.relativize("foo/bar/baz", "foo/bar"))
asserts.equals(env, "bar/baz", paths.relativize("foo/bar/baz", "foo"))
# Try a case where a parent directory is normalized away.
asserts.equals(env, "baz", paths.relativize("foo/bar/../baz", "foo"))
# TODO(allevato): Test failure cases, once that is possible.
unittest.end(env)
relativize_test = unittest.make(_relativize_test)
def _replace_extension_test(ctx):
"""Unit tests for paths.replace_extension."""
env = unittest.begin(ctx)
# Try some degenerate cases.
asserts.equals(env, ".foo", paths.replace_extension("", ".foo"))
asserts.equals(env, "/.foo", paths.replace_extension("/", ".foo"))
asserts.equals(env, "foo.bar", paths.replace_extension("foo", ".bar"))
# Try a directory with an extension and basename that doesn't have one.
asserts.equals(env, "foo.bar/baz.quux",
paths.replace_extension("foo.bar/baz", ".quux"))
# Now try some things with legit extensions.
asserts.equals(env, "a.z", paths.replace_extension("a.b", ".z"))
asserts.equals(env, "a.b.z", paths.replace_extension("a.b.c", ".z"))
asserts.equals(env, "a/b.z", paths.replace_extension("a/b.c", ".z"))
asserts.equals(env, "a.b/c.z", paths.replace_extension("a.b/c.d", ".z"))
asserts.equals(env, ".a/b.z", paths.replace_extension(".a/b.c", ".z"))
asserts.equals(env, ".a.z", paths.replace_extension(".a.b", ".z"))
# Verify that we don't insert a period on the extension if none is provided.
asserts.equals(env, "foobaz", paths.replace_extension("foo.bar", "baz"))
unittest.end(env)
replace_extension_test = unittest.make(_replace_extension_test)
def _split_extension_test(ctx):
"""Unit tests for paths.split_extension."""
env = unittest.begin(ctx)
# Try some degenerate cases.
asserts.equals(env, ("", ""), paths.split_extension(""))
asserts.equals(env, ("/", ""), paths.split_extension("/"))
asserts.equals(env, ("foo", ""), paths.split_extension("foo"))
# Try some paths whose basenames start with ".".
asserts.equals(env, (".", ""), paths.split_extension("."))
asserts.equals(env, (".bashrc", ""), paths.split_extension(".bashrc"))
asserts.equals(env, ("foo/.bashrc", ""), paths.split_extension("foo/.bashrc"))
asserts.equals(env, (".foo/.bashrc", ""),
paths.split_extension(".foo/.bashrc"))
# Try some directories with extensions with basenames that don't have one.
asserts.equals(env, ("foo.bar/baz", ""), paths.split_extension("foo.bar/baz"))
asserts.equals(env, ("foo.bar/.bashrc", ""),
paths.split_extension("foo.bar/.bashrc"))
# Now try some things that will actually get split.
asserts.equals(env, ("a", ".b"), paths.split_extension("a.b"))
asserts.equals(env, ("a.b", ".c"), paths.split_extension("a.b.c"))
asserts.equals(env, ("a/b", ".c"), paths.split_extension("a/b.c"))
asserts.equals(env, ("a.b/c", ".d"), paths.split_extension("a.b/c.d"))
asserts.equals(env, (".a/b", ".c"), paths.split_extension(".a/b.c"))
asserts.equals(env, (".a", ".b"), paths.split_extension(".a.b"))
unittest.end(env)
split_extension_test = unittest.make(_split_extension_test)
def paths_test_suite():
"""Creates the test targets and test suite for paths.bzl tests."""
unittest.suite(
"paths_tests",
basename_test,
dirname_test,
is_absolute_test,
join_test,
normalize_test,
relativize_test,
replace_extension_test,
split_extension_test,
)

53
tests/selects_tests.bzl Normal file
View File

@ -0,0 +1,53 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for selects.bzl."""
load("//:lib.bzl", "selects", "asserts", "unittest")
def _with_or_test(ctx):
"""Unit tests for with_or."""
env = unittest.begin(ctx)
# We actually test on with_or_dict because Skylark can't get the
# dictionary from a select().
# Test select()-compatible input syntax.
input_dict = {":foo": ":d1", "//conditions:default": ":d1"}
asserts.equals(env, input_dict, selects.with_or_dict(input_dict))
# Test OR syntax.
or_dict = {(":foo", ":bar"): ":d1"}
asserts.equals(env, {":foo": ":d1", ":bar": ":d1"},
selects.with_or_dict(or_dict))
# Test mixed syntax.
mixed_dict = {":foo": ":d1", (":bar", ":baz"): ":d2",
"//conditions:default": ":d3"}
asserts.equals(env, {":foo": ":d1", ":bar": ":d2", ":baz": ":d2",
"//conditions:default": ":d3"},
selects.with_or_dict(mixed_dict))
unittest.end(env)
with_or_test = unittest.make(_with_or_test)
def selects_test_suite():
"""Creates the test targets and test suite for selects.bzl tests."""
unittest.suite(
"selects_tests",
with_or_test,
)

160
tests/sets_tests.bzl Normal file
View File

@ -0,0 +1,160 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for sets.bzl."""
load("//:lib.bzl", "sets", "asserts", "unittest")
def _is_equal_test(ctx):
"""Unit tests for sets.is_equal.
Note that if this test fails, the results for the other `sets` tests will be
inconclusive because they use `asserts.set_equals`, which in turn calls
`sets.is_equal`.
"""
env = unittest.begin(ctx)
asserts.true(env, sets.is_equal([], []))
asserts.false(env, sets.is_equal([], [1]))
asserts.false(env, sets.is_equal([1], []))
asserts.true(env, sets.is_equal([1], [1]))
asserts.false(env, sets.is_equal([1], [1, 2]))
asserts.false(env, sets.is_equal([1], [2]))
asserts.false(env, sets.is_equal([1], depset([1, 2])))
# Verify that the implementation is not using == on the sets directly.
asserts.true(env, sets.is_equal(depset([1]), depset([1])))
# If passing a list, verify that duplicate elements are ignored.
asserts.true(env, sets.is_equal([1, 1], [1]))
unittest.end(env)
is_equal_test = unittest.make(_is_equal_test)
def _is_subset_test(ctx):
"""Unit tests for sets.is_subset."""
env = unittest.begin(ctx)
asserts.true(env, sets.is_subset([], []))
asserts.true(env, sets.is_subset([], [1]))
asserts.false(env, sets.is_subset([1], []))
asserts.true(env, sets.is_subset([1], [1]))
asserts.true(env, sets.is_subset([1], [1, 2]))
asserts.false(env, sets.is_subset([1], [2]))
asserts.true(env, sets.is_subset([1], depset([1, 2])))
# If passing a list, verify that duplicate elements are ignored.
asserts.true(env, sets.is_subset([1, 1], [1, 2]))
unittest.end(env)
is_subset_test = unittest.make(_is_subset_test)
def _disjoint_test(ctx):
"""Unit tests for sets.disjoint."""
env = unittest.begin(ctx)
asserts.true(env, sets.disjoint([], []))
asserts.true(env, sets.disjoint([], [1]))
asserts.true(env, sets.disjoint([1], []))
asserts.false(env, sets.disjoint([1], [1]))
asserts.false(env, sets.disjoint([1], [1, 2]))
asserts.true(env, sets.disjoint([1], [2]))
asserts.true(env, sets.disjoint([1], depset([2])))
# If passing a list, verify that duplicate elements are ignored.
asserts.false(env, sets.disjoint([1, 1], [1, 2]))
unittest.end(env)
disjoint_test = unittest.make(_disjoint_test)
def _intersection_test(ctx):
"""Unit tests for sets.intersection."""
env = unittest.begin(ctx)
asserts.set_equals(env, [], sets.intersection([], []))
asserts.set_equals(env, [], sets.intersection([], [1]))
asserts.set_equals(env, [], sets.intersection([1], []))
asserts.set_equals(env, [1], sets.intersection([1], [1]))
asserts.set_equals(env, [1], sets.intersection([1], [1, 2]))
asserts.set_equals(env, [], sets.intersection([1], [2]))
asserts.set_equals(env, [1], sets.intersection([1], depset([1])))
# If passing a list, verify that duplicate elements are ignored.
asserts.set_equals(env, [1], sets.intersection([1, 1], [1, 2]))
unittest.end(env)
intersection_test = unittest.make(_intersection_test)
def _union_test(ctx):
"""Unit tests for sets.union."""
env = unittest.begin(ctx)
asserts.set_equals(env, [], sets.union())
asserts.set_equals(env, [1], sets.union([1]))
asserts.set_equals(env, [], sets.union([], []))
asserts.set_equals(env, [1], sets.union([], [1]))
asserts.set_equals(env, [1], sets.union([1], []))
asserts.set_equals(env, [1], sets.union([1], [1]))
asserts.set_equals(env, [1, 2], sets.union([1], [1, 2]))
asserts.set_equals(env, [1, 2], sets.union([1], [2]))
asserts.set_equals(env, [1], sets.union([1], depset([1])))
# If passing a list, verify that duplicate elements are ignored.
asserts.set_equals(env, [1, 2], sets.union([1, 1], [1, 2]))
unittest.end(env)
union_test = unittest.make(_union_test)
def _difference_test(ctx):
"""Unit tests for sets.difference."""
env = unittest.begin(ctx)
asserts.set_equals(env, [], sets.difference([], []))
asserts.set_equals(env, [], sets.difference([], [1]))
asserts.set_equals(env, [1], sets.difference([1], []))
asserts.set_equals(env, [], sets.difference([1], [1]))
asserts.set_equals(env, [], sets.difference([1], [1, 2]))
asserts.set_equals(env, [1], sets.difference([1], [2]))
asserts.set_equals(env, [], sets.difference([1], depset([1])))
# If passing a list, verify that duplicate elements are ignored.
asserts.set_equals(env, [2], sets.difference([1, 2], [1, 1]))
unittest.end(env)
difference_test = unittest.make(_difference_test)
def sets_test_suite():
"""Creates the test targets and test suite for sets.bzl tests."""
unittest.suite(
"sets_tests",
disjoint_test,
intersection_test,
is_equal_test,
is_subset_test,
difference_test,
union_test,
)

108
tests/shell_tests.bzl Normal file
View File

@ -0,0 +1,108 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for shell.bzl."""
load("//:lib.bzl", "shell", "asserts", "unittest")
def _shell_array_literal_test(ctx):
"""Unit tests for shell.array_literal."""
env = unittest.begin(ctx)
asserts.equals(env, "()", shell.array_literal([]))
asserts.equals(env, "('1')", shell.array_literal([1]))
asserts.equals(env, "('1' '2' '3')", shell.array_literal([1, 2, 3]))
asserts.equals(env, "('$foo')", shell.array_literal(["$foo"]))
asserts.equals(env, "('qu\"o\"te')", shell.array_literal(['qu"o"te']))
unittest.end(env)
shell_array_literal_test = unittest.make(_shell_array_literal_test)
def _shell_quote_test(ctx):
"""Unit tests for shell.quote."""
env = unittest.begin(ctx)
asserts.equals(env, "'foo'", shell.quote("foo"))
asserts.equals(env, "'foo bar'", shell.quote("foo bar"))
asserts.equals(env, "'three spaces'", shell.quote("three spaces"))
asserts.equals(env, "' leading'", shell.quote(" leading"))
asserts.equals(env, "'trailing '", shell.quote("trailing "))
asserts.equals(env, "'new\nline'", shell.quote("new\nline"))
asserts.equals(env, "'tab\tcharacter'", shell.quote("tab\tcharacter"))
asserts.equals(env, "'$foo'", shell.quote("$foo"))
asserts.equals(env, "'qu\"o\"te'", shell.quote('qu"o"te'))
asserts.equals(env, "'it'\\''s'", shell.quote("it's"))
asserts.equals(env, "'foo\\bar'", shell.quote(r"foo\bar"))
asserts.equals(env, "'back`echo q`uote'", shell.quote(r"back`echo q`uote"))
unittest.end(env)
shell_quote_test = unittest.make(_shell_quote_test)
def _shell_spawn_e2e_test_impl(ctx):
"""Test spawning a real shell."""
args = [
"foo",
"foo bar",
"three spaces",
" leading",
"trailing ",
"new\nline",
"tab\tcharacter",
"$foo",
'qu"o"te',
"it's",
r"foo\bar",
"back`echo q`uote",
]
script_content = "\n".join([
"#!/bin/bash",
"myarray=" + shell.array_literal(args),
'output=$(echo "${myarray[@]}")',
# For logging:
'echo "DEBUG: output=[${output}]" >&2',
# The following is a shell representation of what the echo of the quoted
# array will look like. It looks a bit confusing considering it's shell
# quoted into Python. Shell using single quotes to minimize shell
# escaping, so only the single quote needs to be escaped as '\'', all
# others are essentially kept literally.
"expected='foo foo bar three spaces leading trailing new",
"line tab\tcharacter $foo qu\"o\"te it'\\''s foo\\bar back`echo q`uote'",
'[[ "${output}" == "${expected}" ]]',
])
ctx.file_action(
output = ctx.outputs.executable,
content = script_content,
executable = True,
)
return struct()
shell_spawn_e2e_test = rule(
test = True,
implementation = _shell_spawn_e2e_test_impl,
)
def shell_test_suite():
"""Creates the test targets and test suite for shell.bzl tests."""
unittest.suite(
"shell_tests",
shell_array_literal_test,
shell_quote_test,
shell_spawn_e2e_test,
)

49
tests/structs_tests.bzl Normal file
View File

@ -0,0 +1,49 @@
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for structs.bzl."""
load("//:lib.bzl", "structs", "asserts", "unittest")
def _add_test(ctx):
"""Unit tests for dicts.add."""
env = unittest.begin(ctx)
# Test zero- and one-argument behavior.
asserts.equals(env, {}, structs.to_dict(struct()))
asserts.equals(env, {"a": 1}, structs.to_dict(struct(a=1)))
# Test simple two-argument behavior.
asserts.equals(env, {"a": 1, "b": 2}, structs.to_dict(struct(a=1, b=2)))
# Test simple more-than-two-argument behavior.
asserts.equals(env, {"a": 1, "b": 2, "c": 3, "d": 4},
structs.to_dict(struct(a=1, b=2, c=3, d=4)))
# Test transformation is not applied transitively.
asserts.equals(env, {"a": 1, "b": struct(bb=1)},
structs.to_dict(struct(a=1, b=struct(bb=1))))
unittest.end(env)
add_test = unittest.make(_add_test)
def structs_test_suite():
"""Creates the test targets and test suite for structs.bzl tests."""
unittest.suite(
"structs_tests",
add_test,
)