diff --git a/expansion.bzl b/expansion.bzl new file mode 100644 index 0000000..a0452a9 --- /dev/null +++ b/expansion.bzl @@ -0,0 +1,795 @@ +""" +Module containing functions that aid in environment variable expansion. + +This could be replaced with the same impl via Skylib dependency, if/when PR is merged. +See https://github.com/bazelbuild/bazel-skylib/pull/486. +""" + +_CONSIDERED_KEY_FORMATS = ("${}", "${{{}}}", "$({})") + +def _valid_char_for_env_var_name(char): + """ + Determines if the given character could be used as a part of variable name. + + Args: + char: (Required) A string (intended to be length 1) to be checked. + + Returns: + True if the character could be a part of a variable name. False otherwise. + """ + return char.isalnum() or char == "_" + +def _find_env_var_name_index_index( + string, + str_len, + search_start_index, + special_ending_char = None): + """ + Searches for the end of a variable name in the given string, starting from the given index. + + Search will start from `search_start_index` and conclude once a character, which cannot be part + of a variable name, is encountered or until the end of the string is reached. + + Args: + string: (Required) The string to search through. + str_len: (Required) The precomputed length of the given `string` parameter. + search_start_index: (Required) The index to start searching from. This is intended to be + somewhere within (the start?) of a variable name. + special_ending_char: (Optional) A special character which will count as the end of the + variable name. This can be used for `$(VAR)`, `${VAR}`, or similar. + This replaces the "valid variable name character" checking, + allowing for other characters to occur before the given special + ending character. + If set to `None`, no special character will be checked for + (only checking for non-variable characters or the end of the + string). + The default value is `None`. + + Returns: + The index (with respect to the start of `string`) of the last character of the variable + name. + """ + for offset in range(str_len - search_start_index): + index = search_start_index + offset + char = string[index] + if special_ending_char: + if char == special_ending_char: + return index + elif not _valid_char_for_env_var_name(char): + return index - 1 + return str_len - 1 + +def _even_count_dollar_sign_repeat(containing_str, end_of_dollar_signs_index): + """ + Searches backwards through the given string, counting the contiguous `$` characters. + + An even number of `$` characters is indicative of escaped variables, which should not be + expanded (left as is in a string). + + Args: + containing_str: (Required) The string to search through. + end_of_dollar_signs_index: (Required) The index of the end of the contiguous `$` + characters in `containing_str`. This is the starting + index for the backwards search. + + Returns: + True if the set of contiguous `$` characters has even length. False if the length is odd. + """ + dollar_sign_count = 0 + for index in range(end_of_dollar_signs_index, -1, -1): + if containing_str[index] != "$": + break + dollar_sign_count += 1 + return (dollar_sign_count % 2) == 0 + +def _key_to_be_expanded(str_with_key, key, start_of_key_index): + """ + Examines the given string and determines if the given "key" should be expanded. + + The "key" was located within the given string (as a substring). This function + determines whether the key is complete and is to be expanded. + + Args: + str_with_key: (Required) The string that `key` is found within. + key: (Required) The found substring in `str_with_key` which needs to possibly be + expanded. + start_of_key_index: (Required) The index where `key` was found within `str_with_key`. + + Returns: + True if the found key is complete (not a substring of another potential key) and is not + escaped (even number of preceding `$`). + """ + + # Check that the string at index is prefixed with an even number of `$`. + # An even number means that the last `$` is escaped. + if _even_count_dollar_sign_repeat(str_with_key, start_of_key_index): + return False + + # Check that the key is correctly matched. + # Specifically, check the key isn't matching to another key (substring). + key_mismatch = False + if key[-1] not in (")", "}"): + index_after_key = start_of_key_index + len(key) + key_mismatch = ( + (index_after_key < len(str_with_key)) and + _valid_char_for_env_var_name(str_with_key[index_after_key]) + ) + + return not key_mismatch + +def _fail_validation(fail_instead_of_return, found_errors_list, failure_message): + """ + This is called when a failure has occured and handles propagation of a failure message. + + Will either call `fail()` with the given failure message (to hard fail immediately) or append + the given failure message to the given list. + + Args: + fail_instead_of_return: (Required) If set to True, `fail()` will be called (will not + return). If set to False, `found_errors_list` will be appended to + and the function will return normally. + found_errors_list: (Required) In/out list for error messages to be appended into. Will + only be used if `fail_instead_of_return` is False. + failure_message: (Required) Failure message to be either passed to `fail()` or + appended into `found_errors_list`. + """ + if fail_instead_of_return: + fail(failure_message) + else: + found_errors_list.append(failure_message) + +def _validate_unterminated_expression( + expanded_val, + fail_instead_of_return, + found_errors, + dollar_sign_index, + next_char_after_dollar_sign): + """ + Checks if given string contains an unterminated expression of the form `$(VAR)` or `${VAR}`. + + If the given variable/expression is of the correct form, and unterminated, an error will be + noted (either by calling `fail()` or by appending it into the given error list). + + Args: + expanded_val: (Required) The string which contains a `$` preceding a variable (to be + expanded). + fail_instead_of_return: (Required) If set to True, `fail()` will be called (will not + return) when an unterminated variable is found. If set to False, + `found_errors` will be appended to and the function will return + normally. + found_errors: (Required) In/out list for error messages to be appended into. Will only be + used if `fail_instead_of_return` is False. + dollar_sign_index: (Required) The index of the `$` at the start of the expression. + next_char_after_dollar_sign: (Required) The character that immediately follows the `$`. + + Returns: + The validaity of the string. + Returns False if the variable was of the form and unterminated. Returns True otherwise. + """ + if next_char_after_dollar_sign == "(": + if expanded_val.find(")", dollar_sign_index + 1) < 0: + unterminated_expr = expanded_val[dollar_sign_index:] + _fail_validation( + fail_instead_of_return, + found_errors, + "Unterminated '$(...)' expression ('{}' in '{}').".format(unterminated_expr, expanded_val), + ) + return False + elif next_char_after_dollar_sign == "{": + if expanded_val.find("}", dollar_sign_index + 1) < 0: + unterminated_expr = expanded_val[dollar_sign_index:] + _fail_validation( + fail_instead_of_return, + found_errors, + "Unterminated '${{...}}' expression ('{}' in '{}').".format(unterminated_expr, expanded_val), + ) + return False + return True + +def _validate_unexpanded_expression( + expanded_val, + fail_instead_of_return, + str_len, + found_errors, + dollar_sign_index, + next_char_after_dollar_sign): + """ + Always generates an error for the given string (containing unexpanded variable). + + The given string contains a variable which unexpanded (and is not escaped), an error will be + noted (either by calling `fail()` or by appending it into the given error list). + + Args: + expanded_val: (Required) The string which contains a `$` preceding a variable (to be + expanded). + fail_instead_of_return: (Required) If set to True, `fail()` will be called (will not + return). If set to False, `found_errors` will be appended to and + the function will return normally. + str_len: (Required) The precomputed length of the given `expanded_val` parameter. + found_errors: (Required) In/out list for error messages to be appended into. Will only be + used if `fail_instead_of_return` is False. + dollar_sign_index: (Required) The index of the `$` at the start of the unexpanded + expression. + next_char_after_dollar_sign: (Required) The character that immediately follows the `$`. + """ + + # Find special ending char, if wrapped expression. + special_ending_char = None + if next_char_after_dollar_sign == "(": + special_ending_char = ")" + elif next_char_after_dollar_sign == "{": + special_ending_char = "}" + + # Find info for unexpanded expression and fail. + name_end_index = _find_env_var_name_index_index( + expanded_val, + str_len, + dollar_sign_index + 1, + special_ending_char = special_ending_char, + ) + _fail_validation( + fail_instead_of_return, + found_errors, + "Unexpanded expression ('{}' in '{}').".format( + expanded_val[dollar_sign_index:name_end_index + 1], + expanded_val, + ), + ) + +def _validate_all_keys_expanded(expanded_val, fail_instead_of_return): + """ + Iterates over the entire given string, searching for any unexpanded variables/expressions. + + If any unexpanded/unterminated variables/expressions are found, an error will be noted (either + by calling `fail()` and hard failing immediately, or by collecting all such found errors and + returning it in a list). + + Args: + expanded_val: (Required) The string to be checked for any potentially unescaped and + unexpanded/unterminated variables/expressions. + fail_instead_of_return: (Required) If set to True, `fail()` will be called (will not + return) when the first error has been found. If set to False, the + function will return normally and return a list of all found + errors. + + Returns: + A list of found errors. Each element in the list is a failure message with details about + the unescaped and unexpanded/unterminated variable/expression. The list will be empty if + no such expressions were found. This function does not return if `fail_instead_of_return` + was set to True (`fail()` will be called). + """ + str_len = len(expanded_val) + str_iter = 0 + found_errors = [] + + # Max iterations at the length of the str; will likely break out earlier. + for _ in range(str_len): + if str_iter >= str_len: + break + next_dollar_sign_index = expanded_val.find("$", str_iter) + if next_dollar_sign_index < 0: + break + str_iter = next_dollar_sign_index + 1 + + # Check for unterminated (non-escaped) ending dollar sign(s). + if next_dollar_sign_index == str_len - 1: + if not _even_count_dollar_sign_repeat(expanded_val, next_dollar_sign_index): + _fail_validation( + fail_instead_of_return, + found_errors, + "Unterminated '$' expression in '{}'.".format(expanded_val), + ) + + # No error if escaped. Still at end of string, break out. + break + + next_char_after_dollar_sign = expanded_val[next_dollar_sign_index + 1] + + # Check for continued dollar signs string (no need to handle yet). + if next_char_after_dollar_sign == "$": + continue + + # Check for escaped dollar signs (which are ok). + if _even_count_dollar_sign_repeat(expanded_val, next_dollar_sign_index): + continue + + # Check for unterminated expressions. + if _validate_unterminated_expression( + expanded_val, + fail_instead_of_return, + found_errors, + next_dollar_sign_index, + next_char_after_dollar_sign, + ): + # If not unterminated, it's unexpanded. + _validate_unexpanded_expression( + expanded_val, + fail_instead_of_return, + str_len, + found_errors, + next_dollar_sign_index, + next_char_after_dollar_sign, + ) + + return found_errors + +def _expand_key_in_str(key, val, unexpanded_str): + """ + Expand the given key, by replacing it with the given value, in the given string. + + The given `key` may or may not be contained in the given string `unexpanded_str`. + If the given key is found, it will be expanded/replaced by the given `val` string. + The key is given in its full formatted form with preceding `$` (`$VAR`, `$(VAR)`, `${VAR}`, + `$(VAR VAL)`, etc). + The key will not be expanded if it is escaped (an even number of contiguous `$` characters at + the start) or if the found key is a substring of another potential key (e.g. `$VAR` will not be + expanded if the found location is `$VARIABLE`). + The given key will be replaced (as appropriate) for all occurences within the given string. + + Args: + key: (Required) The key to search for (within the given string, `unexpanded_str`) and + replace all occurences of the key with the given replacement value, `val`. + val: (Required) The value to replace all found occurences of the given key, `key`, into + the given string, `unexpanded_str`. + unexpanded_str: (Required) The string to search for `key` and replace with `val`. + + Returns: + A copy of `unexpanded_str` with all occurences of `key` replaced with `val` (as necessary). + The returned string will be `unexpanded_str` (not a copy), if `key` is not found/expanded. + """ + key_len = len(key) + val_len = len(val) + searched_index = 0 + expanded_str = unexpanded_str + + # Max iterations at the length of the str; will likely break out earlier. + for _ in range(len(expanded_str)): + used_key_index = expanded_str.find(key, searched_index) + if used_key_index < 0: + break + if _key_to_be_expanded(expanded_str, key, used_key_index): + # Only replace this one instance that we have verified (count = 1). + # Avoid extra string splicing, if possible. + if searched_index == 0: + expanded_str = expanded_str.replace(key, val, 1) + else: + expanded_str = ( + expanded_str[0:searched_index - 1] + + expanded_str[searched_index:].replace(key, val, 1) + ) + searched_index += val_len + else: + searched_index += key_len + return expanded_str + +def _expand_all_keys_in_str_from_dict(replacement_dict, unexpanded_str): + """ + Uses the given dictionary to replace keys with values in the given string. + + Each key is intended to be a variable name (e.g. `VARIABLE_NAME`), which can be wrapped with + `$`, `$( )`, or `${ }` (to express the given example key as the "formatted key" + `$VARIABLE_NAME`, `$(VARIABLE_NAME)`, or `${VARIABLE_NAME}`). The corresponding value (in the + dict) is to be used as the intended replacement string when any matching formatted key (of the + given variable name key) is found within the given string, `unexpanded_str`. + + Args: + replacement_dict: (Required) The set of key/value pairs to be used for search/replacement + within the given `unexpanded_str` string. + unexpanded_str: (Required) The string to search for the formatted versions of each key + set within `replacement_dict`, where each found occurence will be + expanded/replaced with the associated value. + + Returns: + A copy of `unexpanded_str` with all occurences of each key (when formatted into an + unexpanded variable) within `replacement_dict` replaced with corresponding value (as + necessary). + The returned string will be `unexpanded_str` (not a copy), if no expansion occurs. + """ + + # Manually expand variables based on the var dict. + # Do not use `ctx.expand_make_variables()` as it will error out if any variable expands to + # `$(location )` (or similar) instead of leaving it untouched. + expanded_val = unexpanded_str + for avail_key, corresponding_val in replacement_dict.items(): + if expanded_val.find(avail_key) < 0: + continue + formatted_keys = [key_format.format(avail_key) for key_format in _CONSIDERED_KEY_FORMATS] + + # Skip self-references (e.g. {"VAR": "$(VAR)"}) + # This may happen (and is ok) for the `env` attribute, where keys can be reused to be + # expanded by the resolved dict. + if corresponding_val in formatted_keys: + continue + + # Expand each format style of this key, if it exists. + for formatted_key in formatted_keys: + expanded_val = _expand_key_in_str(formatted_key, corresponding_val, expanded_val) + return expanded_val + +def _expand_all_keys_in_str( + expand_location, + resolved_replacement_dict, + env_replacement_dict, + unexpanded_str): + """ + Uses the given dictionaries to replace keys with values in the given string. + + Each key, in the given dictionaries, is intended to be a variable name (e.g. `VARIABLE_NAME`), + which can be wrapped with `$`, `$( )`, or `${ }` (to express the given example key as the + "formatted key" `$VARIABLE_NAME`, `$(VARIABLE_NAME)`, or `${VARIABLE_NAME}`). The corresponding + value (in the dict) is to be used as the intended replacement string when any matching + formatted key (of the given variable name key) is found within the given string, + `unexpanded_str`. + + Expansion happens iteratively. In each iteration, three steps occur: + 1) If `expand_location` is not `None`, it will be invoked to replace any occurrences of + `$(location ...)` (or similar). Technically, this function can execute any high-priority + expansion logic -- but it is intended for functionality similar to `ctx.expand_location()`. + 2) Each variable name key in `env_replacement_dict` will be searched for (in `unexpanded_str`) + and expanded into the corresponding value within the dict for the given found variable + name. This is intended for the use with the `env` attribute for a given target (but + supports any general "higher priority" dict replacement). + 3) Each variable name key in `resolved_replacement_dict` will be searched for (in + `unexpanded_str`) and expanded into the corresponding value within the dict for the given + found variable name. This is intended for the use with `ctx.var` which contains toolchain + resolved key/values (but supports any general "lower priority" dict replacement). + + Args: + expand_location: (Required) A None-able function used for optional "location" + expansion logic (`$(location ...)` or similar). + resolved_replacement_dict: (Required) A set of key/value pairs to be used for + search/replacement within the given `unexpanded_str` string. + Replacement logic will occur after (lower priority) replacement + for `env_replacement_dict`. + env_replacement_dict: (Required) A set of key/value pairs to be used for + search/replacement within the given `unexpanded_str` string. + Replacement logic will occur before (higher priority) replacement + for `resolved_replacement_dict`. + unexpanded_str: (Required) The string to perform expansion variable upon (optionally + invoke `expand_location`, and search for the formatted versions of each + key set within `env_replacement_dict` and `resolved_replacement_dict`, + where each found occurence will be expanded/replaced with the + associated value). + + Returns: + A copy of `unexpanded_str` with all occurences of each key (when formatted into an + unexpanded variable) within `replacement_dict` replaced with corresponding value (as + necessary). + The returned string will be `unexpanded_str` (not a copy), if no expansion occurs. + """ + if unexpanded_str.find("$") < 0: + return unexpanded_str + + expanded_val = unexpanded_str + prev_val = expanded_val + + # Max iterations at the length of the str; will likely break out earlier. + for _ in range(len(expanded_val)): + # First let's try the safe `location` (et al) expansion logic. + # `$VAR`, `$(VAR)`, and `${VAR}` will be left untouched. + if expand_location: + expanded_val = expand_location(expanded_val) + + # Break early if nothing left to expand. + if expanded_val.find("$") < 0: + break + + # Expand values first from the `env` attribute, then by the toolchain resolved values. + expanded_val = _expand_all_keys_in_str_from_dict(env_replacement_dict, expanded_val) + expanded_val = _expand_all_keys_in_str_from_dict(resolved_replacement_dict, expanded_val) + + # Break out early if nothing changed in this iteration. + if prev_val == expanded_val: + break + prev_val = expanded_val + + return expanded_val + +def _expand_with_manual_dict(resolution_dict, source_env_dict, validate_expansion = False): + """ + Recursively expands all values in `source_env_dict` using the given lookup data. + + All keys of `source_env_dict` are returned in the resultant dict with values expanded by + lookups via `resolution_dict` dict. + This function does not modify any of the given parameters. + + Args: + resolution_dict: (Required) A dictionary with resolved key/value pairs to be used for + lookup when resolving values. This may come from toolchains (via + `ctx.var`) or other sources. + source_env_dict: (Required) The source for all desired expansions. All key/value pairs + will appear within the returned dictionary, with all values fully + expanded by lookups in `resolution_dict`. + validate_expansion: (Optional) If set to True, all expanded strings will be validated to + ensure that no unexpanded (but seemingly expandable) values remain. If + any unexpanded values are found, `fail()` will be called. The + validation logic is the same as + `expansion.validate_expansions_in_dict()`. + Default value is False. + + Returns: + A new dict with all key/values from `source_env_dict`, where all values have been recursively + expanded. + """ + expanded_envs = {} + for env_key, unexpanded_val in source_env_dict.items(): + expanded_val = _expand_all_keys_in_str( + None, # No `expand_location` available + resolution_dict, + source_env_dict, + unexpanded_val, + ) + if validate_expansion: + _validate_all_keys_expanded(expanded_val, fail_instead_of_return = True) + expanded_envs[env_key] = expanded_val + return expanded_envs + +def _expand_with_manual_dict_and_location( + expand_location, + resolution_dict, + source_env_dict, + validate_expansion = False): + """ + Recursively expands all values in `source_env_dict` using the given logic / lookup data. + + All keys of `source_env_dict` are returned in the resultant dict with values expanded by + location expansion logic via `expand_location` and by lookups via `resolution_dict` dict. + This function does not modify any of the given parameters. + + Args: + expand_location: (Required) A function that takes in a string and properly replaces + `$(location ...)` (and similar) with the corresponding values. This + likely should correspond to `ctx.expand_location()`. + resolution_dict: (Required) A dictionary with resolved key/value pairs to be used for + lookup when resolving values. This may come from toolchains (via + `ctx.var`) or other sources. + source_env_dict: (Required) The source for all desired expansions. All key/value pairs + will appear within the returned dictionary, with all values fully + expanded by the logic expansion logic of `expand_location` and by + lookup in `resolution_dict`. + validate_expansion: (Optional) If set to True, all expanded strings will be validated to + ensure that no unexpanded (but seemingly expandable) values remain. If + any unexpanded values are found, `fail()` will be called. The + validation logic is the same as + `expansion.validate_expansions_in_dict()`. + Default value is False. + + Returns: + A new dict with all key/values from `source_env_dict`, where all values have been recursively + expanded. + """ + expanded_envs = {} + for env_key, unexpanded_val in source_env_dict.items(): + expanded_val = _expand_all_keys_in_str( + expand_location, + resolution_dict, + source_env_dict, + unexpanded_val, + ) + if validate_expansion: + _validate_all_keys_expanded(expanded_val, fail_instead_of_return = True) + expanded_envs[env_key] = expanded_val + return expanded_envs + +def _expand_with_toolchains( + ctx, + source_env_dict, + additional_lookup_dict = None, + validate_expansion = False): + """ + Recursively expands all values in `source_env_dict` using the given lookup data. + + All keys of `source_env_dict` are returned in the resultant dict with values expanded by + lookups via `ctx.var` dict (unioned with optional `additional_lookup_dict` parameter). + Expansion occurs recursively through all given dicts. + This function does not modify any of the given parameters. + + Args: + ctx: (Required) The bazel context object. This is used to access the `ctx.var` member + for use as the "resolution dict". This makes use of providers from toolchains for + environment variable expansion. + source_env_dict: (Required) The source for all desired expansions. All key/value pairs + will appear within the returned dictionary, with all values fully + expanded by lookups in `ctx.var` and optional `additional_lookup_dict`. + additional_lookup_dict: (Optional) Additional dict to be used with `ctx.var` (union) for + variable expansion. + validate_expansion: (Optional) If set to True, all expanded strings will be validated + to ensure that no unexpanded (but seemingly expandable) values + remain. If any unexpanded values are found, `fail()` will be + called. The validation logic is the same as + `expansion.validate_expansions_in_dict()`. + Default value is False. + + Returns: + A new dict with all key/values from `source_env_dict`, where all values have been recursively + expanded. + """ + additional_lookup_dict = additional_lookup_dict or {} + return _expand_with_manual_dict( + ctx.var | additional_lookup_dict, + source_env_dict, + validate_expansion = validate_expansion, + ) + +def _expand_with_toolchains_and_location( + ctx, + deps, + source_env_dict, + additional_lookup_dict = None, + validate_expansion = False): + """ + Recursively expands all values in `source_env_dict` using the `ctx` logic / lookup data. + + All keys of `source_env_dict` are returned in the resultant dict with values expanded by + location expansion logic via `ctx.expand_location` and by lookups via `ctx.var` dict (unioned + with optional `additional_lookup_dict` parameter). + This function does not modify any of the given parameters. + + Args: + ctx: (Required) The bazel context object. This is used to access the `ctx.var` member + for use as the "resolution dict". This makes use of providers from toolchains for + environment variable expansion. This object is also used for the + `ctx.expand_location` method to handle `$(location ...)` (and similar) expansion + logic. + deps: (Required) The set of targets used with `ctx.expand_location` for expanding + `$(location ...)` (and similar) expressions. + source_env_dict: (Required) The source for all desired expansions. All key/value pairs + will appear within the returned dictionary, with all values fully + expanded by the logic expansion logic of `expand_location` and by + lookups in `ctx.var` and optional `additional_lookup_dict`. + additional_lookup_dict: (Optional) Additional dict to be used with `ctx.var` (union) for + variable expansion. + validate_expansion: (Optional) If set to True, all expanded strings will be validated + to ensure that no unexpanded (but seemingly expandable) values + remain. If any unexpanded values are found, `fail()` will be + called. The validation logic is the same as + `expansion.validate_expansions_in_dict()`. + Default value is False. + + Returns: + A new dict with all key/values from `source_env_dict`, where all values have been recursively + expanded. + """ + + def _simpler_expand_location(input_str): + return ctx.expand_location(input_str, deps) + + additional_lookup_dict = additional_lookup_dict or {} + return _expand_with_manual_dict_and_location( + _simpler_expand_location, + ctx.var | additional_lookup_dict, + source_env_dict, + validate_expansion = validate_expansion, + ) + +def _expand_with_toolchains_attr( + ctx, + env_attr_name = "env", + additional_lookup_dict = None, + validate_expansion = False): + """ + Recursively expands all values in "env" attr dict using the `ctx` lookup data. + + All keys of `env` attribute are returned in the resultant dict with values expanded by + lookups via `ctx.var` dict (unioned with optional `additional_lookup_dict` parameter). + The attribute used can be changed (instead of `env`) via the optional `env_attr_name` paramter. + This function does not modify any of the given parameters. + + Args: + ctx: (Required) The bazel context object. This is used to access the `ctx.var` member + for use as the "resolution dict". This makes use of providers from toolchains for + environment variable expansion. This object is also used to retrieve various + necessary attributes via `ctx.attr.`. + env_attr_name: (Optional) The name of the attribute that is used as the source for all + desired expansions. All key/value pairs will appear within the returned + dictionary, with all values fully expanded by lookups in `ctx.var` and + optional `additional_lookup_dict`. + Default value is "env". + additional_lookup_dict: (Optional) Additional dict to be used with `ctx.var` (union) for + variable expansion. + validate_expansion: (Optional) If set to True, all expanded strings will be validated + to ensure that no unexpanded (but seemingly expandable) values + remain. If any unexpanded values are found, `fail()` will be + called. The validation logic is the same as + `expansion.validate_expansions_in_dict()`. + Default value is False. + + Returns: + A new dict with all key/values from source attribute (default "env" attribute), where all + values have been recursively expanded. + """ + return _expand_with_toolchains( + ctx, + getattr(ctx.attr, env_attr_name), + additional_lookup_dict = additional_lookup_dict, + validate_expansion = validate_expansion, + ) + +def _expand_with_toolchains_and_location_attr( + ctx, + deps_attr_name = "deps", + env_attr_name = "env", + additional_lookup_dict = None, + validate_expansion = False): + """ + Recursively expands all values in "env" attr dict using the `ctx` logic / lookup data. + + All keys of `env` attribute are returned in the resultant dict with values expanded by + location expansion logic via `ctx.expand_location` and by lookups via `ctx.var` dict (unioned + with optional `additional_lookup_dict` parameter). + This function does not modify any of the given parameters. + + Args: + ctx: (Required) The bazel context object. This is used to access the `ctx.var` member + for use as the "resolution dict". This makes use of providers from toolchains for + environment variable expansion. This object is also used for the + `ctx.expand_location` method to handle `$(location ...)` (and similar) expansion + logic. This object is also used to retrieve various necessary attributes via + `ctx.attr.`. + deps_attr_name: (Optional) The name of the attribute which contains the set of targets used + with `ctx.expand_location` for expanding `$(location ...)` (and similar) + expressions. + Default value is "deps". + env_attr_name: (Optional) The name of the attribute that is used as the source for all + desired expansions. All key/value pairs will appear within the returned + dictionary, with all values fully expanded by lookups in `ctx.var` and + optional `additional_lookup_dict`. + Default value is "env". + additional_lookup_dict: (Optional) Additional dict to be used with `ctx.var` (union) for + variable expansion. + validate_expansion: (Optional) If set to True, all expanded strings will be validated + to ensure that no unexpanded (but seemingly expandable) values + remain. If any unexpanded values are found, `fail()` will be + called. The validation logic is the same as + `expansion.validate_expansions_in_dict()`. + Default value is False. + + Returns: + A new dict with all key/values from source attribute (default "env" attribute), where all + values have been recursively expanded. + """ + return _expand_with_toolchains_and_location( + ctx, + getattr(ctx.attr, deps_attr_name), + getattr(ctx.attr, env_attr_name), + additional_lookup_dict = additional_lookup_dict, + validate_expansion = validate_expansion, + ) + +def _validate_expansions(expanded_values, fail_instead_of_return = True): + """ + Validates all given strings to no longer have unexpanded expressions. + + Validates all expanded strings in `expanded_values` to ensure that no unexpanded (but seemingly + expandable) values remain. + Any unterminated or unexpanded expressions of the form `$VAR`, $(VAR)`, or `${VAR}` will result + in an error (with fail message). + + Args: + expanded_values: (Required) List of string values to validate. + fail_instead_of_return: (Optional) If set to True, `fail()` will be called upon first + invalid (unexpanded) value found. If set to False, error + messages will be collected and returned (no failure will + occur); it will be the caller's responsibility to check the + returned list. + Default value is True. + + Returns: + A list with all found invalid (unexpanded) values. Will be an empty list if all values are + completely expanded. This function will not return if there were unexpanded substrings and if + `fail_instead_of_return` is set to True (due to `fail()` being called). + """ + found_errors = [] + for expanded_val in expanded_values: + found_errors += _validate_all_keys_expanded(expanded_val, fail_instead_of_return) + return found_errors + +expansion = struct( + expand_with_manual_dict = _expand_with_manual_dict, + expand_with_manual_dict_and_location = _expand_with_manual_dict_and_location, + expand_with_toolchains = _expand_with_toolchains, + expand_with_toolchains_attr = _expand_with_toolchains_attr, + expand_with_toolchains_and_location = _expand_with_toolchains_and_location, + expand_with_toolchains_and_location_attr = _expand_with_toolchains_and_location_attr, + validate_expansions = _validate_expansions, +) diff --git a/rules.bzl b/rules.bzl index 25ceade..357c8f0 100644 --- a/rules.bzl +++ b/rules.bzl @@ -1,3 +1,5 @@ +load("//:expansion.bzl", "expansion") + # From: # https://stackoverflow.com/questions/47192668/idiomatic-retrieval-of-the-bazel-execution-path# @@ -32,24 +34,21 @@ def _bats_test_impl(ctx): path = ["$PWD/" + _dirname(b.short_path) for b in ctx.files.deps] sep = ctx.configuration.host_path_separator + expanded_envs = expansion.expand_with_toolchains_and_location_attr( + ctx, + validate_expansion = True, + ) + for env_key, env_val in expanded_envs.items(): + # Postprocess expanded vals to replace any escaped `$` + # (from bazel notation to bash notation). + expanded_envs[env_key] = env_val.replace("$$", "\\$") + content = "\n".join( ["#!/usr/bin/env bash"] + ["set -e"] + ["export TMPDIR=\"$TEST_TMPDIR\""] + ["export PATH=\"{bats_bins_path}\":$PATH".format(bats_bins_path = sep.join(path))] + - [ - # First try and expand `$(location ...)`. - # Then try for make variables (possibly supplied by toolchains). - 'export {}="{}"'.format( - key, - ctx.expand_make_variables( - key, - ctx.expand_location(val, ctx.attr.deps), - {} - ) - ) - for key, val in ctx.attr.env.items() - ] + + ['export {}="{}"'.format(key, val) for key, val in expanded_envs.items()] + [_test_files(ctx.executable._bats, ctx.files.srcs, ctx.attr)], ) ctx.actions.write( @@ -138,6 +137,30 @@ _bats_with_bats_assert_test = rule( ) def bats_test(uses_bats_assert = False, **kwargs): + """ + A rule for creating a test target for running one or more `*.bats` test files. + + This rule can run one or more bats files, utilizing the `bats-core` framework. Optionally, + `bats-assert` (extension library) can be used for these tests. + + `bats_test()` is a macro that handles proper target definition internally. + + Args: + uses_bats_assert (str): Whether this test makes use of `bats_assert` (and `bats_support`). + **kwargs (dict): Additional keyword arguments that are passed to the underyling target. + These attributes may include: + name: (Required) The name for the underlying internal target. + srcs: (Required) The `*.bats` files to be run by this test. + data: (Optional) Files necessary for the test during runtime. + deps: (Optional) Dependency targets for the test. + bats_args: (Optional) Arguments to be passed to the `bats` (bats-core) framework when + running the tests. + env: (Optional) Dictionary of enviroment variables to their set values. Values + are subject to `$(location)` and "Make variable" substitution. This + includes expansion mapping provided via `toolchains`. + toolchains: (Optional) Additional providers for extra logic (e.g. `env` substitution). + *: (Optional) Any other attributes that apply for `*_test` targets. + """ if not uses_bats_assert: _bats_test(**kwargs) else: diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 6dd9d32..19d67a2 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -72,13 +72,55 @@ bats_test( name = "hello_world_test", srcs = ["hello_world.bats"], env = { - "PROGRAM": "hello_world", - "LOCATED": "$(location :dummy)", - "TOOLCHAIN_STRING_FLAG_ENV_VAR": "$(STRING_FLAG_ENV_VAR)", + "SIMPLE_VAL": "hello_world", + "ESCAPED_SIMPLE_VAL": "$$SIMPLE_VAL", + "DOUBLE_DOLLAR_SIGN_ESCAPED_SIMPLE_VAL": "$$$$SIMPLE_VAL", + "LOCATION_VAL": "$(location :dummy)", + "EXECPATH_VAL": "$(execpath :dummy)", + "ROOTPATH_VAL": "$(rootpath :dummy)", + "RLOCATIONPATH_VAL": "$(rlocationpath :dummy)", + "STRING_FLAG_TO_OTHER_ENV_VAR": "$(STRING_FLAG_TO_OTHER_ENV_VAR)", + "STRING_FLAG_TO_STRING_FLAG_ENV_VAR": "$(STRING_FLAG_TO_STRING_FLAG_ENV_VAR)", + "TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW": "$STRING_FLAG_ENV_VAR", + "TOOLCHAIN_STRING_FLAG_ENV_VAR_PAREN": "$(STRING_FLAG_ENV_VAR)", + "TOOLCHAIN_STRING_FLAG_ENV_VAR_CURLY": "${STRING_FLAG_ENV_VAR}", + "STRING_FLAG_TO_LOCATION_ENV_VAR_RAW": "$STRING_FLAG_TO_LOCATION_ENV_VAR", + "STRING_FLAG_TO_LOCATION_ENV_VAR_PAREN": "$(STRING_FLAG_TO_LOCATION_ENV_VAR)", + "STRING_FLAG_TO_LOCATION_ENV_VAR_CURLY": "${STRING_FLAG_TO_LOCATION_ENV_VAR}", + "INDIRECT_SIMPLE_VAL_RAW": "$SIMPLE_VAL", + "INDIRECT_SIMPLE_VAL_PAREN": "$(SIMPLE_VAL)", + "INDIRECT_SIMPLE_VAL_CURLY": "${SIMPLE_VAL}", + "INDIRECT_ESCAPED_SIMPLE_VAL_RAW": "$ESCAPED_SIMPLE_VAL", + "INDIRECT_ESCAPED_SIMPLE_VAL_PAREN": "$(ESCAPED_SIMPLE_VAL)", + "INDIRECT_ESCAPED_SIMPLE_VAL_CURLY": "${ESCAPED_SIMPLE_VAL}", + "INDIRECT_LOCATION_VAL_RAW": "$LOCATION_VAL", + "INDIRECT_LOCATION_VAL_PAREN": "$(LOCATION_VAL)", + "INDIRECT_LOCATION_VAL_CURLY": "${LOCATION_VAL}", + "INDIRECT_EXECPATH_VAL_RAW": "$EXECPATH_VAL", + "INDIRECT_EXECPATH_VAL_PAREN": "$(EXECPATH_VAL)", + "INDIRECT_EXECPATH_VAL_CURLY": "${EXECPATH_VAL}", + "INDIRECT_ROOTPATH_VAL_RAW": "$ROOTPATH_VAL", + "INDIRECT_ROOTPATH_VAL_PAREN": "$(ROOTPATH_VAL)", + "INDIRECT_ROOTPATH_VAL_CURLY": "${ROOTPATH_VAL}", + "INDIRECT_RLOCATIONPATH_VAL_RAW": "$RLOCATIONPATH_VAL", + "INDIRECT_RLOCATIONPATH_VAL_PAREN": "$(RLOCATIONPATH_VAL)", + "INDIRECT_RLOCATIONPATH_VAL_CURLY": "${RLOCATIONPATH_VAL}", + "INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW": "$TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW", + "INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_PAREN": "$(TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW)", + "INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_CURLY": "${TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW}", + "INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_RAW": "$STRING_FLAG_TO_LOCATION_ENV_VAR_RAW", + "INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_PAREN": "$(STRING_FLAG_TO_LOCATION_ENV_VAR_RAW)", + "INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_CURLY": "${STRING_FLAG_TO_LOCATION_ENV_VAR_RAW}", + "MULTI_INDIRECT_RAW": "$INDIRECT_SIMPLE_VAL_RAW-$INDIRECT_ESCAPED_SIMPLE_VAL_RAW-$INDIRECT_LOCATION_VAL_RAW-$INDIRECT_RLOCATIONPATH_VAL_RAW-$INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW-$INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_RAW", + "MULTI_INDIRECT_PAREN": "$(INDIRECT_SIMPLE_VAL_RAW)-$(INDIRECT_ESCAPED_SIMPLE_VAL_RAW)-$(INDIRECT_LOCATION_VAL_RAW)-$(INDIRECT_RLOCATIONPATH_VAL_RAW)-$(INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW)-$(INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_RAW)", + "MULTI_INDIRECT_CURLY": "${INDIRECT_SIMPLE_VAL_RAW}-${INDIRECT_ESCAPED_SIMPLE_VAL_RAW}-${INDIRECT_LOCATION_VAL_RAW}-${INDIRECT_RLOCATIONPATH_VAL_RAW}-${INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW}-${INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_RAW}", }, deps = [":dummy"], toolchains = [ + ":env_var_flag", + ":dummy_location_flag", ":hello_world_flag", + ":string_flag_env_var_flag", ], ) @@ -117,8 +159,26 @@ bats_test_suite( ] ) +string_flag( + name = "env_var_flag", + build_setting_default = "$(LOCATION_VAL)", + make_variable = "STRING_FLAG_TO_OTHER_ENV_VAR", +) + +string_flag( + name = "dummy_location_flag", + build_setting_default = "$(location :dummy)", + make_variable = "STRING_FLAG_TO_LOCATION_ENV_VAR", +) + string_flag( name = "hello_world_flag", build_setting_default = "flag_value", make_variable = "STRING_FLAG_ENV_VAR", ) + +string_flag( + name = "string_flag_env_var_flag", + build_setting_default = "$(STRING_FLAG_TO_LOCATION_ENV_VAR)", + make_variable = "STRING_FLAG_TO_STRING_FLAG_ENV_VAR", +) diff --git a/tests/hello_world.bats b/tests/hello_world.bats index b91867c..33e13ea 100644 --- a/tests/hello_world.bats +++ b/tests/hello_world.bats @@ -6,15 +6,211 @@ } @test "Test simple environment variable" { - [ "${PROGRAM}" == "hello_world" ] + echo "Val: ${SIMPLE_VAL}" + [ "${SIMPLE_VAL}" == "hello_world" ] +} + +@test "Test escaped simple environment variable" { + echo "Val: ${ESCAPED_SIMPLE_VAL}" + [ "${ESCAPED_SIMPLE_VAL}" == "\$SIMPLE_VAL" ] +} + +@test "Test double dollar sign escaped simple environment variable" { + echo "Val: ${DOUBLE_DOLLAR_SIGN_ESCAPED_SIMPLE_VAL}" + [ "${DOUBLE_DOLLAR_SIGN_ESCAPED_SIMPLE_VAL}" == "\$\$SIMPLE_VAL" ] } @test "Test environment variable expanded from bazel location" { - echo "Location: ${LOCATED}" - [ "${LOCATED}" == "tests/dummy.txt" ] + echo "Location: ${LOCATION_VAL}" + [ "${LOCATION_VAL}" == "tests/dummy.txt" ] +} + +@test "Test environment variable expanded from bazel execpath" { + echo "Execpath: ${EXECPATH_VAL}" + [ "${EXECPATH_VAL}" == "tests/dummy.txt" ] +} + +@test "Test environment variable expanded from bazel rootpath" { + echo "Rootpath: ${ROOTPATH_VAL}" + [ "${ROOTPATH_VAL}" == "tests/dummy.txt" ] +} + +@test "Test environment variable expanded from bazel rlocationpath" { + echo "Rlocationpath: ${RLOCATIONPATH_VAL}" + [ "${RLOCATIONPATH_VAL}" == "bazel_bats/tests/dummy.txt" ] +} + +@test "Test environment variable expanded from another variable in env attr" { + echo "Flag env var: ${STRING_FLAG_TO_OTHER_ENV_VAR}" + [ "${STRING_FLAG_TO_OTHER_ENV_VAR}" == "tests/dummy.txt" ] +} + +@test "Test environment variable expanded from another variable from string_flag" { + echo "Flag env var: ${STRING_FLAG_TO_STRING_FLAG_ENV_VAR}" + [ "${STRING_FLAG_TO_STRING_FLAG_ENV_VAR}" == "tests/dummy.txt" ] +} + +@test "Test environment variable expanded from bazel string flag (raw)" { + echo "Flag env var: ${TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW}" + [ "${TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW}" == "flag_value" ] +} + +@test "Test environment variable expanded from bazel string flag (in parenthesis)" { + echo "Flag env var: ${TOOLCHAIN_STRING_FLAG_ENV_VAR_PAREN}" + [ "${TOOLCHAIN_STRING_FLAG_ENV_VAR_PAREN}" == "flag_value" ] +} + +@test "Test environment variable expanded from bazel string flag (in curly braces)" { + echo "Flag env var: ${TOOLCHAIN_STRING_FLAG_ENV_VAR_CURLY}" + [ "${TOOLCHAIN_STRING_FLAG_ENV_VAR_CURLY}" == "flag_value" ] +} + +@test "Test environment variable expanded from bazel string flag to location (raw)" { + echo "Flag env var: ${STRING_FLAG_TO_LOCATION_ENV_VAR_RAW}" + [ "${STRING_FLAG_TO_LOCATION_ENV_VAR_RAW}" == "tests/dummy.txt" ] +} + +@test "Test environment variable expanded from bazel string flag to location (in parenthesis)" { + echo "Flag env var: ${STRING_FLAG_TO_LOCATION_ENV_VAR_PAREN}" + [ "${STRING_FLAG_TO_LOCATION_ENV_VAR_PAREN}" == "tests/dummy.txt" ] +} + +@test "Test environment variable expanded from bazel string flag to location (in curly braces)" { + echo "Flag env var: ${STRING_FLAG_TO_LOCATION_ENV_VAR_CURLY}" + [ "${STRING_FLAG_TO_LOCATION_ENV_VAR_CURLY}" == "tests/dummy.txt" ] +} + +@test "Test indirect simple environment variable (raw)" { + echo "Val: ${INDIRECT_SIMPLE_VAL_RAW}" + [ "${INDIRECT_SIMPLE_VAL_RAW}" == "hello_world" ] +} + +@test "Test indirect simple environment variable (in parenthesis)" { + echo "Val: ${INDIRECT_SIMPLE_VAL_PAREN}" + [ "${INDIRECT_SIMPLE_VAL_PAREN}" == "hello_world" ] +} + +@test "Test indirect simple environment variable (in curly braces)" { + echo "Val: ${INDIRECT_SIMPLE_VAL_CURLY}" + [ "${INDIRECT_SIMPLE_VAL_CURLY}" == "hello_world" ] +} + +@test "Test indirect escaped simple environment variable (raw)" { + echo "Val: ${INDIRECT_ESCAPED_SIMPLE_VAL_RAW}" + [ "${INDIRECT_ESCAPED_SIMPLE_VAL_RAW}" == "\$SIMPLE_VAL" ] +} + +@test "Test indirect escaped simple environment variable (in parenthesis)" { + echo "Val: ${INDIRECT_ESCAPED_SIMPLE_VAL_PAREN}" + [ "${INDIRECT_ESCAPED_SIMPLE_VAL_PAREN}" == "\$SIMPLE_VAL" ] +} + +@test "Test indirect escaped simple environment variable (in curly braces)" { + echo "Val: ${INDIRECT_ESCAPED_SIMPLE_VAL_CURLY}" + [ "${INDIRECT_ESCAPED_SIMPLE_VAL_CURLY}" == "\$SIMPLE_VAL" ] +} + +@test "Test indirect environment variable expanded from bazel location (raw)" { + echo "Location: ${INDIRECT_LOCATION_VAL_RAW}" + [ "${INDIRECT_LOCATION_VAL_RAW}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel location (in parenthesis)" { + echo "Location: ${INDIRECT_LOCATION_VAL_PAREN}" + [ "${INDIRECT_LOCATION_VAL_PAREN}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel location (in curly braces)" { + echo "Location: ${INDIRECT_LOCATION_VAL_CURLY}" + [ "${INDIRECT_LOCATION_VAL_CURLY}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel execpath (raw)" { + echo "Execpath: ${INDIRECT_EXECPATH_VAL_RAW}" + [ "${INDIRECT_EXECPATH_VAL_RAW}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel execpath (in parenthesis)" { + echo "Execpath: ${INDIRECT_EXECPATH_VAL_PAREN}}" + [ "${INDIRECT_EXECPATH_VAL_PAREN}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel execpath (in curly braces)" { + echo "Execpath: ${INDIRECT_EXECPATH_VAL_CURLY}" + [ "${INDIRECT_EXECPATH_VAL_CURLY}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel rootpath (raw)" { + echo "Rootpath: ${INDIRECT_ROOTPATH_VAL_RAW}" + [ "${INDIRECT_ROOTPATH_VAL_RAW}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel rootpath (in parenthesis)" { + echo "Rootpath: ${INDIRECT_ROOTPATH_VAL_PAREN}" + [ "${INDIRECT_ROOTPATH_VAL_PAREN}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel rootpath (in curly braces)" { + echo "Rootpath: ${INDIRECT_ROOTPATH_VAL_CURLY}" + [ "${INDIRECT_ROOTPATH_VAL_CURLY}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel rlocationpath (raw)" { + echo "Rlocationpath: ${INDIRECT_RLOCATIONPATH_VAL_RAW}" + [ "${INDIRECT_RLOCATIONPATH_VAL_RAW}" == "bazel_bats/tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel rlocationpath (in parenthesis)" { + echo "Rlocationpath: ${INDIRECT_RLOCATIONPATH_VAL_PAREN}" + [ "${INDIRECT_RLOCATIONPATH_VAL_PAREN}" == "bazel_bats/tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel rlocationpath (in curly braces)" { + echo "Rlocationpath: ${INDIRECT_RLOCATIONPATH_VAL_CURLY}" + [ "${INDIRECT_RLOCATIONPATH_VAL_CURLY}" == "bazel_bats/tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel string flag (raw)" { + echo "Flag env var: ${INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW}" + [ "${INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_RAW}" == "flag_value" ] +} + +@test "Test indirect environment variable expanded from bazel string flag (in parenthesis)" { + echo "Flag env var: ${INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_PAREN}" + [ "${INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_PAREN}" == "flag_value" ] +} + +@test "Test indirect environment variable expanded from bazel string flag (in curly braces)" { + echo "Flag env var: ${INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_CURLY}" + [ "${INDIRECT_TOOLCHAIN_STRING_FLAG_ENV_VAR_CURLY}" == "flag_value" ] +} + +@test "Test indirect environment variable expanded from bazel string flag to location (raw)" { + echo "Flag env var: ${INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_RAW}" + [ "${INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_RAW}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel string flag to location (in parenthesis)" { + echo "Flag env var: ${INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_PAREN}" + [ "${INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_PAREN}" == "tests/dummy.txt" ] +} + +@test "Test indirect environment variable expanded from bazel string flag to location (in curly braces)" { + echo "Flag env var: ${INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_CURLY}" + [ "${INDIRECT_STRING_FLAG_TO_LOCATION_ENV_VAR_CURLY}" == "tests/dummy.txt" ] +} + +@test "Test indirect multiple environment variables expanded (raw)" { + echo "Muli env var: ${MULTI_INDIRECT_RAW}" + [ "${MULTI_INDIRECT_RAW}" == "hello_world-\$SIMPLE_VAL-tests/dummy.txt-bazel_bats/tests/dummy.txt-flag_value-tests/dummy.txt" ] +} + +@test "Test indirect multiple environment variables expanded (in parenthesis)" { + echo "Muli env var: ${MULTI_INDIRECT_PAREN}" + [ "${MULTI_INDIRECT_PAREN}" == "hello_world-\$SIMPLE_VAL-tests/dummy.txt-bazel_bats/tests/dummy.txt-flag_value-tests/dummy.txt" ] } -@test "Test environment variable expanded from bazel string flag" { - echo "Flag env var: ${TOOLCHAIN_STRING_FLAG_ENV_VAR}" - [ "${TOOLCHAIN_STRING_FLAG_ENV_VAR}" == "flag_value" ] +@test "Test indirect multiple environment variables expanded (in curly braces)" { + echo "Muli env var: ${MULTI_INDIRECT_CURLY}" + [ "${MULTI_INDIRECT_CURLY}" == "hello_world-\$SIMPLE_VAL-tests/dummy.txt-bazel_bats/tests/dummy.txt-flag_value-tests/dummy.txt" ] }