commit 82b3ad6e9ef45cb8e2c2316bf4007278bcba2f99 Author: Tony Allevato Date: Tue Oct 10 07:59:31 2017 -0700 Initial check-in. diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..8f95963 --- /dev/null +++ b/AUTHORS @@ -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 +# The email address is not required for organizations. + +Google Inc. diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..c73e51f --- /dev/null +++ b/BUILD @@ -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"], +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9d3d1aa --- /dev/null +++ b/CONTRIBUTING.md @@ -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). diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..59dedb4 --- /dev/null +++ b/CONTRIBUTORS @@ -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 + +Dmitry Lomov +Jon Brandvein +Laurent Le Brun +Tony Allevato diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d5b03d --- /dev/null +++ b/README.md @@ -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"—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. diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..5458cd5 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "bazel_skylib") diff --git a/lib.bzl b/lib.bzl new file mode 100644 index 0000000..eb3f39a --- /dev/null +++ b/lib.bzl @@ -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") diff --git a/lib/BUILD b/lib/BUILD new file mode 100644 index 0000000..4b63ce4 --- /dev/null +++ b/lib/BUILD @@ -0,0 +1,8 @@ +licenses(["notice"]) + +filegroup( + name = "test_deps", + srcs = ["BUILD"] + glob(["*.bzl"]), + test_only = True, + visibility = ["//visibility:public"], +) diff --git a/lib/collections.bzl b/lib/collections.bzl new file mode 100644 index 0000000..c86842b --- /dev/null +++ b/lib/collections.bzl @@ -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, +) diff --git a/lib/dicts.bzl b/lib/dicts.bzl new file mode 100644 index 0000000..ee1076c --- /dev/null +++ b/lib/dicts.bzl @@ -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, +) diff --git a/lib/paths.bzl b/lib/paths.bzl new file mode 100644 index 0000000..925abcc --- /dev/null +++ b/lib/paths.bzl @@ -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, +) diff --git a/lib/selects.bzl b/lib/selects.bzl new file mode 100755 index 0000000..daf15c7 --- /dev/null +++ b/lib/selects.bzl @@ -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 +) diff --git a/lib/sets.bzl b/lib/sets.bzl new file mode 100644 index 0000000..edaf827 --- /dev/null +++ b/lib/sets.bzl @@ -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, +) diff --git a/lib/shell.bzl b/lib/shell.bzl new file mode 100644 index 0000000..4173fe7 --- /dev/null +++ b/lib/shell.bzl @@ -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, +) diff --git a/lib/structs.bzl b/lib/structs.bzl new file mode 100644 index 0000000..78715cc --- /dev/null +++ b/lib/structs.bzl @@ -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, +) diff --git a/lib/unittest.bzl b/lib/unittest.bzl new file mode 100644 index 0000000..f1208a3 --- /dev/null +++ b/lib/unittest.bzl @@ -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 "", 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("")[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, +) diff --git a/tests/BUILD b/tests/BUILD new file mode 100644 index 0000000..4dd4a46 --- /dev/null +++ b/tests/BUILD @@ -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() diff --git a/tests/collections_tests.bzl b/tests/collections_tests.bzl new file mode 100644 index 0000000..0f5cd2c --- /dev/null +++ b/tests/collections_tests.bzl @@ -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, + ) diff --git a/tests/dicts_tests.bzl b/tests/dicts_tests.bzl new file mode 100644 index 0000000..40b0655 --- /dev/null +++ b/tests/dicts_tests.bzl @@ -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, + ) diff --git a/tests/paths_tests.bzl b/tests/paths_tests.bzl new file mode 100644 index 0000000..5765916 --- /dev/null +++ b/tests/paths_tests.bzl @@ -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, + ) diff --git a/tests/selects_tests.bzl b/tests/selects_tests.bzl new file mode 100644 index 0000000..210fd32 --- /dev/null +++ b/tests/selects_tests.bzl @@ -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, + ) diff --git a/tests/sets_tests.bzl b/tests/sets_tests.bzl new file mode 100644 index 0000000..8c64396 --- /dev/null +++ b/tests/sets_tests.bzl @@ -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, + ) diff --git a/tests/shell_tests.bzl b/tests/shell_tests.bzl new file mode 100644 index 0000000..5e8c280 --- /dev/null +++ b/tests/shell_tests.bzl @@ -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, + ) diff --git a/tests/structs_tests.bzl b/tests/structs_tests.bzl new file mode 100644 index 0000000..b072d5b --- /dev/null +++ b/tests/structs_tests.bzl @@ -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, + )