From 82b3ad6e9ef45cb8e2c2316bf4007278bcba2f99 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 10 Oct 2017 07:59:31 -0700 Subject: [PATCH] Initial check-in. --- AUTHORS | 9 ++ BUILD | 16 +++ CONTRIBUTING.md | 27 ++++ CONTRIBUTORS | 15 ++ LICENSE | 202 ++++++++++++++++++++++++++ README.md | 72 ++++++++++ WORKSPACE | 1 + lib.bzl | 27 ++++ lib/BUILD | 8 ++ lib/collections.bzl | 70 +++++++++ lib/dicts.bzl | 42 ++++++ lib/paths.bzl | 245 +++++++++++++++++++++++++++++++ lib/selects.bzl | 83 +++++++++++ lib/sets.bzl | 145 +++++++++++++++++++ lib/shell.bzl | 55 +++++++ lib/structs.bzl | 36 +++++ lib/unittest.bzl | 270 ++++++++++++++++++++++++++++++++++ tests/BUILD | 23 +++ tests/collections_tests.bzl | 81 +++++++++++ tests/dicts_tests.bzl | 67 +++++++++ tests/paths_tests.bzl | 280 ++++++++++++++++++++++++++++++++++++ tests/selects_tests.bzl | 53 +++++++ tests/sets_tests.bzl | 160 +++++++++++++++++++++ tests/shell_tests.bzl | 108 ++++++++++++++ tests/structs_tests.bzl | 49 +++++++ 25 files changed, 2144 insertions(+) create mode 100644 AUTHORS create mode 100644 BUILD create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS create mode 100644 LICENSE create mode 100644 README.md create mode 100644 WORKSPACE create mode 100644 lib.bzl create mode 100644 lib/BUILD create mode 100644 lib/collections.bzl create mode 100644 lib/dicts.bzl create mode 100644 lib/paths.bzl create mode 100755 lib/selects.bzl create mode 100644 lib/sets.bzl create mode 100644 lib/shell.bzl create mode 100644 lib/structs.bzl create mode 100644 lib/unittest.bzl create mode 100644 tests/BUILD create mode 100644 tests/collections_tests.bzl create mode 100644 tests/dicts_tests.bzl create mode 100644 tests/paths_tests.bzl create mode 100644 tests/selects_tests.bzl create mode 100644 tests/sets_tests.bzl create mode 100644 tests/shell_tests.bzl create mode 100644 tests/structs_tests.bzl 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, + )